Skip to content

Commit 63f57da

Browse files
committed
[O2B-1506] Add null-aware range filters for run duration queries
Introduced greaterThanOrNull and lowerThanOrNull methods in QueryBuilder to handle cases where 0 is stored as null in the database. Used these methods instead of editing _or and applyOperation methods. Added tests to check <= and >= in the usecase work correctly.
1 parent 4e68523 commit 63f57da

3 files changed

Lines changed: 105 additions & 38 deletions

File tree

lib/database/utilities/QueryBuilder.js

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,55 @@ class WhereQueryBuilder {
145145
}
146146

147147
/**
148-
* Set the next filter condition according to a comparison operator ("<", "<=", ">=", ">")
148+
* Set a min range limit using the provided value OR null (useful when 0 is stored as null)
149+
*
150+
* @param {*} min the lower limit criteria
151+
* @param {boolean} strict if true, the comparison is exclusive, else it will be inclusive (equality match)
152+
*
153+
* @return {void}
154+
*/
155+
greaterThanOrNull(min, strict) {
156+
if (this.notFlag) {
157+
this.notFlag = !this.notFlag;
158+
this.lowerThanOrNull(min, !strict);
159+
this.notFlag = !this.notFlag;
160+
return;
161+
}
162+
const operation = {
163+
[Op.or]: [
164+
{ [Op.is]: null },
165+
{ [strict ? Op.gt : Op.gte]: min },
166+
],
167+
};
168+
this._op(operation);
169+
}
170+
171+
/**
172+
* Set a max range limit using the provided value OR null (useful when 0 is stored as null)
173+
*
174+
* @param {*} max the upper limit criteria
175+
* @param {boolean} strict if true, the comparison is exclusive, else it will be inclusive (equality match)
176+
*
177+
* @return {void}
178+
*/
179+
lowerThanOrNull(max, strict) {
180+
if (this.notFlag) {
181+
this.notFlag = !this.notFlag;
182+
this.greaterThanOrNull(max, !strict);
183+
this.notFlag = !this.notFlag;
184+
return;
185+
}
186+
const operation = {
187+
[Op.or]: [
188+
{ [Op.is]: null },
189+
{ [strict ? Op.lt : Op.lte]: max },
190+
],
191+
};
192+
this._op(operation);
193+
}
194+
195+
/**
196+
* Set the next filter condition according to a comparison operator ("<", "<=", ">=", ">", "=")
149197
*
150198
* @param {"<"|"<="|"="|">="|">"} operator the operator to apply
151199
* @param {*} limit the limit to compare to
@@ -360,36 +408,18 @@ class WhereAssociationQueryBuilder extends WhereQueryBuilder {
360408

361409
/**
362410
* Sets the operation.
363-
* If an WhereAssociation is already set it will convert to an OR condition
364411
*
365412
* @param {Object} operation The Sequelize operation to use as where filter.
366413
* @returns {QueryBuilder} The current QueryBuilder instance.
367414
*/
368415
_op(operation) {
369-
// Check if this include association already exists
370-
const existingInclude = this.queryBuilder.options.include?.find((include) => include.association === this.association);
371-
372-
if (existingInclude && existingInclude.where) {
373-
/*
374-
* Replace existing where operation in the include with OR operation.
375-
* This basically encapsulates the existing where operation in a OR operation together with the new operation.
376-
*/
377-
existingInclude.where = {
378-
[Op.or]: [
379-
{ [this.column]: existingInclude.where[this.column] },
380-
{ [this.column]: operation },
381-
],
382-
};
383-
} else {
384-
// Create new include
385-
this.queryBuilder.include({
386-
association: this.association,
387-
required: true,
388-
where: {
389-
[this.column]: operation,
390-
},
391-
});
392-
}
416+
this.queryBuilder.include({
417+
association: this.association,
418+
required: true,
419+
where: {
420+
[this.column]: operation,
421+
},
422+
});
393423

394424
return this.queryBuilder;
395425
}

lib/usecases/lhcFill/GetAllLhcFillsUseCase.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,13 @@ class GetAllLhcFillsUseCase {
6565
}
6666
}
6767

