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,65 @@
1
+ # Read JSON scalars
2
+
3
+ > Extract a string, number, or boolean from a JSON column without writing a raw SQL path.
4
+
5
+ Use a structured jsonPath() when a query needs a scalar or an existence check
6
+ inside a JSON document:
7
+
8
+ ```ts
9
+ import {
10
+ from,
11
+ json,
12
+ jsonBoolean,
13
+ jsonExists,
14
+ jsonNumber,
15
+ jsonPath,
16
+ jsonText,
17
+ select,
18
+ table,
19
+ } from 'qubu'
20
+
21
+ const events = table('events', {
22
+ payload: json<{
23
+ user?: { name?: string; active?: boolean; score?: number }
24
+ }>(),
25
+ })
26
+
27
+ const query = select(
28
+ {
29
+ name: jsonText(events.payload, jsonPath('user', 'name')),
30
+ active: jsonBoolean(events.payload, jsonPath('user', 'active')),
31
+ score: jsonNumber(events.payload, jsonPath('user', 'score')),
32
+ hasUser: jsonExists(events.payload, jsonPath('user')),
33
+ },
34
+ from(events)
35
+ )
36
+ ```
37
+
38
+ Strings are object keys. Non-negative integers are array indexes. Qubu keeps the
39
+ path structured so a dialect can encode each key and index without
40
+ interpolating caller-provided SQL.
41
+
42
+ ## Understand missing values
43
+
44
+ Scalar reads return SQL NULL when the path is missing, contains JSON null, or
45
+ resolves to another JSON scalar type. jsonExists() returns true for a present
46
+ JSON null, false for a missing path, and false when the document itself is SQL
47
+ NULL.
48
+
49
+ These rules keep path existence separate from extraction nullability.
50
+
51
+ ## Check dialect support
52
+
53
+ The standard dialect emits SQL/JSON JSON_VALUE and JSON_EXISTS syntax.
54
+ PostgreSQL, MySQL, and SQLite use their native JSON policies. The current
55
+ policies require PostgreSQL 12 or newer, MySQL 8.0.21 or newer, and SQLite JSON
56
+ functions. An application-created dialect must provide a JSON renderer.
57
+
58
+ ## Know the current limits
59
+
60
+ JSON paths cover deterministic key and index traversal. Wildcards, filters,
61
+ recursive descent, JSON-returning extraction, document mutation, and row
62
+ expansion remain dialect-specific extensions.
63
+
64
+ For the SQL domain and nullability rules behind JSON columns, read
65
+ [SQL semantic types](../sql-semantic-types.md).
@@ -0,0 +1,138 @@
1
+ # Write mutations
2
+
3
+ > Build typed `INSERT`, `UPDATE`, and `DELETE` statements from the same table metadata while keeping destructive operations explicit.
4
+
5
+ ## Define write-time rules
6
+
7
+ Generated and default columns affect which input fields are required. Nullable
8
+ columns accept `null` as a value, which is distinct from omitting a defaulted
9
+ field:
10
+
11
+ ```ts
12
+ import { integer, table, text } from 'qubu'
13
+
14
+ const users = table('users', {
15
+ id: integer({ generated: true }),
16
+ name: text(),
17
+ email: text({ nullable: true, hasDefault: true }),
18
+ })
19
+ ```
20
+
21
+ The generated `id` is omitted from inserts and updates. `email` is optional on
22
+ insert, but `{ email: null }` explicitly writes `NULL` when supplied.
23
+
24
+ ## Insert rows
25
+
26
+ Pass one or more rows to `values()`. Qubu checks that every row uses the same
27
+ columns and that required, non-generated fields are present:
28
+
29
+ ```ts
30
+ import { insertInto, render, returning, values } from 'qubu'
31
+
32
+ const query = insertInto(
33
+ users,
34
+ values(
35
+ { name: 'Ada', email: null },
36
+ { name: 'Grace', email: 'grace@example.com' }
37
+ ),
38
+ returning({ id: users.id, name: users.name })
39
+ )
40
+
41
+ render(query)
42
+ // {
43
+ // text: 'INSERT INTO "users" ("name", "email") VALUES (?, ?), (?, ?) RETURNING "users"."id" AS "id", "users"."name" AS "name"',
44
+ // parameters: ['Ada', null, 'Grace', 'grace@example.com'],
45
+ // }
46
+ ```
47
+
48
+ Use `defaultValues()` only when every non-generated column has a database
49
+ default. Use `insertSelect(query, columns)` for an `INSERT ... SELECT` source.
50
+
51
+ ## Update with a predicate
52
+
53
+ `UPDATE` assignments accept either application values or expressions built from
54
+ the target table:
55
+
56
+ ```ts
57
+ import { eq, returning, update, upper, where } from 'qubu'
58
+
59
+ const query = update(
60
+ users,
61
+ { name: upper(users.name) },
62
+ where(eq(users.id, 7)),
63
+ returning({ id: users.id, name: users.name })
64
+ )
65
+ ```
66
+
67
+ The assignment expression is source-aware, so a column from an unrelated table
68
+ cannot silently enter the update.
69
+
70
+ Use `omit` for a runtime-conditional assignment. Qubu removes omitted fields
71
+ before validating and rendering the effective assignment set:
72
+
73
+ ```ts
74
+ import { eq, omit, update, where } from 'qubu'
75
+
76
+ const query = update(
77
+ users,
78
+ {
79
+ name: rename ? 'Archived' : omit,
80
+ email: clearEmail ? null : omit,
81
+ },
82
+ where(eq(users.id, 7))
83
+ )
84
+ ```
85
+
86
+ `omit` means that the column is absent from `SET`. It is distinct from `null`
87
+ and explicit `undefined`, which remain bound assignment values, and it does not
88
+ emit SQL `DEFAULT`. At least one assignment must remain; `update()` throws
89
+ before rendering when every field is omitted. Possible expression branches
90
+ remain source- and capability-aware even when their runtime alternative is
91
+ `omit`.
92
+
93
+ ## Delete with a predicate
94
+
95
+ ```ts
96
+ import { deleteFrom, eq, returning, where } from 'qubu'
97
+
98
+ const query = deleteFrom(
99
+ users,
100
+ where(eq(users.id, 8)),
101
+ returning({ id: users.id })
102
+ )
103
+ ```
104
+
105
+ ## Keep unrestricted writes explicit
106
+
107
+ Both `UPDATE` and `DELETE` require a `WHERE` clause by default. If an operation
108
+ really must affect every row, opt in at the call site:
109
+
110
+ ```ts
111
+ import { allowAll, update } from 'qubu'
112
+
113
+ const query = update(users, { name: 'Archived' }, allowAll())
114
+ ```
115
+
116
+ `allowAll()` is a safety boundary, not a replacement for authorization or
117
+ application-level confirmation. Keep it close to the code that proves the
118
+ unrestricted operation is intended.
119
+
120
+ ## Return typed rows
121
+
122
+ `returning()` uses the same named object projection as `SELECT`. Reserve
123
+ `{ ...all(table) }` for the intentional contract of returning every table
124
+ column. When present, the mutation's `row` type is inferred from that projection, so
125
+ `(await db.execute(query)).rows` has the same shape as a read query when `db`
126
+ comes from `qubu(adapter)`. The projection's SQL semantic domains are
127
+ retained too, so a returned query used by typed composition does not collapse
128
+ UUID, text, numeric, or other known fields to their JavaScript types alone.
129
+
130
+ `db.execute()` and the standalone `execute()` function also return optional
131
+ `affectedRows`, `changedRows`, and `insertId` facts supplied by the adapter. Use
132
+ `db.rows()` or `executeRows()` when only the returned rows matter. The
133
+ [execution guide](../dialects-and-execution.md) defines which driver facts
134
+ belong in each field.
135
+
136
+ See [Column behavior and write types](../schema/columns-and-writes.md)
137
+ for custom output, insert, and update types, then [Dialects and execution](../dialects-and-execution.md)
138
+ for the driver boundary.
@@ -0,0 +1,110 @@
1
+ # Add optional conditions
2
+
3
+ > Keep optional predicates, null checks, and empty-list behavior explicit while
4
+ > the query keeps the same structural shape.
5
+
6
+ The examples use the `users` table from [Build a `SELECT`](overview.md).
7
+
8
+ ## Omit query parts conditionally
9
+
10
+ Use `omit` as the other branch of a JavaScript conditional when a `WHERE`,
11
+ `HAVING`, `ORDER BY`, or `DISTINCT` clause is optional:
12
+
13
+ ```ts
14
+ import { eq, from, omit, select, where } from 'qubu'
15
+
16
+ declare const userId: number | undefined
17
+
18
+ const query = select(
19
+ { id: users.id, name: users.name },
20
+ from(users),
21
+ userId === undefined ? omit : where(eq(users.id, userId))
22
+ )
23
+ ```
24
+
25
+ The ordinary ternary narrows `userId`, and the unused clause is never built.
26
+ Qubu removes `omit` before validating and ordering the remaining clauses.
27
+
28
+ Use the same token inside `and()`, `or()`, and `orderBy()` when individual
29
+ predicates or ordering terms are conditional:
30
+
31
+ ```ts
32
+ import { and, desc, eq, omit, orderBy, where } from 'qubu'
33
+
34
+ declare const includeName: boolean
35
+ declare const newestFirst: boolean
36
+
37
+ const filter = where(
38
+ and(eq(users.id, 7), includeName ? eq(users.name, 'Ada') : omit)
39
+ )
40
+ const ordering = orderBy(newestFirst ? desc(users.name) : omit)
41
+ ```
42
+
43
+ Each helper removes omitted members while retaining the source and grouping
44
+ requirements of every member that may be present. If no predicate remains,
45
+ `and()` or `or()` propagates `omit` through `where()` or `having()`; if no
46
+ ordering term remains, `orderBy()` propagates `omit` directly. The resulting
47
+ query emits no empty clause.
48
+
49
+ The same token can conditionally include a projection field:
50
+
51
+ ```ts
52
+ declare const includeEmail: boolean
53
+
54
+ const query = select(
55
+ {
56
+ id: users.id,
57
+ email: includeEmail ? users.email : omit,
58
+ },
59
+ from(users)
60
+ )
61
+
62
+ // typeof query.row:
63
+ // { id: number; email?: string | null }
64
+ ```
65
+
66
+ Here `omit` affects only whether `email` belongs to the projection. It does
67
+ not make the expression nullable: a non-nullable expression would produce
68
+ `email?: string`, while this nullable column produces `email?: string | null`.
69
+
70
+ This support is specific to boolean operand lists, query-level ordering terms,
71
+ and the complete clauses named above. Generic `sequence()` and
72
+ `commaSeparated()` collections do not discard `omit`. Clauses that provide
73
+ sources or change structural guarantees cannot be conditional. Pagination is
74
+ the exception: `offset()`, `fetchFirst()`, and `fetchNext()` can be paired
75
+ with `omit`, but a conditional row bound keeps the query's inferred
76
+ cardinality at `many`. Qubu still rejects `omit` branches paired with
77
+ `from()`, joins, `groupBy()`, correlation, CTEs, or custom clauses. Build
78
+ separate queries when those structural parts differ at runtime.
79
+
80
+ ## Handle `NULL` and empty lists deliberately
81
+
82
+ Equality with `null` is translated to the SQL null predicate:
83
+
84
+ ```ts
85
+ import { eq, isDistinctFrom, ne } from 'qubu'
86
+
87
+ eq(users.name, null) // ... IS NULL
88
+ ne(users.name, null) // ... IS NOT NULL
89
+ isDistinctFrom(users.name, null) // ... IS DISTINCT FROM ?
90
+ ```
91
+
92
+ Relational comparisons such as `gt(users.id, null)` are rejected because SQL
93
+ does not give them ordinary boolean comparison semantics. Use `isNull`,
94
+ `isNotNull`, or a distinctness predicate when that is the intended operation.
95
+
96
+ Empty membership lists remain valid and portable:
97
+
98
+ ```ts
99
+ inList(users.id, []) // (1 = 0)
100
+ notIn(users.id, []) // (1 = 1)
101
+ ```
102
+
103
+ ## Read next
104
+
105
+ - [Order and paginate](ordering-and-pagination.md) covers stable ordering and
106
+ optional row limits.
107
+ - [Group and rank rows](grouping-and-windows.md) covers aggregates and window
108
+ expressions.
109
+ - [Source scope](../../query-model/source-scope.md) explains why a column is
110
+ valid only after its source enters the query.
@@ -0,0 +1,82 @@
1
+ # Group and rank rows
2
+
3
+ > Use aggregates, grouping, and window expressions while preserving the dependencies and result types that make each expression valid.
4
+
5
+ The examples use the `users` and `posts` tables from [Build a
6
+ `SELECT`](overview.md).
7
+
8
+ ## Group and aggregate
9
+
10
+ Object projection keys provide stable names for aggregates. Group every
11
+ non-aggregate column dependency:
12
+
13
+ ```ts
14
+ import {
15
+ count,
16
+ desc,
17
+ eq,
18
+ from,
19
+ groupBy,
20
+ gt,
21
+ having,
22
+ leftJoin,
23
+ orderBy,
24
+ select,
25
+ } from 'qubu'
26
+
27
+ const counts = select(
28
+ {
29
+ name: users.name,
30
+ postCount: count(posts.id),
31
+ },
32
+ from(users),
33
+ leftJoin(posts, eq(users.id, posts.authorId)),
34
+ groupBy(users.name),
35
+ having(gt(count(posts.id), 0)),
36
+ orderBy(desc(count(posts.id)))
37
+ )
38
+ ```
39
+
40
+ `users.name` is grouped, while `posts.id` is consumed by `COUNT()`. The
41
+ same dependency rule applies to `HAVING` and grouped `ORDER BY` expressions.
42
+ A projection such as `{ email: users.email, postCount: count(posts.id) }` is
43
+ rejected unless `users.email` is grouped or is functionally determined by a
44
+ grouped primary or unique key declared in the table schema. Qubu uses only
45
+ explicit key metadata and keeps the proof within the source boundary. See
46
+ [Constraints, keys, and indexes](../../schema/constraints-and-indexes.md#use-key-metadata-for-grouped-queries).
47
+
48
+ ## Window functions
49
+
50
+ Use `over()` to attach an inline window specification to a typed expression.
51
+ The initial window scope supports `PARTITION BY` and `ORDER BY`; the same
52
+ `asc()` and `desc()` terms used by a query-level `orderBy()` can be reused:
53
+
54
+ ```ts
55
+ import { desc, from, over, rowNumber, select } from 'qubu'
56
+
57
+ const rankedUsers = select(
58
+ {
59
+ id: users.id,
60
+ rowNumber: over(rowNumber(), {
61
+ partitionBy: [users.name],
62
+ orderBy: [desc(users.id)],
63
+ }),
64
+ },
65
+ from(users)
66
+ )
67
+ ```
68
+
69
+ Window expressions remain ordinary expressions. They can be projected, aliased,
70
+ and passed to `orderBy()`. Their source requirements and result types are
71
+ retained through `over()`, and values rendered inside the window
72
+ specification remain parameters. Named windows and frame clauses are outside
73
+ the initial inline scope.
74
+
75
+ ## Read next
76
+
77
+ - [Order and paginate](ordering-and-pagination.md) covers stable ordering and
78
+ row limits.
79
+ - [Compose queries](../compose-queries.md) covers CTEs, derived tables, scalar
80
+ subqueries, and set operations.
81
+ - [Result shapes and cardinality](../../query-model/result-shapes.md) explains
82
+ the row and scalar types produced by nested queries.
@@ -0,0 +1,78 @@
1
+ # Order and paginate
2
+
3
+ > Choose a stable order and apply optional row limits while keeping SQL clause order and query cardinality visible.
4
+
5
+ The examples use the `users` table from [Build a `SELECT`](overview.md).
6
+
7
+ ## Order rows
8
+
9
+ Wrap ordering terms in `orderBy()`:
10
+
11
+ ```ts
12
+ import { desc, from, orderBy, select } from 'qubu'
13
+
14
+ const ordered = select(
15
+ { id: users.id, name: users.name },
16
+ from(users),
17
+ orderBy(desc(users.name))
18
+ )
19
+ ```
20
+
21
+ Use `asc()` or `desc()` for each term. The selected dialect controls the
22
+ identifier quoting and any dialect-specific ordering syntax.
23
+
24
+ ## Paginate results
25
+
26
+ Choose `fetchFirst()` or `offset()` for pagination:
27
+
28
+ ```ts
29
+ import {
30
+ desc,
31
+ eq,
32
+ fetchFirst,
33
+ from,
34
+ offset,
35
+ orderBy,
36
+ select,
37
+ where,
38
+ } from 'qubu'
39
+
40
+ const page = select(
41
+ { id: users.id, name: users.name },
42
+ from(users),
43
+ where(eq(users.id, 7)),
44
+ orderBy(desc(users.name)),
45
+ offset(20),
46
+ fetchFirst(20)
47
+ )
48
+ ```
49
+
50
+ The rendered clause order is still `FROM`, `WHERE`, `ORDER BY`, and
51
+ pagination. The active dialect decides whether pagination uses standard
52
+ `FETCH` syntax or a driver-specific `LIMIT` form.
53
+
54
+ Pair a pagination clause with `omit` when the row bound is optional at runtime:
55
+
56
+ ```ts
57
+ import { fetchFirst, omit } from 'qubu'
58
+
59
+ declare const pageSize: number | undefined
60
+
61
+ const page = select(
62
+ { id: users.id, name: users.name },
63
+ from(users),
64
+ pageSize === undefined ? omit : fetchFirst(pageSize)
65
+ )
66
+ ```
67
+
68
+ When `pageSize` is undefined, the query emits no pagination. Because the row
69
+ bound may be absent, conditional pagination does not narrow the query's
70
+ cardinality. An unconditional `fetchFirst(1)` retains its existing
71
+ `zero-or-one` inference.
72
+
73
+ ## Read next
74
+
75
+ - [Add optional conditions](conditions.md) covers conditional clauses and
76
+ null-aware predicates.
77
+ - [Group and rank rows](grouping-and-windows.md) covers aggregates and window
78
+ expressions.
@@ -0,0 +1,139 @@
1
+ # Build a `SELECT`
2
+
3
+ > Build a `SELECT` from typed tables, then inspect its sources, projection, joins, and result row.
4
+
5
+ ## Start with a projection and a source
6
+
7
+ The first argument to `select()` is the projection. The remaining arguments are
8
+ independent clauses. A source-aware column must be provided by `from()` or a
9
+ join:
10
+
11
+ ```ts
12
+ import { eq, from, integer, render, select, table, text, where } from 'qubu'
13
+
14
+ const users = table('users', {
15
+ id: integer(),
16
+ name: text(),
17
+ email: text({ nullable: true }),
18
+ })
19
+
20
+ const query = select(
21
+ { id: users.id, name: users.name },
22
+ from(users),
23
+ where(eq(users.id, 7))
24
+ )
25
+
26
+ render(query).text
27
+ // SELECT "users"."id" AS "id", "users"."name" AS "name" FROM "users" WHERE ("users"."id" = ?)
28
+ ```
29
+
30
+ An object projection uses its keys as result names. Name the fields you intend
31
+ to return in the usual case. Reserve `all(source)` for a result contract that
32
+ intentionally returns every source column. It returns the source's columns as a
33
+ named projection object, so it can still be spread alongside computed
34
+ expressions:
35
+
36
+ ```ts
37
+ import { all, from, select, upper } from 'qubu'
38
+
39
+ const query = select(
40
+ { ...all(users), normalizedName: upper(users.name) },
41
+ from(users)
42
+ )
43
+ ```
44
+
45
+ `all(source)` expands to explicit named columns rather than emitting
46
+ `source.*`. That keeps the SQL output and the inferred row keys aligned.
47
+
48
+ Named object projections keep the row shape visible at the selection site,
49
+ which is useful when the result is consumed by application code.
50
+
51
+ ## Add joins and predicates
52
+
53
+ Join functions add a source and make the join condition part of the same scope
54
+ check as the projection and `WHERE` clause:
55
+
56
+ ```ts
57
+ import {
58
+ count,
59
+ eq,
60
+ from,
61
+ innerJoin,
62
+ isNotNull,
63
+ leftJoin,
64
+ select,
65
+ table,
66
+ text,
67
+ integer,
68
+ where,
69
+ } from 'qubu'
70
+
71
+ const posts = table('posts', {
72
+ id: integer(),
73
+ authorId: integer(),
74
+ title: text(),
75
+ })
76
+
77
+ const query = select(
78
+ { userId: users.id, title: posts.title },
79
+ from(users),
80
+ innerJoin(posts, eq(users.id, posts.authorId)),
81
+ where(isNotNull(users.email))
82
+ )
83
+ ```
84
+
85
+ Use `innerJoin`, `leftJoin`, `rightJoin`, or `fullJoin` with an `ON`
86
+ condition. `crossJoin` and `naturalJoin` add a source without a condition;
87
+ use them only when that SQL behavior is intentional.
88
+
89
+ `leftJoin()` also carries nullability into the selected row. A column from the
90
+ joined source is nullable because the row may be missing, while an expression
91
+ with a deliberately non-nullable result such as `count()` remains non-null:
92
+
93
+ ```ts
94
+ const summary = select(
95
+ {
96
+ userName: users.name,
97
+ postTitle: posts.title,
98
+ postCount: count(posts.id),
99
+ },
100
+ from(users),
101
+ leftJoin(posts, eq(users.id, posts.authorId))
102
+ )
103
+
104
+ // typeof summary.row:
105
+ // { userName: string; postTitle: string | null; postCount: number }
106
+ ```
107
+
108
+ Compose boolean expressions explicitly:
109
+
110
+ ```ts
111
+ import { and, gt, inList, or } from 'qubu'
112
+
113
+ const filter = and(
114
+ gt(users.id, 0),
115
+ or(inList(users.id, [7, 8]), eq(users.name, 'Ada'))
116
+ )
117
+
118
+ const filtered = select(
119
+ { id: users.id, name: users.name },
120
+ from(users),
121
+ where(filter)
122
+ )
123
+ ```
124
+
125
+ Values such as `7`, `'Ada'`, and list members become parameters. They are not
126
+ interpolated into SQL.
127
+
128
+ ## Read next
129
+
130
+ - [Add optional conditions](conditions.md) covers conditional clauses, null
131
+ checks, and empty membership lists.
132
+ - [Order and paginate](ordering-and-pagination.md) covers stable ordering and
133
+ optional row limits.
134
+ - [Group and rank rows](grouping-and-windows.md) covers aggregates and window
135
+ expressions.
136
+ - [Source scope](../../query-model/source-scope.md) explains why each column
137
+ must come from a source in the query.
138
+ - [Result shapes and cardinality](../../query-model/result-shapes.md) explains
139
+ how joins and nested queries affect row types.