@sigitex/outlaw 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -5
- package/package.json +1 -1
- package/src/api/DatabaseTable.ts +38 -24
- package/src/api/DatabaseView.ts +36 -14
- package/src/api/api.types.ts +31 -79
- package/src/framework/Format.ts +4 -0
- package/src/framework/definitions.d.ts +4 -1
- package/src/queryBuilder/Mappings.ts +1 -1
- package/src/queryBuilder/Projection.ts +83 -0
- package/src/queryBuilder/QueryScope.ts +272 -0
- package/src/queryBuilder/QuerySource.ts +231 -0
- package/src/queryBuilder/QuerySourceBuilder.ts +76 -0
- package/src/queryBuilder/SelectBuilder.ts +14 -20
- package/src/queryBuilder/SelectQueryBuilder.ts +61 -72
- package/src/queryBuilder/index.ts +2 -0
- package/src/queryBuilder/queryBuilders.types.ts +21 -16
- package/src/queryGenerator/Clause.ts +36 -35
- package/src/queryGenerator/generateSelect.ts +38 -73
- package/src/schemaBuilder/ColumnRef.ts +36 -0
- package/src/schemaBuilder/columnBuilders.ts +7 -2
- package/src/schemaBuilder/createTable.ts +33 -26
- package/src/schemaBuilder/createView.ts +24 -12
- package/src/schemaBuilder/index.ts +1 -0
- package/src/schemaBuilder/metadata.ts +12 -6
- package/src/schemaBuilder/schemaBuilder.types.ts +57 -30
package/README.md
CHANGED
|
@@ -238,17 +238,182 @@ db.users.select("*")
|
|
|
238
238
|
|
|
239
239
|
### Joins
|
|
240
240
|
|
|
241
|
+
Compose sources **before** selecting results. Examples in this section use a
|
|
242
|
+
`Connection` named `connection` and these schema definitions:
|
|
243
|
+
|
|
241
244
|
```ts
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
const users = createTable("users", {
|
|
246
|
+
id: integer.primaryKey,
|
|
247
|
+
name: text.notNull,
|
|
248
|
+
managerId: integer,
|
|
249
|
+
})
|
|
250
|
+
const posts = createTable("posts", {
|
|
251
|
+
id: integer.primaryKey,
|
|
252
|
+
userId: integer.notNull,
|
|
253
|
+
tenantId: integer.notNull,
|
|
254
|
+
title: text.notNull,
|
|
255
|
+
published: integer.map.boolean.notNull,
|
|
256
|
+
createdAt: integer.map.timestamp.notNull,
|
|
257
|
+
})
|
|
258
|
+
const profiles = createTable("profiles", {
|
|
259
|
+
userId: integer.notNull,
|
|
260
|
+
tenantId: integer.notNull,
|
|
261
|
+
bio: text,
|
|
262
|
+
})
|
|
263
|
+
const db = createDatabase(connection, createSchema({ users, posts, profiles }))
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Strings work throughout joined queries. Bare names retain SQLite name resolution;
|
|
267
|
+
qualified strings identify a specific source. Only selected columns appear in the
|
|
268
|
+
result:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await db.users
|
|
272
|
+
.join(posts)
|
|
273
|
+
.on("users.id", "=", "posts.userId")
|
|
274
|
+
.select("name", "title")
|
|
275
|
+
.where("title", "Hello")
|
|
276
|
+
.orderBy([["name", "asc"]])
|
|
277
|
+
.limit(20)
|
|
278
|
+
.offset(40)
|
|
244
279
|
.fetch()
|
|
280
|
+
```
|
|
245
281
|
|
|
246
|
-
|
|
247
|
-
|
|
282
|
+
Refs and strings can be mixed. Projection aliases are optional; use them when
|
|
283
|
+
distinct output names are useful. `users.all` selects `users.*`, not every joined
|
|
284
|
+
column:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
await db.users
|
|
288
|
+
.leftJoin(posts)
|
|
289
|
+
.on(users.id, "=", posts.userId)
|
|
290
|
+
.select(users.id.as("userId"), posts.id.as("postId"), posts.title)
|
|
248
291
|
.fetch()
|
|
292
|
+
|
|
293
|
+
await db.users
|
|
294
|
+
.join(posts)
|
|
295
|
+
.on("users.id", "=", posts.userId)
|
|
296
|
+
.select(users.all, posts.title)
|
|
297
|
+
.where(posts.published, true)
|
|
298
|
+
.orderBy([[posts.createdAt, "desc"]])
|
|
299
|
+
.fetch()
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Join forms and constraints
|
|
303
|
+
|
|
304
|
+
`join`, `leftJoin`, `rightJoin`, `fullJoin`, and `crossJoin` each accept tables,
|
|
305
|
+
views, or SELECT subqueries. Constraints are optional for every form. Repeated
|
|
306
|
+
`on()` calls combine with `AND` on the latest join. `using()` accepts one or more
|
|
307
|
+
common column names and cannot be combined with `on()` on that same join.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
await db.users.rightJoin(posts).on(users.id, "=", posts.userId)
|
|
311
|
+
.select("name", "title").fetch()
|
|
312
|
+
await db.users.fullJoin(posts).on(users.id, "=", posts.userId)
|
|
313
|
+
.select("name", "title").fetch()
|
|
314
|
+
await db.users.crossJoin(posts).select("name", "title").fetch()
|
|
315
|
+
await db.users.join(posts).select("name", "title").fetch()
|
|
316
|
+
|
|
317
|
+
await db.users.crossJoin(posts)
|
|
318
|
+
.on(users.id, "=", posts.userId)
|
|
319
|
+
.on(users.name, "!=", posts.title)
|
|
320
|
+
.select("name", "title").fetch()
|
|
321
|
+
|
|
322
|
+
await db.posts.join(profiles).using("userId", "tenantId").select("*").fetch()
|
|
323
|
+
await db.posts.crossJoin(profiles).using("userId").select("userId").fetch()
|
|
324
|
+
await db.posts.fullJoin(profiles).using("userId").select("userId").fetch()
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Both operands of `on(left, operator, right)` are **column identifiers**, including
|
|
328
|
+
strings. By contrast, the right-hand value of `where(column, value)` or
|
|
329
|
+
`where(column, operator, value)` is always a **literal value**, even if it happens
|
|
330
|
+
to equal a column name. Literal-value ON expressions are not supported.
|
|
331
|
+
|
|
332
|
+
`select("*")` emits an actual `*`, preserving SQLite's common-column suppression
|
|
333
|
+
with `USING`. Qualified refs and wildcards retain each source's own columns.
|
|
334
|
+
Unqualified common keys from right/full joins include surviving right-side values.
|
|
335
|
+
For explicitly projected common columns coalesced by `FULL JOIN ... USING`, Outlaw
|
|
336
|
+
emits a same-name `AS` solely to prevent SQLite from retaining identifier quotes in
|
|
337
|
+
the output key. No source names or distinct output names are invented.
|
|
338
|
+
|
|
339
|
+
`RIGHT JOIN` and `FULL JOIN` require **SQLite 3.39.0 or newer**. Outlaw does not probe
|
|
340
|
+
capabilities, emulate unsupported joins, validate rows, or preflight SQL structure.
|
|
341
|
+
SQLite errors, including ambiguous bare identifiers, propagate from the connection.
|
|
342
|
+
`NATURAL` joins and comma-separated sources are deferred. Join verification uses
|
|
343
|
+
local Bun SQLite; no live D1 verification is claimed.
|
|
344
|
+
|
|
345
|
+
#### Self-joins, subqueries, and views
|
|
346
|
+
|
|
347
|
+
Source aliases are explicit and non-mutating:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const managers = users.as("managers")
|
|
351
|
+
await db.users.leftJoin(managers)
|
|
352
|
+
.on(users.managerId, "=", managers.id)
|
|
353
|
+
.select(users.name.as("employeeName"), managers.name.as("managerName"))
|
|
354
|
+
.orderBy([["employeeName", "asc"]])
|
|
355
|
+
.fetch()
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Subqueries expose only their projected outputs. Aliases remain optional; both
|
|
359
|
+
forms execute inline as part of one SELECT. Aliasing or embedding snapshots the
|
|
360
|
+
query, so later changes to the original builder do not alter an embedded query:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const publishedPosts = db.posts
|
|
364
|
+
.select("id", "userId", "title", "createdAt")
|
|
365
|
+
.where("published", true)
|
|
366
|
+
.as("publishedPosts")
|
|
367
|
+
|
|
368
|
+
await db.users.join(publishedPosts)
|
|
369
|
+
.on(users.id, "=", publishedPosts.userId)
|
|
370
|
+
.select("name", publishedPosts.title, publishedPosts.createdAt)
|
|
371
|
+
.fetch()
|
|
372
|
+
|
|
373
|
+
await db.users.join(db.posts.select("userId", "title"))
|
|
374
|
+
.on("users.id", "=", "userId")
|
|
375
|
+
.select("name", "title")
|
|
376
|
+
.fetch()
|
|
377
|
+
|
|
378
|
+
const userPosts = createView("user_posts",
|
|
379
|
+
users.leftJoin(posts).on(users.id, "=", posts.userId)
|
|
380
|
+
.select(users.name.as("userName"), posts.title),
|
|
381
|
+
)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Schema-level queries need no connection. Add `userPosts` to `createSchema` to query
|
|
385
|
+
the view through the database API; it exposes only `userName` and nullable `title`.
|
|
386
|
+
|
|
387
|
+
#### Result types and duplicate names
|
|
388
|
+
|
|
389
|
+
Results stay flat. Left joins add `null` to incoming columns; right joins add
|
|
390
|
+
`null` to all accumulated left sources; full joins add it to both sides. Selected
|
|
391
|
+
properties remain required, never optional. For example, selecting `users.name`
|
|
392
|
+
and `posts.title` after a left join yields `{ name: string; title: string | null }`.
|
|
393
|
+
|
|
394
|
+
Mappings follow the selected source and output alias, including through subqueries
|
|
395
|
+
and views. Outer-join SQL nulls stay null; mapped values decode once at execution.
|
|
396
|
+
Filters use the referenced source's storage mapping.
|
|
397
|
+
|
|
398
|
+
Duplicate output names are allowed without aliases, nesting, or automatic
|
|
399
|
+
renaming. The connection's object-row behavior determines which duplicate value
|
|
400
|
+
survives; ordinary collisions have conservative union types. Incompatible mapping
|
|
401
|
+
collisions stay undecoded and are typed as `unknown`. Explicit distinct aliases
|
|
402
|
+
preserve precise field types when both values are needed.
|
|
403
|
+
|
|
404
|
+
#### Migrating the old join chain
|
|
405
|
+
|
|
406
|
+
`db.users.select(...).join(posts).on(...)` is removed, including schema-level
|
|
407
|
+
join-after-select calls. Move `select(...)` after source composition:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
await db.users.join(posts).on(users.id, "=", posts.userId)
|
|
411
|
+
.select(users.name, posts.title).fetch()
|
|
249
412
|
```
|
|
250
413
|
|
|
251
|
-
|
|
414
|
+
Joined columns are no longer implicitly appended to the projection. Select the
|
|
415
|
+
outputs needed explicitly, or use `*`/qualified wildcards. Existing single-table
|
|
416
|
+
SELECT and unrelated INSERT/UPDATE/DELETE chains retain their interfaces.
|
|
252
417
|
|
|
253
418
|
### Insert
|
|
254
419
|
|
package/package.json
CHANGED
package/src/api/DatabaseTable.ts
CHANGED
|
@@ -1,36 +1,50 @@
|
|
|
1
1
|
// oxlint-disable typescript/no-explicit-any
|
|
2
|
-
import type {
|
|
3
|
-
Delete,
|
|
4
|
-
Connection,
|
|
5
|
-
Insert,
|
|
6
|
-
Select,
|
|
7
|
-
TableApi,
|
|
8
|
-
Update,
|
|
9
|
-
} from "./api.types"
|
|
2
|
+
import type { Delete, Connection, Insert, Update } from "./api.types"
|
|
10
3
|
import type { TableData } from "../schemaBuilder"
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export class DatabaseTable implements TableApi<any> {
|
|
19
|
-
private connection: Connection
|
|
4
|
+
import { DeleteBuilder, InsertBuilder, UpdateBuilder } from "../queryBuilder"
|
|
5
|
+
import { QuerySourceBuilder } from "../queryBuilder/QuerySourceBuilder"
|
|
6
|
+
import type { QuerySource } from "../queryBuilder/QuerySource"
|
|
7
|
+
import type { Projection } from "../queryBuilder/Projection"
|
|
8
|
+
|
|
9
|
+
export class DatabaseTable {
|
|
10
|
+
private readonly connection: Connection
|
|
20
11
|
private table: TableData
|
|
12
|
+
private readonly source: QuerySource.Data
|
|
21
13
|
|
|
22
14
|
constructor(connection: Connection, table: TableData) {
|
|
23
15
|
this.connection = connection
|
|
24
16
|
this.table = table
|
|
17
|
+
this.source = { kind: "table", name: table.name, tableData: table }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
select(...columns: Projection.Input[]) {
|
|
21
|
+
return new QuerySourceBuilder(this.source, this.connection).select(
|
|
22
|
+
...columns,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
join(target: QuerySource.Input) {
|
|
27
|
+
return new QuerySourceBuilder(this.source, this.connection).join(target)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
leftJoin(target: QuerySource.Input) {
|
|
31
|
+
return new QuerySourceBuilder(this.source, this.connection).leftJoin(target)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
rightJoin(target: QuerySource.Input) {
|
|
35
|
+
return new QuerySourceBuilder(this.source, this.connection).rightJoin(
|
|
36
|
+
target,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fullJoin(target: QuerySource.Input) {
|
|
41
|
+
return new QuerySourceBuilder(this.source, this.connection).fullJoin(target)
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
select(...columns: ("*" | string)[]): Select<any, any> {
|
|
32
|
-
const selected = columns[0] === "*" || columns.length === 0 ? "*" : columns
|
|
33
|
-
return new SelectBuilder(this.connection, this.table, selected)
|
|
44
|
+
crossJoin(target: QuerySource.Input) {
|
|
45
|
+
return new QuerySourceBuilder(this.source, this.connection).crossJoin(
|
|
46
|
+
target,
|
|
47
|
+
)
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
insert(...rows: [Partial<any>, ...Partial<any>[]]): Insert<any, any, number>
|
package/src/api/DatabaseView.ts
CHANGED
|
@@ -1,23 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
import type { Connection, Select, ViewApi } from "./api.types"
|
|
1
|
+
import type { Connection } from "./api.types"
|
|
3
2
|
import type { TableData } from "../schemaBuilder"
|
|
4
|
-
import {
|
|
3
|
+
import { QuerySourceBuilder } from "../queryBuilder/QuerySourceBuilder"
|
|
4
|
+
import type { QuerySource } from "../queryBuilder/QuerySource"
|
|
5
|
+
import type { Projection } from "../queryBuilder/Projection"
|
|
5
6
|
|
|
6
|
-
export class DatabaseView
|
|
7
|
-
private connection: Connection
|
|
8
|
-
private
|
|
7
|
+
export class DatabaseView {
|
|
8
|
+
private readonly connection: Connection
|
|
9
|
+
private readonly source: QuerySource.Data
|
|
9
10
|
|
|
10
11
|
constructor(connection: Connection, tableData: TableData) {
|
|
11
12
|
this.connection = connection
|
|
12
|
-
this.
|
|
13
|
+
this.source = { kind: "table", name: tableData.name, tableData }
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
select(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
select(...columns: Projection.Input[]) {
|
|
17
|
+
return new QuerySourceBuilder(this.source, this.connection).select(
|
|
18
|
+
...columns,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
join(target: QuerySource.Input) {
|
|
23
|
+
return new QuerySourceBuilder(this.source, this.connection).join(target)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
leftJoin(target: QuerySource.Input) {
|
|
27
|
+
return new QuerySourceBuilder(this.source, this.connection).leftJoin(target)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rightJoin(target: QuerySource.Input) {
|
|
31
|
+
return new QuerySourceBuilder(this.source, this.connection).rightJoin(
|
|
32
|
+
target,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fullJoin(target: QuerySource.Input) {
|
|
37
|
+
return new QuerySourceBuilder(this.source, this.connection).fullJoin(target)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
crossJoin(target: QuerySource.Input) {
|
|
41
|
+
return new QuerySourceBuilder(this.source, this.connection).crossJoin(
|
|
42
|
+
target,
|
|
43
|
+
)
|
|
22
44
|
}
|
|
23
45
|
}
|
package/src/api/api.types.ts
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
// oxlint-disable typescript/consistent-type-definitions
|
|
4
4
|
import type { BINARY_OPERATORS, UNARY_OPERATORS } from "../queryBuilder"
|
|
5
5
|
import type {
|
|
6
|
-
BuildTable,
|
|
7
|
-
BuildView,
|
|
8
|
-
ColumnRef,
|
|
9
6
|
InferColumn,
|
|
10
7
|
SchemaMembers,
|
|
11
8
|
TablesOf,
|
|
12
9
|
ViewsOf,
|
|
13
10
|
} from "../schemaBuilder"
|
|
11
|
+
import type { QueryScope } from "../queryBuilder/QueryScope"
|
|
14
12
|
|
|
15
13
|
// Utilities
|
|
16
14
|
|
|
@@ -27,8 +25,9 @@ export type InsertRecord<R> = Partial<R> &
|
|
|
27
25
|
|
|
28
26
|
// Deconstruction
|
|
29
27
|
|
|
30
|
-
export type ColumnsOf<BT> =
|
|
31
|
-
|
|
28
|
+
export type ColumnsOf<BT> = BT extends { readonly $columns: infer Columns }
|
|
29
|
+
? Columns
|
|
30
|
+
: never
|
|
32
31
|
|
|
33
32
|
export type ColumnTypesOf<BCS> = {
|
|
34
33
|
readonly [BCK in keyof BCS]: InferColumn<BCS[BCK]>
|
|
@@ -64,9 +63,15 @@ export type DatabaseApi<
|
|
|
64
63
|
M extends SchemaMembers,
|
|
65
64
|
C extends Connection = Connection,
|
|
66
65
|
> = {
|
|
67
|
-
readonly [K in keyof TablesOf<M>]: TableApi<
|
|
66
|
+
readonly [K in keyof TablesOf<M>]: TableApi<
|
|
67
|
+
ColumnsOf<TablesOf<M>[K]>,
|
|
68
|
+
QueryScope.Of<TablesOf<M>[K]>["name"]
|
|
69
|
+
>
|
|
68
70
|
} & {
|
|
69
|
-
readonly [K in keyof ViewsOf<M>]: ViewApi<
|
|
71
|
+
readonly [K in keyof ViewsOf<M>]: ViewApi<
|
|
72
|
+
QueryScope.Of<ViewsOf<M>[K]>["columns"],
|
|
73
|
+
QueryScope.Of<ViewsOf<M>[K]>["name"]
|
|
74
|
+
>
|
|
70
75
|
} & {
|
|
71
76
|
readonly connection: C
|
|
72
77
|
readonly transaction: <Result>(
|
|
@@ -76,25 +81,23 @@ export type DatabaseApi<
|
|
|
76
81
|
) => Promise<Result>
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
export type ColumnsOfView<BV> =
|
|
80
|
-
BV extends BuildView<infer Columns> ? Columns : never
|
|
84
|
+
export type ColumnsOfView<BV> = QueryScope.Of<BV>["columns"]
|
|
81
85
|
|
|
82
|
-
export type ViewApi<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
86
|
+
export type ViewApi<
|
|
87
|
+
Columns extends QueryScope.Columns,
|
|
88
|
+
Name extends string = string,
|
|
89
|
+
> = QueryScope.Composition<
|
|
90
|
+
QueryScope.Initial<QueryScope.State<Name, Columns>>,
|
|
91
|
+
true
|
|
92
|
+
>
|
|
90
93
|
|
|
91
|
-
export type TableApi<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
export type TableApi<
|
|
95
|
+
Columns,
|
|
96
|
+
Name extends string = string,
|
|
97
|
+
> = QueryScope.Composition<
|
|
98
|
+
QueryScope.Initial<QueryScope.State<Name, QueryScope.SchemaColumns<Columns>>>,
|
|
99
|
+
true
|
|
100
|
+
> & {
|
|
98
101
|
/** Issue an `INSERT` statement. */
|
|
99
102
|
insert<InsertColumns extends Partial<ColumnTypesOf<Columns>>>(
|
|
100
103
|
...rows: [InsertColumns, ...InsertColumns[]]
|
|
@@ -125,61 +128,10 @@ export interface HasWhereClause<Columns> {
|
|
|
125
128
|
): this
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
> extends HasWhereClause<Columns> {
|
|
133
|
-
/** Issue the query, expecting an array of results. */
|
|
134
|
-
fetch(): Promise<ColumnTypesOf<SelectColumns>[]>
|
|
135
|
-
/** Issue the query, returning a single result, or throwing.. */
|
|
136
|
-
first(): Promise<ColumnTypesOf<SelectColumns>>
|
|
137
|
-
/** Add a `LIMIT` clause. */
|
|
138
|
-
limit(n: number): this
|
|
139
|
-
/** Add an `OFFSET` clause. */
|
|
140
|
-
offset(n: number): this
|
|
141
|
-
/** Add an `ORDER BY` sort expression. */
|
|
142
|
-
orderBy(sorts: [keyof Columns, "asc" | "desc"][]): this
|
|
143
|
-
|
|
144
|
-
/** Inner join on a table. */
|
|
145
|
-
join<JC>(
|
|
146
|
-
table: BuildTable<JC>,
|
|
147
|
-
): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
|
|
148
|
-
/** Inner join on a subquery. */
|
|
149
|
-
join<SC>(
|
|
150
|
-
query: Select<SC, any>,
|
|
151
|
-
): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
|
|
152
|
-
|
|
153
|
-
/** Left join on a table — joined columns become nullable. */
|
|
154
|
-
leftJoin<JC>(
|
|
155
|
-
table: BuildTable<JC>,
|
|
156
|
-
): Select<SelectColumns & Partial<ColumnTypesOf<JC>>, Columns & JC>
|
|
157
|
-
/** Left join on a subquery — joined columns become nullable. */
|
|
158
|
-
leftJoin<SC>(
|
|
159
|
-
query: Select<SC, any>,
|
|
160
|
-
): Select<SelectColumns & Partial<ColumnTypesOf<SC>>, Columns & SC>
|
|
161
|
-
|
|
162
|
-
/** Right join on a table. */
|
|
163
|
-
rightJoin<JC>(
|
|
164
|
-
table: BuildTable<JC>,
|
|
165
|
-
): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
|
|
166
|
-
/** Right join on a subquery. */
|
|
167
|
-
rightJoin<SC>(
|
|
168
|
-
query: Select<SC, any>,
|
|
169
|
-
): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
|
|
170
|
-
|
|
171
|
-
/** Cross join on a table. */
|
|
172
|
-
crossJoin<JC>(
|
|
173
|
-
table: BuildTable<JC>,
|
|
174
|
-
): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
|
|
175
|
-
/** Cross join on a subquery. */
|
|
176
|
-
crossJoin<SC>(
|
|
177
|
-
query: Select<SC, any>,
|
|
178
|
-
): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
|
|
179
|
-
|
|
180
|
-
/** Add an ON condition to the most recent join. */
|
|
181
|
-
on(left: ColumnRef, operator: BinaryOperator, right: ColumnRef): this
|
|
182
|
-
}
|
|
131
|
+
export type Select<
|
|
132
|
+
SelectColumns extends QueryScope.Columns,
|
|
133
|
+
Scope extends QueryScope,
|
|
134
|
+
> = QueryScope.Selection<SelectColumns, Scope, true>
|
|
183
135
|
|
|
184
136
|
/** Insert statement API. */
|
|
185
137
|
export interface Insert<InsertColumns, Columns, Returning> {
|
package/src/framework/Format.ts
CHANGED
|
@@ -11,6 +11,10 @@ export namespace Format {
|
|
|
11
11
|
return SqlString.escapeId(name)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function identifier(name: string) {
|
|
15
|
+
return SqlString.escapeId(name, true)
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export function number(number: number) {
|
|
15
19
|
if (typeof number !== "number" && typeof number !== "bigint") {
|
|
16
20
|
throw new Error("Not a number.")
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
declare module "sqlstring-sqlite" {
|
|
2
2
|
export function escape(text: string | null | undefined): string
|
|
3
|
-
export function escapeId(
|
|
3
|
+
export function escapeId(
|
|
4
|
+
text: string | null | undefined,
|
|
5
|
+
forbidQualified?: boolean,
|
|
6
|
+
): string
|
|
4
7
|
}
|
|
@@ -41,7 +41,7 @@ export namespace Mappings {
|
|
|
41
41
|
return rows.map((row) => {
|
|
42
42
|
const mapped: Record<string, unknown> = { ...row }
|
|
43
43
|
for (const [name, mapping] of mappings) {
|
|
44
|
-
if (name in mapped) {
|
|
44
|
+
if (name in mapped && mapped[name] !== null) {
|
|
45
45
|
mapped[name] = mapping.from(mapped[name])
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ColumnRef } from "../schemaBuilder/ColumnRef"
|
|
2
|
+
import type { ColumnData } from "../schemaBuilder/metadata"
|
|
3
|
+
import type { ColumnIdentifier, SelectQuery } from "./queryBuilders.types"
|
|
4
|
+
import { QuerySource } from "./QuerySource"
|
|
5
|
+
|
|
6
|
+
export type Projection =
|
|
7
|
+
| "*"
|
|
8
|
+
| { column: ColumnIdentifier; alias?: string }
|
|
9
|
+
| ColumnRef.Wildcard
|
|
10
|
+
|
|
11
|
+
export namespace Projection {
|
|
12
|
+
export type Input =
|
|
13
|
+
| "*"
|
|
14
|
+
| string
|
|
15
|
+
| ColumnRef
|
|
16
|
+
| ColumnRef.Aliased<string, string, string>
|
|
17
|
+
| ColumnRef.Wildcard
|
|
18
|
+
|
|
19
|
+
export function create(scope: QuerySource.Scope, input: Input): Projection {
|
|
20
|
+
if (input === "*") {
|
|
21
|
+
return input
|
|
22
|
+
}
|
|
23
|
+
if (typeof input === "object" && "wildcard" in input) {
|
|
24
|
+
return { ...input }
|
|
25
|
+
}
|
|
26
|
+
const column = QuerySource.identifier(scope, input)
|
|
27
|
+
const alias =
|
|
28
|
+
typeof input === "object" && "alias" in input
|
|
29
|
+
? input.alias
|
|
30
|
+
: column.table === undefined && scope.coalesced.has(column.column)
|
|
31
|
+
? column.column
|
|
32
|
+
: undefined
|
|
33
|
+
return {
|
|
34
|
+
column,
|
|
35
|
+
...(alias !== undefined ? { alias } : {}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function columns(query: SelectQuery): ColumnData[] {
|
|
40
|
+
const scope = QuerySource.scope(query)
|
|
41
|
+
const selected = query.selected.flatMap((projection) => {
|
|
42
|
+
if (projection === "*") {
|
|
43
|
+
return scope.output
|
|
44
|
+
}
|
|
45
|
+
if ("wildcard" in projection) {
|
|
46
|
+
return scope.sources
|
|
47
|
+
.filter(
|
|
48
|
+
(source) => QuerySource.qualifier(source) === projection.table,
|
|
49
|
+
)
|
|
50
|
+
.flatMap((source) => source.tableData.columns)
|
|
51
|
+
}
|
|
52
|
+
return QuerySource.resolve(scope, projection.column).map((column) =>
|
|
53
|
+
renameColumn(column, projection.alias ?? column.name),
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
const outputs = new Map<string, ColumnData[]>()
|
|
57
|
+
for (const column of selected) {
|
|
58
|
+
const existing = outputs.get(column.name)
|
|
59
|
+
if (existing) {
|
|
60
|
+
existing.push(column)
|
|
61
|
+
} else {
|
|
62
|
+
outputs.set(column.name, [column])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Array.from(outputs.values(), merge)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function merge(columns: ColumnData[]): ColumnData {
|
|
69
|
+
const first = columns[0]
|
|
70
|
+
return {
|
|
71
|
+
...first,
|
|
72
|
+
notNull: columns.every((column) => column.notNull || !!column.primaryKey),
|
|
73
|
+
primaryKey: undefined,
|
|
74
|
+
mapping: columns.every((column) => column.mapping === first.mapping)
|
|
75
|
+
? first.mapping
|
|
76
|
+
: undefined,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renameColumn(column: ColumnData, name: string): ColumnData {
|
|
81
|
+
return { ...column, name }
|
|
82
|
+
}
|
|
83
|
+
}
|