qubu 0.0.0 → 0.3.4

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-Cec0xzo4.mjs +116 -0
  61. package/dist/types-H4vyCw8_.d.mts +45 -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 +130 -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,157 @@
1
+ # Compose SQL templates
2
+
3
+ > Use trusted SQL syntax with bound runtime values while retaining the Qubu metadata carried by interpolated expressions, fragments, and queries.
4
+
5
+ ## Bind every runtime value
6
+
7
+ The `sql` tag treats template text as SQL syntax and every ordinary
8
+ substitution as a parameter. This includes strings, numbers, objects, arrays,
9
+ and `null`:
10
+
11
+ ```ts
12
+ import { integer, render, sql, table, text } from 'qubu'
13
+
14
+ const users = table('users', { name: text() })
15
+ const posts = table('posts', { id: integer() })
16
+ const search = 'Ada%'
17
+ const predicate = sql`${users.name} LIKE ${search}`
18
+
19
+ render(predicate)
20
+ // {
21
+ // text: '"users"."name" LIKE ?',
22
+ // parameters: ['Ada%'],
23
+ // }
24
+ ```
25
+
26
+ `${users.name}` is a Qubu expression, so the tag renders its quoted column
27
+ reference. `${search}` is an ordinary value, so it cannot become SQL text.
28
+
29
+ ## Compose expressions, templates, and queries
30
+
31
+ Fragment substitutions use the same rendering context as the enclosing
32
+ statement. Parameters keep one placeholder sequence across nested templates
33
+ and queries:
34
+
35
+ ```ts
36
+ import { eq, from, render, select, sql, where } from 'qubu'
37
+ import { postgresDialect } from 'qubu/postgres'
38
+
39
+ const selectedNames = select(
40
+ { displayName: users.name },
41
+ from(users),
42
+ where(eq(users.name, 'Ada'))
43
+ )
44
+
45
+ const exists = sql`EXISTS (${selectedNames}) AND ${users.name} <> ${'root'}`
46
+
47
+ render(exists, postgresDialect())
48
+ // {
49
+ // text: 'EXISTS (SELECT "users"."name" AS "display_name" FROM "users" WHERE ("users"."name" = $1)) AND "users"."name" <> $2',
50
+ // parameters: ['Ada', 'root'],
51
+ // }
52
+ ```
53
+
54
+ The template owns punctuation such as the parentheses around the query. Qubu
55
+ renders a query substitution with SQL-facing projection names, as it does for
56
+ CTEs and scalar subqueries.
57
+
58
+ ## Declare a result domain
59
+
60
+ An unannotated template has application output `unknown` and SQL domain
61
+ `SqlUnknown`. State both facts when the expression feeds typed operations or a
62
+ named projection:
63
+
64
+ ```ts
65
+ import { from, select, sql } from 'qubu'
66
+ import type { SqlText } from 'qubu'
67
+
68
+ const normalizedName = sql.type<string, SqlText>()`LOWER(${users.name})`
69
+
70
+ const query = select({ name: normalizedName }, from(users))
71
+ // typeof query.row is { name: string }
72
+ ```
73
+
74
+ `sql.type<Output, SqlType>()` changes only TypeScript metadata. It does not
75
+ parse the SQL or validate the declared types against the database. The tag
76
+ still binds ordinary substitutions.
77
+
78
+ Interpolated fragments contribute their source requirements and
79
+ nullability. For example, a `sql.type<string, SqlText>()` template containing a
80
+ column from the right side of a `leftJoin()` produces `string | null` in the
81
+ selected row.
82
+
83
+ ## Keep identifiers and dynamic syntax explicit
84
+
85
+ Use the substitution kind that matches the value:
86
+
87
+ | Input | Rendering behavior |
88
+ | ------------------------------------- | ------------------------------------------------------------- |
89
+ | Fixed template text | Trusted SQL syntax, appended unchanged |
90
+ | An ordinary substitution | Bound parameter |
91
+ | A Qubu expression, query, or fragment | Rendered through the current dialect and placeholder sequence |
92
+ | `identifier(name)` | One dialect-quoted identifier |
93
+ | `qualifiedIdentifier(schema, table)` | Several dialect-quoted identifier parts |
94
+ | `unsafeExpression(text)` | Unchecked SQL text |
95
+
96
+ Do not use a dotted string as an identifier. Pass each part to
97
+ `qualifiedIdentifier()`. Keep `unsafeExpression()` for application-controlled
98
+ syntax that cannot use a fixed template segment:
99
+
100
+ ```ts
101
+ import { sql } from 'qubu'
102
+ import { identifier, unsafeExpression } from 'qubu/core'
103
+
104
+ const sortColumn = 'display_name'
105
+ const direction = 'DESC' as const
106
+
107
+ const ordering = sql`ORDER BY ${identifier(sortColumn)} ${unsafeExpression(direction)}`
108
+ ```
109
+
110
+ Validate any dynamic syntax against an application-owned allowlist before it
111
+ reaches `unsafeExpression()`.
112
+
113
+ ## Preserve metadata through interpolated fragments
114
+
115
+ The tag inherits source dependencies, conservative outer-join nullability,
116
+ grouping facts, aggregate and window state, subquery state, and dialect
117
+ capability requirements from Qubu fragment substitutions. It does not infer
118
+ those facts from unchecked template text.
119
+
120
+ Use a built-in expression as the substitution when its semantics matter:
121
+
122
+ ```ts
123
+ import { count, sql } from 'qubu'
124
+ import type { SqlInteger } from 'qubu'
125
+
126
+ const postCount = sql.type<number, SqlInteger>()`${count(posts.id)}`
127
+ ```
128
+
129
+ This wrapper retains the aggregate dependency recorded by `count()`. Writing
130
+ the aggregate name in template text would not record that fact:
131
+
132
+ ```ts
133
+ const untrackedPostCount = sql.type<number, SqlInteger>()`COUNT(${posts.id})`
134
+ ```
135
+
136
+ This renders valid SQL, but Qubu sees an ordinary column dependency because it
137
+ does not parse `COUNT` from the template text.
138
+
139
+ Declare a capability when the template text itself uses dialect-specific
140
+ syntax:
141
+
142
+ ```ts
143
+ import { sql } from 'qubu'
144
+ import { withDialectCapability } from 'qubu/core'
145
+ import type { SqlBoolean } from 'qubu'
146
+
147
+ const postgresMatch = withDialectCapability(
148
+ sql.type<boolean, SqlBoolean>()`${users.name} ILIKE ${search}`,
149
+ 'ilike'
150
+ )
151
+ ```
152
+
153
+ Rendering `postgresMatch` now requires a dialect that advertises `ilike`.
154
+
155
+ Use [Fragments and metadata](../query-model/fragments.md) for the inherited
156
+ facts, and [SQL semantic types](../sql-semantic-types.md) for result-domain
157
+ compatibility.
@@ -0,0 +1,92 @@
1
+ # Vite compiler hint
2
+
3
+ > Opt a JavaScript or TypeScript module into Qubu's ambient query API while keeping the transform limited to explicit directive-bearing files.
4
+
5
+ The optional Vite plugin recognizes the `"use qubu"` directive and injects only
6
+ the referenced named imports from the configured module.
7
+
8
+ ## Install the plugin
9
+
10
+ Add the plugin to Vite and add the matching ambient declarations to TypeScript:
11
+
12
+ ```ts
13
+ // vite.config.ts
14
+ import { defineConfig } from 'vite'
15
+ import { qubu } from 'qubu/vite'
16
+
17
+ export default defineConfig({
18
+ plugins: [qubu()],
19
+ })
20
+ ```
21
+
22
+ ```jsonc
23
+ {
24
+ "compilerOptions": {
25
+ // Merge this with any existing project-specific entries.
26
+ "types": ["qubu/globals"],
27
+ },
28
+ }
29
+ ```
30
+
31
+ The plugin supplies runtime imports. `qubu/globals` supplies the corresponding
32
+ ambient value and type declarations for the TypeScript compiler.
33
+
34
+ ## Mark a module explicitly
35
+
36
+ Put the directive in the module's initial directive prologue:
37
+
38
+ ```ts
39
+ 'use qubu'
40
+
41
+ const users = table('users', {
42
+ id: integer(),
43
+ name: text(),
44
+ })
45
+
46
+ const query = select(
47
+ { id: users.id, name: users.name },
48
+ from(users),
49
+ where(eq(users.id, 42))
50
+ )
51
+ ```
52
+
53
+ Conceptually, the transform adds the imports that this module references:
54
+
55
+ ```ts
56
+ import { eq, from, integer, select, table, text, where } from 'qubu'
57
+ ```
58
+
59
+ Existing imports remain valid. The transform does not rewrite member properties,
60
+ strings, comments, or names outside the public Qubu global catalog.
61
+
62
+ ## Configure the transform
63
+
64
+ ```ts
65
+ qubu({
66
+ module: 'qubu',
67
+ include: id => id.includes('/src/'),
68
+ exclude: /\.stories\./,
69
+ globals: ['select', 'from', 'where', 'eq', 'table'],
70
+ })
71
+ ```
72
+
73
+ | Option | Effect | Default |
74
+ | --------- | --------------------------------------- | --------------------- |
75
+ | `module` | Import source for injected names | `'qubu'` |
76
+ | `include` | Restrict matching module IDs | no filter |
77
+ | `exclude` | Skip matching module IDs | no filter |
78
+ | `globals` | Narrow the names eligible for injection | Qubu's public catalog |
79
+
80
+ ## Know the boundaries
81
+
82
+ - Only JavaScript and TypeScript script extensions are considered.
83
+ - Files under `node_modules` are skipped.
84
+ - `include` and `exclude` filters run against the module ID.
85
+ - A module without the directive is unchanged.
86
+ - A module that references no eligible Qubu global is unchanged.
87
+ - Normal ES module imports remain the explicit fallback when the hint is not a
88
+ good fit.
89
+
90
+ The plugin has no runtime dependency on Vite; it returns the small transform
91
+ shape Vite expects. Use the package root directly when you want ordinary import
92
+ semantics everywhere.
package/docs/index.md ADDED
@@ -0,0 +1,111 @@
1
+ # Qubu
2
+
3
+ > Build parameterized SQL from typed tables, expressions, and clauses.
4
+
5
+ Qubu builds SQL from values. Tables, expressions, clauses, and complete queries
6
+ compose without a mutable query builder. TypeScript tracks selected row shapes,
7
+ source scope, and nullability, while rendering returns SQL text and ordered
8
+ parameters.
9
+
10
+ The preferred source style names each projected field and writes the final
11
+ `select()` clauses in SQL order. Clause values remain order-independent at
12
+ runtime, so a reusable `where()` or `orderBy()` fragment can be built earlier
13
+ and placed in that final call where it reads best.
14
+
15
+ ## Start here
16
+
17
+ If this is your first query, follow [Getting started](getting-started.md) to
18
+ define a table, build a `SELECT`, and inspect its SQL and parameters.
19
+
20
+ ## Choose a task
21
+
22
+ - [Build a `SELECT`](guides/select/overview.md) with projections, joins,
23
+ predicates, ordering, and grouping.
24
+ - [Compose queries](guides/compose-queries.md) with CTEs, derived tables,
25
+ subqueries, and set operations.
26
+ - [Compose SQL templates](guides/sql-templates.md) for trusted syntax with
27
+ bound values and metadata-preserving fragment substitutions.
28
+ - [Write mutations](guides/mutations.md) with typed `INSERT`, `UPDATE`, and
29
+ `DELETE` statements.
30
+ - [Use Qubu tables with Drizzle](guides/drizzle.md) while moving query call
31
+ sites without duplicating schema declarations.
32
+ - [Extend Qubu](guides/extensions/overview.md) with a custom source, clause,
33
+ dialect policy, or typed expression.
34
+ - [Read JSON scalars](guides/json.md) from structured JSON paths.
35
+ - [Enable the Vite compiler hint](guides/vite-plugin.md) when query modules
36
+ should opt into named imports through a directive.
37
+ - [Inspect an existing database](schema/introspection.md) through the optional
38
+ user-owned catalog boundary.
39
+ - [Generate a schema module](schema/code-generation.md) from one complete,
40
+ non-lossy Snapshot v1 introspection result.
41
+ - [Compare snapshots](schema/diff.md) with explicit rename hints and reviewable
42
+ safety diagnostics.
43
+ - [Build migration plans](schema/migration-plans.md) as reviewed, deterministic
44
+ data before DDL emission.
45
+ - [Emit DDL](schema/ddl-emission.md) from an approved migration plan without
46
+ handing Qubu a database connection.
47
+
48
+ ## The query pipeline
49
+
50
+ The same query value can be rendered for inspection or passed to an adapter for
51
+ execution. The application-owned adapter handles the driver, database
52
+ connection, and driver-specific row and mutation-result details.
53
+
54
+ ```mermaid
55
+ flowchart LR
56
+ A["Tables and columns"] --> B["Expressions and clauses"]
57
+ B --> C["Typed query"]
58
+ C --> D["Dialect renderer"]
59
+ D --> E["SQL text + ordered parameters"]
60
+ E --> F["Application-owned adapter"]
61
+ F --> G["Rows + optional mutation facts"]
62
+ ```
63
+
64
+ Values become bound parameters, and the active dialect quotes identifiers. Raw
65
+ SQL is available through explicit unsafe helpers. The call site shows where
66
+ that unchecked syntax enters the query.
67
+
68
+ ## Understand the Qubu model
69
+
70
+ Use these pages when a guide leaves a rule unexplained or when an extension
71
+ needs to preserve a fact across composition:
72
+
73
+ | Model | Start with | Covers |
74
+ | ------------------------ | --------------------------------------------------- | ----------------------------------------------------------------------------------- |
75
+ | Query model | [Source scope](query-model/source-scope.md) | Source identity, result shapes, fragments, metadata, and query composition |
76
+ | Schema model | [Tables and names](schema/tables-and-names.md) | Tables, snapshots, diffs, migration plans, and DDL emission |
77
+ | Database introspection | [Database introspection](schema/introspection.md) | Catalog readers, Snapshot v1 mapping, identities, diagnostics, and support limits |
78
+ | Schema source generation | [Generate a schema](schema/code-generation.md) | Machine-owned TypeScript, identity handoff, controlled mappings, and v1 exclusions |
79
+ | Rendering and execution | [Dialects and execution](dialects-and-execution.md) | Placeholder and identifier policies, capabilities, adapters, and raw-SQL boundaries |
80
+ | SQL semantic types | [SQL semantic types](sql-semantic-types.md) | Application types, SQL domains, nullability, and compatible operations |
81
+
82
+ ## A small example
83
+
84
+ ```ts
85
+ import { eq, from, integer, render, select, table, text, where } from 'qubu'
86
+
87
+ const users = table('users', {
88
+ id: integer(),
89
+ name: text(),
90
+ })
91
+
92
+ const query = select(
93
+ { id: users.id, name: users.name },
94
+ from(users),
95
+ where(eq(users.id, 7))
96
+ )
97
+
98
+ render(query)
99
+ // {
100
+ // text: 'SELECT "users"."id" AS "id", "users"."name" AS "name" FROM "users" WHERE ("users"."id" = ?)',
101
+ // parameters: [7],
102
+ // }
103
+ ```
104
+
105
+ The inferred row is `{ id: number; name: string }`. The value `7` stays out
106
+ of the SQL text and appears in the `parameters` array in placeholder order.
107
+
108
+ The [supported features](reference/supported-surface.md) page is the canonical
109
+ package-entrypoint and ownership map. Qubu emits DDL, but the application owns
110
+ migration execution and database lifecycle. [Troubleshooting](troubleshooting.md)
111
+ starts from common errors and points to the concept page behind each one.
@@ -0,0 +1,109 @@
1
+ # Fragments and metadata
2
+
3
+ > Build custom SQL from values while preserving the source, result, grouping, and capability facts that later composition checks.
4
+
5
+ ## A fragment has a renderer and metadata
6
+
7
+ The runtime unit is a `Fragment`:
8
+
9
+ ```ts
10
+ Fragment<Metadata>
11
+ ```
12
+
13
+ The renderer appends SQL and parameters to a context. The metadata type records
14
+ facts that the next composition step needs. The main groups are:
15
+
16
+ | Fact | What it records |
17
+ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
18
+ | `ResultMeta<Output, NullableFrom, SqlType>` | The value or row produced, sources that can make it `null`, and its SQL domain. |
19
+ | `CardinalityMeta<QueryCardinality>` | Whether a query returns `many`, `zero-or-one`, or `exactly-one` rows. |
20
+ | `RequiresSourceMeta` and `ProvidesSourceMeta` | Which source a fragment needs and which source a `FROM` or join introduces. |
21
+ | `RequiresOuterSourceMeta` and `ProvidesOuterSourceMeta` | Which enclosing source a correlated or LATERAL query reads and how `correlate()` provides it. |
22
+ | `NullableSourceMeta` | Which source became nullable through an outer join. |
23
+ | `ExpressionMeta`, `AggregateMeta`, and `GroupingMeta` | The columns an expression reads, consumes inside an aggregate, or makes available through `GROUP BY`. |
24
+
25
+ An untyped extension uses `SqlUnknown` for its SQL domain. Declare a domain when
26
+ the extension needs incompatible operations to fail at the call site. See
27
+ [SQL semantic types](../sql-semantic-types.md).
28
+
29
+ ## Preserve metadata when composing fragments
30
+
31
+ Composition helpers keep the facts that their children already carry:
32
+
33
+ | Composition | Result |
34
+ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
35
+ | `sequence()`, `commaSeparated()`, `keyword()`, `parenthesize()` | Preserve inherited source and nullability facts without inventing a result or query cardinality. |
36
+ | An expression wrapper such as `expressionFragment()` | Preserve the wrapped result, source, nullability, and SQL domain. |
37
+ | A source-aware operator such as `upper(column)` | Create a new result while inheriting the operand's source and nullability facts. |
38
+ | An aggregate such as `count(column)` | Mark argument dependencies as aggregate-consumed, so they do not need to appear in `GROUP BY`. |
39
+ | `groupBy()` | Record grouping expressions and the column dependencies they make available. |
40
+ | `leftJoin()` | Add `NullableSourceMeta` for the joined source. |
41
+
42
+ The type-level contract stays small. `OutputOf<T>` describes a result,
43
+ `SqlTypeOf<T>` its SQL domain, `RequiresOf<T>` its required sources, and
44
+ `NullabilityOf<T>` the sources that can make it null after an outer join.
45
+
46
+ Qubu does not infer every SQL rule. Grouping checks use declared dependencies,
47
+ and functional dependencies from database keys are handled where the source
48
+ model explicitly proves them. New metadata belongs in this union only when a
49
+ producer, consumer, and regression test all exist.
50
+
51
+ ## Parameters are runtime data
52
+
53
+ Parameter values are not fragment metadata. A renderer calls
54
+ `context.parameter(value)`, and `render()` collects values in placeholder order:
55
+
56
+ ```ts
57
+ import {
58
+ and,
59
+ eq,
60
+ from,
61
+ integer,
62
+ like,
63
+ render,
64
+ select,
65
+ table,
66
+ text,
67
+ where,
68
+ } from 'qubu'
69
+
70
+ const users = table('users', {
71
+ id: integer(),
72
+ name: text(),
73
+ })
74
+
75
+ const query = select(
76
+ { id: users.id },
77
+ from(users),
78
+ where(and(eq(users.id, 7), like(users.name, '%Ada%')))
79
+ )
80
+
81
+ render(query)
82
+ // text: ... WHERE (("users"."id" = ?) AND ("users"."name" LIKE ?))
83
+ // parameters: [7, '%Ada%']
84
+ ```
85
+
86
+ The parameter array follows the placeholders in the rendered text. `select()`
87
+ normalizes independent clause values, but keep the final call in SQL order in
88
+ new code so source scope and repair hints are visible at a glance.
89
+
90
+ The public [`sql` template tag](../guides/sql-templates.md) uses the same
91
+ renderer. Ordinary substitutions call `context.parameter()`, while expression,
92
+ query, and fragment substitutions call back into the active render context.
93
+ The tag therefore keeps placeholder numbering, dialect behavior, and inherited
94
+ metadata in one composition path.
95
+
96
+ ## Keep the boundary explicit
97
+
98
+ Qubu tracks source scope, result shape, nullability, cardinality, and portable
99
+ SQL capabilities. It does not encode every vendor grammar rule or implicit
100
+ conversion. Use standard fragments for portable SQL and move vendor-specific
101
+ syntax to [dialects or custom extensions](../dialects-and-execution.md).
102
+
103
+ ## Read next
104
+
105
+ - [Source scope](source-scope.md) covers source identities and correlation.
106
+ - [Result shapes and cardinality](result-shapes.md) covers projections and
107
+ nullable results.
108
+ - [Extend Qubu](../guides/extensions/overview.md) applies this model to custom
109
+ expressions, clauses, and sources.
@@ -0,0 +1,125 @@
1
+ # Result shapes and cardinality
2
+
3
+ > Choose a projection, understand how joins change its nullability, and check when a nested query can return no row.
4
+
5
+ ## Name the selected row
6
+
7
+ An object projection uses its keys as result names:
8
+
9
+ ```ts
10
+ import { from, integer, select, table, text, upper } from 'qubu'
11
+
12
+ const users = table('users', {
13
+ id: integer(),
14
+ name: text(),
15
+ })
16
+
17
+ const query = select(
18
+ {
19
+ id: users.id,
20
+ displayName: upper(users.name),
21
+ },
22
+ from(users)
23
+ )
24
+
25
+ type Row = typeof query.row
26
+ // { id: number; displayName: string }
27
+ ```
28
+
29
+ The projection key also names the SQL output column. Use explicit fields for a
30
+ shaped result. Reserve `all(source)` for a whole-source result contract. It
31
+ expands to named columns, so the SQL columns and inferred row keys stay aligned:
32
+
33
+ ```ts
34
+ import { all, from, select, upper } from 'qubu'
35
+
36
+ const query = select(
37
+ { ...all(users), normalizedName: upper(users.name) },
38
+ from(users)
39
+ )
40
+ ```
41
+
42
+ When a query becomes a CTE or derived table, its row shape becomes the columns
43
+ available from that new source. `RETURNING` uses the same projection rules.
44
+ See [Compose queries](../guides/compose-queries.md) and
45
+ [Write mutations](../guides/mutations.md) for those workflows.
46
+
47
+ The examples below continue with the `users` table from the first example.
48
+
49
+ ## Account for nullable joins
50
+
51
+ `leftJoin()` marks the joined source as nullable. A selected column from that
52
+ source widens with `null`, while an expression with its own non-null result
53
+ contract can stay non-null:
54
+
55
+ ```ts
56
+ import { count, eq, from, integer, leftJoin, select, table, text } from 'qubu'
57
+
58
+ const users = table('users', {
59
+ id: integer(),
60
+ name: text(),
61
+ })
62
+ const posts = table('posts', {
63
+ id: integer(),
64
+ authorId: integer(),
65
+ title: text(),
66
+ })
67
+
68
+ const query = select(
69
+ {
70
+ userName: users.name,
71
+ postTitle: posts.title,
72
+ postCount: count(posts.id),
73
+ },
74
+ from(users),
75
+ leftJoin(posts, eq(users.id, posts.authorId))
76
+ )
77
+
78
+ type Row = typeof query.row
79
+ // { userName: string; postTitle: string | null; postCount: number }
80
+ ```
81
+
82
+ The same rule applies inside expressions. `upper(posts.title)` remains
83
+ nullable because it depends on the joined row. `coalesce()` and a `CASE`
84
+ expression with non-null branches can return a non-null result.
85
+
86
+ ## Check scalar cardinality
87
+
88
+ `scalar()` turns a query with exactly one selected field into an expression. The
89
+ result includes `null` when the query may return no rows:
90
+
91
+ ```ts
92
+ import { fetchFirst, from, scalar, select, table, value } from 'qubu'
93
+
94
+ const users = table('users', { id: integer() })
95
+ const firstUser = select({ id: users.id }, from(users), fetchFirst(1))
96
+
97
+ const firstId = scalar(firstUser)
98
+ // OutputOf<typeof firstId> is number | null
99
+ ```
100
+
101
+ `fetchFirst(1)` proves an upper bound, not that a row exists. A source-free
102
+ select is different:
103
+
104
+ ```ts
105
+ const constant = select({ value: value(42) })
106
+ const constantValue = scalar(constant)
107
+ // OutputOf<typeof constantValue> is number
108
+ ```
109
+
110
+ Qubu does not infer exactness from a predicate such as `WHERE id = 1`. That
111
+ predicate can match no rows, so the scalar result remains nullable.
112
+
113
+ ## Keep SQL domains with the row
114
+
115
+ Projections preserve the SQL domains of their expressions. A text expression
116
+ stays `SqlText` through a CTE, derived table, scalar subquery, set operation, or
117
+ mutation `RETURNING` projection. See [SQL semantic types](../sql-semantic-types.md)
118
+ for the domain rules.
119
+
120
+ ## Read next
121
+
122
+ - [Source scope](source-scope.md) explains why a column must come from `FROM`,
123
+ a join, or an intentional correlation.
124
+ - [Fragments and metadata](fragments.md) explains how result and nullability
125
+ facts move through custom composition.