@oneuptime/common 11.3.8 → 11.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/ExceptionInstance.ts +27 -0
- package/Models/AnalyticsModels/Log.ts +1 -0
- package/Models/AnalyticsModels/Metric.ts +1 -0
- package/Models/AnalyticsModels/MutableMetric.ts +333 -0
- package/Models/AnalyticsModels/Profile.ts +1 -0
- package/Models/AnalyticsModels/Span.ts +1 -0
- package/Server/Infrastructure/Queue.ts +19 -2
- package/Server/Infrastructure/QueueWorker.ts +67 -1
- package/Server/Services/AlertService.ts +167 -206
- package/Server/Services/AlertStateTimelineService.ts +121 -1
- package/Server/Services/AnalyticsDatabaseService.ts +19 -23
- package/Server/Services/IncidentService.ts +179 -252
- package/Server/Services/IncidentStateTimelineService.ts +120 -1
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/MetricAggregationService.ts +73 -3
- package/Server/Services/MetricService.ts +228 -0
- package/Server/Services/MutableMetricService.ts +275 -0
- package/Server/Services/TelemetryAttributeService.ts +186 -5
- package/Server/Utils/AnalyticsDatabase/ClusterConfig.ts +2 -0
- package/Server/Utils/AnalyticsDatabase/Statement.ts +2 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +81 -2
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +143 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +208 -0
- package/Tests/Types/Metrics/MetricPipelineRuleFilterCondition.test.ts +208 -0
- package/Tests/Types/Monitor/MonitorStepEntityScope.test.ts +0 -56
- package/Types/AnalyticsDatabase/AnalyticsTableEngine.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
- package/Types/AnalyticsDatabase/TableColumn.ts +16 -0
- package/Types/Monitor/MonitorStepMetricMonitor.ts +0 -27
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +24 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +1 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MutableMetric.js +275 -0
- package/build/dist/Models/AnalyticsModels/MutableMetric.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Profile.js +1 -0
- package/build/dist/Models/AnalyticsModels/Profile.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +14 -2
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +45 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +133 -170
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +97 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +16 -15
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +142 -207
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +97 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +49 -3
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +155 -0
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/MutableMetricService.js +182 -0
- package/build/dist/Server/Services/MutableMetricService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js +132 -7
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js +2 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/ClusterConfig.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +2 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +66 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableEngine.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +0 -13
- package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
- package/package.json +1 -1
|
@@ -82,6 +82,38 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
82
82
|
this.database = data.database;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
private appendMapKeyPresenceFilter(input: {
|
|
86
|
+
whereStatement: Statement;
|
|
87
|
+
mapColumn: AnalyticsTableColumn;
|
|
88
|
+
mapKey: string;
|
|
89
|
+
skip?: boolean;
|
|
90
|
+
}): void {
|
|
91
|
+
const mapKeysColumnName: string | undefined = input.mapColumn.mapKeysColumn;
|
|
92
|
+
|
|
93
|
+
if (input.skip || !mapKeysColumnName) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const mapKeysColumn: AnalyticsTableColumn | null =
|
|
98
|
+
this.model.getTableColumn(mapKeysColumnName);
|
|
99
|
+
|
|
100
|
+
if (!mapKeysColumn || mapKeysColumn.type !== TableColumnType.ArrayText) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Keep empty key-array rows eligible so data written before the
|
|
106
|
+
* denormalized column existed, or by a lagging ingest path, is still
|
|
107
|
+
* checked by the canonical map['key'] predicate that follows.
|
|
108
|
+
*/
|
|
109
|
+
input.whereStatement.append(
|
|
110
|
+
SQL`AND (empty(${mapKeysColumn.key}) OR hasAny(${mapKeysColumn.key}, ${{
|
|
111
|
+
value: [input.mapKey],
|
|
112
|
+
type: TableColumnType.ArrayText,
|
|
113
|
+
}})) `,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
85
117
|
public toUpdateStatement(updateBy: UpdateBy<TBaseModel>): Statement {
|
|
86
118
|
const setStatement: Statement = this.toSetStatement(updateBy.data);
|
|
87
119
|
const whereStatement: Statement = this.toWhereStatement(updateBy.query);
|
|
@@ -708,6 +740,11 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
708
740
|
}
|
|
709
741
|
|
|
710
742
|
if (mapEntry instanceof NotNull) {
|
|
743
|
+
this.appendMapKeyPresenceFilter({
|
|
744
|
+
whereStatement,
|
|
745
|
+
mapColumn: tableColumn,
|
|
746
|
+
mapKey,
|
|
747
|
+
});
|
|
711
748
|
whereStatement.append(
|
|
712
749
|
SQL`AND mapContains(${key}, ${{
|
|
713
750
|
value: mapKey,
|
|
@@ -789,12 +826,21 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
789
826
|
}
|
|
790
827
|
|
|
791
828
|
if (mapEntry instanceof EqualTo) {
|
|
829
|
+
const equalityValue: string = String(
|
|
830
|
+
(mapEntry as EqualTo<any>).value ?? "",
|
|
831
|
+
);
|
|
832
|
+
this.appendMapKeyPresenceFilter({
|
|
833
|
+
whereStatement,
|
|
834
|
+
mapColumn: tableColumn,
|
|
835
|
+
mapKey,
|
|
836
|
+
skip: equalityValue === "",
|
|
837
|
+
});
|
|
792
838
|
whereStatement.append(
|
|
793
839
|
SQL`AND ${key}[${{
|
|
794
840
|
value: mapKey,
|
|
795
841
|
type: TableColumnType.Text,
|
|
796
842
|
}}] = ${{
|
|
797
|
-
value:
|
|
843
|
+
value: equalityValue,
|
|
798
844
|
type: TableColumnType.Text,
|
|
799
845
|
}}`,
|
|
800
846
|
);
|
|
@@ -809,6 +855,11 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
809
855
|
* threshold and naturally drops those rows.
|
|
810
856
|
*/
|
|
811
857
|
if (mapEntry instanceof GreaterThan) {
|
|
858
|
+
this.appendMapKeyPresenceFilter({
|
|
859
|
+
whereStatement,
|
|
860
|
+
mapColumn: tableColumn,
|
|
861
|
+
mapKey,
|
|
862
|
+
});
|
|
812
863
|
whereStatement.append(
|
|
813
864
|
SQL`AND toFloat64OrNull(${key}[${{
|
|
814
865
|
value: mapKey,
|
|
@@ -822,6 +873,11 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
822
873
|
}
|
|
823
874
|
|
|
824
875
|
if (mapEntry instanceof GreaterThanOrEqual) {
|
|
876
|
+
this.appendMapKeyPresenceFilter({
|
|
877
|
+
whereStatement,
|
|
878
|
+
mapColumn: tableColumn,
|
|
879
|
+
mapKey,
|
|
880
|
+
});
|
|
825
881
|
whereStatement.append(
|
|
826
882
|
SQL`AND toFloat64OrNull(${key}[${{
|
|
827
883
|
value: mapKey,
|
|
@@ -835,6 +891,11 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
835
891
|
}
|
|
836
892
|
|
|
837
893
|
if (mapEntry instanceof LessThan) {
|
|
894
|
+
this.appendMapKeyPresenceFilter({
|
|
895
|
+
whereStatement,
|
|
896
|
+
mapColumn: tableColumn,
|
|
897
|
+
mapKey,
|
|
898
|
+
});
|
|
838
899
|
whereStatement.append(
|
|
839
900
|
SQL`AND toFloat64OrNull(${key}[${{
|
|
840
901
|
value: mapKey,
|
|
@@ -848,6 +909,11 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
848
909
|
}
|
|
849
910
|
|
|
850
911
|
if (mapEntry instanceof LessThanOrEqual) {
|
|
912
|
+
this.appendMapKeyPresenceFilter({
|
|
913
|
+
whereStatement,
|
|
914
|
+
mapColumn: tableColumn,
|
|
915
|
+
mapKey,
|
|
916
|
+
});
|
|
851
917
|
whereStatement.append(
|
|
852
918
|
SQL`AND toFloat64OrNull(${key}[${{
|
|
853
919
|
value: mapKey,
|
|
@@ -876,6 +942,12 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
876
942
|
if (includesValues.length === 0) {
|
|
877
943
|
continue;
|
|
878
944
|
}
|
|
945
|
+
this.appendMapKeyPresenceFilter({
|
|
946
|
+
whereStatement,
|
|
947
|
+
mapColumn: tableColumn,
|
|
948
|
+
mapKey,
|
|
949
|
+
skip: includesValues.includes(""),
|
|
950
|
+
});
|
|
879
951
|
whereStatement.append(
|
|
880
952
|
SQL`AND ${key}[${{
|
|
881
953
|
value: mapKey,
|
|
@@ -919,12 +991,19 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
|
|
|
919
991
|
}
|
|
920
992
|
|
|
921
993
|
// Bare string/number/boolean — direct Map subscript.
|
|
994
|
+
const equalityValue: string = String(mapEntry);
|
|
995
|
+
this.appendMapKeyPresenceFilter({
|
|
996
|
+
whereStatement,
|
|
997
|
+
mapColumn: tableColumn,
|
|
998
|
+
mapKey,
|
|
999
|
+
skip: equalityValue === "",
|
|
1000
|
+
});
|
|
922
1001
|
whereStatement.append(
|
|
923
1002
|
SQL`AND ${key}[${{
|
|
924
1003
|
value: mapKey,
|
|
925
1004
|
type: TableColumnType.Text,
|
|
926
1005
|
}}] = ${{
|
|
927
|
-
value:
|
|
1006
|
+
value: equalityValue,
|
|
928
1007
|
type: TableColumnType.Text,
|
|
929
1008
|
}}`,
|
|
930
1009
|
);
|
|
@@ -13,6 +13,8 @@ import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
|
|
|
13
13
|
import AggregationType from "../../../Types/BaseDatabase/AggregationType";
|
|
14
14
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
15
15
|
import GenericObject from "../../../Types/GenericObject";
|
|
16
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
17
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
16
18
|
import {
|
|
17
19
|
describe,
|
|
18
20
|
expect,
|
|
@@ -379,6 +381,147 @@ describe("AnalyticsDatabaseService", () => {
|
|
|
379
381
|
* ids per call. Must be strictly additive: option-less calls keep the
|
|
380
382
|
* exact pre-existing exec/query payload.
|
|
381
383
|
*/
|
|
384
|
+
/*
|
|
385
|
+
* Required-field validation. A required column is satisfied by any present
|
|
386
|
+
* value (including the boolean `false`); only null/undefined triggers a
|
|
387
|
+
* default-fill or a "<field> is required" error. This is regression coverage
|
|
388
|
+
* for the boolean branch that previously rejected every required Boolean
|
|
389
|
+
* column (both true and false), which made models like MutableMetric — whose
|
|
390
|
+
* `isDeleted` tombstone flag is a required Boolean — impossible to insert.
|
|
391
|
+
*/
|
|
392
|
+
describe("checkRequiredFields", () => {
|
|
393
|
+
class RequiredFieldsModel extends AnalyticsBaseModel {
|
|
394
|
+
public constructor() {
|
|
395
|
+
super({
|
|
396
|
+
tableName: "<required-fields-table>",
|
|
397
|
+
singularName: "<singular-name>",
|
|
398
|
+
pluralName: "<plural-name>",
|
|
399
|
+
tableColumns: [
|
|
400
|
+
new AnalyticsTableColumn({
|
|
401
|
+
key: "requiredText",
|
|
402
|
+
title: "<title>",
|
|
403
|
+
description: "<description>",
|
|
404
|
+
required: true,
|
|
405
|
+
type: TableColumnType.Text,
|
|
406
|
+
}),
|
|
407
|
+
new AnalyticsTableColumn({
|
|
408
|
+
key: "requiredBoolWithDefault",
|
|
409
|
+
title: "<title>",
|
|
410
|
+
description: "<description>",
|
|
411
|
+
required: true,
|
|
412
|
+
type: TableColumnType.Boolean,
|
|
413
|
+
defaultValue: false,
|
|
414
|
+
}),
|
|
415
|
+
],
|
|
416
|
+
crudApiPath: new Route("route"),
|
|
417
|
+
primaryKeys: ["requiredText"],
|
|
418
|
+
sortKeys: ["requiredText"],
|
|
419
|
+
partitionKey: "requiredText",
|
|
420
|
+
tableEngine: AnalyticsTableEngine.MergeTree,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// checkRequiredFields reads columns via property accessors, so the model
|
|
425
|
+
// must expose getters/setters that delegate to get/setColumnValue (real
|
|
426
|
+
// analytics models do the same for every column).
|
|
427
|
+
public get requiredText(): string | undefined {
|
|
428
|
+
return this.getColumnValue("requiredText") as string | undefined;
|
|
429
|
+
}
|
|
430
|
+
public set requiredText(v: string | undefined) {
|
|
431
|
+
this.setColumnValue("requiredText", v);
|
|
432
|
+
}
|
|
433
|
+
public get requiredBoolWithDefault(): boolean | undefined {
|
|
434
|
+
return this.getColumnValue("requiredBoolWithDefault") as
|
|
435
|
+
| boolean
|
|
436
|
+
| undefined;
|
|
437
|
+
}
|
|
438
|
+
public set requiredBoolWithDefault(v: boolean | undefined) {
|
|
439
|
+
this.setColumnValue("requiredBoolWithDefault", v);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let requiredFieldsService: AnalyticsDatabaseService<RequiredFieldsModel>;
|
|
444
|
+
|
|
445
|
+
beforeEach(() => {
|
|
446
|
+
requiredFieldsService = new AnalyticsDatabaseService({
|
|
447
|
+
modelType: RequiredFieldsModel,
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Build a model with every required column satisfied except as overridden.
|
|
452
|
+
const buildModel: (
|
|
453
|
+
overrides?: (model: RequiredFieldsModel) => void,
|
|
454
|
+
) => RequiredFieldsModel = (
|
|
455
|
+
overrides?: (model: RequiredFieldsModel) => void,
|
|
456
|
+
): RequiredFieldsModel => {
|
|
457
|
+
const model: RequiredFieldsModel = new RequiredFieldsModel();
|
|
458
|
+
// _id and createdAt are required on the base model and carry no default.
|
|
459
|
+
model.setColumnValue("_id", ObjectID.generate());
|
|
460
|
+
model.setColumnValue("createdAt", OneUptimeDate.getCurrentDate());
|
|
461
|
+
model.setColumnValue("requiredText", "value");
|
|
462
|
+
if (overrides) {
|
|
463
|
+
overrides(model);
|
|
464
|
+
}
|
|
465
|
+
return model;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const check: (model: RequiredFieldsModel) => RequiredFieldsModel = (
|
|
469
|
+
model: RequiredFieldsModel,
|
|
470
|
+
): RequiredFieldsModel => {
|
|
471
|
+
return (requiredFieldsService as any).checkRequiredFields(model);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
test("accepts a required boolean set to false (regression)", () => {
|
|
475
|
+
const model: RequiredFieldsModel = buildModel(
|
|
476
|
+
(m: RequiredFieldsModel): void => {
|
|
477
|
+
m.setColumnValue("requiredBoolWithDefault", false);
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
expect(() => {
|
|
482
|
+
return check(model);
|
|
483
|
+
}).not.toThrow();
|
|
484
|
+
expect(model.getColumnValue("requiredBoolWithDefault")).toBe(false);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("accepts a required boolean set to true", () => {
|
|
488
|
+
const model: RequiredFieldsModel = buildModel(
|
|
489
|
+
(m: RequiredFieldsModel): void => {
|
|
490
|
+
m.setColumnValue("requiredBoolWithDefault", true);
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
expect(() => {
|
|
495
|
+
return check(model);
|
|
496
|
+
}).not.toThrow();
|
|
497
|
+
expect(model.getColumnValue("requiredBoolWithDefault")).toBe(true);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("fills the default when a required boolean is unset", () => {
|
|
501
|
+
const model: RequiredFieldsModel = buildModel();
|
|
502
|
+
|
|
503
|
+
expect(() => {
|
|
504
|
+
return check(model);
|
|
505
|
+
}).not.toThrow();
|
|
506
|
+
expect(model.getColumnValue("requiredBoolWithDefault")).toBe(false);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("throws when a required column without a default is missing", () => {
|
|
510
|
+
const model: RequiredFieldsModel = new RequiredFieldsModel();
|
|
511
|
+
model.setColumnValue("_id", ObjectID.generate());
|
|
512
|
+
model.setColumnValue("createdAt", OneUptimeDate.getCurrentDate());
|
|
513
|
+
model.setColumnValue("requiredBoolWithDefault", false);
|
|
514
|
+
// requiredText (required, no default) intentionally left unset.
|
|
515
|
+
|
|
516
|
+
expect(() => {
|
|
517
|
+
return check(model);
|
|
518
|
+
}).toThrow(BadDataException);
|
|
519
|
+
expect(() => {
|
|
520
|
+
return check(model);
|
|
521
|
+
}).toThrow("requiredText is required");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
382
525
|
describe("execute / executeQuery per-call options", () => {
|
|
383
526
|
let exec: ReturnType<typeof jest.fn>;
|
|
384
527
|
let query: ReturnType<typeof jest.fn>;
|
|
@@ -395,6 +395,214 @@ describe("StatementGenerator", () => {
|
|
|
395
395
|
expect(statement.query).toBe("");
|
|
396
396
|
expect(statement.query_params).toStrictEqual({});
|
|
397
397
|
});
|
|
398
|
+
|
|
399
|
+
describe("map key-array pruning", () => {
|
|
400
|
+
class MapModelWithAttributeKeys extends AnalyticsBaseModel {
|
|
401
|
+
public constructor() {
|
|
402
|
+
super({
|
|
403
|
+
tableName: "<map-table-with-attribute-keys>",
|
|
404
|
+
singularName: "<singular>",
|
|
405
|
+
pluralName: "<plural>",
|
|
406
|
+
tableColumns: [
|
|
407
|
+
new AnalyticsTableColumn({
|
|
408
|
+
key: "_id",
|
|
409
|
+
title: "<title>",
|
|
410
|
+
description: "<description>",
|
|
411
|
+
required: true,
|
|
412
|
+
type: TableColumnType.ObjectID,
|
|
413
|
+
}),
|
|
414
|
+
new AnalyticsTableColumn({
|
|
415
|
+
key: "attributes",
|
|
416
|
+
title: "<title>",
|
|
417
|
+
description: "<description>",
|
|
418
|
+
required: true,
|
|
419
|
+
defaultValue: {},
|
|
420
|
+
type: TableColumnType.MapStringString,
|
|
421
|
+
mapKeysColumn: "attributeKeys",
|
|
422
|
+
}),
|
|
423
|
+
new AnalyticsTableColumn({
|
|
424
|
+
key: "attributeKeys",
|
|
425
|
+
title: "<title>",
|
|
426
|
+
description: "<description>",
|
|
427
|
+
required: true,
|
|
428
|
+
defaultValue: [],
|
|
429
|
+
type: TableColumnType.ArrayText,
|
|
430
|
+
}),
|
|
431
|
+
],
|
|
432
|
+
crudApiPath: new Route("route"),
|
|
433
|
+
primaryKeys: ["_id"],
|
|
434
|
+
sortKeys: ["_id"],
|
|
435
|
+
partitionKey: "_id",
|
|
436
|
+
tableEngine: AnalyticsTableEngine.MergeTree,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
class MapModelWithCustomMapKeys extends AnalyticsBaseModel {
|
|
442
|
+
public constructor() {
|
|
443
|
+
super({
|
|
444
|
+
tableName: "<map-table-with-custom-key-column>",
|
|
445
|
+
singularName: "<singular>",
|
|
446
|
+
pluralName: "<plural>",
|
|
447
|
+
tableColumns: [
|
|
448
|
+
new AnalyticsTableColumn({
|
|
449
|
+
key: "_id",
|
|
450
|
+
title: "<title>",
|
|
451
|
+
description: "<description>",
|
|
452
|
+
required: true,
|
|
453
|
+
type: TableColumnType.ObjectID,
|
|
454
|
+
}),
|
|
455
|
+
new AnalyticsTableColumn({
|
|
456
|
+
key: "tags",
|
|
457
|
+
title: "<title>",
|
|
458
|
+
description: "<description>",
|
|
459
|
+
required: true,
|
|
460
|
+
defaultValue: {},
|
|
461
|
+
type: TableColumnType.MapStringString,
|
|
462
|
+
mapKeysColumn: "tagKeys",
|
|
463
|
+
}),
|
|
464
|
+
new AnalyticsTableColumn({
|
|
465
|
+
key: "tagKeys",
|
|
466
|
+
title: "<title>",
|
|
467
|
+
description: "<description>",
|
|
468
|
+
required: true,
|
|
469
|
+
defaultValue: [],
|
|
470
|
+
type: TableColumnType.ArrayText,
|
|
471
|
+
}),
|
|
472
|
+
],
|
|
473
|
+
crudApiPath: new Route("route"),
|
|
474
|
+
primaryKeys: ["_id"],
|
|
475
|
+
sortKeys: ["_id"],
|
|
476
|
+
partitionKey: "_id",
|
|
477
|
+
tableEngine: AnalyticsTableEngine.MergeTree,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let mapGeneratorWithAttributeKeys: StatementGenerator<MapModelWithAttributeKeys>;
|
|
483
|
+
|
|
484
|
+
beforeEach(() => {
|
|
485
|
+
mapGeneratorWithAttributeKeys =
|
|
486
|
+
new StatementGenerator<MapModelWithAttributeKeys>({
|
|
487
|
+
modelType: MapModelWithAttributeKeys,
|
|
488
|
+
database: ClickhouseAppInstance,
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("adds linked key-array bloom-index predicate for positive equality filters", () => {
|
|
493
|
+
const statement: Statement =
|
|
494
|
+
mapGeneratorWithAttributeKeys.toWhereStatement({
|
|
495
|
+
attributes: { requestId: "uuid-123" },
|
|
496
|
+
} as any);
|
|
497
|
+
|
|
498
|
+
expect(statement.query).toBe(
|
|
499
|
+
"AND (empty({p0:Identifier}) OR hasAny({p1:Identifier}, {p2:Array(String)})) AND {p3:Identifier}[{p4:String}] = {p5:String}",
|
|
500
|
+
);
|
|
501
|
+
expect(statement.query_params).toStrictEqual({
|
|
502
|
+
p0: "attributeKeys",
|
|
503
|
+
p1: "attributeKeys",
|
|
504
|
+
p2: ["requestId"],
|
|
505
|
+
p3: "attributes",
|
|
506
|
+
p4: "requestId",
|
|
507
|
+
p5: "uuid-123",
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("does not add linked key-array predicate for empty equality because missing keys also match", () => {
|
|
512
|
+
const statement: Statement =
|
|
513
|
+
mapGeneratorWithAttributeKeys.toWhereStatement({
|
|
514
|
+
attributes: { requestId: "" },
|
|
515
|
+
} as any);
|
|
516
|
+
|
|
517
|
+
expect(statement.query).toBe(
|
|
518
|
+
"AND {p0:Identifier}[{p1:String}] = {p2:String}",
|
|
519
|
+
);
|
|
520
|
+
expect(statement.query_params).toStrictEqual({
|
|
521
|
+
p0: "attributes",
|
|
522
|
+
p1: "requestId",
|
|
523
|
+
p2: "",
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("adds linked key-array bloom-index predicate for positive Includes filters", () => {
|
|
528
|
+
const statement: Statement =
|
|
529
|
+
mapGeneratorWithAttributeKeys.toWhereStatement({
|
|
530
|
+
attributes: {
|
|
531
|
+
"k8s.cluster.name": new Includes(["prod-east", "prod-west"]),
|
|
532
|
+
},
|
|
533
|
+
} as any);
|
|
534
|
+
|
|
535
|
+
expect(statement.query).toBe(
|
|
536
|
+
"AND (empty({p0:Identifier}) OR hasAny({p1:Identifier}, {p2:Array(String)})) AND {p3:Identifier}[{p4:String}] IN {p5:Array(String)}",
|
|
537
|
+
);
|
|
538
|
+
expect(statement.query_params).toStrictEqual({
|
|
539
|
+
p0: "attributeKeys",
|
|
540
|
+
p1: "attributeKeys",
|
|
541
|
+
p2: ["k8s.cluster.name"],
|
|
542
|
+
p3: "attributes",
|
|
543
|
+
p4: "k8s.cluster.name",
|
|
544
|
+
p5: ["prod-east", "prod-west"],
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("does not add linked key-array predicate for Includes containing empty string", () => {
|
|
549
|
+
const statement: Statement =
|
|
550
|
+
mapGeneratorWithAttributeKeys.toWhereStatement({
|
|
551
|
+
attributes: {
|
|
552
|
+
"k8s.cluster.name": new Includes(["", "prod-east"]),
|
|
553
|
+
},
|
|
554
|
+
} as any);
|
|
555
|
+
|
|
556
|
+
expect(statement.query).toBe(
|
|
557
|
+
"AND {p0:Identifier}[{p1:String}] IN {p2:Array(String)}",
|
|
558
|
+
);
|
|
559
|
+
expect(statement.query_params).toStrictEqual({
|
|
560
|
+
p0: "attributes",
|
|
561
|
+
p1: "k8s.cluster.name",
|
|
562
|
+
p2: ["", "prod-east"],
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("does not add linked key-array predicate for NotEqual because missing keys still match", () => {
|
|
567
|
+
const statement: Statement =
|
|
568
|
+
mapGeneratorWithAttributeKeys.toWhereStatement({
|
|
569
|
+
attributes: { requestId: new NotEqual("uuid-123") },
|
|
570
|
+
} as any);
|
|
571
|
+
|
|
572
|
+
expect(statement.query).toBe(
|
|
573
|
+
"AND {p0:Identifier}[{p1:String}] != {p2:String}",
|
|
574
|
+
);
|
|
575
|
+
expect(statement.query_params).toStrictEqual({
|
|
576
|
+
p0: "attributes",
|
|
577
|
+
p1: "requestId",
|
|
578
|
+
p2: "uuid-123",
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("uses mapKeysColumn metadata instead of hardcoded telemetry column names", () => {
|
|
583
|
+
const customMapGenerator: StatementGenerator<MapModelWithCustomMapKeys> =
|
|
584
|
+
new StatementGenerator<MapModelWithCustomMapKeys>({
|
|
585
|
+
modelType: MapModelWithCustomMapKeys,
|
|
586
|
+
database: ClickhouseAppInstance,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const statement: Statement = customMapGenerator.toWhereStatement({
|
|
590
|
+
tags: { region: "us-east" },
|
|
591
|
+
} as any);
|
|
592
|
+
|
|
593
|
+
expect(statement.query).toBe(
|
|
594
|
+
"AND (empty({p0:Identifier}) OR hasAny({p1:Identifier}, {p2:Array(String)})) AND {p3:Identifier}[{p4:String}] = {p5:String}",
|
|
595
|
+
);
|
|
596
|
+
expect(statement.query_params).toStrictEqual({
|
|
597
|
+
p0: "tagKeys",
|
|
598
|
+
p1: "tagKeys",
|
|
599
|
+
p2: ["region"],
|
|
600
|
+
p3: "tags",
|
|
601
|
+
p4: "region",
|
|
602
|
+
p5: "us-east",
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
});
|
|
398
606
|
});
|
|
399
607
|
|
|
400
608
|
describe("ArrayText columns", () => {
|