68-
// Run duration filter and corresponding operator.
6968
if (runDuration?.limit !== undefined && runDuration?.operator) {
7069
associatedStatisticsRequired = true;
7170
// 00:00:00 aka 0 value is saved in the DB as null (bookkeeping.fill_statistics.runs_coverage)
72-
if ((runDuration.operator === '>=' || runDuration.operator === '<=') && Number(runDuration.limit) === 0) {
73-
// Include 00:00:00 = 0 = null AND everything above 00:00:00 which is more or less than 0.
74-
queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, 0);
75-
queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator('=', null);
71+
if (runDuration.operator === '<=' && Number(runDuration.limit) === 0) {
72+
queryBuilder.whereAssociation('statistics', 'runsCoverage').lowerThanOrNull(0, false);
73+
} else if (runDuration.operator === '>=' && Number(runDuration.limit) === 0) {
74+
queryBuilder.whereAssociation('statistics', 'runsCoverage').greaterThanOrNull(0, false);
7675
} else if ((runDuration.operator === '>' || runDuration.operator === '<') && Number(runDuration.limit) === 0) {
7776
queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, 0);
7877
} else if (Number(runDuration.limit) === 0) {

test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ module.exports = () => {
111111
});
112112
});
113113

114-
it('should only contain specified stable beam durations, <= 12:00:00', async () => {
114+
it('should only contain specified stable beam durations, <= 12:00:00', async () => {
115115
getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '43200', operator: '<='} } };
116116
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
117117
expect(lhcFills).to.be.an('array').and.lengthOf(4)
@@ -129,32 +129,70 @@ module.exports = () => {
129129
});
130130
});
131131

132-
it('should only contain specified stable beam durations, >= 00:01:40', async () => {
132+
it('should only contain specified stable beam durations, >= 00:01:40', async () => {
133133
getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '100', operator: '>='} } };
134134
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
135135

136136
expect(lhcFills).to.be.an('array').and.lengthOf(4)
137137
lhcFills.forEach((lhcFill) => {
138138
expect(lhcFill.stableBeamsDuration).greaterThanOrEqual(100)
139139
});
140-
})
140+
})
141141

142-
it('should only contain specified stable beam durations, > 00:01:40', async () => {
142+
it('should only contain specified stable beam durations, > 00:01:40', async () => {
143143
getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '100', operator: '>'} } };
144144
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
145145

146146
expect(lhcFills).to.be.an('array').and.lengthOf(1)
147147
lhcFills.forEach((lhcFill) => {
148148
expect(lhcFill.stableBeamsDuration).greaterThan(100)
149149
});
150-
})
150+
})
151151

152-
it('should only contain specified stable beam durations, = 00:00:00', async () => {
152+
it('should only contain specified stable beam durations, = 00:00:00', async () => {
153153
getAllLhcFillsDto.query = { filter: { hasStableBeams: true, beamDuration: {limit: '0', operator: '='} } };
154154
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
155155

156156
expect(lhcFills).to.be.an('array').and.lengthOf(0)
157-
})
157+
})
158+
159+
it('should only contain specified total run duration, < 00:00:00', async () => {
160+
getAllLhcFillsDto.query = { filter: { runDuration: {limit: '0', operator: '<'} } };
161+
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
162+
163+
expect(lhcFills).to.be.an('array').and.lengthOf(0)
164+
});
165+
166+
it('should only contain specified total run duration, > 00:00:00', async () => {
167+
getAllLhcFillsDto.query = { filter: { runDuration: {limit: '0', operator: '>'} } };
168+
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
169+
170+
expect(lhcFills).to.be.an('array').and.lengthOf(1)
171+
lhcFills.forEach((lhcFill) => {
172+
expect(lhcFill.statistics.runsCoverage).greaterThan(0)
173+
});
174+
});
175+
176+
it('should only contain specified total run duration, <= 00:00:00', async () => {
177+
getAllLhcFillsDto.query = { filter: { runDuration: {limit: '0', operator: '<='} } };
178+
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
179+
180+
expect(lhcFills).to.be.an('array').and.lengthOf(4)
181+
lhcFills.forEach((lhcFill) => {
182+
expect(lhcFill.statistics.runsCoverage).equals(0)
183+
});
184+
});
185+
186+
it('should only contain specified total run duration, >= 00:00:00', async () => {
187+
getAllLhcFillsDto.query = { filter: { runDuration: {limit: '0', operator: '>='} } };
188+
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto)
189+
190+
expect(lhcFills).to.be.an('array').and.lengthOf(5)
191+
lhcFills.forEach((lhcFill) => {
192+
expect(lhcFill.statistics.runsCoverage).greaterThanOrEqual(0)
193+
});
194+
});
195+
158196

159197
it('should only contain specified total run duration, > 04:00:00', async () => {
160198
getAllLhcFillsDto.query = { filter: { runDuration: {limit: '14400', operator: '>'} } };

0 commit comments

Comments
 (0)