From ee6c697c4403eca99e8c80dbcbf96cc7bd8ce588 Mon Sep 17 00:00:00 2001 From: Clayton Grassick Date: Thu, 18 Mar 2021 14:47:43 -0400 Subject: [PATCH] Added includes --- lib/ExprCompiler.js | 33 ++++++++++++++++++++++++----- lib/PromiseExprEvaluator.js | 5 +++++ package-lock.json | 2 +- src/ExprCompiler.ts | 40 ++++++++++++++++++++++++++++------- src/PromiseExprEvaluator.ts | 5 +++++ test/ExprCompilerTests.coffee | 36 ++++++++++++++++++++++++------- test/testExprs.coffee | 3 +++ 7 files changed, 102 insertions(+), 22 deletions(-) diff --git a/lib/ExprCompiler.js b/lib/ExprCompiler.js index a489e80..0c03654 100644 --- a/lib/ExprCompiler.js +++ b/lib/ExprCompiler.js @@ -675,17 +675,40 @@ var ExprCompiler = /** @class */ (function () { if (compiledExprs[0] == null || compiledExprs[1] == null) { return null; } - // Null if no expressions in literal list - if ((compiledExprs[1].type === "literal") && (compiledExprs[1].value.length === 0)) { + // Use (select bool_or(x.value) from (select LEFT::jsonb @> jsonb_array_elements(RIGHT::jsonb) as value) as x) + return { + type: "scalar", + expr: { type: "op", op: "bool_or", exprs: [{ type: "field", tableAlias: "elements", column: "value" }] }, + from: { + type: "subquery", + alias: "elements", + query: { + type: "query", + selects: [ + { + type: "select", + expr: { type: "op", op: "@>", exprs: [ + convertToJsonB(compiledExprs[0]), + { type: "op", op: "jsonb_array_elements", exprs: [convertToJsonB(compiledExprs[1])] } + ] }, + alias: "value" + } + ] + } + } + }; + case "includes": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { return null; } - // Cast to jsonb and use ?| Also convert to json first to handle literal arrays + // Cast both to jsonb and use @>. Also convert both to json first to handle literal arrays return { type: "op", - op: "?|", + op: "@>", exprs: [ convertToJsonB(compiledExprs[0]), - compiledExprs[1] + convertToJsonB(compiledExprs[1]) ] }; case "length": diff --git a/lib/PromiseExprEvaluator.js b/lib/PromiseExprEvaluator.js index fc7f9ff..ad34a21 100644 --- a/lib/PromiseExprEvaluator.js +++ b/lib/PromiseExprEvaluator.js @@ -825,6 +825,11 @@ var PromiseExprEvaluator = /** @class */ (function () { return null; } return lodash_1.default.intersection(values[0], values[1]).length > 0; + case "includes": + if (hasNull) { + return null; + } + return lodash_1.default.includes(values[0], values[1]); case "length": if (hasNull) { return 0; diff --git a/package-lock.json b/package-lock.json index 598c6d6..d65486e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5751,7 +5751,7 @@ "dev": true }, "jsonql": { - "version": "github:mWater/jsonql#f7784950e617785940ad3928b9d7262f13d3a09d", + "version": "github:mWater/jsonql#8cf8a59d13b09b1648d3ee1c04387ef6449d2372", "from": "github:mWater/jsonql", "requires": { "js-yaml": "^3.3.1", diff --git a/src/ExprCompiler.ts b/src/ExprCompiler.ts index df9feb8..4364f77 100644 --- a/src/ExprCompiler.ts +++ b/src/ExprCompiler.ts @@ -723,7 +723,7 @@ export default class ExprCompiler { convertToJsonB(compiledExprs[0]), convertToJsonB(compiledExprs[1]) ] - }; + } case "intersects": // Null if either not present @@ -731,21 +731,45 @@ export default class ExprCompiler { return null; } - // Null if no expressions in literal list - if (((compiledExprs[1] as any).type === "literal") && ((compiledExprs[1] as any).value.length === 0)) { + // Use (select bool_or(x.value) from (select LEFT::jsonb @> jsonb_array_elements(RIGHT::jsonb) as value) as x) + return { + type: "scalar", + expr: { type: "op", op: "bool_or", exprs: [{ type: "field", tableAlias: "elements", column: "value" }] }, + from: { + type: "subquery", + alias: "elements", + query: { + type: "query", + selects: [ + { + type: "select", + expr: { type: "op", op: "@>", exprs: [ + convertToJsonB(compiledExprs[0]), + { type: "op", op: "jsonb_array_elements", exprs: [convertToJsonB(compiledExprs[1])] } + ]}, + alias: "value" + } + ] + } + } + } + + case "includes": + // Null if either not present + if (compiledExprs[0] == null || compiledExprs[1] == null) { return null; } - // Cast to jsonb and use ?| Also convert to json first to handle literal arrays + // Cast both to jsonb and use @>. Also convert both to json first to handle literal arrays return { type: "op", - op: "?|", + op: "@>", exprs: [ convertToJsonB(compiledExprs[0]), - compiledExprs[1] + convertToJsonB(compiledExprs[1]) ] - }; - + } + case "length": // 0 if null if ((compiledExprs[0] == null)) { diff --git a/src/PromiseExprEvaluator.ts b/src/PromiseExprEvaluator.ts index a0e1c03..21af6c1 100644 --- a/src/PromiseExprEvaluator.ts +++ b/src/PromiseExprEvaluator.ts @@ -659,6 +659,11 @@ export class PromiseExprEvaluator { return null } return _.intersection(values[0], values[1]).length > 0 + case "includes": + if (hasNull) { + return null + } + return _.includes(values[0], values[1]) case "length": if (hasNull) { return 0 diff --git a/test/ExprCompilerTests.coffee b/test/ExprCompilerTests.coffee index 92aeaa7..fd42dc4 100644 --- a/test/ExprCompilerTests.coffee +++ b/test/ExprCompilerTests.coffee @@ -1493,37 +1493,57 @@ describe "ExprCompiler", -> null ) - it "compiles intersects", -> + it "compiles includes", -> @compile( { type: "op" - op: "intersects", + op: "includes", exprs: [ { type: "field", table: "t1", column: "enumset" } - { type: "literal", valueType: "enumset", value: ["a", "b"] } + { type: "literal", valueType: "enum", value: "a" } ] } { type: "op" - op: "?|" + op: "@>" exprs: [ { type: "op", op: "to_jsonb", exprs: [{ type: "field", tableAlias: "T1", column: "enumset" }] } - { type: "literal", value: ["a", "b"] } + { type: "op", op: "::jsonb", exprs: [{ type: "literal", value: '"a"' }]} ] } ) - it "compiles empty intersects", -> + it "compiles intersects", -> @compile( { type: "op" op: "intersects", exprs: [ { type: "field", table: "t1", column: "enumset" } - { type: "literal", valueType: "enumset", value: [] } + { type: "literal", valueType: "enumset", value: ["a", "b"] } ] } - null + { + type: "scalar", + expr: { type: "op", op: "bool_or", exprs: [{ type: "field", tableAlias: "elements", column: "value" }] }, + from: { + type: "subquery", + alias: "elements", + query: { + type: "query", + selects: [ + { + type: "select", + expr: { type: "op", op: "@>", exprs: [ + { type: "op", op: "to_jsonb", exprs: [{ type: "field", tableAlias: "T1", column: "enumset" }] } + { type: "op", op: "jsonb_array_elements", exprs: [{ type: "op", op: "::jsonb", exprs: [{ type: "literal", value: '["a","b"]' }]}] } + ]}, + alias: "value" + } + ] + } + } + } ) it "compiles length", -> diff --git a/test/testExprs.coffee b/test/testExprs.coffee index ef3eac2..245573c 100644 --- a/test/testExprs.coffee +++ b/test/testExprs.coffee @@ -161,6 +161,9 @@ addOp(false, "intersects", literal(["a", "b", "c"], "enumset"), literal(["d"], " addOp(true, "intersects", literal(["a", "b", "c"], "text[]"), literal(["a", "x"], "text[]")) addOp(false, "intersects", literal(["a", "b", "c"], "text[]"), literal(["d"], "text[]")) +addOp(true, "includes", literal(["a", "b", "c"], "text[]"), literal("a", "text")) +addOp(false, "includes", literal(["a", "b", "c"], "enumset"), literal("d", "enum")) + # Length of null returns 0 as enumsets are not stored as [] when empty, but rather as null addOp(2, "length", literal(["a", "b"], "enumset")) addOp(0, "length", literal(null, "enumset"))