@oneuptime/common 11.3.7 → 11.3.9
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/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/TelemetryAttributeService.ts +3 -4
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +81 -2
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +208 -0
- package/Tests/Types/Metrics/MetricPipelineRuleFilterCondition.test.ts +208 -0
- package/Types/AnalyticsDatabase/TableColumn.ts +16 -0
- 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/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/TelemetryAttributeService.js +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.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/TableColumn.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
- package/package.json +1 -1
|
@@ -591,6 +591,7 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
|
|
|
591
591
|
required: true,
|
|
592
592
|
defaultValue: {},
|
|
593
593
|
type: TableColumnType.MapStringString,
|
|
594
|
+
mapKeysColumn: "attributeKeys",
|
|
594
595
|
accessControl: {
|
|
595
596
|
read: [
|
|
596
597
|
Permission.ProjectOwner,
|
|
@@ -614,6 +615,23 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
|
|
|
614
615
|
},
|
|
615
616
|
});
|
|
616
617
|
|
|
618
|
+
const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
|
|
619
|
+
key: "attributeKeys",
|
|
620
|
+
codec: { codec: "ZSTD", level: 3 },
|
|
621
|
+
title: "Attribute Keys",
|
|
622
|
+
description: "Attribute keys extracted from attributes",
|
|
623
|
+
required: true,
|
|
624
|
+
defaultValue: [],
|
|
625
|
+
type: TableColumnType.ArrayText,
|
|
626
|
+
skipIndex: {
|
|
627
|
+
name: "idx_attribute_keys",
|
|
628
|
+
type: SkipIndexType.BloomFilter,
|
|
629
|
+
params: [0.01],
|
|
630
|
+
granularity: 1,
|
|
631
|
+
},
|
|
632
|
+
accessControl: attributesColumn.accessControl,
|
|
633
|
+
});
|
|
634
|
+
|
|
617
635
|
const entityKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
|
|
618
636
|
key: "entityKeys",
|
|
619
637
|
codec: { codec: "ZSTD", level: 3 },
|
|
@@ -788,6 +806,7 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
|
|
|
788
806
|
environmentColumn,
|
|
789
807
|
parsedFramesColumn,
|
|
790
808
|
attributesColumn,
|
|
809
|
+
attributeKeysColumn,
|
|
791
810
|
entityKeysColumn,
|
|
792
811
|
...scalarEntityKeyColumns,
|
|
793
812
|
retentionDateColumn,
|
|
@@ -918,6 +937,14 @@ export default class ExceptionInstance extends AnalyticsBaseModel {
|
|
|
918
937
|
this.setColumnValue("attributes", v);
|
|
919
938
|
}
|
|
920
939
|
|
|
940
|
+
public get attributeKeys(): Array<string> | undefined {
|
|
941
|
+
return this.getColumnValue("attributeKeys") as Array<string> | undefined;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
public set attributeKeys(v: Array<string> | undefined) {
|
|
945
|
+
this.setColumnValue("attributeKeys", v);
|
|
946
|
+
}
|
|
947
|
+
|
|
921
948
|
public get entityKeys(): Array<string> | undefined {
|
|
922
949
|
return this.getColumnValue("entityKeys") as Array<string> | undefined;
|
|
923
950
|
}
|
|
@@ -455,6 +455,23 @@ export default class Queue {
|
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
private static normalizeFailedReason(reason: string | undefined): string {
|
|
459
|
+
if (typeof reason !== "string") {
|
|
460
|
+
return "No reason provided";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const normalized: string = reason.trim().toLowerCase();
|
|
464
|
+
if (
|
|
465
|
+
normalized.length === 0 ||
|
|
466
|
+
normalized === "null" ||
|
|
467
|
+
normalized === "undefined"
|
|
468
|
+
) {
|
|
469
|
+
return "No reason provided";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return reason;
|
|
473
|
+
}
|
|
474
|
+
|
|
458
475
|
@CaptureSpan()
|
|
459
476
|
public static async getQueueSize(queueName: QueueName): Promise<number> {
|
|
460
477
|
const queue: BullQueue = this.getQueue(queueName);
|
|
@@ -534,7 +551,7 @@ export default class Queue {
|
|
|
534
551
|
id: job.id || "unknown",
|
|
535
552
|
name: job.name || "unknown",
|
|
536
553
|
data: job.data as JSONObject,
|
|
537
|
-
failedReason: job.failedReason
|
|
554
|
+
failedReason: Queue.normalizeFailedReason(job.failedReason),
|
|
538
555
|
processedOn: job.processedOn ? new Date(job.processedOn) : null,
|
|
539
556
|
finishedOn: job.finishedOn ? new Date(job.finishedOn) : null,
|
|
540
557
|
attemptsMade: job.attemptsMade || 0,
|
|
@@ -641,7 +658,7 @@ export default class Queue {
|
|
|
641
658
|
opts: (job.opts as unknown as JSONObject) || {},
|
|
642
659
|
returnValue: job.returnvalue ?? null,
|
|
643
660
|
progress: (job.progress as number | Record<string, unknown>) ?? null,
|
|
644
|
-
failedReason: job.failedReason
|
|
661
|
+
failedReason: Queue.normalizeFailedReason(job.failedReason),
|
|
645
662
|
stackTrace: job.stacktrace || [],
|
|
646
663
|
logs: logs,
|
|
647
664
|
attemptsMade: job.attemptsMade || 0,
|
|
@@ -92,7 +92,10 @@ export default class QueueWorker {
|
|
|
92
92
|
(err as { name?: string })?.name === "TimeoutException"
|
|
93
93
|
? "timeout"
|
|
94
94
|
: "failure";
|
|
95
|
-
throw
|
|
95
|
+
throw QueueWorker.toReportableError(
|
|
96
|
+
err,
|
|
97
|
+
`${queueName}/${job.name || "unknown"} (job ${job.id ?? "unknown"})`,
|
|
98
|
+
);
|
|
96
99
|
} finally {
|
|
97
100
|
const elapsedNs: bigint = process.hrtime.bigint() - startNs;
|
|
98
101
|
const durationMs: number = Number(elapsedNs) / 1e6;
|
|
@@ -134,6 +137,69 @@ export default class QueueWorker {
|
|
|
134
137
|
return worker;
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
private static toReportableError(err: unknown, context: string): unknown {
|
|
141
|
+
const messageIsUseful: (value: unknown) => boolean = (
|
|
142
|
+
value: unknown,
|
|
143
|
+
): boolean => {
|
|
144
|
+
const message: unknown = (value as { message?: unknown })?.message;
|
|
145
|
+
if (typeof message !== "string") {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const normalized: string = message.trim().toLowerCase();
|
|
150
|
+
return (
|
|
151
|
+
normalized.length > 0 &&
|
|
152
|
+
normalized !== "null" &&
|
|
153
|
+
normalized !== "undefined"
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (err instanceof Error && messageIsUseful(err)) {
|
|
158
|
+
return err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const detailParts: Array<string> = [];
|
|
162
|
+
|
|
163
|
+
const name: unknown = (err as { name?: unknown })?.name;
|
|
164
|
+
if (typeof name === "string" && name && name !== "Error") {
|
|
165
|
+
detailParts.push(name);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const rawMessage: unknown = (err as { message?: unknown })?.message;
|
|
169
|
+
if (typeof rawMessage === "string" && rawMessage.trim()) {
|
|
170
|
+
detailParts.push(rawMessage.trim());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (detailParts.length === 0) {
|
|
174
|
+
let serialized: string;
|
|
175
|
+
try {
|
|
176
|
+
serialized =
|
|
177
|
+
typeof err === "object" && err !== null
|
|
178
|
+
? JSON.stringify(err)
|
|
179
|
+
: String(err);
|
|
180
|
+
} catch {
|
|
181
|
+
serialized = Object.prototype.toString.call(err);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
detailParts.push(
|
|
185
|
+
serialized && serialized !== "{}" ? serialized : String(err),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const reportable: Error = new Error(
|
|
190
|
+
`${context} failed with a non-descriptive error (original message was empty/null). Detail: ${detailParts.join(
|
|
191
|
+
": ",
|
|
192
|
+
)}`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const originalStack: unknown = (err as { stack?: unknown })?.stack;
|
|
196
|
+
if (typeof originalStack === "string" && originalStack) {
|
|
197
|
+
reportable.stack = originalStack;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return reportable;
|
|
201
|
+
}
|
|
202
|
+
|
|
137
203
|
@CaptureSpan()
|
|
138
204
|
public static async runJobWithTimeout(
|
|
139
205
|
timeoutInMS: number,
|
|
@@ -20,9 +20,8 @@ type TelemetrySource = {
|
|
|
20
20
|
tableName: string;
|
|
21
21
|
attributesColumn: string;
|
|
22
22
|
/*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* undefined for those; the SQL falls back to mapKeys(attributes).
|
|
23
|
+
* Older telemetry tables may lack a separate attributeKeys array column.
|
|
24
|
+
* Leave this undefined for those; the SQL falls back to mapKeys(attributes).
|
|
26
25
|
*/
|
|
27
26
|
attributeKeysColumn?: string | undefined;
|
|
28
27
|
timeColumn: string;
|
|
@@ -83,7 +82,7 @@ export class TelemetryAttributeService {
|
|
|
83
82
|
service: ExceptionInstanceService,
|
|
84
83
|
tableName: ExceptionInstanceService.model.tableName,
|
|
85
84
|
attributesColumn: "attributes",
|
|
86
|
-
|
|
85
|
+
attributeKeysColumn: "attributeKeys",
|
|
87
86
|
timeColumn: "time",
|
|
88
87
|
};
|
|
89
88
|
default:
|
|
@@ -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
|
);
|
|
@@ -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", () => {
|