Skip to content
Snippets Groups Projects
Unverified Commit 2b9ee08a authored by Tom Robinson's avatar Tom Robinson
Browse files

Add multiple metrics functions

parent a310b63d
Branches
Tags
No related merge requests found
......@@ -4,15 +4,19 @@ import Database from "./metadata/Database";
import Table from "./metadata/Table";
import Question from "./Question";
import Dimension from "./Dimension";
import Action, { ActionClick } from "./Action";
import * as Q from "metabase/lib/query/query";
import Q_deprecated, {
AggregationClause,
NamedClause
} from "metabase/lib/query";
import { format as formatExpression } from "metabase/lib/expressions/formatter";
import { getAggregator } from "metabase/lib/schema_metadata";
import _ from "underscore";
import { updateIn } from "icepick";
import Q_deprecated from "metabase/lib/query";
import * as Q from "metabase/lib/query/query";
import type { DatasetQuery } from "metabase/meta/types/Card";
import type {
StructuredQuery,
......@@ -103,6 +107,45 @@ export default class Query {
return Q.isBareRows(this.query());
}
aggregationName(index: number = 0): string {
if (this.isStructured()) {
const aggregation = this.aggregations()[0];
if (NamedClause.isNamed(aggregation)) {
return NamedClause.getName(aggregation);
} else if (AggregationClause.isCustom(aggregation)) {
return formatExpression(aggregation, {
tableMetadata: this.tableMetadata(),
customFields: this.expressions()
});
} else if (AggregationClause.isMetric(aggregation)) {
const metricId = AggregationClause.getMetric(aggregation);
const metric = this._metadata.metrics[metricId];
if (metric) {
return metric.name;
}
} else {
const selectedAggregation = getAggregator(
AggregationClause.getOperator(aggregation)
);
if (selectedAggregation) {
let aggregationName = selectedAggregation.name.replace(
" of ...",
""
);
const fieldId = Q_deprecated.getFieldTargetId(
AggregationClause.getField(aggregation)
);
const field = fieldId && this._metadata.fields[fieldId];
if (field) {
aggregationName += " of " + field.display_name;
}
return aggregationName;
}
}
}
return "";
}
addAggregation(aggregation: Aggregation) {
return this._updateQuery(Q.addAggregation, arguments);
}
......@@ -253,6 +296,7 @@ export default class Query {
* Query is valid (as far as we know) and can be executed
*/
canRun(): boolean {
// TODO:
return false;
}
......
import Query from "./Query";
const FIELD = {
id: 1,
display_name: "Field"
};
const _metadata = {
fields: {
1: FIELD
},
metrics: {
1: {
name: "Metric"
}
},
tables: {
1: {
fields: [FIELD]
}
}
};
const makeQuery = agg => ({
type: "query",
database: 1,
query: {
source_table: 1,
aggregation: [agg]
}
});
describe("Query", () => {
describe("aggregationName", () => {
it("should return a saved metric's name", () => {
expect(
new Query(
{ _metadata },
makeQuery(["METRIC", 1])
).aggregationName()
).toBe("Metric");
});
it("should return a standard aggregation name", () => {
expect(
new Query({ _metadata }, makeQuery(["count"])).aggregationName()
).toBe("Count of rows");
});
it("should return a standard aggregation name with field", () => {
expect(
new Query(
{ _metadata },
makeQuery(["sum", ["field-id", 1]])
).aggregationName()
).toBe("Sum of Field");
});
xit("should return a standard aggregation name with fk field", () => {
expect(
new Query(
{ _metadata },
makeQuery(["sum", ["fk", 2, 3]])
).aggregationName()
).toBe("Sum of Field");
});
it("should return a custom expression description", () => {
expect(
new Query(
{ _metadata },
makeQuery(["+", 1, ["sum", ["field-id", 1]]])
).aggregationName()
).toBe("1 + Sum(Field)");
});
it("should return a named expression name", () => {
expect(
new Query(
{ _metadata },
makeQuery(["named", ["sum", ["field-id", 1]], "Named"])
).aggregationName()
).toBe("Named");
});
});
});
......@@ -13,6 +13,10 @@ import type { ParameterId } from "metabase/meta/types/Parameter";
import type { Metadata as MetadataObject } from "metabase/meta/types/Metadata";
import type { Card as CardObject } from "metabase/meta/types/Card";
import * as Q from "metabase/lib/query/query";
import { updateIn } from "icepick";
// TODO: move these
type DownloadFormat = "csv" | "json" | "xlsx";
type RevisionId = number;
......@@ -36,7 +40,39 @@ export default class Question {
constructor(metadata: MetadataObject, card: CardObject) {
this._metadata = metadata;
this._card = card;
this._queries = [new Query(this, card.dataset_query)];
// TODO: real multiple metric persistence
this._queries = Q.getAggregations(card.dataset_query.query).map(
aggregation =>
new Query(this, {
...card.dataset_query,
query: Q.addAggregation(
Q.clearAggregations(card.dataset_query.query),
aggregation
)
})
);
}
updateQuery(index: number, newQuery: Query): Question {
// TODO: real multiple metric persistence
let query = Q.clearAggregations(newQuery.query());
for (let i = 0; i < this._queries.length; i++) {
query = Q.addAggregation(
query,
(i === index ? newQuery : this._queries[i]).aggregations()[0]
);
}
return new Question(this._metadata, {
...this._card,
dataset_query: {
...newQuery.datasetQuery(),
query: query
}
});
}
card() {
return this._card;
}
/**
......@@ -58,41 +94,77 @@ export default class Question {
return true;
}
metrics(): Query[] {
return this._queries;
}
availableMetrics(): MetricMetadata[] {
return Object.values(this._metadata.metrics);
}
canAddMetric(): boolean {
// only structured queries with 0 or 1 breakouts can have multiple series
return this.query().isStructured() &&
this.query().breakouts().length <= 1;
}
canRemoveMetric(): boolean {
// can't remove last metric
return this.metrics().length > 1;
}
addSavedMetric(metric: Metric): Question {
return this.addMetric({
type: "query",
database: metric.table.db.id,
query: {
aggregation: ["METRIC", metric.id]
}
});
}
addMetric(datasetQuery: DatasetQuery): Question {
// TODO: multiple metrics persistence
return new Question(
this._metadata,
updateIn(this.card(), ["dataset_query", "query"], query =>
Q.addAggregation(
query,
Q.getAggregations(datasetQuery.query)[0]
))
);
}
removeMetric(index: number): Question {
// TODO: multiple metrics persistence
return new Question(
this._metadata,
updateIn(this.card(), ["dataset_query", "query"], query =>
Q.removeAggregation(query, index))
);
}
// multiple series can be pivoted
breakouts(): Breakout[] {
return [];
// TODO: real multiple metric persistence
return this.query().breakouts();
}
breakoutDimensions(unused: boolean = false): Dimension[] {
return [];
// TODO: real multiple metric persistence
return this.query().breakoutDimensions();
}
canAddBreakout(): boolean {
return false;
return this.breakouts() === 0;
}
// multiple series can be filtered by shared dimensions
filters(): Filter[] {
return [];
// TODO: real multiple metric persistence
return this.query().filters();
}
filterableDimensions(): Dimension[] {
return [];
// TODO: real multiple metric persistence
return this.query().filterableDimensions();
}
canAddFilter(): boolean {
return false;
}
// canAddMetric(): boolean {
// return false;
// }
// addMetric(datasetQuery: DatasetQuery, dimensionMapping: DimensionMapping): void {
// }
// getMetrics(): Query[] {
// return this.queries;
// }
// removeMetric(metricId: number) {
// }
// remapMetricDimension(metricID, newDimensionMapping: DimensionMapping) {
// }
// top-level actions
actions(): Action[] {
// if this is a single query question, the top level actions are
......
import Question from "./Question";
const METADATA = {};
const METRIC = {
id: 123,
table: {
db: {
id: 1
}
}
};
const CARD_WITH_ONE_METRIC = {
dataset_query: {
type: "query",
query: {
aggregation: [["count"]]
}
}
};
const CARD_WITH_TWO_METRICS = {
dataset_query: {
type: "query",
query: {
aggregation: [["count"], ["sum", ["field-id", 1]]]
}
}
};
describe("Question", () => {
it("work with one metric", () => {
const question = new Question(METADATA, CARD_WITH_ONE_METRIC);
expect(question.metrics()).toHaveLength(1);
expect(question.card()).toEqual({
dataset_query: {
type: "query",
query: {
aggregation: [["count"]]
}
}
});
});
it("should add a new custom metric", () => {
let question = new Question(METADATA, CARD_WITH_ONE_METRIC);
question = question.addMetric({
type: "query",
query: {
aggregation: [["sum", ["field-id", 1]]]
}
});
expect(question.metrics()).toHaveLength(2);
expect(question.card()).toEqual({
dataset_query: {
type: "query",
query: {
aggregation: [["count"], ["sum", ["field-id", 1]]]
}
}
});
});
it("should add a new saved metric", () => {
let question = new Question(METADATA, CARD_WITH_ONE_METRIC);
question = question.addSavedMetric(METRIC);
expect(question.metrics()).toHaveLength(2);
expect(question.card()).toEqual({
dataset_query: {
type: "query",
query: {
aggregation: [["count"], ["METRIC", 123]]
}
}
});
});
it("should remove a metric", () => {
let question = new Question(METADATA, CARD_WITH_TWO_METRICS);
question = question.removeMetric(0);
expect(question.metrics()).toHaveLength(1);
expect(question.card()).toEqual({
dataset_query: {
type: "query",
query: {
aggregation: [["sum", ["field-id", 1]]]
}
}
});
});
it("should add a filter", () => {
let question = new Question(METADATA, CARD_WITH_TWO_METRICS);
const query = question
.metrics()[0]
.addFilter(["=", ["field-id", 1], 42]);
question = question.updateQuery(0, query);
expect(question.metrics()).toHaveLength(2);
expect(question.card()).toEqual({
dataset_query: {
type: "query",
query: {
filter: ["=", ["field-id", 1], 42],
aggregation: [["count"], ["sum", ["field-id", 1]]]
}
}
});
});
describe("canRun", () => {
it("return false when a single metric can't run", () => {
const question = new Question(METADATA, CARD_WITH_ONE_METRIC);
question._queries[0].canRun = () => false;
expect(question.canRun()).toBe(false);
});
it("return true when a single metric can run", () => {
const question = new Question(METADATA, CARD_WITH_ONE_METRIC);
question._queries[0].canRun = () => true;
expect(question.canRun()).toBe(true);
});
it("return false when one of two metrics can't run", () => {
const question = new Question(METADATA, CARD_WITH_TWO_METRICS);
question._queries[0].canRun = () => true;
question._queries[1].canRun = () => false;
expect(question.canRun()).toBe(false);
});
it("return true when both metrics can run", () => {
const question = new Question(METADATA, CARD_WITH_TWO_METRICS);
question._queries[0].canRun = () => true;
question._queries[1].canRun = () => true;
expect(question.canRun()).toBe(true);
});
});
});
......@@ -120,9 +120,9 @@ function setExpressionClause(query: SQ, expressionClause: ?ExpressionClause): SQ
return setClause("expressions", query, expressionClause);
}
// TODO: remove mutation
type FilterClauseName = "filter"|"aggregation"|"breakout"|"order_by"|"limit"|"expressions";
function setClause(clauseName: FilterClauseName, query: SQ, clause: ?any): SQ {
query = { ...query };
if (clause == null) {
delete query[clauseName];
} else {
......
import _ from "underscore";
import { isa, isFK, TYPE } from "metabase/lib/types";
import { isa, isFK as isTypeFK, isPK as isTypePK, TYPE } from "metabase/lib/types";
// primary field types used for picking operators, etc
export const NUMBER = "NUMBER";
......@@ -108,6 +108,9 @@ export const isCategory = isFieldType.bind(null, CATEGORY);
export const isDimension = (col) => (col && col.source !== "aggregation");
export const isMetric = (col) => (col && col.source !== "breakout") && isSummable(col);
export const isFK = (field) => field && isTypeFK(field.special_type);
export const isPK = (field) => field && isTypePK(field.special_type);
export const isAny = (col) => true;
export const isNumericBaseType = (field) => isa(field && field.base_type, TYPE.Number);
......@@ -550,7 +553,7 @@ export function computeMetadataStrength(table) {
table.fields.forEach(function(field) {
score(field.description);
score(field.special_type);
if (isFK(field.special_type)) {
if (isFK(field)) {
score(field.target);
}
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment