Skip to content

Commit

Permalink
Wip granularities rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
retro committed Aug 6, 2024
1 parent ae2b2e5 commit 56da423
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 157 deletions.
11 changes: 5 additions & 6 deletions src/__tests__/granularity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,11 @@ const playlistModel = semanticLayer
type: "string",
sql: ({ model }) => model.column("Name"),
})
.withGranularity("name", [
{
key: "name",
elements: ["name", "playlist_id"],
display: "name",
},
.withCategoricalGranularity("name", ({ element }) => [
element("name")
.withDimensions("playlist_id", "name")
.withKey("playlist_id")
.withFormat(({ dimension }) => dimension("name")),
]);

const playlistTrackModel = semanticLayer
Expand Down
162 changes: 162 additions & 0 deletions src/lib/custom-granularity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { AnyModel } from "./model.js";
import { AnyRepository } from "./repository.js";
import invariant from "tiny-invariant";

export interface CustomGranularityElementConfig {
name: string;
dimensions: string[];
keyDimensions: string[];
formatDimensions: string[];
formatter: (row: Record<string, unknown>) => string;
}

export type AnyCustomGranularityElement = CustomGranularityElement<any>;

export abstract class CustomGranularityElementFormatter {
abstract getFormatter(
parent: AnyModel | AnyRepository,
): (row: Record<string, unknown>) => string;
abstract getReferencedDimensionNames(): string[];
}

export class CustomGranularityElementDimensionRef extends CustomGranularityElementFormatter {
constructor(public readonly dimensionName: string) {
super();
}
getFormatter(parent: AnyModel | AnyRepository) {
const dimension = parent.getDimension(this.dimensionName);
return (row: Record<string, unknown>) => {
const value = row[dimension.getAlias()];
return dimension.unsafeFormatValue(value);
};
}
getReferencedDimensionNames() {
return [this.dimensionName];
}
}
export class CustomGranularityElementTemplateWithDimensionRefs extends CustomGranularityElementFormatter {
constructor(
public readonly strings: string[],
public readonly values: unknown[],
) {
super();
}
getReferencedDimensionNames() {
const dimensions: string[] = [];
for (const value of this.values) {
if (value instanceof CustomGranularityElementDimensionRef) {
dimensions.push(value.dimensionName);
}
}
return dimensions;
}
getFormatter(parent: AnyModel | AnyRepository) {
return (row: Record<string, unknown>) => {
const result = [];
for (let i = 0; i < this.strings.length; i++) {
result.push(this.strings[i]!);
const nextValue = this.values[i];
if (nextValue) {
if (nextValue instanceof CustomGranularityElementDimensionRef) {
const dimension = parent.getDimension(nextValue.dimensionName);
const value = row[dimension.getAlias()];
result.push(dimension.unsafeFormatValue(value));
} else {
result.push(nextValue);
}
}
}
return result.join("");
};
}
}

export class CustomGranularityElement<D extends string> {
private readonly dimensionRefs: Record<
string,
CustomGranularityElementDimensionRef
>;
private keys: string[] | null = null;
private formatter: CustomGranularityElementFormatter | null = null;
constructor(
public readonly name: string,
private readonly dimensionNames: string[],
) {
this.dimensionRefs = dimensionNames.reduce<
Record<string, CustomGranularityElementDimensionRef>
>((acc, dimensionName) => {
acc[dimensionName] = new CustomGranularityElementDimensionRef(
dimensionName,
);
return acc;
}, {});
}
withKey<K extends D>(...keys: K[]) {
this.keys = keys;
return this;
}
withFormat(
formatter: (props: {
dimension: (name: D) => CustomGranularityElementDimensionRef;
template: (
strings: TemplateStringsArray,
...values: unknown[]
) => CustomGranularityElementTemplateWithDimensionRefs;
}) =>
| CustomGranularityElementDimensionRef
| CustomGranularityElementTemplateWithDimensionRefs,
) {
this.formatter = formatter({
dimension: (name: D) => {
const dimensionRef = this.dimensionRefs[name];
invariant(dimensionRef, `Dimension ${name} not found`);
return dimensionRef;
},
template: (strings, ...values) => {
return new CustomGranularityElementTemplateWithDimensionRefs(
[...strings],
values,
);
},
});
return this;
}
gerDefaultFormatter(parent: AnyModel | AnyRepository) {
return (row: Record<string, unknown>) =>
this.dimensionNames
.map((dimensionName) =>
parent
.getDimension(dimensionName)
.unsafeFormatValue(row[dimensionName]),
)
.join(", ");
}
getConfig(parent: AnyModel | AnyRepository): CustomGranularityElementConfig {
const dimensionNames = this.dimensionNames.map((dimensionName) =>
parent.getDimension(dimensionName).getPath(),
);
return {
name: this.name,
dimensions: dimensionNames,
keyDimensions:
this.keys?.map((dimensionNames) =>
parent.getDimension(dimensionNames).getPath(),
) ?? dimensionNames,
formatDimensions:
this.formatter?.getReferencedDimensionNames() ?? dimensionNames,
formatter:
this.formatter?.getFormatter(parent) ??
this.gerDefaultFormatter(parent),
};
}
}

export class CustomGranularityElementInit<D extends string> {
constructor(
public readonly parent: AnyModel | AnyRepository,
public readonly name: string,
) {}
withDimensions<GD extends D>(...dimensionNames: GD[]) {
return new CustomGranularityElement<GD>(this.name, dimensionNames);
}
}
92 changes: 63 additions & 29 deletions src/lib/model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Get, Simplify } from "type-fest";
import {
CustomGranularity,
CustomGranularityElements,
AnyCustomGranularityElement,
CustomGranularityElementInit,
} from "./custom-granularity.js";
import {
DimensionWithTemporalGranularity,
GranularityType,
MemberFormat,
Expand All @@ -12,9 +13,10 @@ import {
TemporalGranularityIndex,
makeTemporalGranularityElementsForDimension,
} from "./types.js";
import { Get, Simplify } from "type-fest";

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

export type NextColumnRefOrDimensionRefAlias = () => string;

Expand Down Expand Up @@ -253,10 +255,11 @@ export abstract class Member {
abstract isMetric(): this is Metric;
abstract isDimension(): this is Dimension;

getAlias(dialect: AnyBaseDialect) {
return dialect.asIdentifier(
`${this.model.name}___${this.name.replaceAll(".", "___")}`,
);
getQuotedAlias(dialect: AnyBaseDialect) {
return dialect.asIdentifier(this.getAlias());
}
getAlias() {
return `${this.model.name}.${this.name}`;
}
getPath() {
return `${this.model.name}.${this.name}`;
Expand Down Expand Up @@ -294,6 +297,19 @@ export abstract class Member {
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;
}

Expand Down Expand Up @@ -417,7 +433,14 @@ export class Model<
> {
public readonly dimensions: Record<string, Dimension> = {};
public readonly metrics: Record<string, Metric> = {};
public readonly granularities: CustomGranularity[] = [];
public readonly categoricalGranularities: {
name: string;
elements: AnyCustomGranularityElement[];
}[] = [];
public readonly temporalGranularities: {
name: string;
elements: AnyCustomGranularityElement[];
}[] = [];
public readonly granularitiesNames: Set<string> = new Set();

constructor(
Expand Down Expand Up @@ -461,6 +484,7 @@ export class Model<
...dimensionWithoutFormat,
type: TemporalGranularityIndex[g].type,
description: TemporalGranularityIndex[g].description,
format: (value: unknown) => `${value}`,
},
g,
);
Expand All @@ -469,7 +493,6 @@ export class Model<
name,
makeTemporalGranularityElementsForDimension(name, dimension.type),
"temporal",
"bottom",
);
}
return this;
Expand All @@ -488,36 +511,46 @@ export class Model<
}
unsafeWithGranularity(
granularityName: string,
elements: CustomGranularityElements,
elements: AnyCustomGranularityElement[],
type: GranularityType,
position: "top" | "bottom" = "bottom",
) {
invariant(
this.granularitiesNames.has(granularityName) === false,
`Granularity ${granularityName} already exists`,
);
this.granularitiesNames.add(granularityName);
if (position === "top") {
this.granularities.unshift({
name: granularityName,
type,
elements,
});
} else {
this.granularities.push({
name: granularityName,
type,
elements,
});
if (type === "categorical") {
this.categoricalGranularities.push({ name: granularityName, elements });
} else if (type === "temporal") {
this.temporalGranularities.push({ name: granularityName, elements });
}
return this;
}
withGranularity<GN extends string>(
withCategoricalGranularity<GN extends string>(
granularityName: Exclude<GN, G>,
elements: CustomGranularityElements<Extract<keyof M | keyof D, string>>,
type: GranularityType = "custom",
builder: (args: {
element: (
name: string,
) => CustomGranularityElementInit<Extract<keyof D, string>>;
}) => AnyCustomGranularityElement[],
): Model<C, N, D, M, G | GN> {
return this.unsafeWithGranularity(granularityName, elements, type, "top");
const elements = builder({
element: (name) => new CustomGranularityElementInit(this, name),
});
return this.unsafeWithGranularity(granularityName, elements, "categorical");
}
withTemporalGranularity<GN extends string>(
granularityName: Exclude<GN, G>,
builder: (args: {
element: (
name: string,
) => CustomGranularityElementInit<Extract<keyof D, string>>;
}) => AnyCustomGranularityElement[],
): Model<C, N, D, M, G | GN> {
const elements = builder({
element: (name) => new CustomGranularityElementInit(this, name),
});
return this.unsafeWithGranularity(granularityName, elements, "temporal");
}
getMetric(name: string & keyof M) {
const metric = this.metrics[name];
Expand Down Expand Up @@ -593,7 +626,8 @@ export class Model<
for (const [key, value] of Object.entries(this.metrics)) {
newModel.metrics[key] = value.clone(newModel);
}
newModel.granularities.push(...this.granularities);
newModel.temporalGranularities.push(...this.temporalGranularities);
newModel.categoricalGranularities.push(...this.categoricalGranularities);
for (const granularityName of this.granularitiesNames) {
newModel.granularitiesNames.add(granularityName);
}
Expand Down
Loading

0 comments on commit 56da423

Please sign in to comment.