diff --git a/src/helper/utils.ts b/src/helper/utils.ts index 606345c1..2e376a61 100644 --- a/src/helper/utils.ts +++ b/src/helper/utils.ts @@ -167,13 +167,20 @@ export function pipeWithError(src: ReadableStream, dest: WritableStream): any { export function handleNullCheckIfNeeded( xs: T[], nullCheck: string, - orAnd: 'OR' | 'AND', + andOr: 'AND' | 'OR', fn: (withoutNull: T[]) => string, ): string { + if (!xs.length) { + // This should never happen in real usage but in general return the 'zero' value of the andOr op + return andOr === 'AND' ? 'FALSE' : 'TRUE'; + } + const withoutNull = xs.filter(x => x != null); if (withoutNull.length === xs.length) { return fn(xs); + } else if (withoutNull.length === 0) { + return nullCheck; } else { - return `(${nullCheck} ${orAnd} ${fn(withoutNull)})`; + return `(${nullCheck} ${andOr} ${fn(withoutNull)})`; } } diff --git a/test/simulate/simulateDruidSql.mocha.js b/test/simulate/simulateDruidSql.mocha.js index 21fc9773..616c81b6 100644 --- a/test/simulate/simulateDruidSql.mocha.js +++ b/test/simulate/simulateDruidSql.mocha.js @@ -229,6 +229,35 @@ describe('simulate DruidSql', () => { ]); }); + it('works with overlap of [null]', () => { + const ex = ply() + .apply('diamonds', $('diamonds')) + .apply('Tags', $('diamonds').split('$tags', 'Tag')); + + const queryPlan = ex.simulateQueryPlan({ + diamonds: External.fromJS({ + engine: 'druidsql', + version: '0.20.0', + source: 'dia.monds', + timeAttribute: 'time', + attributes, + allowSelectQueries: true, + filter: $('pugs').overlap([null]), + }), + }); + expect(queryPlan.length).to.equal(1); + expect(queryPlan).to.deep.equal([ + [ + { + context: { + sqlTimeZone: 'Etc/UTC', + }, + query: 'SELECT\n"tags" AS "Tag"\nFROM "dia.monds" AS t\nWHERE "pugs" IS NULL\nGROUP BY 1', + }, + ], + ]); + }); + it('works with null and null string are both included in a filter expression (mvOverlap)', () => { const ex = ply() .apply('diamonds', $('diamonds').filter($('tags').mvOverlap(['tagA', 'tagB', null, 'null'])))