mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat(analytics): add materialized view support to analytics models and table management
- add MaterializedView type - wire materializedViews into AnalyticsBaseModel (constructor param, property, getter/setter) - implement materialized view processing in AnalyticsTableManagement (validation, existence check, creation)
This commit is contained in:
@@ -4,6 +4,7 @@ import AnalyticsTableEngine from "../../../Types/AnalyticsDatabase/AnalyticsTabl
|
||||
import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
|
||||
import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
|
||||
import Projection from "../../../Types/AnalyticsDatabase/Projection";
|
||||
import MaterializedView from "../../../Types/AnalyticsDatabase/MaterializedView";
|
||||
import {
|
||||
ColumnAccessControl,
|
||||
TableAccessControl,
|
||||
@@ -42,6 +43,7 @@ export default class AnalyticsBaseModel extends CommonModel {
|
||||
enableRealtimeEventsOn?: EnableRealtimeEventsOn | undefined;
|
||||
partitionKey: string;
|
||||
projections?: Array<Projection> | undefined;
|
||||
materializedViews?: Array<MaterializedView> | undefined;
|
||||
}) {
|
||||
super({
|
||||
tableColumns: data.tableColumns,
|
||||
@@ -143,6 +145,7 @@ export default class AnalyticsBaseModel extends CommonModel {
|
||||
this.enableRealtimeEventsOn = data.enableRealtimeEventsOn;
|
||||
this.partitionKey = data.partitionKey;
|
||||
this.projections = data.projections || [];
|
||||
this.materializedViews = data.materializedViews || [];
|
||||
}
|
||||
|
||||
private _enableWorkflowOn: EnableWorkflowOn | undefined;
|
||||
@@ -261,6 +264,14 @@ export default class AnalyticsBaseModel extends CommonModel {
|
||||
this._projections = v;
|
||||
}
|
||||
|
||||
private _materializedViews: Array<MaterializedView> = [];
|
||||
public get materializedViews(): Array<MaterializedView> {
|
||||
return this._materializedViews;
|
||||
}
|
||||
public set materializedViews(v: Array<MaterializedView>) {
|
||||
this._materializedViews = v;
|
||||
}
|
||||
|
||||
public getTenantColumn(): AnalyticsTableColumn | null {
|
||||
const column: AnalyticsTableColumn | undefined = this.tableColumns.find(
|
||||
(column: AnalyticsTableColumn) => {
|
||||
|
||||
4
Common/Types/AnalyticsDatabase/MaterializedView.ts
Normal file
4
Common/Types/AnalyticsDatabase/MaterializedView.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default interface MaterializedView {
|
||||
name: string;
|
||||
query: string;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import AnalyticsDatabaseService, {
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import logger from "Common/Server/Utils/Logger";
|
||||
import Projection from "Common/Types/AnalyticsDatabase/Projection";
|
||||
import MaterializedView from "Common/Types/AnalyticsDatabase/MaterializedView";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
export default class AnalyticsTableManagement {
|
||||
@@ -67,6 +68,57 @@ export default class AnalyticsTableManagement {
|
||||
projection.name,
|
||||
);
|
||||
}
|
||||
|
||||
const materializedViews: Array<MaterializedView> =
|
||||
service.model.materializedViews;
|
||||
|
||||
if (materializedViews.length > 0) {
|
||||
logger.debug(
|
||||
`Processing ${materializedViews.length} materialized views for ${service.model.tableName}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const materializedView of materializedViews) {
|
||||
if (!materializedView.query || materializedView.query.trim().length === 0) {
|
||||
logger.debug(
|
||||
`Skipping materialized view with empty query on ${service.model.tableName}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!materializedView.name || materializedView.name.trim().length === 0) {
|
||||
logger.debug(
|
||||
`Skipping materialized view with empty name on ${service.model.tableName}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Ensuring materialized view ${materializedView.name} exists on ${service.model.tableName}`,
|
||||
);
|
||||
|
||||
const viewExists: boolean =
|
||||
await AnalyticsTableManagement.doesMaterializedViewExist(
|
||||
service,
|
||||
materializedView.name,
|
||||
);
|
||||
|
||||
if (viewExists) {
|
||||
logger.debug(
|
||||
`Materialized view ${materializedView.name} already exists on ${service.model.tableName}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Creating materialized view ${materializedView.name} on ${service.model.tableName}`,
|
||||
);
|
||||
|
||||
await AnalyticsTableManagement.createMaterializedView(
|
||||
service,
|
||||
materializedView,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,4 +207,59 @@ export default class AnalyticsTableManagement {
|
||||
private static escapeIdentifier(value: string): string {
|
||||
return `\`${value.replace(/`/g, "``")}\``;
|
||||
}
|
||||
|
||||
private static async doesMaterializedViewExist(
|
||||
service: AnalyticsDatabaseService<AnalyticsBaseModel>,
|
||||
viewName: string,
|
||||
): Promise<boolean> {
|
||||
const databaseName: string | undefined =
|
||||
service.database.getDatasourceOptions().database;
|
||||
|
||||
if (!databaseName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const escapedDatabaseName: string = this.escapeForQuery(databaseName);
|
||||
const escapedViewName: string = this.escapeForQuery(viewName);
|
||||
|
||||
const statement: string = `SELECT name FROM system.tables WHERE database = '${escapedDatabaseName}' AND name = '${escapedViewName}' AND engine = 'MaterializedView' LIMIT 1`;
|
||||
|
||||
let result: Results;
|
||||
|
||||
try {
|
||||
result = await service.executeQuery(statement);
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
message: `Failed to verify materialized view ${viewName} on ${service.model.tableName}`,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
const response: DbJSONResponse = await result.json<{
|
||||
data?: Array<JSONObject>;
|
||||
}>();
|
||||
|
||||
return Boolean(response.data && response.data.length > 0);
|
||||
}
|
||||
|
||||
private static async createMaterializedView(
|
||||
service: AnalyticsDatabaseService<AnalyticsBaseModel>,
|
||||
materializedView: MaterializedView,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await service.execute(materializedView.query);
|
||||
} catch (error) {
|
||||
const clickhouseError: { code?: string } = error as { code?: string };
|
||||
|
||||
logger.error({
|
||||
message: `Failed to create materialized view ${materializedView.name} on ${service.model.tableName}`,
|
||||
error: (error as Error).message,
|
||||
code: clickhouseError?.code,
|
||||
stack: (error as Error).stack,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user