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.
- package/dist/canonical-BbnqavJm.mjs +972 -0
- package/dist/codegen.d.mts +107 -0
- package/dist/codegen.mjs +1199 -0
- package/dist/column-CXMxx8Hq.mjs +118 -0
- package/dist/complete-D5Djh-zo.mjs +1788 -0
- package/dist/complete-types-BdFqUfbb.d.mts +371 -0
- package/dist/core.d.mts +33 -0
- package/dist/core.mjs +36 -0
- package/dist/ddl.d.mts +118 -0
- package/dist/ddl.mjs +1120 -0
- package/dist/dialect-b2-Z6uBF.mjs +13 -0
- package/dist/diff.d.mts +2 -0
- package/dist/diff.mjs +883 -0
- package/dist/drizzle-mysql.d.mts +24 -0
- package/dist/drizzle-mysql.mjs +72 -0
- package/dist/drizzle-postgres.d.mts +24 -0
- package/dist/drizzle-postgres.mjs +73 -0
- package/dist/drizzle-sqlite.d.mts +24 -0
- package/dist/drizzle-sqlite.mjs +82 -0
- package/dist/drizzle.d.mts +13 -0
- package/dist/drizzle.mjs +2 -0
- package/dist/errors-BGCoLe_r.mjs +14 -0
- package/dist/errors-Dxv73YJu.mjs +26 -0
- package/dist/explain-CkIK13L_.mjs +98 -0
- package/dist/index-Ds7-mhJi.d.mts +146 -0
- package/dist/index-Dug5HnLB.d.mts +204 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1276 -0
- package/dist/introspection.d.mts +117 -0
- package/dist/introspection.mjs +4341 -0
- package/dist/json-CUZlv4HT.mjs +169 -0
- package/dist/migration.d.mts +215 -0
- package/dist/migration.mjs +1161 -0
- package/dist/mysql-DqkqXB6A.mjs +355 -0
- package/dist/mysql.d.mts +5 -0
- package/dist/mysql.mjs +50 -0
- package/dist/naming-QVCOnSj2.mjs +20 -0
- package/dist/omit-OxV58AwX.mjs +5 -0
- package/dist/on-conflict-BxnxubMb.mjs +143 -0
- package/dist/postgres-DEBBeh52.mjs +235 -0
- package/dist/postgres.d.mts +123 -0
- package/dist/postgres.mjs +52 -0
- package/dist/registry-BufIskVN.mjs +569 -0
- package/dist/relational-DCZrrNia.mjs +77 -0
- package/dist/runtime-BTr-MTlo.mjs +193 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +5 -0
- package/dist/serialize-PF1cfH2P.mjs +556 -0
- package/dist/snapshot-CWPgzxNx.mjs +392 -0
- package/dist/snapshot.d.mts +4 -0
- package/dist/snapshot.mjs +8 -0
- package/dist/source-DUoJVXmL.mjs +68 -0
- package/dist/sqlite-BU6DBxef.mjs +320 -0
- package/dist/sqlite.d.mts +5 -0
- package/dist/sqlite.mjs +48 -0
- package/dist/standard-BTVYKh_F.mjs +12 -0
- package/dist/table-llv9tsZ8.mjs +384 -0
- package/dist/types-4Q076HKo.d.mts +3636 -0
- package/dist/types-BX0mckiU.d.mts +644 -0
- package/dist/types-Cec0xzo4.mjs +116 -0
- package/dist/types-H4vyCw8_.d.mts +45 -0
- package/dist/value-BvilP0oz.mjs +29 -0
- package/dist/vite/ambient.d.ts +449 -0
- package/dist/vite.d.mts +35 -0
- package/dist/vite.mjs +498 -0
- package/docs/config.json +21 -0
- package/docs/dialects-and-execution.md +427 -0
- package/docs/getting-started.md +96 -0
- package/docs/guides/compose-queries.md +225 -0
- package/docs/guides/drizzle.md +130 -0
- package/docs/guides/extensions/dialects.md +40 -0
- package/docs/guides/extensions/overview.md +36 -0
- package/docs/guides/extensions/sources-and-clauses.md +82 -0
- package/docs/guides/extensions/typed-expressions.md +94 -0
- package/docs/guides/extensions/unsafe-syntax.md +27 -0
- package/docs/guides/json.md +65 -0
- package/docs/guides/mutations.md +138 -0
- package/docs/guides/select/conditions.md +110 -0
- package/docs/guides/select/grouping-and-windows.md +82 -0
- package/docs/guides/select/ordering-and-pagination.md +78 -0
- package/docs/guides/select/overview.md +139 -0
- package/docs/guides/sql-templates.md +157 -0
- package/docs/guides/vite-plugin.md +92 -0
- package/docs/index.md +111 -0
- package/docs/query-model/fragments.md +109 -0
- package/docs/query-model/result-shapes.md +125 -0
- package/docs/query-model/source-scope.md +182 -0
- package/docs/reference/introspection-support.md +196 -0
- package/docs/reference/mysql-snapshot.md +55 -0
- package/docs/reference/postgres-snapshot.md +61 -0
- package/docs/reference/sqlite-snapshot.md +53 -0
- package/docs/reference/supported-surface.md +104 -0
- package/docs/schema/catalog-model.md +58 -0
- package/docs/schema/code-generation.md +139 -0
- package/docs/schema/columns-and-writes.md +130 -0
- package/docs/schema/constraints-and-indexes.md +180 -0
- package/docs/schema/ddl-emission.md +75 -0
- package/docs/schema/diff.md +75 -0
- package/docs/schema/introspection.md +214 -0
- package/docs/schema/migration-plans.md +76 -0
- package/docs/schema/snapshots.md +93 -0
- package/docs/schema/storage-and-schema-sql.md +104 -0
- package/docs/schema/tables-and-names.md +106 -0
- package/docs/sql-semantic-types.md +141 -0
- package/docs/troubleshooting.md +151 -0
- package/package.json +132 -10
- package/skills/qubu/SKILL.md +42 -0
- package/skills/qubu/agents/openai.yaml +6 -0
- 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.
|