1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291 |
1x
99x
99x
792x
792x
99x
99x
32x
32x
99x
67x
68x
99x
106x
10x
10x
106x
10x
10x
112x
50x
50x
64x
78x
29x
13x
10x
4x
78x
64x
78x
78x
49x
16x
3x
6x
4x
4x
99x
2x
3x
99x
4x
5x
99x
27x
92x
92x
138x
138x
42x
11x
11x
7x
7x
7x
6x
6x
258x
143x
| import QueryBuilder from "./builder";
import { ColumnWhereClause, NullWhereClause, RawWhereClause, SimpleWhereClause, NestedWhereClause } from "./types";
import { DatabaseType } from "../base-model";
export type Builder = QueryBuilder<any>;
export type QueryComponent = { sql: string, args: DatabaseType[] };
/**
* This class is responsible for "compiling" QueryBuilders into the raw SQL
* that will be executed by the database engine. It returns both SQL and bound
* values, so we can safely let the database engine handle escaping of arguments.
*/
export default class SQLGrammarCompiler {
/**
* Compiles a full SELECT query.
*/
compileSelect(builder: Builder): QueryComponent {
const components: { [key: string]: QueryComponent } = this.compileComponents(builder);
return {
sql: Object.keys(components).map(x => components[x].sql).filter(x => !!x).join(" "),
args: Object.keys(components).map(x => components[x].args).reduce((p, c) => [...p, ...c], [])
};
}
/**
* Compiles all components of a normal SELECT query. This can be overridden by subclasses
* to reorder this or to add/remove components.
*/
protected compileComponents(builder: Builder) {
return {
aggregate: this.compileAggregate(builder),
columns: this.compileColumns(builder),
from: this.compileFrom(builder),
joins: this.compileJoins(builder),
wheres: this.compileWheres(builder),
groups: this.compileGroups(builder),
orders: this.compileOrders(builder),
limit: this.compileLimit(builder)
};
}
/**
* Compiles the aggregate function part of a SELECT query.
*/
compileAggregate(builder: Builder): QueryComponent {
if (!builder.aggregateFunction) return { sql: "", args: [] };
const columns = builder.columns.map(x => this.escapeColumn(x)).join(", ");
return {
sql: `SELECT ${builder.aggregateFunction}(${builder.isDistinct && columns !== "*" ? "DISTINCT " : ""}${columns}) AS aggregate`,
args: []
};
}
/**
* Compiles the SELECT `columns` part of a SELECT query, unless
* an aggregate function was specified.
*/
compileColumns(builder: Builder): QueryComponent {
// We will let aggregate start the query if its present.
if (builder.aggregateFunction) return { sql: "", args: [] };
return {
sql: (builder.isDistinct ? "SELECT DISTINCT " : "SELECT ") + builder.columns.map(c => this.escapeColumn(c)).join(", "),
args: []
};
}
/**
* Compiles the FROM `table` part of a SELECT query.
*/
compileFrom(builder: Builder): QueryComponent {
return {
sql: "FROM " + this.escapeTable(builder.table),
args: []
};
}
/**
* Compiles all JOIN conditions of the query.
*/
compileJoins(builder: Builder): QueryComponent {
const parts = builder.joins.map(join => {
const condition = this.compileConditions(join);
return {
sql: `${join.type} JOIN ${this.escapeTable(join.joiningOn)} ON ${condition.sql}`,
args: condition.args
};
});
return {
sql: parts.map(x => x.sql).join(" "),
args: parts.map(x => x.args).reduce((p, c) => [...p, ...c], [])
};
}
/**
* Compiles all WHERE clauses of the query.
*/
compileWheres(builder: Builder): QueryComponent {
if (!builder.wheres.length) return { sql: "", args: [] };
const part = this.compileConditions(builder);
return {
sql: "WHERE " + part.sql,
args: part.args
};
}
/**
* Compiles the set of WHERE conditions in the specified query. Used
* for both JOIN and WHERE compiling.
*/
compileConditions(builder: Builder): QueryComponent {
/* istanbul ignore next This should be caught by compileWheres, but is here as a sanity check. */
if (!builder.wheres.length) return { sql: "", args: [] };
const parts = builder.wheres.map((where, i) => {
let content;
if (where.type === "basic") content = this.compileBasicWhere(builder, where);
else if (where.type === "column") content = this.compileColumnWhere(builder, where);
else if (where.type === "raw") content = this.compileRawWhere(builder, where);
else if (where.type === "null") content = this.compileNullWhere(builder, where);
else Eif (where.type === "nested") content = this.compileNestedWhere(builder, where);
/* istanbul ignore next Impossible with a properly formed querybuilder. */
if (!content) {
throw new Error("Invalid type for where clause.");
}
return {
sql: (i === 0 ? "" : where.boolean + " ") + content.sql,
args: content.args
};
});
return {
sql: parts.map(x => x.sql).join(" "),
args: parts.map(x => x.args).reduce((p, c) => [...p, ...c], [])
};
}
/**
* Compiles a simple WHERE clause.
*/
compileBasicWhere(builder: Builder, where: SimpleWhereClause): QueryComponent {
return {
sql: `${this.escapeColumn(where.column)} ${where.operator} ?`,
args: [where.value]
};
}
/**
* Compiles a WHERE clause comparing two columns.
*/
compileColumnWhere(builder: Builder, where: ColumnWhereClause): QueryComponent {
return {
sql: `${this.escapeColumn(where.first)} ${where.operator} ${this.escapeColumn(where.second)}`,
args: []
};
}
/**
* Compiles a raw SQL where clause.
*/
compileRawWhere(builder: Builder, where: RawWhereClause): QueryComponent {
return {
sql: where.sql,
args: where.values
};
}
/**
* Compiles a WHERE clause checking for (NOT) NULL.
*/
compileNullWhere(builder: Builder, where: NullWhereClause): QueryComponent {
return {
sql: this.escapeColumn(where.column) + " IS " + (where.negate ? "NOT " : "") + "NULL",
args: []
};
}
/**
* Compiles a nested WHERE clause.
*/
compileNestedWhere(builder: Builder, where: NestedWhereClause): QueryComponent {
const ret = this.compileConditions(where.builder);
return {
sql: "(" + ret.sql + ")",
args: ret.args
};
}
/**
* Compiles the GROUP BY clauses of the query.
*/
compileGroups(builder: Builder): QueryComponent {
if (!builder.groups.length) return { sql: "", args: [] };
return {
sql: "GROUP BY " + builder.groups.map(x => this.escapeColumn(x)).join(", "),
args: []
};
}
/**
* Compile the ORDER BY clauses of the query.
*/
compileOrders(builder: Builder): QueryComponent {
if (!builder.orders.length) return { sql: "", args: [] };
return {
sql: "ORDER BY " + builder.orders.map(x => this.escapeColumn(x.column) + " " + x.direction).join(", "),
args: []
};
}
/**
* Compiles the LIMIT part of the query.
*/
compileLimit(builder: Builder): QueryComponent {
if (builder.limitCount === -1) return { sql: "", args: [] };
return {
sql: "LIMIT ?",
args: [builder.limitCount]
};
}
/**
* Compiles a full INSERT query for the specified values. This accepts either a model
* or a keyed database value object. This assumes that every element in the `values`
* array has the same structure, and that it is not an empty array.
*/
compileInsert<T>(builder: QueryBuilder<T>, values: T[]): QueryComponent {
const keys = Object.keys(values[0]).filter(x => x !== "id" && typeof (<any>values[0])[x] !== "function");
const columns = keys.map(x => this.escapeColumn(x)).join(",");
const args = values.map(x => keys.map(k => (<any>x)[k])).reduce((p, c) => [...p, ...c], []);
const placeholders = values.map(x => "(" + keys.map(k => "?").join(",") + ")").join(", ");
return {
sql: `INSERT INTO ${this.escapeTable(builder.table)} (${columns}) VALUES ${placeholders}`,
args
};
}
/**
* Compiles a full UPDATE query for the specified partial. This does not diff-check and
* simply assumes that everything specified needs to be updated.
*/
compileUpdate<T>(builder: QueryBuilder<T>, value: Partial<T>): QueryComponent {
const columns = Object.keys(value).filter(x => x !== "id" && typeof (<any>value)[x] !== "function").map(x => x + " = ?").join(", ");
const args = Object.keys(value).filter(x => x !== "id" && typeof (<any>value)[x] !== "function").map(x => <DatabaseType><any>value[x]);
const joins = this.compileJoins(builder);
const wheres = this.compileWheres(builder);
return {
sql: `UPDATE ${this.escapeTable(builder.table)}${joins.sql ? " " + joins.sql : ""} SET ${columns} ${wheres.sql}`,
args: [...joins.args, ...args, ...wheres.args]
};
}
/**
* Compiles a full DELETE query for all rows matching the builder's where clauses.
*/
compileDelete(builder: Builder): QueryComponent {
const wheres = this.compileWheres(builder);
return {
sql: `DELETE FROM ${this.escapeTable(builder.table)}${wheres.sql ? " " + wheres.sql : ""}`,
args: wheres.args
};
}
/**
* Escapes the specified column name. This is intended to be overridden by potential subclasses.
*/
protected escapeColumn(column: string) {
return column;
}
/**
* Escapes the specified table name. This is intended to be overridden by potential subclasses.
*/
protected escapeTable(table: string) {
return table;
}
} |