Skip to content

Commit

Permalink
refactor: implement QueryMember
Browse files Browse the repository at this point in the history
QueryMember is created once per query for each Member referenced in the query.
This allows more efficient computations as some things can be computed once per query.
QueryMember implements everything related to generating SQL so Member classes are now simpler.
  • Loading branch information
retro committed Aug 25, 2024
1 parent abc06c9 commit b8a1324
Show file tree
Hide file tree
Showing 18 changed files with 898 additions and 734 deletions.
3 changes: 3 additions & 0 deletions src/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ const userModel = semanticLayer
})
.withMetric("count", {
type: "string",
sql: ({ model, sql }) => sql`COUNT(DISTINCT ${model.column("CustomerId")})`,
format: (value) => `Count: ${value}`,
})
.withMetric("percentage", {
type: "string",
sql: ({ model, sql }) => sql`COUNT(DISTINCT ${model.column("CustomerId")})`,
format: "percentage",
})
.withMetric("currency", {
type: "string",
sql: ({ model, sql }) => sql`COUNT(DISTINCT ${model.column("CustomerId")})`,
format: "currency",
});

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/query-builder/query-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getQueryPlan } from "../../lib/query-builder/query-plan.js";
it("can crate a query plan", () => {
const { queryBuilder } = fullRepository;

const queryPlan = getQueryPlan(queryBuilder.repository, {
const queryPlan = getQueryPlan(queryBuilder, undefined, {
members: [
"artists.name",
"tracks.name",
Expand Down
5 changes: 0 additions & 5 deletions src/lib/join.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ColumnRef, DimensionRef, IdentifierRef, SqlFn } from "./sql-fn.js";

import invariant from "tiny-invariant";
import { AnyModel } from "./model.js";
import { ModelMemberWithoutModelPrefix } from "./types.js";

Expand Down Expand Up @@ -42,10 +41,6 @@ export function makeModelJoinPayload(model: AnyModel, context: unknown) {
return {
dimension: (dimensionName: string) => {
const dimension = model.getDimension(dimensionName);
invariant(
dimension,
`Dimension ${dimensionName} not found in model ${model.name}`,
);
return new DimensionRef(dimension, context);
},
column: (columnName: string) => new ColumnRef(model, columnName, context),
Expand Down
63 changes: 63 additions & 0 deletions src/lib/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
QueryMember,
QueryMemberCache,
} from "./query-builder/query-plan/query-member.js";

import { AnyBaseDialect } from "./dialect/base.js";
import { pathToAlias } from "./helpers.js";
import { AnyModel } from "./model.js";
import { AnyBasicDimensionProps } from "./model/basic-dimension.js";
import { AnyBasicMetricProps } from "./model/basic-metric.js";
import { GranularityDimension } from "./model/granularity-dimension.js";
import { AnyRepository } from "./repository.js";

export abstract class Member {
public abstract readonly name: string;
public abstract readonly model: AnyModel;
public abstract props: AnyBasicDimensionProps | AnyBasicMetricProps;

abstract isMetric(): this is Metric;
abstract isDimension(): this is Dimension;

getAlias() {
return `${this.model.name}___${pathToAlias(this.name)}`;
}
getPath() {
return `${this.model.name}.${this.name}`;
}
getDescription() {
return this.props.description;
}
getType() {
return this.props.type;
}
getFormat() {
return this.props.format;
}
unsafeFormatValue(value: unknown) {
const format = this.getFormat();
if (typeof format === "function") {
return (format as (value: unknown) => string)(value);
}
if (format === "currency") {
return `$${value}`;
}
if (format === "percentage") {
return `${value}%`;
}
return String(value);
}
abstract clone(model: AnyModel): Member;
abstract getQueryMember(
queryMembers: QueryMemberCache,
repository: AnyRepository,
dialect: AnyBaseDialect,
context: unknown,
): QueryMember;
}

export abstract class Dimension extends Member {
abstract isPrimaryKey(): boolean;
abstract isGranularity(): this is GranularityDimension;
}
export abstract class Metric extends Member {}
66 changes: 46 additions & 20 deletions src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {

import invariant from "tiny-invariant";
import { AnyBaseDialect } from "./dialect/base.js";

import {
BasicDimension,
BasicDimensionProps,
BasicMetric,
BasicMetricProps,
DimensionHasTemporalGranularity,
WithTemporalGranularityDimensions,
} from "./model/member.js";
} from "./model/basic-dimension.js";
import { BasicMetric, BasicMetricProps } from "./model/basic-metric.js";
import { GranularityDimension } from "./model/granularity-dimension.js";
import { QueryMemberCache } from "./query-builder/query-plan/query-member.js";
import { AnyRepository } from "./repository.js";
import { SqlFragment } from "./sql-builder.js";
import { IdentifierRef, SqlFn } from "./sql-fn.js";
Expand Down Expand Up @@ -77,7 +79,7 @@ export class Model<
DG extends boolean = DimensionHasTemporalGranularity<DP>,
>(
name: Exclude<DN1, keyof D | keyof M>,
dimension: DP,
dimensionProps: DP,
): Model<
C,
N,
Expand All @@ -91,20 +93,23 @@ export class Model<
!(this.dimensions[name] || this.metrics[name]),
`Member "${name}" already exists`,
);
const dimension = new BasicDimension(this, name, dimensionProps);

this.dimensions[name] = new BasicDimension(this, name, dimension);
this.dimensions[name] = dimension;
if (
// TODO: figure out why typeHasGranularity is not working anymore
dimension.type === "datetime" ||
dimension.type === "date" ||
(dimension.type === "time" && dimension.omitGranularity !== true)
dimensionProps.type === "datetime" ||
dimensionProps.type === "date" ||
(dimensionProps.type === "time" &&
dimensionProps.omitGranularity !== true)
) {
const granularityDimensions =
TemporalGranularityByDimensionType[dimension.type];
TemporalGranularityByDimensionType[dimensionProps.type];
for (const g of granularityDimensions) {
const { format: _format, ...dimensionWithoutFormat } = dimension;
this.dimensions[`${name}.${g}`] = new BasicDimension(
const { format: _format, ...dimensionWithoutFormat } = dimensionProps;
this.dimensions[`${name}.${g}`] = new GranularityDimension(
this,
dimension,
`${name}.${g}`,
{
...dimensionWithoutFormat,
Expand All @@ -117,7 +122,7 @@ export class Model<
}
this.unsafeWithHierarchy(
name,
makeTemporalHierarchyElementsForDimension(name, dimension.type),
makeTemporalHierarchyElementsForDimension(name, dimensionProps.type),
"temporal",
);
}
Expand Down Expand Up @@ -201,7 +206,12 @@ export class Model<
getMetrics() {
return Object.values(this.metrics);
}
getTableName(repository: AnyRepository, dialect: AnyBaseDialect, context: C) {
getTableName(
repository: AnyRepository,
queryMembers: QueryMemberCache,
dialect: AnyBaseDialect,
context: C,
) {
invariant(this.config.type === "table", "Model is not a table");

if (typeof this.config.name === "string") {
Expand All @@ -221,9 +231,14 @@ export class Model<
getContext: () => context,
});

return result.render(repository, dialect);
return result.render(repository, queryMembers, dialect);
}
getSql(repository: AnyRepository, dialect: AnyBaseDialect, context: C) {
getSql(
repository: AnyRepository,
queryMembers: QueryMemberCache,
dialect: AnyBaseDialect,
context: C,
) {
invariant(this.config.type === "sqlQuery", "Model is not an SQL query");

const result = this.config.sql({
Expand All @@ -232,34 +247,45 @@ export class Model<
new SqlFn([...strings], values),
getContext: () => context,
});
return result.render(repository, dialect);
return result.render(repository, queryMembers, dialect);
}
getTableNameOrSql(
repository: AnyRepository,
queryMembers: QueryMemberCache,
dialect: AnyBaseDialect,
context: C,
) {
if (this.config.type === "table") {
const { sql, bindings } = this.getTableName(repository, dialect, context);
const { sql, bindings } = this.getTableName(
repository,
queryMembers,
dialect,
context,
);
return dialect.fragment(sql, bindings);
}

const modelSql = this.getSql(repository, dialect, context);
const modelSql = this.getSql(repository, queryMembers, dialect, context);
return dialect.fragment(
`(${modelSql.sql}) as ${dialect.asIdentifier(this.config.alias)}`,
modelSql.bindings,
);
}

getAs(repository: AnyRepository, dialect: AnyBaseDialect, context: C) {
getAs(
repository: AnyRepository,
queryMembers: QueryMemberCache,
dialect: AnyBaseDialect,
context: C,
) {
if (this.config.type === "sqlQuery") {
return SqlFragment.make({
sql: dialect.asIdentifier(this.config.alias),
bindings: [],
});
}

return this.getTableName(repository, dialect, context);
return this.getTableName(repository, queryMembers, dialect, context);
}

clone<N extends string>(name: N) {
Expand Down
Loading

0 comments on commit b8a1324

Please sign in to comment.