Skip to content

Commit

Permalink
refactor: introduce getReferencedModels function which returns refere…
Browse files Browse the repository at this point in the history
…nced models for each column, dimension or metric

This is a preperation for calculated metrics and dimension (which can reference
coulumns/dimensions/metrics from multiple models)
  • Loading branch information
retro committed Sep 1, 2024
1 parent 9014b5f commit 37a37f2
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 225 deletions.
16 changes: 16 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2588,5 +2588,21 @@ describe("semantic layer", async () => {
},
]);
});
it("can query only a metric that only references a metric", async () => {
const query = queryBuilder.buildQuery({
members: ["store_sales.median_sales"],
});

const result = await client.query<InferSqlQueryResultType<typeof query>>(
query.sql,
query.bindings,
);

assert.deepEqual(result.rows, [
{
store_sales___median_sales: 90,
},
]);
});
});
});
10 changes: 10 additions & 0 deletions src/lib/model/basic-dimension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import {
TemporalGranularityByDimensionType,
} from "../types.js";

import invariant from "tiny-invariant";
import { AnyBaseDialect } from "../dialect/base.js";
import { AnyModel } from "../model.js";
import { AnyRepository } from "../repository.js";
import { SqlFragment } from "../sql-builder.js";
import { isNonEmptyArray } from "../util.js";

export interface DimensionSqlFnArgs<C, DN extends string = string> {
identifier: (name: string) => IdentifierRef;
Expand Down Expand Up @@ -207,4 +209,12 @@ export class BasicDimensionQueryMember extends QueryMember {
);
return [fragment];
}
getReferencedModels() {
const referencedModels = [this.member.model.name];
invariant(
isNonEmptyArray(referencedModels),
`Referenced models not found for ${this.member.getPath()}`,
);
return referencedModels;
}
}
81 changes: 33 additions & 48 deletions src/lib/model/basic-metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import {
QueryMemberCache,
} from "../query-builder/query-plan/query-member.js";
import {
AliasRef,
ColumnRef,
DimensionRef,
IdentifierRef,
MetricAliasRef,
MetricRef,
SqlFn,
} from "../sql-fn.js";

import invariant from "tiny-invariant";
import { Simplify } from "type-fest";
import { AnyBaseDialect } from "../dialect/base.js";
import { AnyModel } from "../model.js";
import { AnyRepository } from "../repository.js";
import { SqlFragment } from "../sql-builder.js";
import { MemberFormat } from "../types.js";
import { isNonEmptyArray } from "../util.js";

export interface MetricSqlFnArgs<
C,
Expand All @@ -26,9 +28,9 @@ export interface MetricSqlFnArgs<
> {
identifier: (name: string) => IdentifierRef;
model: {
column: (name: string) => AliasRef<ColumnRef>;
dimension: (name: DN) => AliasRef<DimensionRef>;
metric: (name: MN) => AliasRef<MetricRef>;
column: (name: string) => MetricAliasRef<ColumnRef>;
dimension: (name: DN) => MetricAliasRef<DimensionRef>;
metric: (name: MN) => MetricAliasRef<MetricRef>;
};
sql: (strings: TemplateStringsArray, ...values: unknown[]) => SqlFn;
getContext: () => C;
Expand Down Expand Up @@ -129,22 +131,22 @@ export class BasicMetricQueryMember extends QueryMember {
name,
this.context,
);
return new AliasRef(getNextRefAlias(), columnRef);
return new MetricAliasRef(getNextRefAlias(), columnRef);
},
dimension: (name: string) => {
const dimensionRef = new DimensionRef(
this.member.model.getDimension(name),
this.context,
);
return new AliasRef(getNextRefAlias(), dimensionRef);
return new MetricAliasRef(getNextRefAlias(), dimensionRef);
},
metric: (name: string) => {
const metricRef = new MetricRef(
this.member,
this.member.model.getMetric(name),
this.context,
);
return new AliasRef(getNextRefAlias(), metricRef);
return new MetricAliasRef(getNextRefAlias(), metricRef);
},
},
sql: (strings, ...values) => new SqlFn([...strings], values),
Expand All @@ -163,31 +165,19 @@ export class BasicMetricQueryMember extends QueryMember {
}
getModelQueryProjection() {
const sqlFnResult = this.sqlFnResult;

const refs: SqlFragment[] = [];
const valuesQueue = [...sqlFnResult.values];

while (valuesQueue.length > 0) {
const value = valuesQueue.shift()!;
if (value instanceof AliasRef) {
const alias = value.alias;
const { sql, bindings } = value.aliasOf.render(
this.repository,
this.queryMembers,
this.dialect,
);

refs.push(
SqlFragment.make({
sql: `${sql} as ${this.dialect.asIdentifier(alias)}`,
bindings,
}),
);
} else if (value instanceof SqlFn) {
valuesQueue.push(...value.values);
}
}
return refs;
const filterFn = (ref: unknown): ref is MetricAliasRef<any> =>
ref instanceof MetricAliasRef;
return sqlFnResult.filterRefs(filterFn).map(({ alias, aliasOf }) => {
const { sql, bindings } = aliasOf.render(
this.repository,
this.queryMembers,
this.dialect,
);
return SqlFragment.make({
sql: `${sql} as ${this.dialect.asIdentifier(alias)}`,
bindings,
});
});
}
getSegmentQueryProjection(_modelQueryAlias: string) {
const { sql, bindings } = this.getSql();
Expand All @@ -209,22 +199,17 @@ export class BasicMetricQueryMember extends QueryMember {
return [fragment];
}
getMetricRefs() {
const sqlFnResult = this.sqlFnResult;
if (sqlFnResult) {
const valuesToProcess: unknown[] = [sqlFnResult];
const refs: MetricRef[] = [];
const filterFn = (ref: unknown): ref is MetricAliasRef<MetricRef> =>
ref instanceof MetricAliasRef && ref.aliasOf instanceof MetricRef;
return this.sqlFnResult.filterRefs(filterFn).map((v) => v.aliasOf);
}

while (valuesToProcess.length > 0) {
const ref = valuesToProcess.pop()!;
if (ref instanceof AliasRef && ref.aliasOf instanceof MetricRef) {
refs.push(ref.aliasOf);
}
if (ref instanceof SqlFn) {
valuesToProcess.push(...ref.values);
}
}
return refs;
}
return [];
getReferencedModels() {
const referencedModels = [this.member.model.name];
invariant(
isNonEmptyArray(referencedModels),
`Referenced models not found for ${this.member.getPath()}`,
);
return referencedModels;
}
}
72 changes: 9 additions & 63 deletions src/lib/query-builder/build-query.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { SqlFragment, SqlQueryBuilder } from "../sql-builder.js";
import { METRIC_REF_SUBQUERY_ALIAS, isNonEmptyArray } from "../util.js";

