Skip to content

Commit

Permalink
Merge pull request #14928 from Budibase/allow-fields-on-counts
Browse files Browse the repository at this point in the history
Allow fields on count calculations.
  • Loading branch information
samwho authored Oct 31, 2024
2 parents 20ed18a + 945cf52 commit e3b4998
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 43 deletions.
9 changes: 8 additions & 1 deletion packages/backend-core/src/sql/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,14 @@ class InternalBuilder {
)
}
} else {
query = query.count(`* as ${aggregation.name}`)
if (this.client === SqlClient.ORACLE) {
const field = this.convertClobs(`${tableName}.${aggregation.field}`)
query = query.select(
this.knex.raw(`COUNT(??) as ??`, [field, aggregation.name])
)
} else {
query = query.count(`${aggregation.field} as ${aggregation.name}`)
}
}
} else {
const fieldSchema = this.getFieldSchema(aggregation.field)
Expand Down
30 changes: 18 additions & 12 deletions packages/server/src/api/controllers/view/viewsV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
RelationSchemaField,
ViewFieldMetadata,
CalculationType,
CountDistinctCalculationFieldMetadata,
CountCalculationFieldMetadata,
} from "@budibase/types"
import { builderSocket, gridSocket } from "../../../websockets"
import { helpers } from "@budibase/shared-core"
Expand All @@ -22,27 +24,31 @@ function stripUnknownFields(
if (helpers.views.isCalculationField(field)) {
if (field.calculationType === CalculationType.COUNT) {
if ("distinct" in field && field.distinct) {
return {
order: field.order,
width: field.width,
visible: field.visible,
readonly: field.readonly,
icon: field.icon,
distinct: field.distinct,
calculationType: field.calculationType,
field: field.field,
columns: field.columns,
}
const strippedField: RequiredKeys<CountDistinctCalculationFieldMetadata> =
{
order: field.order,
width: field.width,
visible: field.visible,
readonly: field.readonly,
icon: field.icon,
distinct: field.distinct,
calculationType: field.calculationType,
field: field.field,
columns: field.columns,
}
return strippedField
} else {
return {
const strippedField: RequiredKeys<CountCalculationFieldMetadata> = {
order: field.order,
width: field.width,
visible: field.visible,
readonly: field.readonly,
icon: field.icon,
calculationType: field.calculationType,
field: field.field,
columns: field.columns,
}
return strippedField
}
}
const strippedField: RequiredKeys<ViewCalculationFieldMetadata> = {
Expand Down
84 changes: 77 additions & 7 deletions packages/server/src/api/routes/tests/viewV2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,12 @@ describe.each([
calculationType: CalculationType.COUNT,
field: "Price",
},
countDistinct: {
visible: true,
calculationType: CalculationType.COUNT,
distinct: true,
field: "Price",
},
min: {
visible: true,
calculationType: CalculationType.MIN,
Expand All @@ -708,11 +714,6 @@ describe.each([
calculationType: CalculationType.AVG,
field: "Price",
},
sum2: {
visible: true,
calculationType: CalculationType.SUM,
field: "Price",
},
},
},
{
Expand Down Expand Up @@ -763,18 +764,20 @@ describe.each([
count: {
visible: true,
calculationType: CalculationType.COUNT,
field: "Price",
},
count2: {
visible: true,
calculationType: CalculationType.COUNT,
field: "Price",
},
},
},
{
status: 400,
body: {
message:
'Duplicate calculation on field "*", calculation type "count"',
'Duplicate calculation on field "Price", calculation type "count"',
},
}
)
Expand Down Expand Up @@ -805,7 +808,7 @@ describe.each([
status: 400,
body: {
message:
'Duplicate calculation on field "Price", calculation type "count"',
'Duplicate calculation on field "Price", calculation type "count distinct"',
},
}
)
Expand All @@ -820,6 +823,7 @@ describe.each([
count: {
visible: true,
calculationType: CalculationType.COUNT,
field: "Price",
},
count2: {
visible: true,
Expand All @@ -831,6 +835,26 @@ describe.each([
})
})

it("does not confuse counts on different fields in the duplicate check", async () => {
await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
count: {
visible: true,
calculationType: CalculationType.COUNT,
field: "Price",
},
count2: {
visible: true,
calculationType: CalculationType.COUNT,
field: "Category",
},
},
})
})

!isLucene &&
it("does not get confused when a calculation field shadows a basic one", async () => {
const table = await config.api.table.save(
Expand Down Expand Up @@ -1607,6 +1631,7 @@ describe.each([
view.schema!.count = {
visible: true,
calculationType: CalculationType.COUNT,
field: "age",
}
await config.api.viewV2.update(view)

Expand Down Expand Up @@ -3761,6 +3786,51 @@ describe.each([
expect(rows[0].sum).toEqual(55)
})

it("should be able to count non-numeric fields", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
firstName: {
type: FieldType.STRING,
name: "firstName",
},
lastName: {
type: FieldType.STRING,
name: "lastName",
},
},
})
)

const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
type: ViewV2Type.CALCULATION,
schema: {
count: {
visible: true,
calculationType: CalculationType.COUNT,
field: "firstName",
},
},
})

await config.api.row.bulkImport(table._id!, {
rows: [
{ firstName: "Jane", lastName: "Smith" },
{ firstName: "Jane", lastName: "Doe" },
{ firstName: "Alice", lastName: "Smith" },
],
})

const { rows } = await config.api.viewV2.search(view.id, {
query: {},
})

expect(rows).toHaveLength(1)
expect(rows[0].count).toEqual(3)
})

it("should be able to filter rows on the view itself", async () => {
const table = await config.api.table.save(
saveTableRequest({
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/sdk/app/rows/search/internal/sqs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ export async function search(
aggregations.push({
name: key,
calculationType: field.calculationType,
field: mapToUserColumn(field.field),
})
}
} else {
Expand Down
78 changes: 57 additions & 21 deletions packages/server/src/sdk/app/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,42 +61,77 @@ export function isView(view: any): view is ViewV2 {
return view.id && docIds.isViewId(view.id) && view.version === 2
}

async function guardCalculationViewSchema(
table: Table,
view: Omit<ViewV2, "id" | "version">
) {
function guardDuplicateCalculationFields(view: Omit<ViewV2, "id" | "version">) {
const seen: Record<string, Record<CalculationType, boolean>> = {}
const calculationFields = helpers.views.calculationFields(view)
for (const name of Object.keys(calculationFields)) {
const schema = calculationFields[name]
const isCount = schema.calculationType === CalculationType.COUNT
const isDistinct = "distinct" in schema

if (Object.keys(calculationFields).length > 5) {
throw new HTTPError(
"Calculation views can only have a maximum of 5 fields",
400
)
if (isCount && isDistinct) {
// We do these separately.
continue
}

if (seen[schema.field]?.[schema.calculationType]) {
throw new HTTPError(
`Duplicate calculation on field "${schema.field}", calculation type "${schema.calculationType}"`,
400
)
}
seen[schema.field] ??= {} as Record<CalculationType, boolean>
seen[schema.field][schema.calculationType] = true
}
}

function guardDuplicateCountDistinctFields(
view: Omit<ViewV2, "id" | "version">
) {
const seen: Record<string, Record<CalculationType, boolean>> = {}

const calculationFields = helpers.views.calculationFields(view)
for (const name of Object.keys(calculationFields)) {
const schema = calculationFields[name]
const isCount = schema.calculationType === CalculationType.COUNT
const isDistinct = isCount && "distinct" in schema && schema.distinct
if (!isCount) {
continue
}

const field = isCount && !isDistinct ? "*" : schema.field
if (seen[field]?.[schema.calculationType]) {
const isDistinct = "distinct" in schema
if (!isDistinct) {
// We do these separately.
continue
}

if (seen[schema.field]?.[schema.calculationType]) {
throw new HTTPError(
`Duplicate calculation on field "${field}", calculation type "${schema.calculationType}"`,
`Duplicate calculation on field "${schema.field}", calculation type "${schema.calculationType} distinct"`,
400
)
}
seen[field] ??= {} as Record<CalculationType, boolean>
seen[field][schema.calculationType] = true
seen[schema.field] ??= {} as Record<CalculationType, boolean>
seen[schema.field][schema.calculationType] = true
}
}

// Count fields that aren't distinct don't need to reference another field,
// so we don't validate it.
if (isCount && !isDistinct) {
continue
}
async function guardCalculationViewSchema(
table: Table,
view: Omit<ViewV2, "id" | "version">
) {
const calculationFields = helpers.views.calculationFields(view)

guardDuplicateCalculationFields(view)
guardDuplicateCountDistinctFields(view)

if (Object.keys(calculationFields).length > 5) {
throw new HTTPError(
"Calculation views can only have a maximum of 5 fields",
400
)
}

for (const name of Object.keys(calculationFields)) {
const schema = calculationFields[name]
if (!schema.field) {
throw new HTTPError(
`Calculation field "${name}" is missing a "field" property`,
Expand All @@ -112,6 +147,7 @@ async function guardCalculationViewSchema(
)
}

const isCount = schema.calculationType === CalculationType.COUNT
if (!isCount && !isNumeric(targetSchema.type)) {
throw new HTTPError(
`Calculation field "${name}" references field "${schema.field}" which is not a numeric field`,
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/documents/app/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export interface NumericCalculationFieldMetadata

export interface CountCalculationFieldMetadata extends BasicViewFieldMetadata {
calculationType: CalculationType.COUNT
field: string
}

export interface CountDistinctCalculationFieldMetadata
extends CountCalculationFieldMetadata {
distinct: true
field: string
}

export type ViewCalculationFieldMetadata =
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/sdk/row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ export interface NumericAggregation extends BaseAggregation {

export interface CountAggregation extends BaseAggregation {
calculationType: CalculationType.COUNT
field: string
}

export interface CountDistinctAggregation extends CountAggregation {
distinct: true
field: string
}

export type Aggregation =
Expand Down

0 comments on commit e3b4998

Please sign in to comment.