This is an automated email from the ASF dual-hosted git repository.
gian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 16f5ac5bd5 json_value adjustments (#12968)
16f5ac5bd5 is described below
commit 16f5ac5bd5ea4d8ce1eef546793c90298f1a43e0
Author: Clint Wylie <[email protected]>
AuthorDate: Sat Aug 27 07:15:47 2022 -0700
json_value adjustments (#12968)
* json_value adjustments
changes:
* native json_value expression now has optional 3rd argument to specify
type, which will cast all values to the specified type
* rework how JSON_VALUE is wired up in SQL. Now we are using a custom
convertlet to translate JSON_VALUE(... RETURNING type) into dedicated
JSON_VALUE_BIGINT, JSON_VALUE_DOUBLE, JSON_VALUE_VARCHAR, JSON_VALUE_ANY
instead of using the calcite StandardConvertletTable that wraps JSON_VALUE_ANY
in a CAST, so that we preserve the typing of JSON_VALUE to pass down to the
native expression as the 3rd argument
* fix json_value_any to be usable by humans too, coverage
* fix bug
* checkstyle
* checkstyle
* review stuff
* validate that options to json_value are the supported options rather than
ignore them
* remove more legacy undocumented functions
---
docs/misc/math-expr.md | 4 +-
.../query/expression/NestedDataExpressions.java | 287 +++++----------
.../expression/NestedDataExpressionsTest.java | 136 ++-----
.../druid/query/expression/TestExprMacroTable.java | 2 -
.../org/apache/druid/guice/ExpressionModule.java | 4 -
.../builtin/NestedDataOperatorConversions.java | 394 ++++++++++++++-------
.../sql/calcite/planner/DruidOperatorTable.java | 6 +-
.../planner/convertlet/DruidConvertletTable.java | 2 +
.../sql/calcite/CalciteNestedDataQueryTest.java | 284 +++++++++------
9 files changed, 579 insertions(+), 540 deletions(-)
diff --git a/docs/misc/math-expr.md b/docs/misc/math-expr.md
index 94167800c4..8fe4aba1ab 100644
--- a/docs/misc/math-expr.md
+++ b/docs/misc/math-expr.md
@@ -63,7 +63,7 @@ The following built-in functions are available.
|name|description|
|----|-----------|
-|cast|cast(expr,'LONG' or 'DOUBLE' or 'STRING' or 'LONG_ARRAY', or
'DOUBLE_ARRAY' or 'STRING_ARRAY') returns expr with specified type. exception
can be thrown. Scalar types may be cast to array types and will take the form
of a single element list (null will still be null). |
+|cast|cast(expr,'LONG' or 'DOUBLE' or 'STRING' or 'ARRAY<LONG>', or
'ARRAY<DOUBLE>' or 'ARRAY<STRING>') returns expr with specified type. exception
can be thrown. Scalar types may be cast to array types and will take the form
of a single element list (null will still be null). |
|if|if(predicate,then,else) returns 'then' if 'predicate' evaluates to a
positive number, otherwise it returns 'else' |
|nvl|nvl(expr,expr-for-null) returns 'expr-for-null' if 'expr' is null (or
empty string for string type) |
|like|like(expr, pattern[, escape]) is equivalent to SQL `expr LIKE pattern`|
@@ -232,7 +232,7 @@ JSON functions provide facilities to extract, transform,
and create `COMPLEX<jso
| function | description |
|---|---|
-| json_value(expr, path) | Extract a Druid literal (`STRING`, `LONG`,
`DOUBLE`) value from `expr` using JSONPath syntax of `path` |
+| json_value(expr, path[, type]) | Extract a Druid literal (`STRING`, `LONG`,
`DOUBLE`) value from `expr` using JSONPath syntax of `path`. The optional
`type` argument can be set to `'LONG'`,`'DOUBLE'` or `'STRING'` to cast values
to that type. |
| json_query(expr, path) | Extract a `COMPLEX<json>` value from `expr` using
JSONPath syntax of `path` |
| json_object(expr1, expr2[, expr3, expr4 ...]) | Construct a `COMPLEX<json>`
with alternating 'key' and 'value' arguments|
| parse_json(expr) | Deserialize a JSON `STRING` into a `COMPLEX<json>`. If
the input is not a `STRING` or it is invalid JSON, this function will result in
an error.|
diff --git
a/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java
b/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java
index 45dc5a127f..768d519631 100644
---
a/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java
+++
b/processing/src/main/java/org/apache/druid/query/expression/NestedDataExpressions.java
@@ -23,7 +23,6 @@ package org.apache.druid.query.expression;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableList;
import org.apache.druid.guice.annotations.Json;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.math.expr.Expr;
@@ -50,9 +49,9 @@ public class NestedDataExpressions
ExpressionType.fromColumnType(NestedDataComplexTypeSerde.TYPE)
);
- public static class StructExprMacro implements ExprMacroTable.ExprMacro
+ public static class JsonObjectExprMacro implements ExprMacroTable.ExprMacro
{
- public static final String NAME = "struct";
+ public static final String NAME = "json_object";
@Override
public String name()
@@ -104,17 +103,6 @@ public class NestedDataExpressions
}
}
- public static class JsonObjectExprMacro extends StructExprMacro
- {
- public static final String NAME = "json_object";
-
- @Override
- public String name()
- {
- return NAME;
- }
- }
-
public static class ToJsonStringExprMacro implements ExprMacroTable.ExprMacro
{
public static final String NAME = "to_json_string";
@@ -138,7 +126,7 @@ public class NestedDataExpressions
@Override
public Expr apply(List<Expr> args)
{
- class ToJsonStringExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
+ final class ToJsonStringExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
public ToJsonStringExpr(List<Expr> args)
{
@@ -203,7 +191,7 @@ public class NestedDataExpressions
@Override
public Expr apply(List<Expr> args)
{
- class ParseJsonExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
+ final class ParseJsonExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
public ParseJsonExpr(List<Expr> args)
{
@@ -278,7 +266,7 @@ public class NestedDataExpressions
@Override
public Expr apply(List<Expr> args)
{
- class ParseJsonExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
+ final class ParseJsonExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
public ParseJsonExpr(List<Expr> args)
{
@@ -327,11 +315,9 @@ public class NestedDataExpressions
}
}
-
-
- public static class GetPathExprMacro implements ExprMacroTable.ExprMacro
+ public static class JsonValueExprMacro implements ExprMacroTable.ExprMacro
{
- public static final String NAME = "get_path";
+ public static final String NAME = "json_value";
@Override
public String name()
@@ -342,39 +328,78 @@ public class NestedDataExpressions
@Override
public Expr apply(List<Expr> args)
{
- final List<NestedPathPart> parts = getArg1PathPartsFromLiteral(name(),
args);
- class GetPathExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
- {
- public GetPathExpr(List<Expr> args)
- {
- super(name(), args);
+ final List<NestedPathPart> parts = getJsonPathPartsFromLiteral(name(),
args.get(1));
+ if (args.size() == 3 && args.get(2).isLiteral()) {
+ final ExpressionType castTo = ExpressionType.fromString((String)
args.get(2).getLiteralValue());
+ if (castTo == null) {
+ throw new IAE("Invalid output type: [%s]",
args.get(2).getLiteralValue());
}
-
- @Override
- public ExprEval eval(ObjectBinding bindings)
+ final class JsonValueCastExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
- ExprEval input = args.get(0).eval(bindings);
- return ExprEval.bestEffortOf(
- NestedPathFinder.findLiteral(unwrap(input), parts)
- );
- }
+ public JsonValueCastExpr(List<Expr> args)
+ {
+ super(name(), args);
+ }
- @Override
- public Expr visit(Shuttle shuttle)
- {
- List<Expr> newArgs = args.stream().map(x ->
x.visit(shuttle)).collect(Collectors.toList());
- return shuttle.visit(new GetPathExpr(newArgs));
- }
+ @Override
+ public ExprEval eval(ObjectBinding bindings)
+ {
+ ExprEval input = args.get(0).eval(bindings);
+ return ExprEval.bestEffortOf(
+ NestedPathFinder.findLiteral(unwrap(input), parts)
+ ).castTo(castTo);
+ }
- @Nullable
- @Override
- public ExpressionType getOutputType(InputBindingInspector inspector)
+ @Override
+ public Expr visit(Shuttle shuttle)
+ {
+ List<Expr> newArgs = args.stream().map(x ->
x.visit(shuttle)).collect(Collectors.toList());
+ return shuttle.visit(new JsonValueCastExpr(newArgs));
+ }
+
+ @Nullable
+ @Override
+ public ExpressionType getOutputType(InputBindingInspector inspector)
+ {
+ return castTo;
+ }
+ }
+ return new JsonValueCastExpr(args);
+ } else {
+ final class JsonValueExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
- // we cannot infer the output type (well we could say it is 'STRING'
right now because is all we support...
- return null;
+
+ public JsonValueExpr(List<Expr> args)
+ {
+ super(name(), args);
+ }
+
+ @Override
+ public ExprEval eval(ObjectBinding bindings)
+ {
+ ExprEval input = args.get(0).eval(bindings);
+ return ExprEval.bestEffortOf(
+ NestedPathFinder.findLiteral(unwrap(input), parts)
+ );
+ }
+
+ @Override
+ public Expr visit(Shuttle shuttle)
+ {
+ List<Expr> newArgs = args.stream().map(x ->
x.visit(shuttle)).collect(Collectors.toList());
+ return shuttle.visit(new JsonValueExpr(newArgs));
+ }
+
+ @Nullable
+ @Override
+ public ExpressionType getOutputType(InputBindingInspector inspector)
+ {
+ // we cannot infer output type because there could be anything at
the path, and, we lack a proper VARIANT type
+ return null;
+ }
}
+ return new JsonValueExpr(args);
}
- return new GetPathExpr(args);
}
}
@@ -391,8 +416,8 @@ public class NestedDataExpressions
@Override
public Expr apply(List<Expr> args)
{
- final List<NestedPathPart> parts =
getArg1JsonPathPartsFromLiteral(name(), args);
- class JsonQueryExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
+ final List<NestedPathPart> parts = getJsonPathPartsFromLiteral(name(),
args.get(1));
+ final class JsonQueryExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
public JsonQueryExpr(List<Expr> args)
{
@@ -428,114 +453,6 @@ public class NestedDataExpressions
}
}
- public static class JsonValueExprMacro implements ExprMacroTable.ExprMacro
- {
- public static final String NAME = "json_value";
-
- @Override
- public String name()
- {
- return NAME;
- }
-
- @Override
- public Expr apply(List<Expr> args)
- {
- final List<NestedPathPart> parts =
getArg1JsonPathPartsFromLiteral(name(), args);
- class JsonValueExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
- {
- public JsonValueExpr(List<Expr> args)
- {
- super(name(), args);
- }
-
- @Override
- public ExprEval eval(ObjectBinding bindings)
- {
- ExprEval input = args.get(0).eval(bindings);
- return ExprEval.bestEffortOf(
- NestedPathFinder.findLiteral(unwrap(input), parts)
- );
- }
-
- @Override
- public Expr visit(Shuttle shuttle)
- {
- List<Expr> newArgs = args.stream().map(x ->
x.visit(shuttle)).collect(Collectors.toList());
- return shuttle.visit(new JsonValueExpr(newArgs));
- }
-
- @Nullable
- @Override
- public ExpressionType getOutputType(InputBindingInspector inspector)
- {
- // we cannot infer the output type (well we could say it is 'STRING'
right now because is all we support...
- return null;
- }
- }
- return new JsonValueExpr(args);
- }
- }
-
- public static class ListPathsExprMacro implements ExprMacroTable.ExprMacro
- {
- public static final String NAME = "list_paths";
-
- @Override
- public String name()
- {
- return NAME;
- }
-
- @Override
- public Expr apply(List<Expr> args)
- {
- final StructuredDataProcessor processor = new StructuredDataProcessor()
- {
- @Override
- public int processLiteralField(String fieldName, Object fieldValue)
- {
- // do nothing, we only want the list of fields returned by this
processor
- return 0;
- }
- };
-
- class ListPathsExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
- {
- public ListPathsExpr(List<Expr> args)
- {
- super(name(), args);
- }
-
- @Override
- public ExprEval eval(ObjectBinding bindings)
- {
- ExprEval input = args.get(0).eval(bindings);
- StructuredDataProcessor.ProcessResults info =
processor.processFields(unwrap(input));
- return ExprEval.ofType(
- ExpressionType.STRING_ARRAY,
- ImmutableList.copyOf(info.getLiteralFields())
- );
- }
-
- @Override
- public Expr visit(Shuttle shuttle)
- {
- List<Expr> newArgs = args.stream().map(x ->
x.visit(shuttle)).collect(Collectors.toList());
- return shuttle.visit(new ListPathsExpr(newArgs));
- }
-
- @Nullable
- @Override
- public ExpressionType getOutputType(InputBindingInspector inspector)
- {
- return ExpressionType.STRING_ARRAY;
- }
- }
- return new ListPathsExpr(args);
- }
- }
-
public static class JsonPathsExprMacro implements ExprMacroTable.ExprMacro
{
public static final String NAME = "json_paths";
@@ -559,7 +476,7 @@ public class NestedDataExpressions
}
};
- class JsonPathsExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
+ final class JsonPathsExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
public JsonPathsExpr(List<Expr> args)
{
@@ -600,9 +517,9 @@ public class NestedDataExpressions
}
}
- public static class ListKeysExprMacro implements ExprMacroTable.ExprMacro
+ public static class JsonKeysExprMacro implements ExprMacroTable.ExprMacro
{
- public static final String NAME = "list_keys";
+ public static final String NAME = "json_keys";
@Override
public String name()
@@ -613,10 +530,10 @@ public class NestedDataExpressions
@Override
public Expr apply(List<Expr> args)
{
- final List<NestedPathPart> parts = getArg1PathPartsFromLiteral(name(),
args);
- class ListKeysExpr extends ExprMacroTable.BaseScalarMacroFunctionExpr
+ final List<NestedPathPart> parts = getJsonPathPartsFromLiteral(name(),
args.get(1));
+ final class JsonKeysExpr extends
ExprMacroTable.BaseScalarMacroFunctionExpr
{
- public ListKeysExpr(List<Expr> args)
+ public JsonKeysExpr(List<Expr> args)
{
super(name(), args);
}
@@ -636,7 +553,7 @@ public class NestedDataExpressions
public Expr visit(Shuttle shuttle)
{
List<Expr> newArgs = args.stream().map(x ->
x.visit(shuttle)).collect(Collectors.toList());
- return shuttle.visit(new ListKeysExpr(newArgs));
+ return shuttle.visit(new JsonKeysExpr(newArgs));
}
@Nullable
@@ -646,18 +563,7 @@ public class NestedDataExpressions
return ExpressionType.STRING_ARRAY;
}
}
- return new ListKeysExpr(args);
- }
- }
-
- public static class JsonKeysExprMacro extends ListKeysExprMacro
- {
- public static final String NAME = "json_keys";
-
- @Override
- public String name()
- {
- return NAME;
+ return new JsonKeysExpr(args);
}
}
@@ -676,39 +582,18 @@ public class NestedDataExpressions
}
- static List<NestedPathPart> getArg1PathPartsFromLiteral(String fnName,
List<Expr> args)
- {
- if (!(args.get(1).isLiteral() && args.get(1).getLiteralValue() instanceof
String)) {
- throw new IAE(
- "Function[%s] second argument [%s] must be a literal [%s] value",
- fnName,
- args.get(1).stringify(),
- ExpressionType.STRING
- );
- }
- final String path = (String) args.get(1).getLiteralValue();
- List<NestedPathPart> parts;
- try {
- parts = NestedPathFinder.parseJsonPath(path);
- }
- catch (IllegalArgumentException iae) {
- parts = NestedPathFinder.parseJqPath(path);
- }
- return parts;
- }
-
- static List<NestedPathPart> getArg1JsonPathPartsFromLiteral(String fnName,
List<Expr> args)
+ static List<NestedPathPart> getJsonPathPartsFromLiteral(String fnName, Expr
arg)
{
- if (!(args.get(1).isLiteral() && args.get(1).getLiteralValue() instanceof
String)) {
+ if (!(arg.isLiteral() && arg.getLiteralValue() instanceof String)) {
throw new IAE(
"Function[%s] second argument [%s] must be a literal [%s] value",
fnName,
- args.get(1).stringify(),
+ arg.stringify(),
ExpressionType.STRING
);
}
final List<NestedPathPart> parts = NestedPathFinder.parseJsonPath(
- (String) args.get(1).getLiteralValue()
+ (String) arg.getLiteralValue()
);
return parts;
}
diff --git
a/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java
b/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java
index ce495bb97c..82415f18c6 100644
---
a/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java
+++
b/processing/src/test/java/org/apache/druid/query/expression/NestedDataExpressionsTest.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import org.apache.druid.common.config.NullHandling;
import org.apache.druid.jackson.DefaultObjectMapper;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.Pair;
@@ -45,10 +46,6 @@ public class NestedDataExpressionsTest extends
InitializedNullHandlingTest
private static final ObjectMapper JSON_MAPPER = new DefaultObjectMapper();
private static final ExprMacroTable MACRO_TABLE = new ExprMacroTable(
ImmutableList.of(
- new NestedDataExpressions.StructExprMacro(),
- new NestedDataExpressions.GetPathExprMacro(),
- new NestedDataExpressions.ListKeysExprMacro(),
- new NestedDataExpressions.ListPathsExprMacro(),
new NestedDataExpressions.JsonPathsExprMacro(),
new NestedDataExpressions.JsonKeysExprMacro(),
new NestedDataExpressions.JsonObjectExprMacro(),
@@ -84,20 +81,6 @@ public class NestedDataExpressionsTest extends
InitializedNullHandlingTest
.build()
);
- @Test
- public void testStructExpression()
- {
- Expr expr = Parser.parse("struct('x',100,'y',200,'z',300)", MACRO_TABLE);
- ExprEval eval = expr.eval(inputBindings);
- Assert.assertEquals(NEST, eval.value());
-
- expr =
Parser.parse("struct('x',array('a','b','c'),'y',struct('a','hello','b','world'))",
MACRO_TABLE);
- eval = expr.eval(inputBindings);
- // decompose because of array equals
- Assert.assertArrayEquals(new Object[]{"a", "b", "c"}, (Object[]) ((Map)
eval.value()).get("x"));
- Assert.assertEquals(ImmutableMap.of("a", "hello", "b", "world"), ((Map)
eval.value()).get("y"));
- }
-
@Test
public void testJsonObjectExpression()
{
@@ -112,73 +95,30 @@ public class NestedDataExpressionsTest extends
InitializedNullHandlingTest
Assert.assertEquals(ImmutableMap.of("a", "hello", "b", "world"), ((Map)
eval.value()).get("y"));
}
- @Test
- public void testListKeysExpression()
- {
- Expr expr = Parser.parse("list_keys(nest, '.')", MACRO_TABLE);
- ExprEval eval = expr.eval(inputBindings);
- Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
- Assert.assertArrayEquals(new Object[]{"x", "y", "z"}, (Object[])
eval.value());
-
-
- expr = Parser.parse("list_keys(nester, '.x')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
- Assert.assertArrayEquals(new Object[]{"0", "1", "2"}, (Object[])
eval.value());
-
- expr = Parser.parse("list_keys(nester, '.y')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
- Assert.assertArrayEquals(new Object[]{"a", "b"}, (Object[]) eval.value());
-
- expr = Parser.parse("list_keys(nester, '.x.a')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
-
- expr = Parser.parse("list_keys(nester, '.x.a.b')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
- }
-
- @Test
- public void testListPathsExpression()
- {
- Expr expr = Parser.parse("list_paths(nest)", MACRO_TABLE);
- ExprEval eval = expr.eval(inputBindings);
- Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
- Assert.assertArrayEquals(new Object[]{".\"y\"", ".\"z\"", ".\"x\""},
(Object[]) eval.value());
-
- expr = Parser.parse("list_paths(nester)", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
- Assert.assertArrayEquals(new Object[]{".\"x\"[0]", ".\"x\"[1]",
".\"x\"[2]", ".\"y\".\"b\"", ".\"y\".\"a\""}, (Object[]) eval.value());
-
- }
-
@Test
public void testJsonKeysExpression()
{
- Expr expr = Parser.parse("json_keys(nest, '.')", MACRO_TABLE);
+ Expr expr = Parser.parse("json_keys(nest, '$.')", MACRO_TABLE);
ExprEval eval = expr.eval(inputBindings);
Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
Assert.assertArrayEquals(new Object[]{"x", "y", "z"}, (Object[])
eval.value());
- expr = Parser.parse("json_keys(nester, '.x')", MACRO_TABLE);
+ expr = Parser.parse("json_keys(nester, '$.x')", MACRO_TABLE);
eval = expr.eval(inputBindings);
Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
Assert.assertArrayEquals(new Object[]{"0", "1", "2"}, (Object[])
eval.value());
- expr = Parser.parse("json_keys(nester, '.y')", MACRO_TABLE);
+ expr = Parser.parse("json_keys(nester, '$.y')", MACRO_TABLE);
eval = expr.eval(inputBindings);
Assert.assertEquals(ExpressionType.STRING_ARRAY, eval.type());
Assert.assertArrayEquals(new Object[]{"a", "b"}, (Object[]) eval.value());
- expr = Parser.parse("json_keys(nester, '.x.a')", MACRO_TABLE);
+ expr = Parser.parse("json_keys(nester, '$.x.a')", MACRO_TABLE);
eval = expr.eval(inputBindings);
Assert.assertNull(eval.value());
- expr = Parser.parse("json_keys(nester, '.x.a.b')", MACRO_TABLE);
+ expr = Parser.parse("json_keys(nester, '$.x.a.b')", MACRO_TABLE);
eval = expr.eval(inputBindings);
Assert.assertNull(eval.value());
}
@@ -197,45 +137,6 @@ public class NestedDataExpressionsTest extends
InitializedNullHandlingTest
Assert.assertArrayEquals(new Object[]{"$.x[0]", "$.x[1]", "$.x[2]",
"$.y.b", "$.y.a"}, (Object[]) eval.value());
}
- @Test
- public void testGetPathExpression()
- {
- Expr expr = Parser.parse("get_path(nest, '.x')", MACRO_TABLE);
- ExprEval eval = expr.eval(inputBindings);
- Assert.assertEquals(100L, eval.value());
- Assert.assertEquals(ExpressionType.LONG, eval.type());
-
- expr = Parser.parse("get_path(nester, '.x')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
-
- expr = Parser.parse("get_path(nester, '.x[1]')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertEquals("b", eval.value());
- Assert.assertEquals(ExpressionType.STRING, eval.type());
-
- expr = Parser.parse("get_path(nester, '.x[23]')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
-
- expr = Parser.parse("get_path(nester, '.x[1].b')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
-
- expr = Parser.parse("get_path(nester, '.y[1]')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
-
- expr = Parser.parse("get_path(nester, '.y.a')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertEquals("hello", eval.value());
- Assert.assertEquals(ExpressionType.STRING, eval.type());
-
- expr = Parser.parse("get_path(nester, '.y.a.b.c[12]')", MACRO_TABLE);
- eval = expr.eval(inputBindings);
- Assert.assertNull(eval.value());
- }
-
@Test
public void testJsonValueExpression()
{
@@ -270,6 +171,11 @@ public class NestedDataExpressionsTest extends
InitializedNullHandlingTest
Assert.assertEquals("hello", eval.value());
Assert.assertEquals(ExpressionType.STRING, eval.type());
+ expr = Parser.parse("json_value(nester, '$.y.a', 'LONG')", MACRO_TABLE);
+ eval = expr.eval(inputBindings);
+ Assert.assertEquals(NullHandling.defaultLongValue(), eval.value());
+ Assert.assertEquals(ExpressionType.LONG, eval.type());
+
expr = Parser.parse("json_value(nester, '$.y.a.b.c[12]')", MACRO_TABLE);
eval = expr.eval(inputBindings);
Assert.assertNull(eval.value());
@@ -278,6 +184,26 @@ public class NestedDataExpressionsTest extends
InitializedNullHandlingTest
eval = expr.eval(inputBindings);
Assert.assertEquals(1234L, eval.value());
Assert.assertEquals(ExpressionType.LONG, eval.type());
+
+ expr = Parser.parse("json_value(long, '$', 'STRING')", MACRO_TABLE);
+ eval = expr.eval(inputBindings);
+ Assert.assertEquals("1234", eval.value());
+ Assert.assertEquals(ExpressionType.STRING, eval.type());
+
+ expr = Parser.parse("json_value(nest, '$.x')", MACRO_TABLE);
+ eval = expr.eval(inputBindings);
+ Assert.assertEquals(100L, eval.value());
+ Assert.assertEquals(ExpressionType.LONG, eval.type());
+
+ expr = Parser.parse("json_value(nest, '$.x', 'DOUBLE')", MACRO_TABLE);
+ eval = expr.eval(inputBindings);
+ Assert.assertEquals(100.0, eval.value());
+ Assert.assertEquals(ExpressionType.DOUBLE, eval.type());
+
+ expr = Parser.parse("json_value(nest, '$.x', 'STRING')", MACRO_TABLE);
+ eval = expr.eval(inputBindings);
+ Assert.assertEquals("100", eval.value());
+ Assert.assertEquals(ExpressionType.STRING, eval.type());
}
@Test
diff --git
a/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
b/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
index a4b3c734f9..140bcac3fa 100644
---
a/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
+++
b/processing/src/test/java/org/apache/druid/query/expression/TestExprMacroTable.java
@@ -56,8 +56,6 @@ public class TestExprMacroTable extends ExprMacroTable
new HyperUniqueExpressions.HllEstimateExprMacro(),
new HyperUniqueExpressions.HllRoundEstimateExprMacro(),
new NestedDataExpressions.JsonObjectExprMacro(),
- new NestedDataExpressions.ListKeysExprMacro(),
- new NestedDataExpressions.ListPathsExprMacro(),
new NestedDataExpressions.JsonKeysExprMacro(),
new NestedDataExpressions.JsonPathsExprMacro(),
new NestedDataExpressions.JsonValueExprMacro(),
diff --git a/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
b/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
index 2aa29d4dd7..a9baad566a 100644
--- a/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
+++ b/server/src/main/java/org/apache/druid/guice/ExpressionModule.java
@@ -70,11 +70,7 @@ public class ExpressionModule implements Module
.add(HyperUniqueExpressions.HllAddExprMacro.class)
.add(HyperUniqueExpressions.HllEstimateExprMacro.class)
.add(HyperUniqueExpressions.HllRoundEstimateExprMacro.class)
- .add(NestedDataExpressions.StructExprMacro.class)
.add(NestedDataExpressions.JsonObjectExprMacro.class)
- .add(NestedDataExpressions.GetPathExprMacro.class)
- .add(NestedDataExpressions.ListKeysExprMacro.class)
- .add(NestedDataExpressions.ListPathsExprMacro.class)
.add(NestedDataExpressions.JsonKeysExprMacro.class)
.add(NestedDataExpressions.JsonPathsExprMacro.class)
.add(NestedDataExpressions.JsonValueExprMacro.class)
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java
index 045f4dc4d3..f6d2af5e83 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/NestedDataOperatorConversions.java
@@ -19,19 +19,27 @@
package org.apache.druid.sql.calcite.expression.builtin;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlDataTypeSpec;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
import org.apache.calcite.sql.type.SqlOperandCountRanges;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeTransforms;
+import org.apache.calcite.sql2rel.SqlRexConvertlet;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.math.expr.InputBindings;
@@ -42,123 +50,30 @@ import
org.apache.druid.segment.nested.NestedDataComplexTypeSerde;
import org.apache.druid.segment.nested.NestedPathFinder;
import org.apache.druid.segment.nested.NestedPathPart;
import org.apache.druid.segment.virtual.NestedFieldVirtualColumn;
-import org.apache.druid.sql.calcite.expression.AliasedOperatorConversion;
import org.apache.druid.sql.calcite.expression.DruidExpression;
import org.apache.druid.sql.calcite.expression.Expressions;
import org.apache.druid.sql.calcite.expression.OperatorConversions;
import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
-import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.planner.UnsupportedSQLQueryException;
+import org.apache.druid.sql.calcite.planner.convertlet.DruidConvertletFactory;
import org.apache.druid.sql.calcite.table.RowSignatures;
import javax.annotation.Nullable;
+import java.util.Collections;
import java.util.List;
public class NestedDataOperatorConversions
{
+ public static final DruidJsonValueConvertletFactory
DRUID_JSON_VALUE_CONVERTLET_FACTORY_INSTANCE =
+ new DruidJsonValueConvertletFactory();
+
public static final SqlReturnTypeInference NESTED_RETURN_TYPE_INFERENCE =
opBinding -> RowSignatures.makeComplexType(
opBinding.getTypeFactory(),
NestedDataComplexTypeSerde.TYPE,
true
);
- public static class GetPathOperatorConversion implements
SqlOperatorConversion
- {
- private static final String FUNCTION_NAME =
StringUtils.toUpperCase("get_path");
- private static final SqlFunction SQL_FUNCTION = OperatorConversions
- .operatorBuilder(FUNCTION_NAME)
- .operandTypeChecker(
- OperandTypes.sequence(
- "(expr,path)",
- OperandTypes.family(SqlTypeFamily.ANY),
- OperandTypes.family(SqlTypeFamily.STRING)
- )
- )
- .returnTypeCascadeNullable(SqlTypeName.VARCHAR)
- .functionCategory(SqlFunctionCategory.USER_DEFINED_FUNCTION)
- .build();
-
- @Override
- public SqlOperator calciteOperator()
- {
- return SQL_FUNCTION;
- }
-
- @Nullable
- @Override
- public DruidExpression toDruidExpression(
- PlannerContext plannerContext,
- RowSignature rowSignature,
- RexNode rexNode
- )
- {
- final RexCall call = (RexCall) rexNode;
-
- final List<DruidExpression> druidExpressions =
Expressions.toDruidExpressions(
- plannerContext,
- rowSignature,
- call.getOperands()
- );
-
- if (druidExpressions == null || druidExpressions.size() != 2) {
- return null;
- }
-
- final Expr pathExpr =
Parser.parse(druidExpressions.get(1).getExpression(),
plannerContext.getExprMacroTable());
- if (!pathExpr.isLiteral()) {
- return null;
- }
- // pre-normalize path so that the same expressions with different jq
syntax are collapsed
- final String path = (String)
pathExpr.eval(InputBindings.nilBindings()).value();
- final List<NestedPathPart> parts;
- try {
- parts = NestedPathFinder.parseJqPath(path);
- }
- catch (IllegalArgumentException iae) {
- throw new UnsupportedSQLQueryException(
- "Cannot use [%s]: [%s]",
- call.getOperator().getName(),
- iae.getMessage()
- );
- }
- final String normalized = NestedPathFinder.toNormalizedJqPath(parts);
-
- if (druidExpressions.get(0).isSimpleExtraction()) {
-
- return DruidExpression.ofVirtualColumn(
- Calcites.getColumnTypeForRelDataType(call.getType()),
- (args) -> "get_path(" + args.get(0).getExpression() + ",'" +
normalized + "')",
- ImmutableList.of(
- DruidExpression.ofColumn(NestedDataComplexTypeSerde.TYPE,
druidExpressions.get(0).getDirectColumn())
- ),
- (name, outputType, expression, macroTable) -> new
NestedFieldVirtualColumn(
- druidExpressions.get(0).getDirectColumn(),
- name,
- outputType,
- parts,
- false,
- null,
- null
- )
- );
- }
- throw new UnsupportedSQLQueryException(
- "Cannot use [%s] on expression input: [%s]",
- call.getOperator().getName(),
- druidExpressions.get(0).getExpression()
- );
- }
- }
-
- public static class JsonGetPathAliasOperatorConversion extends
AliasedOperatorConversion
- {
- public JsonGetPathAliasOperatorConversion()
- {
- super(new GetPathOperatorConversion(),
StringUtils.toUpperCase("json_get_path"));
- }
- }
-
public static class JsonPathsOperatorConversion implements
SqlOperatorConversion
{
private static final SqlFunction SQL_FUNCTION = OperatorConversions
@@ -316,12 +231,104 @@ public class NestedDataOperatorConversions
}
}
- public static class JsonValueOperatorConversion implements
SqlOperatorConversion
+
+ /**
+ * The {@link org.apache.calcite.sql2rel.StandardConvertletTable} converts
json_value(.. RETURNING type) into
+ * cast(json_value_any(..), type).
+ *
+ * This is not that useful for us, so we have our own convertlet, to
translate into specialized operators such
+ * as {@link JsonValueBigintOperatorConversion}, {@link
JsonValueDoubleOperatorConversion}, or
+ * {@link JsonValueVarcharOperatorConversion}, before falling back to {@link
JsonValueAnyOperatorConversion}.
+ *
+ * This convertlet still always wraps the function in a {@link
SqlStdOperatorTable#CAST}, to smooth out type
+ * mismatches, such as VARCHAR(2000) vs VARCHAR or whatever else various
type checkers like to complain about not
+ * exactly matching.
+ */
+ public static class DruidJsonValueConvertletFactory implements
DruidConvertletFactory
{
+ @Override
+ public SqlRexConvertlet createConvertlet(PlannerContext plannerContext)
+ {
+ return (cx, call) -> {
+ // we don't support modifying the behavior to be anything other than
'NULL ON EMPTY' / 'NULL ON ERROR'
+ Preconditions.checkArgument(
+
"SQLJSONVALUEEMPTYORERRORBEHAVIOR[NULL]".equals(call.operand(2).toString()),
+ "Unsupported JSON_VALUE parameter 'ON EMPTY' defined - please
re-issue this query without this argument"
+ );
+ Preconditions.checkArgument(
+ "NULL".equals(call.operand(3).toString()),
+ "Unsupported JSON_VALUE parameter 'ON EMPTY' defined - please
re-issue this query without this argument"
+ );
+ Preconditions.checkArgument(
+
"SQLJSONVALUEEMPTYORERRORBEHAVIOR[NULL]".equals(call.operand(4).toString()),
+ "Unsupported JSON_VALUE parameter 'ON ERROR' defined - please
re-issue this query without this argument"
+ );
+ Preconditions.checkArgument(
+ "NULL".equals(call.operand(5).toString()),
+ "Unsupported JSON_VALUE parameter 'ON ERROR' defined - please
re-issue this query without this argument"
+ );
+ SqlDataTypeSpec dataType = call.operand(6);
+ RelDataType sqlType = dataType.deriveType(cx.getValidator());
+ SqlNode rewrite;
+ if (SqlTypeName.INT_TYPES.contains(sqlType.getSqlTypeName())) {
+ rewrite = JsonValueBigintOperatorConversion.FUNCTION.createCall(
+ SqlParserPos.ZERO,
+ call.operand(0),
+ call.operand(1)
+ );
+ } else if
(SqlTypeName.APPROX_TYPES.contains(sqlType.getSqlTypeName())) {
+ rewrite = JsonValueDoubleOperatorConversion.FUNCTION.createCall(
+ SqlParserPos.ZERO,
+ call.operand(0),
+ call.operand(1)
+ );
+ } else if
(SqlTypeName.STRING_TYPES.contains(sqlType.getSqlTypeName())) {
+ rewrite = JsonValueVarcharOperatorConversion.FUNCTION.createCall(
+ SqlParserPos.ZERO,
+ call.operand(0),
+ call.operand(1)
+ );
+ } else {
+ // fallback to json_value_any, e.g. the 'standard' convertlet.
+ rewrite = JsonValueAnyOperatorConversion.FUNCTION.createCall(
+ SqlParserPos.ZERO,
+ call.operand(0),
+ call.operand(1)
+ );
+ }
+
+ // always cast anyway, to prevent haters from complaining that VARCHAR
doesn't match VARCHAR(2000)
+ SqlNode caster = SqlStdOperatorTable.CAST.createCall(
+ SqlParserPos.ZERO,
+ rewrite,
+ call.operand(6)
+ );
+ return cx.convertExpression(caster);
+ };
+ }
+
+ @Override
+ public List<SqlOperator> operators()
+ {
+ return Collections.singletonList(SqlStdOperatorTable.JSON_VALUE);
+ }
+ }
+
+ public abstract static class JsonValueReturningTypeOperatorConversion
implements SqlOperatorConversion
+ {
+ private final SqlFunction function;
+ private final ColumnType druidType;
+
+ public JsonValueReturningTypeOperatorConversion(SqlFunction function,
ColumnType druidType)
+ {
+ this.druidType = druidType;
+ this.function = function;
+ }
+
@Override
public SqlOperator calciteOperator()
{
- return SqlStdOperatorTable.JSON_VALUE;
+ return function;
}
@Nullable
@@ -333,24 +340,12 @@ public class NestedDataOperatorConversions
)
{
final RexCall call = (RexCall) rexNode;
-
- // calcite puts a bunch of junk in here so the call looks something like
- // JSON_VALUE(`nested`.`nest`, '$.x',
SQLJSONVALUEEMPTYORERRORBEHAVIOR[NULL], NULL,
SQLJSONVALUEEMPTYORERRORBEHAVIOR[NULL], NULL, VARCHAR(2000))
- // by the time it gets here
final List<DruidExpression> druidExpressions =
Expressions.toDruidExpressions(
plannerContext,
rowSignature,
- call.getOperands().subList(0, 2)
+ call.getOperands()
);
- ColumnType inferredOutputType = ColumnType.STRING;
- if (call.getOperands().size() == 7) {
- ColumnType maybe =
Calcites.getColumnTypeForRelDataType(call.getOperands().get(6).getType());
- if (maybe != null && !ColumnType.UNKNOWN_COMPLEX.equals(maybe)) {
- inferredOutputType = maybe;
- }
- }
-
if (druidExpressions == null || druidExpressions.size() != 2) {
return null;
}
@@ -374,12 +369,12 @@ public class NestedDataOperatorConversions
}
final String jsonPath = NestedPathFinder.toNormalizedJsonPath(parts);
final DruidExpression.ExpressionGenerator builder = (args) ->
- "json_value(" + args.get(0).getExpression() + ",'" + jsonPath + "')";
+ "json_value(" + args.get(0).getExpression() + ",'" + jsonPath + "',
'" + druidType.asTypeString() + "')";
if (druidExpressions.get(0).isSimpleExtraction()) {
return DruidExpression.ofVirtualColumn(
- inferredOutputType,
+ druidType,
builder,
ImmutableList.of(
DruidExpression.ofColumn(NestedDataComplexTypeSerde.TYPE,
druidExpressions.get(0).getDirectColumn())
@@ -395,24 +390,179 @@ public class NestedDataOperatorConversions
)
);
}
- return DruidExpression.ofExpression(ColumnType.STRING, builder,
druidExpressions);
+ return DruidExpression.ofExpression(druidType, builder,
druidExpressions);
+ }
+
+ static SqlFunction buildFunction(String functionName, SqlTypeName typeName)
+ {
+ return OperatorConversions.operatorBuilder(functionName)
+ .operandTypeChecker(
+ OperandTypes.sequence(
+ "(expr,path)",
+ OperandTypes.family(SqlTypeFamily.ANY),
+
OperandTypes.family(SqlTypeFamily.STRING)
+ )
+ )
+ .returnTypeInference(
+ ReturnTypes.cascade(
+ opBinding ->
opBinding.getTypeFactory().createSqlType(typeName),
+ SqlTypeTransforms.FORCE_NULLABLE
+ )
+ )
+
.functionCategory(SqlFunctionCategory.USER_DEFINED_FUNCTION)
+ .build();
}
}
- // calcite converts JSON_VALUE to JSON_VALUE_ANY so we have to wire that up
too...
- public static class JsonValueAnyOperatorConversion extends
AliasedOperatorConversion
+ public static class JsonValueBigintOperatorConversion extends
JsonValueReturningTypeOperatorConversion
{
- private static final String FUNCTION_NAME =
StringUtils.toUpperCase("json_value_any");
+ private static final SqlFunction FUNCTION =
buildFunction("JSON_VALUE_BIGINT", SqlTypeName.BIGINT);
- public JsonValueAnyOperatorConversion()
+ public JsonValueBigintOperatorConversion()
{
- super(new JsonValueOperatorConversion(), FUNCTION_NAME);
+ super(FUNCTION, ColumnType.LONG);
}
+ }
+
+ public static class JsonValueDoubleOperatorConversion extends
JsonValueReturningTypeOperatorConversion
+ {
+ private static final SqlFunction FUNCTION =
buildFunction("JSON_VALUE_DOUBLE", SqlTypeName.DOUBLE);
+
+ public JsonValueDoubleOperatorConversion()
+ {
+ super(FUNCTION, ColumnType.DOUBLE);
+ }
+ }
+
+ public static class JsonValueVarcharOperatorConversion extends
JsonValueReturningTypeOperatorConversion
+ {
+ private static final SqlFunction FUNCTION =
buildFunction("JSON_VALUE_VARCHAR", SqlTypeName.VARCHAR);
+
+ public JsonValueVarcharOperatorConversion()
+ {
+ super(FUNCTION, ColumnType.STRING);
+ }
+ }
+
+ public static class JsonValueAnyOperatorConversion implements
SqlOperatorConversion
+ {
+ private static final SqlFunction FUNCTION =
+ OperatorConversions.operatorBuilder("JSON_VALUE_ANY")
+ .operandTypeChecker(
+ OperandTypes.or(
+ OperandTypes.sequence(
+ "(expr,path)",
+ OperandTypes.family(SqlTypeFamily.ANY),
+
OperandTypes.family(SqlTypeFamily.STRING)
+ ),
+ OperandTypes.family(
+ SqlTypeFamily.ANY,
+ SqlTypeFamily.CHARACTER,
+ SqlTypeFamily.ANY,
+ SqlTypeFamily.ANY,
+ SqlTypeFamily.ANY,
+ SqlTypeFamily.ANY,
+ SqlTypeFamily.ANY
+ )
+ )
+ )
+ .operandTypeInference((callBinding, returnType,
operandTypes) -> {
+ RelDataTypeFactory typeFactory =
callBinding.getTypeFactory();
+ if (operandTypes.length > 5) {
+ operandTypes[3] =
typeFactory.createSqlType(SqlTypeName.ANY);
+ operandTypes[5] =
typeFactory.createSqlType(SqlTypeName.ANY);
+ }
+ })
+ .returnTypeInference(
+ ReturnTypes.cascade(
+ opBinding ->
opBinding.getTypeFactory().createTypeWithNullability(
+ // STRING is the closest thing we have
to an ANY type
+ // however, this should really be using
SqlTypeName.ANY.. someday
+
opBinding.getTypeFactory().createSqlType(SqlTypeName.VARCHAR),
+ true
+ ),
+ SqlTypeTransforms.FORCE_NULLABLE
+ )
+ )
+ .functionCategory(SqlFunctionCategory.SYSTEM)
+ .build();
@Override
public SqlOperator calciteOperator()
{
- return SqlStdOperatorTable.JSON_VALUE_ANY;
+ return FUNCTION;
+ }
+
+ @Nullable
+ @Override
+ public DruidExpression toDruidExpression(
+ PlannerContext plannerContext,
+ RowSignature rowSignature,
+ RexNode rexNode
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+
+ // calcite parser can allow for a bunch of junk in here that we don't
care about right now, so the call looks
+ // something like this:
+ // JSON_VALUE_ANY(`nested`.`nest`, '$.x',
SQLJSONVALUEEMPTYORERRORBEHAVIOR[NULL], NULL,
SQLJSONVALUEEMPTYORERRORBEHAVIOR[NULL], NULL)
+ // by the time it gets here
+
+ final List<DruidExpression> druidExpressions =
Expressions.toDruidExpressions(
+ plannerContext,
+ rowSignature,
+ call.getOperands().size() > 2 ? call.getOperands().subList(0, 2) :
call.getOperands()
+ );
+
+
+ if (druidExpressions == null || druidExpressions.size() != 2) {
+ return null;
+ }
+
+ final Expr pathExpr =
Parser.parse(druidExpressions.get(1).getExpression(),
plannerContext.getExprMacroTable());
+ if (!pathExpr.isLiteral()) {
+ return null;
+ }
+ // pre-normalize path so that the same expressions with different jq
syntax are collapsed
+ final String path = (String)
pathExpr.eval(InputBindings.nilBindings()).value();
+ final List<NestedPathPart> parts;
+ try {
+ parts = NestedPathFinder.parseJsonPath(path);
+ }
+ catch (IllegalArgumentException iae) {
+ throw new UnsupportedSQLQueryException(
+ "Cannot use [%s]: [%s]",
+ call.getOperator().getName(),
+ iae.getMessage()
+ );
+ }
+ final String jsonPath = NestedPathFinder.toNormalizedJsonPath(parts);
+ final DruidExpression.ExpressionGenerator builder = (args) ->
+ "json_value(" + args.get(0).getExpression() + ",'" + jsonPath + "')";
+
+ // STRING is the closest thing we have to ANY, though maybe someday this
+ // can be replaced with a VARIANT type
+ final ColumnType columnType = ColumnType.STRING;
+
+ if (druidExpressions.get(0).isSimpleExtraction()) {
+ return DruidExpression.ofVirtualColumn(
+ columnType,
+ builder,
+ ImmutableList.of(
+ DruidExpression.ofColumn(NestedDataComplexTypeSerde.TYPE,
druidExpressions.get(0).getDirectColumn())
+ ),
+ (name, outputType, expression, macroTable) -> new
NestedFieldVirtualColumn(
+ druidExpressions.get(0).getDirectColumn(),
+ name,
+ outputType,
+ parts,
+ false,
+ null,
+ null
+ )
+ );
+ }
+ return DruidExpression.ofExpression(columnType, builder,
druidExpressions);
}
}
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index de259402bf..6c6044a672 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -299,13 +299,13 @@ public class DruidOperatorTable implements
SqlOperatorTable
private static final List<SqlOperatorConversion>
NESTED_DATA_OPERATOR_CONVERSIONS =
ImmutableList.<SqlOperatorConversion>builder()
- .add(new
NestedDataOperatorConversions.GetPathOperatorConversion())
- .add(new
NestedDataOperatorConversions.JsonGetPathAliasOperatorConversion())
.add(new
NestedDataOperatorConversions.JsonKeysOperatorConversion())
.add(new
NestedDataOperatorConversions.JsonPathsOperatorConversion())
.add(new
NestedDataOperatorConversions.JsonQueryOperatorConversion())
- .add(new
NestedDataOperatorConversions.JsonValueOperatorConversion())
.add(new
NestedDataOperatorConversions.JsonValueAnyOperatorConversion())
+ .add(new
NestedDataOperatorConversions.JsonValueBigintOperatorConversion())
+ .add(new
NestedDataOperatorConversions.JsonValueDoubleOperatorConversion())
+ .add(new
NestedDataOperatorConversions.JsonValueVarcharOperatorConversion())
.add(new
NestedDataOperatorConversions.JsonObjectOperatorConversion())
.add(new
NestedDataOperatorConversions.ToJsonStringOperatorConversion())
.add(new
NestedDataOperatorConversions.ParseJsonOperatorConversion())
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/convertlet/DruidConvertletTable.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/convertlet/DruidConvertletTable.java
index 3f5652d9aa..59ad4ef24f 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/convertlet/DruidConvertletTable.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/convertlet/DruidConvertletTable.java
@@ -28,6 +28,7 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql2rel.SqlRexConvertlet;
import org.apache.calcite.sql2rel.SqlRexConvertletTable;
import org.apache.calcite.sql2rel.StandardConvertletTable;
+import
org.apache.druid.sql.calcite.expression.builtin.NestedDataOperatorConversions;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import java.util.ArrayList;
@@ -44,6 +45,7 @@ public class DruidConvertletTable implements
SqlRexConvertletTable
ImmutableList.<DruidConvertletFactory>builder()
.add(CurrentTimestampAndFriendsConvertletFactory.INSTANCE)
.add(TimeInIntervalConvertletFactory.INSTANCE)
+
.add(NestedDataOperatorConversions.DRUID_JSON_VALUE_CONVERTLET_FACTORY_INSTANCE)
.build();
// Operators we don't have standard conversions for, but which can be
converted into ones that do by
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
index eb4cf5ba7b..e95ba65643 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java
@@ -239,6 +239,43 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
);
}
+ @Test
+ public void testGroupJsonValueAny()
+ {
+ testQuery(
+ "SELECT "
+ + "JSON_VALUE_ANY(nest, '$.x'), "
+ + "SUM(cnt) "
+ + "FROM druid.nested GROUP BY 1",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(DATA_SOURCE)
+ .setInterval(querySegmentSpec(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setVirtualColumns(
+ new NestedFieldVirtualColumn("nest", "$.x", "v0",
ColumnType.STRING)
+ )
+ .setDimensions(
+ dimensions(
+ new DefaultDimensionSpec("v0", "d0")
+ )
+ )
+ .setAggregatorSpecs(aggregators(new
LongSumAggregatorFactory("a0", "cnt")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{NullHandling.defaultStringValue(), 4L},
+ new Object[]{"100", 2L},
+ new Object[]{"200", 1L}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.STRING)
+ .add("EXPR$1", ColumnType.LONG)
+ .build()
+ );
+ }
+
@Test
public void testGroupByJsonValue()
{
@@ -350,102 +387,6 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
);
}
- @Test
- public void testGroupByGetPaths()
- {
- testQuery(
- "SELECT "
- + "GET_PATH(nest, '.x'), "
- + "GET_PATH(nest, '.\"x\"'), "
- + "GET_PATH(nest, '.[\"x\"]'), "
- + "SUM(cnt) "
- + "FROM druid.nested GROUP BY 1, 2, 3",
- ImmutableList.of(
- GroupByQuery.builder()
- .setDataSource(DATA_SOURCE)
- .setInterval(querySegmentSpec(Filtration.eternity()))
- .setGranularity(Granularities.ALL)
- .setVirtualColumns(
- new NestedFieldVirtualColumn("nest", "$.x", "v0",
ColumnType.STRING)
- )
- .setDimensions(
- dimensions(
- new DefaultDimensionSpec("v0", "d0"),
- new DefaultDimensionSpec("v0", "d1"),
- new DefaultDimensionSpec("v0", "d2")
- )
- )
- .setAggregatorSpecs(aggregators(new
LongSumAggregatorFactory("a0", "cnt")))
- .setContext(QUERY_CONTEXT_DEFAULT)
- .build()
- ),
- ImmutableList.of(
- new Object[]{
- NullHandling.defaultStringValue(),
- NullHandling.defaultStringValue(),
- NullHandling.defaultStringValue(),
- 4L
- },
- new Object[]{"100", "100", "100", 2L},
- new Object[]{"200", "200", "200", 1L}
- ),
- RowSignature.builder()
- .add("EXPR$0", ColumnType.STRING)
- .add("EXPR$1", ColumnType.STRING)
- .add("EXPR$2", ColumnType.STRING)
- .add("EXPR$3", ColumnType.LONG)
- .build()
- );
- }
-
- @Test
- public void testGroupByJsonGetPaths()
- {
- testQuery(
- "SELECT "
- + "JSON_GET_PATH(nest, '.x'), "
- + "JSON_GET_PATH(nest, '.\"x\"'), "
- + "JSON_GET_PATH(nest, '.[\"x\"]'), "
- + "SUM(cnt) "
- + "FROM druid.nested GROUP BY 1, 2, 3",
- ImmutableList.of(
- GroupByQuery.builder()
- .setDataSource(DATA_SOURCE)
- .setInterval(querySegmentSpec(Filtration.eternity()))
- .setGranularity(Granularities.ALL)
- .setVirtualColumns(
- new NestedFieldVirtualColumn("nest", "$.x", "v0",
ColumnType.STRING)
- )
- .setDimensions(
- dimensions(
- new DefaultDimensionSpec("v0", "d0"),
- new DefaultDimensionSpec("v0", "d1"),
- new DefaultDimensionSpec("v0", "d2")
- )
- )
- .setAggregatorSpecs(aggregators(new
LongSumAggregatorFactory("a0", "cnt")))
- .setContext(QUERY_CONTEXT_DEFAULT)
- .build()
- ),
- ImmutableList.of(
- new Object[]{
- NullHandling.defaultStringValue(),
- NullHandling.defaultStringValue(),
- NullHandling.defaultStringValue(),
- 4L
- },
- new Object[]{"100", "100", "100", 2L},
- new Object[]{"200", "200", "200", 1L}
- ),
- RowSignature.builder()
- .add("EXPR$0", ColumnType.STRING)
- .add("EXPR$1", ColumnType.STRING)
- .add("EXPR$2", ColumnType.STRING)
- .add("EXPR$3", ColumnType.LONG)
- .build()
- );
- }
-
@Test
public void testGroupByJsonValues()
{
@@ -2010,6 +1951,57 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
);
}
+ @Test
+ public void testReturningAndSumPathDouble()
+ {
+ testQuery(
+ "SELECT "
+ + "SUM(JSON_VALUE(nest, '$.x' RETURNING DOUBLE)) "
+ + "FROM druid.nested",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(DATA_SOURCE)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .virtualColumns(new NestedFieldVirtualColumn("nest", "$.x",
"v0", ColumnType.DOUBLE))
+ .aggregators(aggregators(new
DoubleSumAggregatorFactory("a0", "v0")))
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{400.0}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.DOUBLE)
+ .build()
+ );
+ }
+
+ @Test
+ public void testReturningAndSumPathDecimal()
+ {
+ testQuery(
+ "SELECT "
+ + "SUM(JSON_VALUE(nest, '$.x' RETURNING DECIMAL)) "
+ + "FROM druid.nested",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(DATA_SOURCE)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .virtualColumns(new NestedFieldVirtualColumn("nest", "$.x",
"v0", ColumnType.DOUBLE))
+ .aggregators(aggregators(new
DoubleSumAggregatorFactory("a0", "v0")))
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{400.0}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.DOUBLE)
+ .build()
+ );
+ }
@Test
public void testReturningAndSumPathStrings()
@@ -2043,7 +2035,7 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
cannotVectorize();
testQuery(
"SELECT "
- + "JSON_KEYS(nester, '.'), "
+ + "JSON_KEYS(nester, '$'), "
+ "SUM(cnt) "
+ "FROM druid.nested GROUP BY 1",
ImmutableList.of(
@@ -2054,7 +2046,7 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
.setVirtualColumns(
new ExpressionVirtualColumn(
"v0",
- "json_keys(\"nester\",'.')",
+ "json_keys(\"nester\",'$')",
ColumnType.STRING_ARRAY,
macroTable
)
@@ -2127,7 +2119,7 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
cannotVectorize();
testQuery(
"SELECT "
- + "JSON_KEYS(nest, '.'), "
+ + "JSON_KEYS(nest, '$'), "
+ "SUM(cnt) "
+ "FROM druid.nested GROUP BY 1",
ImmutableList.of(
@@ -2138,7 +2130,7 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
.setVirtualColumns(
new ExpressionVirtualColumn(
"v0",
- "json_keys(\"nest\",'.')",
+ "json_keys(\"nest\",'$')",
ColumnType.STRING_ARRAY,
macroTable
)
@@ -2253,7 +2245,7 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
(expected) -> {
expected.expect(UnsupportedSQLQueryException.class);
expected.expectMessage(
- "Cannot use [JSON_VALUE_ANY]: [Bad format, '.array.[1]' is not a
valid JSONPath path: must start with '$']");
+ "Cannot use [JSON_VALUE_VARCHAR]: [Bad format, '.array.[1]' is
not a valid JSONPath path: must start with '$']");
}
);
}
@@ -2358,6 +2350,54 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
);
}
+ @Test
+ public void testCompositionTyping()
+ {
+ testQuery(
+ "SELECT "
+ + "JSON_VALUE((JSON_OBJECT(KEY 'x' VALUE JSON_VALUE(nest, '$.x'
RETURNING BIGINT))), '$.x' RETURNING BIGINT)\n"
+ + "FROM druid.nested",
+ ImmutableList.of(
+ Druids.newScanQueryBuilder()
+ .dataSource(DATA_SOURCE)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ new ExpressionVirtualColumn(
+ "v0",
+ "json_value(json_object('x',\"v1\"),'$.x', 'LONG')",
+ ColumnType.LONG,
+ macroTable
+ ),
+ new NestedFieldVirtualColumn(
+ "nest",
+ "v1",
+ ColumnType.LONG,
+ null,
+ false,
+ "$.x",
+ false
+ )
+ )
+ .columns("v0")
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .legacy(false)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{100L},
+ new Object[]{NullHandling.defaultLongValue()},
+ new Object[]{200L},
+ new Object[]{NullHandling.defaultLongValue()},
+ new Object[]{NullHandling.defaultLongValue()},
+ new Object[]{100L},
+ new Object[]{NullHandling.defaultLongValue()}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.LONG)
+ .build()
+ );
+ }
+
@Test
public void testToJsonAndParseJson()
{
@@ -2584,4 +2624,46 @@ public class CalciteNestedDataQueryTest extends
BaseCalciteQueryTest
);
}
+
+ @Test
+ public void testJsonValueUnDocumentedButSupportedOptions()
+ {
+ testQuery(
+ "SELECT "
+ + "SUM(JSON_VALUE(nest, '$.z' RETURNING BIGINT NULL ON EMPTY NULL ON
ERROR)) "
+ + "FROM druid.nested",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(DATA_SOURCE)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .virtualColumns(new NestedFieldVirtualColumn("nest", "$.z",
"v0", ColumnType.LONG))
+ .aggregators(aggregators(new LongSumAggregatorFactory("a0",
"v0")))
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{700L}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.LONG)
+ .build()
+ );
+ }
+
+ @Test
+ public void testJsonValueUnsupportedOptions()
+ {
+ testQueryThrows(
+ "SELECT "
+ + "SUM(JSON_VALUE(nest, '$.z' RETURNING BIGINT ERROR ON EMPTY ERROR ON
ERROR)) "
+ + "FROM druid.nested",
+ exception -> {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage(
+ "Unsupported JSON_VALUE parameter 'ON EMPTY' defined - please
re-issue this query without this argument"
+ );
+ }
+ );
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]