import invariant from "tiny-invariant";
import { AnyQueryBuilder } from "../query-builder.js";
import type { AnyRepository } from "../repository.js";
import { Order } from "../types.js";
import { METRIC_REF_SUBQUERY_ALIAS } from "../util.js";
import { QueryPlan } from "./query-plan.js";
import { QueryMemberCache } from "./query-plan/query-member.js";

function getAlias(index: number) {
return `q${index}`;
}

function getDefaultOrderBy(
repository: AnyRepository,
query: QueryPlan,
Expand All @@ -33,21 +37,14 @@ function getDefaultOrderBy(
return [];
}

function getAlias(index: number) {
return `q${index}`;
}

function arrayHasAtLeastOneElement<T>(value: T[]): value is [T, ...T[]] {
return value.length > 0;
}

function joinModelQueryModels(
queryBuilder: AnyQueryBuilder,
queryMembers: QueryMemberCache,
context: unknown,
segment: QueryPlan["segments"][number],
sqlQuery: SqlQueryBuilder,
) {
invariant(segment.joinPlan, "Join plan not found");
if (segment.joinPlan.hasRowMultiplication) {
sqlQuery.distinct();
}
Expand Down Expand Up @@ -81,57 +78,6 @@ function joinModelQueryModels(
);
}
}
/*const visitedModels = new Set<string>();
const modelsToProcess: { modelName: string; join?: AnyJoin }[] = [
{ modelName: segment.initialModel },
];
while (modelsToProcess.length > 0) {
const { modelName, join } = modelsToProcess.pop()!;
if (visitedModels.has(modelName)) {
continue;
}
visitedModels.add(modelName);
const unvisitedNeighbors = (
segment.joinGraph.neighbors(modelName) ?? []
).filter((modelName) => !visitedModels.has(modelName));
if (join) {
const joinType = join.reversed ? "rightJoin" : "leftJoin";
const joinOn = join
.joinOnDef(context)
.render(
queryBuilder.repository,
queryMembers,
queryBuilder.dialect,
);
const rightModel = queryBuilder.repository.getModel(join.right);
const joinSubject = rightModel.getTableNameOrSql(
queryBuilder.repository,
queryMembers,
queryBuilder.dialect,
context,
);
sqlQuery[joinType](
joinSubject,
queryBuilder.dialect.fragment(joinOn.sql, joinOn.bindings),
);
// We have a join that is multiplying the rows, so we need to use DISTINCT
if (join.type === "manyToMany" || join.type === "oneToMany") {
sqlQuery.distinct();
}
}
modelsToProcess.push(
...unvisitedNeighbors.map((unvisitedModelName) => ({
modelName: unvisitedModelName,
join: queryBuilder.repository.getJoin(modelName, unvisitedModelName),
})),
);
}*/

function joinModelQueryMetricRefsSubQuery(
queryBuilder: AnyQueryBuilder,
Expand Down Expand Up @@ -274,7 +220,7 @@ function buildRootQuery(
): SqlQueryBuilder {
const segments = queryPlan.segments;

invariant(arrayHasAtLeastOneElement(segments), "No query segments found");
invariant(isNonEmptyArray(segments), "No query segments found");

if (segments.length === 1) {
const sqlQuery = buildSegmentQuery(
Expand All @@ -294,8 +240,8 @@ function buildRootQuery(
}));

invariant(
arrayHasAtLeastOneElement(segmentsWithSqlQuery),
"No segments with sql query found",
isNonEmptyArray(segmentsWithSqlQuery),
"No segments query segments found",
);

const [initialSegmentWithSqlQuery, ...restSegmentsWithSqlQuery] =
Expand Down
Loading

0 comments on commit 37a37f2

Please sign in to comment.