@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { BinaryOperator, UnaryOperator } from "../api/api.types"
|
|
2
|
+
import type { ColumnRef } from "../schemaBuilder/ColumnRef"
|
|
3
|
+
import type { InferColumn } from "../schemaBuilder/schemaBuilder.types"
|
|
4
|
+
import type { QuerySource } from "./QuerySource"
|
|
5
|
+
import type { JoinType } from "./queryBuilders.types"
|
|
6
|
+
|
|
7
|
+
export type QueryScope = {
|
|
8
|
+
sources: readonly QueryScope.State[]
|
|
9
|
+
output: QueryScope.Entry
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export namespace QueryScope {
|
|
13
|
+
export type Column = { value: unknown; mapped: boolean }
|
|
14
|
+
export type Columns = Record<string, Column>
|
|
15
|
+
export type State<
|
|
16
|
+
Name extends string = string,
|
|
17
|
+
Fields extends Columns = Columns,
|
|
18
|
+
> = {
|
|
19
|
+
name: Name
|
|
20
|
+
columns: Fields
|
|
21
|
+
}
|
|
22
|
+
export type Entry = {
|
|
23
|
+
name: string
|
|
24
|
+
value: unknown
|
|
25
|
+
mapped: boolean
|
|
26
|
+
origin: string
|
|
27
|
+
}
|
|
28
|
+
export type SchemaColumns<Fields> = {
|
|
29
|
+
[Key in keyof Fields & string]: {
|
|
30
|
+
value: InferColumn<Fields[Key]>
|
|
31
|
+
mapped: Fields[Key] extends { readonly $defined?: infer Defined }
|
|
32
|
+
? "map" extends Defined
|
|
33
|
+
? true
|
|
34
|
+
: false
|
|
35
|
+
: false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export type Of<Target> = Target extends {
|
|
39
|
+
readonly $type: infer Shape extends State
|
|
40
|
+
}
|
|
41
|
+
? Shape
|
|
42
|
+
: never
|
|
43
|
+
export type Initial<Shape extends State> = {
|
|
44
|
+
sources: [Shape]
|
|
45
|
+
output: Entries<Shape>
|
|
46
|
+
}
|
|
47
|
+
export type Entries<Shape extends State> = {
|
|
48
|
+
[Key in keyof Shape["columns"] & string]: {
|
|
49
|
+
value: Shape["columns"][Key]["value"]
|
|
50
|
+
mapped: Shape["columns"][Key]["mapped"]
|
|
51
|
+
name: Key
|
|
52
|
+
origin: `${Shape["name"]}.${Key}`
|
|
53
|
+
}
|
|
54
|
+
}[keyof Shape["columns"] & string]
|
|
55
|
+
|
|
56
|
+
export type Identifier<Scope extends QueryScope> =
|
|
57
|
+
| Scope["output"]["name"]
|
|
58
|
+
| SourceIdentifiers<Scope["sources"][number]>
|
|
59
|
+
export type Projection<Scope extends QueryScope> =
|
|
60
|
+
| Identifier<Scope>
|
|
61
|
+
| "*"
|
|
62
|
+
| Wildcards<Scope["sources"][number]>
|
|
63
|
+
export type Resolve<Scope extends QueryScope, Input> = Input extends {
|
|
64
|
+
table: infer Table
|
|
65
|
+
column: infer Key
|
|
66
|
+
}
|
|
67
|
+
? Qualified<Scope, Table, Key>
|
|
68
|
+
: Input extends Scope["output"]["name"]
|
|
69
|
+
? Extract<Scope["output"], { name: Input }>
|
|
70
|
+
: Input extends `${infer Table}.${infer Key}`
|
|
71
|
+
? Qualified<Scope, Table, Key>
|
|
72
|
+
: never
|
|
73
|
+
|
|
74
|
+
export type Project<
|
|
75
|
+
Scope extends QueryScope,
|
|
76
|
+
Inputs extends readonly unknown[],
|
|
77
|
+
> = Collapse<
|
|
78
|
+
Inputs extends readonly []
|
|
79
|
+
? Scope["output"]
|
|
80
|
+
: ProjectEntry<Scope, Inputs[number]>
|
|
81
|
+
>
|
|
82
|
+
export type Values<Fields extends Columns> = {
|
|
83
|
+
[Key in keyof Fields]: Fields[Key]["value"]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type Join<
|
|
87
|
+
Scope extends QueryScope,
|
|
88
|
+
Right extends State,
|
|
89
|
+
Kind extends JoinType,
|
|
90
|
+
> = {
|
|
91
|
+
sources: [
|
|
92
|
+
...ExtendSources<Scope["sources"], Kind>,
|
|
93
|
+
ExtendRight<Right, Kind>,
|
|
94
|
+
]
|
|
95
|
+
output:
|
|
96
|
+
| ExtendLeft<Scope["output"], Kind>
|
|
97
|
+
| Entries<ExtendRight<Right, Kind>>
|
|
98
|
+
}
|
|
99
|
+
export type Using<
|
|
100
|
+
Scope extends QueryScope,
|
|
101
|
+
Right extends State,
|
|
102
|
+
Kind extends JoinType,
|
|
103
|
+
Keys extends string,
|
|
104
|
+
> = {
|
|
105
|
+
sources: Join<Scope, Right, Kind>["sources"]
|
|
106
|
+
output:
|
|
107
|
+
| Exclude<Join<Scope, Right, Kind>["output"], { name: Keys }>
|
|
108
|
+
| Common<Scope, Right, Kind, Keys>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type Composition<
|
|
112
|
+
Scope extends QueryScope,
|
|
113
|
+
Executable extends boolean = false,
|
|
114
|
+
> = {
|
|
115
|
+
select<const Inputs extends readonly Projection<Scope>[]>(
|
|
116
|
+
...columns: Inputs
|
|
117
|
+
): Selection<Project<Scope, Inputs>, Scope, Executable>
|
|
118
|
+
join<Target extends QuerySource.Target>(
|
|
119
|
+
target: Target,
|
|
120
|
+
): Joined<Scope, Of<Target>, "join", Executable>
|
|
121
|
+
leftJoin<Target extends QuerySource.Target>(
|
|
122
|
+
target: Target,
|
|
123
|
+
): Joined<Scope, Of<Target>, "left join", Executable>
|
|
124
|
+
rightJoin<Target extends QuerySource.Target>(
|
|
125
|
+
target: Target,
|
|
126
|
+
): Joined<Scope, Of<Target>, "right join", Executable>
|
|
127
|
+
fullJoin<Target extends QuerySource.Target>(
|
|
128
|
+
target: Target,
|
|
129
|
+
): Joined<Scope, Of<Target>, "full join", Executable>
|
|
130
|
+
crossJoin<Target extends QuerySource.Target>(
|
|
131
|
+
target: Target,
|
|
132
|
+
): Joined<Scope, Of<Target>, "cross join", Executable>
|
|
133
|
+
}
|
|
134
|
+
export type Joined<
|
|
135
|
+
Left extends QueryScope,
|
|
136
|
+
Right extends State,
|
|
137
|
+
Kind extends JoinType,
|
|
138
|
+
Executable extends boolean,
|
|
139
|
+
Constraint extends "none" | "on" = "none",
|
|
140
|
+
> = Composition<Join<Left, Right, Kind>, Executable> & {
|
|
141
|
+
on(
|
|
142
|
+
left: Identifier<Join<Left, Right, Kind>>,
|
|
143
|
+
operator: BinaryOperator,
|
|
144
|
+
right: Identifier<Join<Left, Right, Kind>>,
|
|
145
|
+
): Joined<Left, Right, Kind, Executable, "on">
|
|
146
|
+
} & (Constraint extends "none"
|
|
147
|
+
? {
|
|
148
|
+
using<
|
|
149
|
+
const Keys extends readonly [
|
|
150
|
+
Left["output"]["name"] & keyof Right["columns"] & string,
|
|
151
|
+
...(Left["output"]["name"] & keyof Right["columns"] & string)[],
|
|
152
|
+
],
|
|
153
|
+
>(
|
|
154
|
+
...columns: Keys
|
|
155
|
+
): Composition<Using<Left, Right, Kind, Keys[number]>, Executable>
|
|
156
|
+
}
|
|
157
|
+
: {})
|
|
158
|
+
|
|
159
|
+
export type Selection<
|
|
160
|
+
Fields extends Columns,
|
|
161
|
+
Scope extends QueryScope,
|
|
162
|
+
Executable extends boolean = false,
|
|
163
|
+
> = {
|
|
164
|
+
readonly $type: State<"", Fields>
|
|
165
|
+
as<Name extends string>(name: Name): QuerySource<Name, Fields>
|
|
166
|
+
where<const Input extends Identifier<Scope>>(
|
|
167
|
+
column: Input,
|
|
168
|
+
value: Value<Resolve<Scope, NoInfer<Input>>> | UnaryOperator,
|
|
169
|
+
): Selection<Fields, Scope, Executable>
|
|
170
|
+
where<const Input extends Identifier<Scope>>(
|
|
171
|
+
column: Input,
|
|
172
|
+
operator: BinaryOperator,
|
|
173
|
+
value: Value<Resolve<Scope, NoInfer<Input>>>,
|
|
174
|
+
): Selection<Fields, Scope, Executable>
|
|
175
|
+
orderBy(
|
|
176
|
+
sorts: readonly (readonly [
|
|
177
|
+
Identifier<Scope> | (keyof Fields & string),
|
|
178
|
+
"asc" | "desc",
|
|
179
|
+
])[],
|
|
180
|
+
): Selection<Fields, Scope, Executable>
|
|
181
|
+
limit(count: number): Selection<Fields, Scope, Executable>
|
|
182
|
+
offset(count: number): Selection<Fields, Scope, Executable>
|
|
183
|
+
} & (Executable extends true
|
|
184
|
+
? {
|
|
185
|
+
fetch(): Promise<Values<Fields>[]>
|
|
186
|
+
first(): Promise<Values<Fields>>
|
|
187
|
+
}
|
|
188
|
+
: {})
|
|
189
|
+
|
|
190
|
+
type SourceIdentifiers<Shape extends State> = Shape extends State
|
|
191
|
+
? {
|
|
192
|
+
[Key in keyof Shape["columns"] & string]:
|
|
193
|
+
| ColumnRef<Shape["name"], Key>
|
|
194
|
+
| (Shape["name"] extends "" ? never : `${Shape["name"]}.${Key}`)
|
|
195
|
+
}[keyof Shape["columns"] & string]
|
|
196
|
+
: never
|
|
197
|
+
type Wildcards<Shape extends State> = Shape extends State
|
|
198
|
+
? ColumnRef.Wildcard<Shape["name"]>
|
|
199
|
+
: never
|
|
200
|
+
type Qualified<Scope extends QueryScope, Name, Key> =
|
|
201
|
+
Entries<
|
|
202
|
+
Extract<Scope["sources"][number], { name: Name }>
|
|
203
|
+
> extends infer Fields
|
|
204
|
+
? Extract<Fields, { name: Key }>
|
|
205
|
+
: never
|
|
206
|
+
type ProjectEntry<Scope extends QueryScope, Input> = Input extends "*"
|
|
207
|
+
? Scope["output"]
|
|
208
|
+
: Input extends { table: infer Table; wildcard: true }
|
|
209
|
+
? Entries<Extract<Scope["sources"][number], { name: Table }>>
|
|
210
|
+
: Input extends { alias: infer Alias extends string }
|
|
211
|
+
? Rename<Resolve<Scope, Input>, Alias>
|
|
212
|
+
: Resolve<Scope, Input>
|
|
213
|
+
type Rename<Fields extends Entry, Name extends string> = Fields extends Entry
|
|
214
|
+
? Omit<Fields, "name"> & { name: Name }
|
|
215
|
+
: never
|
|
216
|
+
type IsUnion<Value, Whole = Value> = Value extends Whole
|
|
217
|
+
? [Whole] extends [Value]
|
|
218
|
+
? false
|
|
219
|
+
: true
|
|
220
|
+
: never
|
|
221
|
+
type Value<Fields extends Entry> =
|
|
222
|
+
true extends IsUnion<Fields["origin"]>
|
|
223
|
+
? true extends Fields["mapped"]
|
|
224
|
+
? unknown
|
|
225
|
+
: Fields["value"]
|
|
226
|
+
: Fields["value"]
|
|
227
|
+
type Collapse<Fields extends Entry> = {
|
|
228
|
+
[Name in Fields["name"]]: {
|
|
229
|
+
value: Value<Extract<Fields, { name: Name }>>
|
|
230
|
+
mapped: true extends IsUnion<Extract<Fields, { name: Name }>["origin"]>
|
|
231
|
+
? false
|
|
232
|
+
: Extract<Fields, { name: Name }>["mapped"]
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
type Nullable<Fields extends Columns> = {
|
|
236
|
+
[Key in keyof Fields]: {
|
|
237
|
+
value: Fields[Key]["value"] | null
|
|
238
|
+
mapped: Fields[Key]["mapped"]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
type ExtendSources<
|
|
242
|
+
Sources extends readonly State[],
|
|
243
|
+
Kind extends JoinType,
|
|
244
|
+
> = {
|
|
245
|
+
[Index in keyof Sources]: Kind extends "right join" | "full join"
|
|
246
|
+
? State<Sources[Index]["name"], Nullable<Sources[Index]["columns"]>>
|
|
247
|
+
: Sources[Index]
|
|
248
|
+
}
|
|
249
|
+
type ExtendRight<Right extends State, Kind extends JoinType> = Kind extends
|
|
250
|
+
| "left join"
|
|
251
|
+
| "full join"
|
|
252
|
+
? State<Right["name"], Nullable<Right["columns"]>>
|
|
253
|
+
: Right
|
|
254
|
+
type ExtendLeft<
|
|
255
|
+
Fields extends Entry,
|
|
256
|
+
Kind extends JoinType,
|
|
257
|
+
> = Fields extends Entry
|
|
258
|
+
? Kind extends "right join" | "full join"
|
|
259
|
+
? Omit<Fields, "value"> & { value: Fields["value"] | null }
|
|
260
|
+
: Fields
|
|
261
|
+
: never
|
|
262
|
+
type Common<
|
|
263
|
+
Scope extends QueryScope,
|
|
264
|
+
Right extends State,
|
|
265
|
+
Kind extends JoinType,
|
|
266
|
+
Keys extends string,
|
|
267
|
+
> = Kind extends "right join"
|
|
268
|
+
? Extract<Entries<Right>, { name: Keys }>
|
|
269
|
+
: Kind extends "full join"
|
|
270
|
+
? Extract<Scope["output"] | Entries<Right>, { name: Keys }>
|
|
271
|
+
: Extract<Scope["output"], { name: Keys }>
|
|
272
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Connection } from "../api/api.types"
|
|
2
|
+
import { ColumnRef } from "../schemaBuilder/ColumnRef"
|
|
3
|
+
import type { ColumnData, TableData } from "../schemaBuilder/metadata"
|
|
4
|
+
import type { QueryScope } from "./QueryScope"
|
|
5
|
+
import type { SelectQuery, ColumnIdentifier } from "./queryBuilders.types"
|
|
6
|
+
import { QuerySourceBuilder } from "./QuerySourceBuilder"
|
|
7
|
+
import { Projection } from "./Projection"
|
|
8
|
+
|
|
9
|
+
export type QuerySource<
|
|
10
|
+
Name extends string,
|
|
11
|
+
Columns extends QueryScope.Columns,
|
|
12
|
+
> = {
|
|
13
|
+
readonly $source: QuerySource.Data
|
|
14
|
+
readonly $type: QueryScope.State<Name, Columns>
|
|
15
|
+
readonly all: ColumnRef.Wildcard<Name>
|
|
16
|
+
as<Alias extends string>(alias: Alias): QuerySource<Alias, Columns>
|
|
17
|
+
} & {
|
|
18
|
+
readonly [Key in keyof Columns & string]: ColumnRef<Name, Key>
|
|
19
|
+
} & QueryScope.Composition<QueryScope.Initial<QueryScope.State<Name, Columns>>>
|
|
20
|
+
|
|
21
|
+
export namespace QuerySource {
|
|
22
|
+
export type Data = (
|
|
23
|
+
| { kind: "table"; name: string }
|
|
24
|
+
| { kind: "subquery"; query: SelectQuery }
|
|
25
|
+
) & { alias?: string; tableData: TableData }
|
|
26
|
+
|
|
27
|
+
export type Target = { readonly $type: QueryScope.State }
|
|
28
|
+
export type Input =
|
|
29
|
+
| { readonly $source: Data }
|
|
30
|
+
| { readonly query: SelectQuery }
|
|
31
|
+
export type Scope = {
|
|
32
|
+
sources: Data[]
|
|
33
|
+
output: ColumnData[]
|
|
34
|
+
coalesced: Set<string>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function create(
|
|
38
|
+
data: Data,
|
|
39
|
+
connection?: Connection,
|
|
40
|
+
): QuerySource<string, QueryScope.Columns> {
|
|
41
|
+
const source = snapshotSource(data)
|
|
42
|
+
const name = qualifier(source)
|
|
43
|
+
const result = {
|
|
44
|
+
$source: source,
|
|
45
|
+
all: { table: name, wildcard: true as const },
|
|
46
|
+
as(alias: string) {
|
|
47
|
+
return create({ ...source, alias }, connection)
|
|
48
|
+
},
|
|
49
|
+
select(...columns: Projection.Input[]) {
|
|
50
|
+
return new QuerySourceBuilder(source, connection).select(...columns)
|
|
51
|
+
},
|
|
52
|
+
join(target: Input) {
|
|
53
|
+
return new QuerySourceBuilder(source, connection).join(target)
|
|
54
|
+
},
|
|
55
|
+
leftJoin(target: Input) {
|
|
56
|
+
return new QuerySourceBuilder(source, connection).leftJoin(target)
|
|
57
|
+
},
|
|
58
|
+
rightJoin(target: Input) {
|
|
59
|
+
return new QuerySourceBuilder(source, connection).rightJoin(target)
|
|
60
|
+
},
|
|
61
|
+
fullJoin(target: Input) {
|
|
62
|
+
return new QuerySourceBuilder(source, connection).fullJoin(target)
|
|
63
|
+
},
|
|
64
|
+
crossJoin(target: Input) {
|
|
65
|
+
return new QuerySourceBuilder(source, connection).crossJoin(target)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
for (const column of source.tableData.columns) {
|
|
69
|
+
Object.defineProperty(result, column.name, {
|
|
70
|
+
value: ColumnRef.create(name, column.name),
|
|
71
|
+
enumerable: true,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
return result as unknown as QuerySource<string, QueryScope.Columns>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function target(input: Input): Data {
|
|
78
|
+
if ("$source" in input) {
|
|
79
|
+
return snapshotSource(input.$source)
|
|
80
|
+
}
|
|
81
|
+
const query = snapshot(input.query)
|
|
82
|
+
return {
|
|
83
|
+
kind: "subquery",
|
|
84
|
+
query,
|
|
85
|
+
tableData: {
|
|
86
|
+
name: "",
|
|
87
|
+
columns: Projection.columns(query),
|
|
88
|
+
constraints: [],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function qualifier(source: Data): string {
|
|
94
|
+
return source.alias ?? (source.kind === "table" ? source.name : "")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function snapshot(query: SelectQuery): SelectQuery {
|
|
98
|
+
return {
|
|
99
|
+
...query,
|
|
100
|
+
source: snapshotSource(query.source),
|
|
101
|
+
selected: query.selected.map((projection) =>
|
|
102
|
+
projection === "*" ? projection : { ...projection },
|
|
103
|
+
),
|
|
104
|
+
conditions: query.conditions?.map((condition) => ({ ...condition })),
|
|
105
|
+
orderBy: query.orderBy?.map((sort) => ({ ...sort })),
|
|
106
|
+
joins: query.joins?.map((clause) => ({
|
|
107
|
+
...clause,
|
|
108
|
+
target: snapshotSource(clause.target),
|
|
109
|
+
on: clause.on.map((condition) => ({ ...condition })),
|
|
110
|
+
using: clause.using ? [...clause.using] : undefined,
|
|
111
|
+
})),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function scope(query: SelectQuery): Scope {
|
|
116
|
+
let sources = [query.source]
|
|
117
|
+
let output = [...query.source.tableData.columns]
|
|
118
|
+
const coalesced = new Set<string>()
|
|
119
|
+
for (const clause of query.joins ?? []) {
|
|
120
|
+
const right = clause.target
|
|
121
|
+
const leftOutput = output
|
|
122
|
+
const extendLeft =
|
|
123
|
+
clause.type === "right join" || clause.type === "full join"
|
|
124
|
+
const extendRight =
|
|
125
|
+
clause.type === "left join" || clause.type === "full join"
|
|
126
|
+
if (extendLeft) {
|
|
127
|
+
sources = sources.map(nullableSource)
|
|
128
|
+
output = output.map(nullableColumn)
|
|
129
|
+
}
|
|
130
|
+
const incoming = extendRight ? nullableSource(right) : right
|
|
131
|
+
sources.push(incoming)
|
|
132
|
+
if (clause.using?.length) {
|
|
133
|
+
const common = new Set(clause.using)
|
|
134
|
+
for (const name of common) {
|
|
135
|
+
if (clause.type === "full join") {
|
|
136
|
+
coalesced.add(name)
|
|
137
|
+
} else if (clause.type === "right join") {
|
|
138
|
+
coalesced.delete(name)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
output = output.map((column) => {
|
|
142
|
+
if (!common.has(column.name)) {
|
|
143
|
+
return column
|
|
144
|
+
}
|
|
145
|
+
const left = leftOutput.find(
|
|
146
|
+
(candidate) => candidate.name === column.name,
|
|
147
|
+
)!
|
|
148
|
+
const rightColumn = right.tableData.columns.find(
|
|
149
|
+
(candidate) => candidate.name === column.name,
|
|
150
|
+
)!
|
|
151
|
+
if (clause.type === "right join") {
|
|
152
|
+
return rightColumn
|
|
153
|
+
}
|
|
154
|
+
if (clause.type === "full join") {
|
|
155
|
+
return Projection.merge([left, rightColumn])
|
|
156
|
+
}
|
|
157
|
+
return left
|
|
158
|
+
})
|
|
159
|
+
output.push(
|
|
160
|
+
...incoming.tableData.columns.filter(
|
|
161
|
+
(column) => !common.has(column.name),
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
} else {
|
|
165
|
+
output.push(...incoming.tableData.columns)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { sources, output, coalesced }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function identifier(
|
|
172
|
+
scope: Scope,
|
|
173
|
+
input: string | ColumnRef,
|
|
174
|
+
): ColumnIdentifier {
|
|
175
|
+
if (typeof input !== "string") {
|
|
176
|
+
return { table: input.table || undefined, column: input.column }
|
|
177
|
+
}
|
|
178
|
+
if (scope.output.some((column) => column.name === input)) {
|
|
179
|
+
return { column: input }
|
|
180
|
+
}
|
|
181
|
+
for (const source of scope.sources) {
|
|
182
|
+
const name = qualifier(source)
|
|
183
|
+
if (name && input.startsWith(`${name}.`)) {
|
|
184
|
+
return { table: name, column: input.slice(name.length + 1) }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { column: input }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function resolve(
|
|
191
|
+
scope: Scope,
|
|
192
|
+
identifier: ColumnIdentifier,
|
|
193
|
+
): ColumnData[] {
|
|
194
|
+
if (identifier.table === undefined) {
|
|
195
|
+
return scope.output.filter((column) => column.name === identifier.column)
|
|
196
|
+
}
|
|
197
|
+
return scope.sources
|
|
198
|
+
.filter((source) => qualifier(source) === identifier.table)
|
|
199
|
+
.flatMap((source) =>
|
|
200
|
+
source.tableData.columns.filter(
|
|
201
|
+
(column) => column.name === identifier.column,
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function snapshotSource(source: Data): Data {
|
|
207
|
+
return {
|
|
208
|
+
...source,
|
|
209
|
+
...(source.kind === "subquery" ? { query: snapshot(source.query) } : {}),
|
|
210
|
+
tableData: {
|
|
211
|
+
...source.tableData,
|
|
212
|
+
columns: [...source.tableData.columns],
|
|
213
|
+
constraints: [...source.tableData.constraints],
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function nullableColumn(column: ColumnData): ColumnData {
|
|
219
|
+
return { ...column, notNull: false, primaryKey: undefined }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function nullableSource(source: Data): Data {
|
|
223
|
+
return {
|
|
224
|
+
...source,
|
|
225
|
+
tableData: {
|
|
226
|
+
...source.tableData,
|
|
227
|
+
columns: source.tableData.columns.map(nullableColumn),
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BinaryOperator, Connection } from "../api/api.types"
|
|
2
|
+
import type { ColumnRef } from "../schemaBuilder/ColumnRef"
|
|
3
|
+
import { Projection } from "./Projection"
|
|
4
|
+
import { QuerySource } from "./QuerySource"
|
|
5
|
+
import { SelectBuilder } from "./SelectBuilder"
|
|
6
|
+
import { SelectQueryBuilder } from "./SelectQueryBuilder"
|
|
7
|
+
import type { JoinType, SelectQuery } from "./queryBuilders.types"
|
|
8
|
+
|
|
9
|
+
export class QuerySourceBuilder {
|
|
10
|
+
private readonly query: SelectQuery
|
|
11
|
+
private readonly connection?: Connection
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
source: QuerySource.Data,
|
|
15
|
+
connection?: Connection,
|
|
16
|
+
query?: SelectQuery,
|
|
17
|
+
) {
|
|
18
|
+
this.query = query ?? { source, selected: ["*"] }
|
|
19
|
+
this.connection = connection
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
select(...columns: Projection.Input[]) {
|
|
23
|
+
const query = QuerySource.snapshot(this.query)
|
|
24
|
+
const scope = QuerySource.scope(query)
|
|
25
|
+
query.selected = columns.length
|
|
26
|
+
? columns.map((column) => Projection.create(scope, column))
|
|
27
|
+
: ["*"]
|
|
28
|
+
return this.connection
|
|
29
|
+
? new SelectBuilder(this.connection, query)
|
|
30
|
+
: new SelectQueryBuilder(query)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
join(target: QuerySource.Input) {
|
|
34
|
+
return this.addJoin("join", target)
|
|
35
|
+
}
|
|
36
|
+
leftJoin(target: QuerySource.Input) {
|
|
37
|
+
return this.addJoin("left join", target)
|
|
38
|
+
}
|
|
39
|
+
rightJoin(target: QuerySource.Input) {
|
|
40
|
+
return this.addJoin("right join", target)
|
|
41
|
+
}
|
|
42
|
+
fullJoin(target: QuerySource.Input) {
|
|
43
|
+
return this.addJoin("full join", target)
|
|
44
|
+
}
|
|
45
|
+
crossJoin(target: QuerySource.Input) {
|
|
46
|
+
return this.addJoin("cross join", target)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
on(
|
|
50
|
+
left: string | ColumnRef,
|
|
51
|
+
operator: BinaryOperator,
|
|
52
|
+
right: string | ColumnRef,
|
|
53
|
+
) {
|
|
54
|
+
const query = QuerySource.snapshot(this.query)
|
|
55
|
+
const scope = QuerySource.scope(query)
|
|
56
|
+
query.joins![query.joins!.length - 1].on.push({
|
|
57
|
+
left: QuerySource.identifier(scope, left),
|
|
58
|
+
operator,
|
|
59
|
+
right: QuerySource.identifier(scope, right),
|
|
60
|
+
})
|
|
61
|
+
return new QuerySourceBuilder(query.source, this.connection, query)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
using(...columns: string[]) {
|
|
65
|
+
const query = QuerySource.snapshot(this.query)
|
|
66
|
+
query.joins![query.joins!.length - 1].using = columns
|
|
67
|
+
return new QuerySourceBuilder(query.source, this.connection, query)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private addJoin(type: JoinType, input: QuerySource.Input) {
|
|
71
|
+
const query = QuerySource.snapshot(this.query)
|
|
72
|
+
query.joins ??= []
|
|
73
|
+
query.joins.push({ type, target: QuerySource.target(input), on: [] })
|
|
74
|
+
return new QuerySourceBuilder(query.source, this.connection, query)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,37 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import type { TableData } from "../schemaBuilder"
|
|
4
|
-
import { generateSelect } from "../queryGenerator"
|
|
1
|
+
import type { Connection } from "../api/api.types"
|
|
2
|
+
import { generateSelect } from "../queryGenerator/generateSelect"
|
|
5
3
|
import { Mappings } from "./Mappings"
|
|
4
|
+
import { Projection } from "./Projection"
|
|
6
5
|
import { SelectQueryBuilder } from "./SelectQueryBuilder"
|
|
6
|
+
import type { SelectQuery } from "./queryBuilders.types"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
export class SelectBuilder extends SelectQueryBuilder implements Select<any, any> {
|
|
8
|
+
export class SelectBuilder extends SelectQueryBuilder {
|
|
10
9
|
readonly connection: Connection
|
|
11
10
|
|
|
12
|
-
constructor(
|
|
13
|
-
|
|
14
|
-
table: TableData,
|
|
15
|
-
columns: "*" | string[],
|
|
16
|
-
) {
|
|
17
|
-
super(table, columns)
|
|
11
|
+
constructor(connection: Connection, query: SelectQuery) {
|
|
12
|
+
super(query)
|
|
18
13
|
this.connection = connection
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
async fetch()
|
|
16
|
+
async fetch() {
|
|
22
17
|
const rows = await this.connection.query(generateSelect(this.query))
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return Mappings.results(tables, rows)
|
|
18
|
+
return Mappings.results(
|
|
19
|
+
{ name: "", columns: Projection.columns(this.query), constraints: [] },
|
|
20
|
+
rows,
|
|
21
|
+
)
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
async first()
|
|
24
|
+
async first() {
|
|
31
25
|
const results = await this.fetch()
|
|
32
26
|
if (results[0] === undefined) {
|
|
33
27
|
throw new Error(
|
|
34
|
-
`Query did not return a result (table: "${this.
|
|
28
|
+
`Query did not return a result (table: "${this.query.source.tableData.name}").`,
|
|
35
29
|
)
|
|
36
30
|
}
|
|
37
31
|
return results[0]
|