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

Split Query into StructuredQuery and NativeQuery

parent d69813ff
Branches
Tags
No related merge requests found
import Query from "./Query";
export default class NativeQuery extends Query {
isNative() {
return true;
}
}
......@@ -2,52 +2,15 @@
import Database from "./metadata/Database";
import Table from "./metadata/Table";
import Field from "./metadata/Field";
import Question from "./Question";
import Action, { ActionClick } from "./Action";
import { ExpressionDimension } from "./Dimension";
import { TYPE } from "metabase/lib/types";
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 type { DatasetQuery } from "metabase/meta/types/Card";
import type {
StructuredQuery,
Aggregation,
Breakout,
Filter,
LimitClause,
OrderBy
} from "metabase/meta/types/Query";
import type {
Metadata as MetadataObject,
FieldMetadata,
TableMetadata
} from "metabase/meta/types/Metadata";
import type { Metadata as MetadataObject } from "metabase/meta/types/Metadata";
import Dimension from "metabase-lib/lib/Dimension";
// TODO: replace this with a list of Dimension objects
type FieldOptions = {
count: 0,
dimensions: Dimension[],
fks: Array<{
field: FieldMetadata,
dimensions: Dimension[]
}>
};
/**
* This is a wrapper around a single MBQL or Native query
*/
......@@ -63,264 +26,25 @@ export default class Query {
}
isStructured(): boolean {
return this._datasetQuery.type === "query";
return false;
}
isNative(): boolean {
return this._datasetQuery.type === "native";
return false;
}
// legacy
tableMetadata(): ?TableMetadata {
if (this.isStructured()) {
// $FlowFixMe
return this._metadata.tables[this._datasetQuery.query.source_table];
}
}
// datasetQuery
datasetQuery(): DatasetQuery {
return this._datasetQuery;
}
query(): StructuredQuery {
// $FlowFixMe
return this._datasetQuery.query;
}
isEditable(): boolean {
return !!this.tableMetadata();
dimensions(): Dimension[] {
return [];
}
setDatabase(database: Database) {}
setTable(table: Table) {}
// AGGREGATIONS
aggregations(): Aggregation[] {
return Q.getAggregations(this.query());
}
aggregationOptions(): any[] {
return [];
}
canAddAggregation(): boolean {
return false;
}
canRemoveAggregation(): boolean {
return this.aggregations().length > 1;
}
isBareRows(): boolean {
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);
}
updateAggregation(index: number, aggregation: Aggregation) {
return this._updateQuery(Q.updateAggregation, arguments);
}
removeAggregation(index: number) {
return this._updateQuery(Q.removeAggregation, arguments);
}
clearAggregations() {
return this._updateQuery(Q.clearAggregations, arguments);
}
// BREAKOUTS
breakouts(): Breakout[] {
return Q.getBreakouts(this.query());
}
breakoutOptions(breakout?: any): FieldOptions {
const fieldOptions = {
count: 0,
fks: [],
dimensions: []
};
const table = this.tableMetadata();
if (table) {
// the set of field ids being used by other breakouts
const usedFields = new Set(
this.breakouts()
.filter(b => !_.isEqual(b, breakout))
.map(b => Q_deprecated.getFieldTargetId(b))
);
const dimensionFilter = dimension => {
const field = dimension.field && dimension.field();
return !field ||
(field.isDimension() && !usedFields.has(field.id));
};
for (const dimension of this.dimensions().filter(dimensionFilter)) {
const field = dimension.field && dimension.field();
if (field && field.isFK()) {
const fkDimensions = dimension
.dimensions()
.filter(dimensionFilter);
if (fkDimensions.length > 0) {
fieldOptions.count += fkDimensions.length;
fieldOptions.fks.push({
field: field,
dimension: dimension,
dimensions: fkDimensions
});
}
}
// else {
fieldOptions.count++;
fieldOptions.dimensions.push(dimension);
// }
}
}
return fieldOptions;
}
canAddBreakout(): boolean {
return this.breakoutOptions().count > 0;
}
addBreakout(breakout: Breakout) {
return this._updateQuery(Q.addBreakout, arguments);
}
updateBreakout(index: number, breakout: Breakout) {
return this._updateQuery(Q.updateBreakout, arguments);
}
removeBreakout(index: number) {
return this._updateQuery(Q.removeBreakout, arguments);
}
clearBreakouts() {
return this._updateQuery(Q.clearBreakouts, arguments);
}
// FILTERS
filters(): Filter[] {
return Q.getFilters(this.query());
}
filterOptions(): FieldOptions {
return { count: 0, dimensions: [], fks: [] };
}
canAddFilter(): boolean {
return Q.canAddFilter(this.query());
}
addFilter(filter: Filter) {
return this._updateQuery(Q.addFilter, arguments);
}
updateFilter(index: number, filter: Filter) {
return this._updateQuery(Q.updateFilter, arguments);
}
removeFilter(index: number) {
return this._updateQuery(Q.removeFilter, arguments);
}
clearFilters() {
return this._updateQuery(Q.clearFilters, arguments);
}
// SORTS
// TODO: standardize SORT vs ORDER_BY terminology
sorts(): OrderBy[] {
return [];
}
sortOptions(): any[] {
return [];
}
canAddSort(): boolean {
return false;
}
addOrderBy(order_by: OrderBy) {
return this._updateQuery(Q.addOrderBy, arguments);
}
updateOrderBy(index: number, order_by: OrderBy) {
return this._updateQuery(Q.updateOrderBy, arguments);
}
removeOrderBy(index: number) {
return this._updateQuery(Q.removeOrderBy, arguments);
}
clearOrderBy() {
return this._updateQuery(Q.clearOrderBy, arguments);
}
// LIMIT
updateLimit(limit: LimitClause) {
return this._updateQuery(Q.updateLimit, arguments);
}
clearLimit() {
return this._updateQuery(Q.clearLimit, arguments);
}
// EXPRESSIONS
expressions(): { [key: string]: any } {
return Q.getExpressions(this.query());
}
// DIMENSIONS
dimensions(): Dimension[] {
return [...this.expressionDimensions(), ...this.tableDimensions()];
}
tableDimensions(): Dimension[] {
// $FlowFixMe
const table: Table = this.tableMetadata();
return table ? table.dimensions() : [];
}
expressionDimensions(): Dimension[] {
return Object.entries(this.expressions()).map(([
expressionName,
expression
]) => {
return new ExpressionDimension(null, [expressionName]);
});
}
// NATIVE QUERY
getNativeQuery(): string {
......@@ -356,20 +80,4 @@ export default class Query {
update(fn: (datasetQuery: DatasetQuery) => void) {
return fn(this.datasetQuery());
}
// INTERNAL
_updateQuery(
updateFunction: (
query: StructuredQuery,
...args: any[]
) => StructuredQuery,
args: any[]
) {
return new Query(
this._question,
updateIn(this._datasetQuery, ["query"], query =>
updateFunction(query, ...args))
);
}
}
......@@ -13,6 +13,9 @@ 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 StructuredQuery from "./StructuredQuery";
import NativeQuery from "./NativeQuery";
import * as Q from "metabase/lib/query/query";
import { updateIn } from "icepick";
......@@ -40,40 +43,59 @@ export default class Question {
constructor(metadata: MetadataObject, card: CardObject) {
this._metadata = metadata;
this._card = card;
const aggregations = Q.getAggregations(card.dataset_query.query);
const aggregations = card.dataset_query.type === "query"
? Q.getAggregations(card.dataset_query.query)
: [];
if (aggregations.length > 1) {
// TODO: real multiple metric persistence
this._queries = aggregations.map(
aggregation =>
new Query(this, {
...card.dataset_query,
query: Q.addAggregation(
Q.clearAggregations(card.dataset_query.query),
aggregation
)
})
);
this._queries = aggregations.map(aggregation =>
this.createQuery({
...card.dataset_query,
query: Q.addAggregation(
Q.clearAggregations(card.dataset_query.query),
aggregation
)
}));
} else {
this._queries = [new Query(this, card.dataset_query)];
this._queries = [this.createQuery(card.dataset_query)];
}
}
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]
);
createQuery(datasetQuery: DatasetQuery): Query {
if (datasetQuery.type === "query") {
return new StructuredQuery(this, datasetQuery);
} else if (datasetQuery.type === "native") {
return new NativeQuery(this, datasetQuery);
}
return new Question(this._metadata, {
...this._card,
dataset_query: {
...newQuery.datasetQuery(),
query: query
throw new Error("Unknown query type: " + datasetQuery.type);
}
updateQuery(index: number, newQuery: Query): Question {
if (newQuery.isStructured()) {
// 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
}
});
} else {
return new Question(this._metadata, {
...this._card,
dataset_query: newQuery.datasetQuery()
});
}
}
card() {
......
import Query from "./Query";
import { ExpressionDimension } from "./Dimension";
import { TYPE } from "metabase/lib/types";
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 type {
StructuredQuery as StructuredQueryObject,
Aggregation,
Breakout,
Filter,
LimitClause,
OrderBy
} from "metabase/meta/types/Query";
import type {
FieldMetadata,
TableMetadata
} from "metabase/meta/types/Metadata";
import Dimension from "metabase-lib/lib/Dimension";
// TODO: replace this with a list of Dimension objects
type FieldOptions = {
count: 0,
dimensions: Dimension[],
fks: Array<{
field: FieldMetadata,
dimensions: Dimension[]
}>
};
export default class StructuredQuery extends Query {
isStructured(): boolean {
return true;
}
isEditable(): boolean {
return !!this.tableMetadata();
}
query(): StructuredQueryObject {
// $FlowFixMe
return this._datasetQuery.query;
}
// legacy
tableMetadata(): ?TableMetadata {
if (this.isStructured()) {
// $FlowFixMe
return this._metadata.tables[this._datasetQuery.query.source_table];
}
}
// AGGREGATIONS
aggregations(): Aggregation[] {
return Q.getAggregations(this.query());
}
aggregationOptions(): any[] {
return [];
}
canAddAggregation(): boolean {
return false;
}
canRemoveAggregation(): boolean {
return this.aggregations().length > 1;
}
isBareRows(): boolean {
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);
}
updateAggregation(index: number, aggregation: Aggregation) {
return this._updateQuery(Q.updateAggregation, arguments);
}
removeAggregation(index: number) {
return this._updateQuery(Q.removeAggregation, arguments);
}
clearAggregations() {
return this._updateQuery(Q.clearAggregations, arguments);
}
// BREAKOUTS
breakouts(): Breakout[] {
return Q.getBreakouts(this.query());
}
breakoutOptions(breakout?: any): FieldOptions {
const fieldOptions = {
count: 0,
fks: [],
dimensions: []
};
const table = this.tableMetadata();
if (table) {
// the set of field ids being used by other breakouts
const usedFields = new Set(
this.breakouts()
.filter(b => !_.isEqual(b, breakout))
.map(b => Q_deprecated.getFieldTargetId(b))
);
const dimensionFilter = dimension => {
const field = dimension.field && dimension.field();
return !field ||
(field.isDimension() && !usedFields.has(field.id));
};
for (const dimension of this.dimensions().filter(dimensionFilter)) {
const field = dimension.field && dimension.field();
if (field && field.isFK()) {
const fkDimensions = dimension
.dimensions()
.filter(dimensionFilter);
if (fkDimensions.length > 0) {
fieldOptions.count += fkDimensions.length;
fieldOptions.fks.push({
field: field,
dimension: dimension,
dimensions: fkDimensions
});
}
}
// else {
fieldOptions.count++;
fieldOptions.dimensions.push(dimension);
// }
}
}
return fieldOptions;
}
canAddBreakout(): boolean {
return this.breakoutOptions().count > 0;
}
addBreakout(breakout: Breakout) {
return this._updateQuery(Q.addBreakout, arguments);
}
updateBreakout(index: number, breakout: Breakout) {
return this._updateQuery(Q.updateBreakout, arguments);
}
removeBreakout(index: number) {
return this._updateQuery(Q.removeBreakout, arguments);
}
clearBreakouts() {
return this._updateQuery(Q.clearBreakouts, arguments);
}
// FILTERS
filters(): Filter[] {
return Q.getFilters(this.query());
}
filterOptions(): FieldOptions {
return { count: 0, dimensions: [], fks: [] };
}
canAddFilter(): boolean {
return Q.canAddFilter(this.query());
}
addFilter(filter: Filter) {
return this._updateQuery(Q.addFilter, arguments);
}
updateFilter(index: number, filter: Filter) {
return this._updateQuery(Q.updateFilter, arguments);
}
removeFilter(index: number) {
return this._updateQuery(Q.removeFilter, arguments);
}
clearFilters() {
return this._updateQuery(Q.clearFilters, arguments);
}
// SORTS
// TODO: standardize SORT vs ORDER_BY terminology
sorts(): OrderBy[] {
return [];
}
sortOptions(): any[] {
return [];
}
canAddSort(): boolean {
return false;
}
addOrderBy(order_by: OrderBy) {
return this._updateQuery(Q.addOrderBy, arguments);
}
updateOrderBy(index: number, order_by: OrderBy) {
return this._updateQuery(Q.updateOrderBy, arguments);
}
removeOrderBy(index: number) {
return this._updateQuery(Q.removeOrderBy, arguments);
}
clearOrderBy() {
return this._updateQuery(Q.clearOrderBy, arguments);
}
// LIMIT
updateLimit(limit: LimitClause) {
return this._updateQuery(Q.updateLimit, arguments);
}
clearLimit() {
return this._updateQuery(Q.clearLimit, arguments);
}
// EXPRESSIONS
expressions(): { [key: string]: any } {
return Q.getExpressions(this.query());
}
// DIMENSIONS
dimensions(): Dimension[] {
return [...this.expressionDimensions(), ...this.tableDimensions()];
}
tableDimensions(): Dimension[] {
// $FlowFixMe
const table: Table = this.tableMetadata();
return table ? table.dimensions() : [];
}
expressionDimensions(): Dimension[] {
return Object.entries(this.expressions()).map(([
expressionName,
expression
]) => {
return new ExpressionDimension(null, [expressionName]);
});
}
// INTERNAL
_updateQuery(
updateFunction: (
query: StructuredQueryObject,
...args: any[]
) => StructuredQueryObject,
args: any[]
) {
return new StructuredQuery(
this._question,
updateIn(this._datasetQuery, ["query"], query =>
updateFunction(query, ...args))
);
}
}
......@@ -206,7 +206,7 @@ export default class QueryBuilder extends Component {
class LegacyQueryBuilder extends Component {
render() {
const { card, isDirty, databases, uiControls, mode } = this.props;
const { query, card, isDirty, databases, uiControls, mode } = this.props;
// if we don't have a card at all or no databases then we are initializing, so keep it simple
if (!card || !databases) {
......@@ -226,20 +226,20 @@ class LegacyQueryBuilder extends Component {
</div>
<div id="react_qb_editor" className="z2 hide sm-show">
{ card && card.dataset_query && card.dataset_query.type === "native" ?
{ query.isNative() ?
<NativeQueryEditor
{...this.props}
isOpen={!card.dataset_query.native.query || isDirty}
datasetQuery={card && card.dataset_query}
/>
:
: query.isStructured() ?
<div className="wrapper">
<GuiQueryEditor
{...this.props}
datasetQuery={card && card.dataset_query}
/>
</div>
}
: null }
</div>
<div ref="viz" id="react_qb_viz" className="flex z1" style={{ "transition": "opacity 0.25s ease-in-out" }}>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment