terrier-engine 4.7.8 → 4.8.2
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/data-dive/dives/dive-editor.ts +14 -1
- package/data-dive/queries/columns.ts +122 -74
- package/data-dive/queries/filters.ts +1 -1
- package/data-dive/queries/queries.ts +24 -0
- package/data-dive/queries/query-editor.ts +4 -1
- package/data-dive/queries/tables.ts +5 -1
- package/data-dive/queries/validation.ts +9 -3
- package/package.json +1 -1
|
@@ -62,7 +62,7 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
62
62
|
|
|
63
63
|
this.queries = this.state.dive.query_data?.queries || []
|
|
64
64
|
for (const query of this.queries) {
|
|
65
|
-
this.
|
|
65
|
+
this.addQueryTag(query)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
this.listenMessage(QueryForm.settingsChangedKey, m => {
|
|
@@ -82,7 +82,20 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
|
|
|
82
82
|
})
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Add a new query to the dive.
|
|
87
|
+
* @param query
|
|
88
|
+
*/
|
|
85
89
|
addQuery(query: Query) {
|
|
90
|
+
this.queries.push(query)
|
|
91
|
+
this.addQueryTag(query)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Adds a tab for an existing query.
|
|
96
|
+
* @param query
|
|
97
|
+
*/
|
|
98
|
+
private addQueryTag(query: Query) {
|
|
86
99
|
const state = {...this.state, query}
|
|
87
100
|
this.tabs.upsertTab({key: query.id, title: query.name}, QueryEditor, state)
|
|
88
101
|
}
|
|
@@ -11,7 +11,9 @@ import {Dropdown} from "../../terrier/dropdowns"
|
|
|
11
11
|
import DiveEditor from "../dives/dive-editor"
|
|
12
12
|
import Messages from "tuff-core/messages"
|
|
13
13
|
import Arrays from "tuff-core/arrays"
|
|
14
|
-
import
|
|
14
|
+
import Dom from "tuff-core/dom"
|
|
15
|
+
import Validation, {ColumnValidationError} from "./validation"
|
|
16
|
+
import Queries, {Query} from "./queries"
|
|
15
17
|
|
|
16
18
|
const log = new Logger("Columns")
|
|
17
19
|
|
|
@@ -22,7 +24,7 @@ const log = new Logger("Columns")
|
|
|
22
24
|
/**
|
|
23
25
|
* Possible functions used to aggregate a column.
|
|
24
26
|
*/
|
|
25
|
-
const AggFunctions = ['count', 'min', 'max'] as const
|
|
27
|
+
const AggFunctions = ['count', 'sum', 'min', 'max'] as const
|
|
26
28
|
|
|
27
29
|
export type AggFunction = typeof AggFunctions[number]
|
|
28
30
|
|
|
@@ -40,7 +42,7 @@ export type Function = AggFunction | DateFunction
|
|
|
40
42
|
* @param fun a function name
|
|
41
43
|
* @return the type of function
|
|
42
44
|
*/
|
|
43
|
-
function functionType(fun: Function): 'aggregate' | 'time' | undefined {
|
|
45
|
+
function functionType(fun: Function | undefined): 'aggregate' | 'time' | undefined {
|
|
44
46
|
if (AggFunctions.includes(fun as AggFunction)) {
|
|
45
47
|
return 'aggregate'
|
|
46
48
|
}
|
|
@@ -105,6 +107,7 @@ function render(parent: PartTag, col: ColumnRef) {
|
|
|
105
107
|
|
|
106
108
|
export type ColumnsEditorState = {
|
|
107
109
|
schema: SchemaDef
|
|
110
|
+
query: Query
|
|
108
111
|
tableView: TableView<TableRef>
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -112,6 +115,7 @@ const saveKey = Messages.untypedKey()
|
|
|
112
115
|
const addKey = Messages.untypedKey()
|
|
113
116
|
const addSingleKey = Messages.typedKey<{ name: string }>()
|
|
114
117
|
const removeKey = Messages.typedKey<{id: string}>()
|
|
118
|
+
const valueChangedKey = Messages.untypedKey()
|
|
115
119
|
|
|
116
120
|
/**
|
|
117
121
|
* A modal that lets the user edit the columns being referenced for a particular table.
|
|
@@ -172,33 +176,35 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
172
176
|
})
|
|
173
177
|
|
|
174
178
|
this.onClick(addSingleKey, m => {
|
|
175
|
-
|
|
176
|
-
log.info(`Add column ${m.data.name}`)
|
|
177
|
-
if (colDef) {
|
|
178
|
-
this.addState({name: colDef.name})
|
|
179
|
-
this.updateColumnEditors()
|
|
180
|
-
this.dirty()
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
alert(`Unknown column name '${m.data.name}'`)
|
|
184
|
-
}
|
|
179
|
+
this.addColumn(m.data.name)
|
|
185
180
|
})
|
|
186
181
|
|
|
187
182
|
this.onClick(addKey, m => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
this.updateColumnEditors()
|
|
197
|
-
}
|
|
198
|
-
this.toggleDropdown(SelectColumnsDropdown, {modelDef: this.modelDef, callback: onSelected}, m.event.target)
|
|
183
|
+
this.toggleDropdown(SelectColumnsDropdown, {editor: this as ColumnsEditorModal}, m.event.target)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
this.onChange(valueChangedKey, m => {
|
|
187
|
+
log.info(`Column value changed`, m)
|
|
188
|
+
this.validate().then()
|
|
199
189
|
})
|
|
200
190
|
}
|
|
201
191
|
|
|
192
|
+
addColumn(col: string) {
|
|
193
|
+
const colDef = this.modelDef.columns[col]
|
|
194
|
+
log.info(`Add column ${col}`, colDef)
|
|
195
|
+
if (colDef) {
|
|
196
|
+
this.addState({name: colDef.name})
|
|
197
|
+
this.updateColumnEditors()
|
|
198
|
+
this.dirty()
|
|
199
|
+
} else {
|
|
200
|
+
alert(`Unknown column name '${col}'`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get currentColumnNames(): Set<string> {
|
|
205
|
+
return new Set(this.columnStates.map(s => s.name))
|
|
206
|
+
}
|
|
207
|
+
|
|
202
208
|
renderContent(parent: PartTag): void {
|
|
203
209
|
// fields
|
|
204
210
|
parent.div('.tt-flex.tt-form.padded.gap.justify-end.align-center', row => {
|
|
@@ -222,7 +228,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
222
228
|
})
|
|
223
229
|
|
|
224
230
|
// common column quick links
|
|
225
|
-
const includedNames =
|
|
231
|
+
const includedNames = this.currentColumnNames
|
|
226
232
|
const commonCols = Object.values(this.modelDef.columns).filter(c => c.metadata?.visibility == 'common' && !includedNames.has(c.name))
|
|
227
233
|
if (commonCols.length) {
|
|
228
234
|
parent.h3('.centered.large-top-padding', h3 => {
|
|
@@ -260,10 +266,44 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
260
266
|
}
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
|
|
264
|
-
|
|
269
|
+
serialize(): ColumnRef[] {
|
|
270
|
+
return this.columnStates.map(state => {
|
|
265
271
|
return Objects.omit(state, 'schema', 'columnsEditor', 'id') as ColumnRef
|
|
266
272
|
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Performs client-side validation against the current values in the editors.
|
|
277
|
+
*/
|
|
278
|
+
async validate() {
|
|
279
|
+
// serialize the columns and table settings
|
|
280
|
+
const columns = this.serialize()
|
|
281
|
+
const tableData = await this.tableFields.serialize()
|
|
282
|
+
|
|
283
|
+
// make a deep copy of the query and update this table's columns and settings
|
|
284
|
+
this.table._id = this.id // we need this to identify the table after the deep copy
|
|
285
|
+
const query = Objects.deepCopy(this.state.query)
|
|
286
|
+
Queries.eachTable(query, table => {
|
|
287
|
+
if (table._id == this.id) {
|
|
288
|
+
table.columns = columns
|
|
289
|
+
table.prefix = tableData.prefix
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// validate the temporary query
|
|
294
|
+
log.info(`Validating temporary query with column changes`, query)
|
|
295
|
+
Validation.validateQuery(query)
|
|
296
|
+
|
|
297
|
+
// copy the column errors over
|
|
298
|
+
for (let i = 0; i < columns.length; i++) {
|
|
299
|
+
this.columnStates[i].errors = columns[i].errors
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.dirty()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async save() {
|
|
306
|
+
const columns = this.serialize()
|
|
267
307
|
const tableData = await this.tableFields.serialize()
|
|
268
308
|
this.state.tableView.updateColumns(columns, tableData.prefix)
|
|
269
309
|
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
@@ -312,18 +352,26 @@ class ColumnEditor extends TerrierFormPart<ColumnState> {
|
|
|
312
352
|
})
|
|
313
353
|
parent.div('.alias', col => {
|
|
314
354
|
this.textInput(col, "alias", {placeholder: "Alias"})
|
|
355
|
+
.emitChange(valueChangedKey)
|
|
315
356
|
})
|
|
316
357
|
parent.div('.function', col => {
|
|
317
358
|
this.select(col, "function", this.functionOptions)
|
|
359
|
+
.emitChange(valueChangedKey)
|
|
318
360
|
})
|
|
319
361
|
parent.div('.group-by', col => {
|
|
320
362
|
this.checkbox(col, "grouped")
|
|
363
|
+
.emitChange(valueChangedKey)
|
|
321
364
|
})
|
|
322
365
|
parent.div('.actions', actions => {
|
|
323
366
|
actions.a(a => {
|
|
324
367
|
a.i('.glyp-close')
|
|
325
368
|
}).emitClick(removeKey, {id: this.state.id})
|
|
326
369
|
})
|
|
370
|
+
if (this.state.errors?.length) {
|
|
371
|
+
for (const error of this.state.errors) {
|
|
372
|
+
parent.div('.error.tt-bubble.alert').text(error.message)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
327
375
|
}
|
|
328
376
|
|
|
329
377
|
}
|
|
@@ -333,19 +381,21 @@ class ColumnEditor extends TerrierFormPart<ColumnState> {
|
|
|
333
381
|
// Add Column Dropdown
|
|
334
382
|
////////////////////////////////////////////////////////////////////////////////
|
|
335
383
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
type SelectColumnsCallback = (columns: string[]) => any
|
|
384
|
+
type SelectableColumn = ColumnDef & {
|
|
385
|
+
included: boolean
|
|
386
|
+
sortOrder: string
|
|
387
|
+
}
|
|
341
388
|
|
|
342
389
|
/**
|
|
343
390
|
* Shows a dropdown that allows the user to select one or more columns from the given model.
|
|
344
391
|
*/
|
|
345
|
-
class SelectColumnsDropdown extends Dropdown<{
|
|
392
|
+
class SelectColumnsDropdown extends Dropdown<{editor: ColumnsEditorModal}> {
|
|
346
393
|
|
|
394
|
+
addAllKey = Messages.untypedKey()
|
|
395
|
+
addKey = Messages.typedKey<{ name: string }>()
|
|
347
396
|
checked: Set<string> = new Set()
|
|
348
|
-
columns!:
|
|
397
|
+
columns!: SelectableColumn[]
|
|
398
|
+
modelDef!: ModelDef
|
|
349
399
|
|
|
350
400
|
get autoClose(): boolean {
|
|
351
401
|
return true
|
|
@@ -354,63 +404,61 @@ class SelectColumnsDropdown extends Dropdown<{modelDef: ModelDef, callback: Sele
|
|
|
354
404
|
async init() {
|
|
355
405
|
await super.init()
|
|
356
406
|
|
|
357
|
-
this.
|
|
407
|
+
this.modelDef = this.state.editor.modelDef
|
|
358
408
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
for (const c of this.columns) {
|
|
367
|
-
this.checked.add(c)
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
this.dirty()
|
|
409
|
+
// sort the columns by whether they're in the editor already
|
|
410
|
+
const includedNames = this.state.editor.currentColumnNames
|
|
411
|
+
this.columns = Object.values(this.modelDef.columns).map(col => {
|
|
412
|
+
const included = includedNames.has(col.name)
|
|
413
|
+
const sortOrder = `${included ? '1' : '0'}${col.name}`
|
|
414
|
+
return {included, sortOrder,...col}
|
|
371
415
|
})
|
|
416
|
+
this.columns = Arrays.sortBy(this.columns, 'sortOrder')
|
|
372
417
|
|
|
373
|
-
this.onClick(
|
|
374
|
-
|
|
375
|
-
this.
|
|
418
|
+
this.onClick(this.addKey, m => {
|
|
419
|
+
log.info(`Adding column ${m.data.name}`)
|
|
420
|
+
this.state.editor.addColumn(m.data.name)
|
|
421
|
+
|
|
422
|
+
// remove the link
|
|
423
|
+
const link = Dom.queryAncestorClass(m.event.target as HTMLInputElement, 'column')
|
|
424
|
+
link?.remove()
|
|
376
425
|
})
|
|
377
426
|
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
this.checked.delete(col)
|
|
427
|
+
this.onClick(this.addAllKey, _ => {
|
|
428
|
+
// add all of the unincluded columns and close the dropdown
|
|
429
|
+
for (const col of this.columns) {
|
|
430
|
+
if (!col.included) {
|
|
431
|
+
this.state.editor.addColumn(col.name)
|
|
432
|
+
}
|
|
387
433
|
}
|
|
434
|
+
this.clear()
|
|
388
435
|
})
|
|
389
436
|
}
|
|
390
437
|
|
|
391
438
|
|
|
392
439
|
get parentClasses(): Array<string> {
|
|
393
|
-
return super.parentClasses.concat(['
|
|
440
|
+
return super.parentClasses.concat(['tt-actions-dropdown']);
|
|
394
441
|
}
|
|
395
442
|
|
|
396
443
|
renderContent(parent: PartTag) {
|
|
397
|
-
parent.a('.header', a => {
|
|
398
|
-
a.i('.glyp-check_all')
|
|
399
|
-
a.span({text: "Toggle All"})
|
|
400
|
-
}).emitClick(checkAllKey)
|
|
401
|
-
|
|
402
444
|
for (const col of this.columns) {
|
|
403
|
-
parent.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
445
|
+
parent.a('.column', a => {
|
|
446
|
+
a.div('.name').text(col.name)
|
|
447
|
+
a.div('.right-title').text(col.type)
|
|
448
|
+
if (col.included) {
|
|
449
|
+
// style the columns that are already included differently
|
|
450
|
+
a.class('inactive')
|
|
451
|
+
}
|
|
452
|
+
if (col.metadata?.description?.length) {
|
|
453
|
+
a.div('.subtitle').text(col.metadata.description)
|
|
454
|
+
}
|
|
455
|
+
}).emitClick(this.addKey, {name: col.name})
|
|
408
456
|
}
|
|
409
457
|
|
|
410
|
-
parent.a('.
|
|
411
|
-
a.i('.glyp-
|
|
412
|
-
a.span({text: "Add
|
|
413
|
-
}).emitClick(
|
|
458
|
+
parent.a('.primary', a => {
|
|
459
|
+
a.i('.glyp-check_all')
|
|
460
|
+
a.span({text: "Add All"})
|
|
461
|
+
}).emitClick(this.addAllKey)
|
|
414
462
|
}
|
|
415
463
|
|
|
416
464
|
}
|
|
@@ -618,7 +618,7 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
|
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
get parentClasses(): Array<string> {
|
|
621
|
-
return super.parentClasses.concat(['
|
|
621
|
+
return super.parentClasses.concat(['tt-actions-dropdown'])
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
renderContent(parent: PartTag) {
|
|
@@ -32,6 +32,28 @@ export type Query = {
|
|
|
32
32
|
// Utilities
|
|
33
33
|
////////////////////////////////////////////////////////////////////////////////
|
|
34
34
|
|
|
35
|
+
type TableFunction = (table: TableRef) => any
|
|
36
|
+
|
|
37
|
+
function eachChildTable(table: TableRef, fn: TableFunction) {
|
|
38
|
+
if (!table.joins) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
for (const joinedTable of Object.values(table.joins)) {
|
|
42
|
+
fn(joinedTable)
|
|
43
|
+
eachChildTable(joinedTable, fn)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recursively iterates through each table reference in the query and evaluates the function.
|
|
49
|
+
* @param query
|
|
50
|
+
* @param fn
|
|
51
|
+
*/
|
|
52
|
+
function eachTable(query: Query, fn: TableFunction) {
|
|
53
|
+
fn(query.from)
|
|
54
|
+
eachChildTable(query.from, fn)
|
|
55
|
+
}
|
|
56
|
+
|
|
35
57
|
type ColumnFunction = (table: TableRef, col: ColumnRef) => any
|
|
36
58
|
|
|
37
59
|
function eachColumnForTable(table: TableRef, fn: ColumnFunction) {
|
|
@@ -50,6 +72,7 @@ function eachColumnForTable(table: TableRef, fn: ColumnFunction) {
|
|
|
50
72
|
/**
|
|
51
73
|
* Recursively iterates over all columns in a query.
|
|
52
74
|
* @param query
|
|
75
|
+
* @param fn a function to evaluate for each column in the query
|
|
53
76
|
*/
|
|
54
77
|
function eachColumn(query: Query, fn: ColumnFunction) {
|
|
55
78
|
eachColumnForTable(query.from, fn)
|
|
@@ -288,6 +311,7 @@ export class QueryModelPicker extends TerrierPart<QueryModelPickerState> {
|
|
|
288
311
|
|
|
289
312
|
const Queries = {
|
|
290
313
|
eachColumn,
|
|
314
|
+
eachTable,
|
|
291
315
|
validate,
|
|
292
316
|
preview,
|
|
293
317
|
renderPreview
|
|
@@ -120,7 +120,10 @@ class PreviewPart extends ContentPart<SubEditorState> {
|
|
|
120
120
|
|
|
121
121
|
renderContent(parent: PartTag) {
|
|
122
122
|
if (this.result) {
|
|
123
|
-
if (this.result.
|
|
123
|
+
if (this.result.status == 'error') {
|
|
124
|
+
parent.div('.tt-bubble.alert').text(this.result.message)
|
|
125
|
+
}
|
|
126
|
+
else if (this.result.columns?.length) {
|
|
124
127
|
parent.div('.table-container', col => {
|
|
125
128
|
Queries.renderPreview(col, this.result!)
|
|
126
129
|
})
|
|
@@ -12,6 +12,7 @@ import DiveEditor from "../dives/dive-editor"
|
|
|
12
12
|
import Messages from "tuff-core/messages"
|
|
13
13
|
import Arrays from "tuff-core/arrays"
|
|
14
14
|
import QueryEditor from "./query-editor"
|
|
15
|
+
import {Query} from "./queries";
|
|
15
16
|
|
|
16
17
|
const log = new Logger("Tables")
|
|
17
18
|
|
|
@@ -25,6 +26,7 @@ export type TableRef = {
|
|
|
25
26
|
columns?: ColumnRef[]
|
|
26
27
|
joins?: Record<string, JoinedTableRef>
|
|
27
28
|
filters?: Filter[]
|
|
29
|
+
_id?: string // ephemeral
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export type JoinedTableRef = TableRef & {
|
|
@@ -73,6 +75,7 @@ function computeFilterInputs(schema: SchemaDef, table: TableRef, filters: Record
|
|
|
73
75
|
export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaDef, queryEditor: QueryEditor, table: T }> {
|
|
74
76
|
|
|
75
77
|
schema!: SchemaDef
|
|
78
|
+
query!: Query
|
|
76
79
|
table!: T
|
|
77
80
|
tableName!: string
|
|
78
81
|
displayName!: string
|
|
@@ -87,6 +90,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
87
90
|
|
|
88
91
|
async init() {
|
|
89
92
|
this.schema = this.state.schema
|
|
93
|
+
this.query = this.state.queryEditor.state.query
|
|
90
94
|
this.table = this.state.table
|
|
91
95
|
this.modelDef = this.schema.models[this.table.model]
|
|
92
96
|
this.tableName = inflection.titleize(inflection.tableize(this.table.model))
|
|
@@ -95,7 +99,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
|
|
|
95
99
|
|
|
96
100
|
this.onClick(this.editColumnsKey, _ => {
|
|
97
101
|
log.info(`Edit ${this.displayName} Columns`)
|
|
98
|
-
this.app.showModal(ColumnsEditorModal, {schema: this.schema, tableView: this as TableView<TableRef>})
|
|
102
|
+
this.app.showModal(ColumnsEditorModal, {schema: this.schema, query: this.query, tableView: this as TableView<TableRef>})
|
|
99
103
|
})
|
|
100
104
|
|
|
101
105
|
this.onClick(this.editFiltersKey, _ => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Queries, {Query} from "./queries"
|
|
2
2
|
import Columns, {ColumnRef} from "./columns"
|
|
3
|
+
import {TableRef} from "./tables"
|
|
3
4
|
|
|
4
5
|
export type ColumnValidationError = {
|
|
5
6
|
message: string
|
|
@@ -32,6 +33,7 @@ function validateQuery(query: Query): QueryClientValidation {
|
|
|
32
33
|
const usedNames: Set<string> = new Set<string>()
|
|
33
34
|
|
|
34
35
|
let isGrouped = false
|
|
36
|
+
const groupedTables: Set<TableRef> = new Set()
|
|
35
37
|
Queries.eachColumn(query, (table, col) => {
|
|
36
38
|
// clear the errors
|
|
37
39
|
col.errors = undefined
|
|
@@ -39,6 +41,10 @@ function validateQuery(query: Query): QueryClientValidation {
|
|
|
39
41
|
// determine if there's a _group by_ in the query
|
|
40
42
|
if (col.grouped) {
|
|
41
43
|
isGrouped = true
|
|
44
|
+
if (col.name == 'id') {
|
|
45
|
+
// ungrouped columns on this table are okay
|
|
46
|
+
groupedTables.add(table)
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
// each select name should only be used once
|
|
@@ -50,10 +56,10 @@ function validateQuery(query: Query): QueryClientValidation {
|
|
|
50
56
|
})
|
|
51
57
|
|
|
52
58
|
// if the query is grouped, ensure that all other column refs
|
|
53
|
-
// are either grouped
|
|
59
|
+
// are either grouped, have an aggregate function, or are on a grouped table
|
|
54
60
|
if (isGrouped) {
|
|
55
|
-
Queries.eachColumn(query, (
|
|
56
|
-
if (!col.grouped &&
|
|
61
|
+
Queries.eachColumn(query, (table, col) => {
|
|
62
|
+
if (!col.grouped && Columns.functionType(col.function) != 'aggregate' && !groupedTables.has(table)) {
|
|
57
63
|
addColumnError(col, `<strong>${col.name}</strong> must be grouped or have an aggregate function`)
|
|
58
64
|
}
|
|
59
65
|
})
|