This is an automated email from the ASF dual-hosted git repository. hongze pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/master by this push: new 2d33c57 [CALCITE-2808] Add the JSON_LENGTH function (xuqianjin) 2d33c57 is described below commit 2d33c57bff41b6ea473f8f60505a10b196ea79d7 Author: XuQianJin-Stars <x1q...@163.com> AuthorDate: Tue Mar 5 18:35:24 2019 +0800 [CALCITE-2808] Add the JSON_LENGTH function (xuqianjin) Close apache/calcite#1070 --- babel/src/main/codegen/config.fmpp | 5 +- core/src/main/codegen/config.fmpp | 1 + core/src/main/codegen/templates/Parser.jj | 50 ++++++++++++---- .../calcite/adapter/enumerable/RexImpTable.java | 7 ++- .../apache/calcite/runtime/.SqlFunctions.java.swp | Bin 0 -> 16384 bytes .../apache/calcite/runtime/CalciteResource.java | 14 +++-- .../org/apache/calcite/runtime/SqlFunctions.java | 62 ++++++++++++++++++-- .../sql/fun/SqlJsonApiCommonSyntaxOperator.java | 34 ++++++++--- .../calcite/sql/fun/SqlJsonLengthFunction.java | 65 +++++++++++++++++++++ .../calcite/sql/fun/SqlStdOperatorTable.java | 7 ++- .../org/apache/calcite/util/BuiltInMethod.java | 3 + .../calcite/runtime/CalciteResource.properties | 6 +- core/src/test/codegen/config.fmpp | 1 + .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 9 +++ .../apache/calcite/sql/parser/SqlParserTest.java | 11 ++++ .../calcite/sql/test/SqlOperatorBaseTest.java | 54 ++++++++++++++++- .../java/org/apache/calcite/test/JdbcTest.java | 25 +++++--- .../apache/calcite/test/SqlJsonFunctionsTest.java | 36 ++++++++++++ .../org/apache/calcite/test/SqlValidatorTest.java | 9 +++ server/src/main/codegen/config.fmpp | 1 + site/_docs/reference.md | 21 +++++++ 21 files changed, 378 insertions(+), 43 deletions(-) diff --git a/babel/src/main/codegen/config.fmpp b/babel/src/main/codegen/config.fmpp index dab4251..5439d14 100644 --- a/babel/src/main/codegen/config.fmpp +++ b/babel/src/main/codegen/config.fmpp @@ -141,6 +141,8 @@ data: { "JSON" "JSON_TYPE" "JSON_DEPTH" + "JSON_LENGTH" + "JSON_PRETTY" "K" "KEY" "KEY_MEMBER" @@ -555,9 +557,6 @@ data: { "JSON_ARRAYAGG", "JSON_EXISTS", "JSON_VALUE", - "JSON_TYPE", - "JSON_DEPTH" - "JSON_PRETTY", "JSON_OBJECT", "JSON_OBJECTAGG", "JSON_QUERY", diff --git a/core/src/main/codegen/config.fmpp b/core/src/main/codegen/config.fmpp index c8f9ec8..7b3fca2 100644 --- a/core/src/main/codegen/config.fmpp +++ b/core/src/main/codegen/config.fmpp @@ -161,6 +161,7 @@ data: { "JSON" "JSON_TYPE" "JSON_DEPTH" + "JSON_LENGTH" "JSON_PRETTY" "K" "KEY" diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 98e3f1b..5dfa66e 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -4864,6 +4864,8 @@ SqlNode BuiltinFunctionCall() : | node = JsonDepthFunctionCall() { return node; } | + node = JsonLengthFunctionCall() { return node; } + | node = JsonObjectAggFunctionCall() { return node; } | node = JsonArrayFunctionCall() { return node; } @@ -4958,21 +4960,32 @@ SqlNode JsonPathSpec() : } } -SqlNode JsonApiCommonSyntax() : +SqlNode JsonApiCommonSyntax(boolean acceptNonPath) : { SqlNode e; List<SqlNode> args = new ArrayList<SqlNode>(); Span span; + SqlOperator op; } { e = JsonValueExpression(true) { args.add(e); span = Span.of(e); } - <COMMA> - e = Expression(ExprContext.ACCEPT_NON_QUERY) { - args.add(e); - } + ( + <COMMA> + e = Expression(ExprContext.ACCEPT_NON_QUERY) { + op = SqlStdOperatorTable.JSON_API_COMMON_SYNTAX; + args.add(e); + } + | + { + if (!acceptNonPath) { + throw new ParseException(RESOURCE.jsonPathMustBeSpecified().str()); + } + op = SqlStdOperatorTable.JSON_API_COMMON_SYNTAX_WITHOUT_PATH; + } + ) [ <PASSING> e = JsonValueExpression(false) { args.add(e); @@ -4991,9 +5004,8 @@ SqlNode JsonApiCommonSyntax() : )* ] { - return SqlStdOperatorTable.JSON_API_COMMON_SYNTAX.createCall(span.end(this), args); + return op.createCall(span.end(this), args); } - } SqlJsonExistsErrorBehavior JsonExistsErrorBehavior() : @@ -5019,7 +5031,7 @@ SqlCall JsonExistsFunctionCall() : } { <JSON_EXISTS> { span = span(); } - <LPAREN> e = JsonApiCommonSyntax() { + <LPAREN> e = JsonApiCommonSyntax(false) { args = new ArrayList<SqlNode>(); args.add(e); } @@ -5076,7 +5088,7 @@ SqlCall JsonValueFunctionCall() : } { <JSON_VALUE> { span = span(); } - <LPAREN> e = JsonApiCommonSyntax() { + <LPAREN> e = JsonApiCommonSyntax(false) { args[0] = e; } [ @@ -5185,7 +5197,7 @@ SqlCall JsonQueryFunctionCall() : } { <JSON_QUERY> { span = span(); } - <LPAREN> e = JsonApiCommonSyntax() { + <LPAREN> e = JsonApiCommonSyntax(false) { args[0] = e; } [ @@ -5331,6 +5343,23 @@ SqlCall JsonDepthFunctionCall() : } } +SqlCall JsonLengthFunctionCall() : +{ + final SqlNode[] args = new SqlNode[1]; + SqlNode e; + final Span span; + List<SqlNode> behavior; +} +{ + <JSON_LENGTH> { span = span(); } + <LPAREN> e = JsonApiCommonSyntax(true) { + args[0] = e; + } + <RPAREN> { + return SqlStdOperatorTable.JSON_LENGTH.createCall(span.end(this), args); + } +} + SqlCall JsonObjectAggFunctionCall() : { final SqlNode[] args = new SqlNode[2]; @@ -6408,6 +6437,7 @@ SqlPostfixOperator PostfixRowOperator() : | < JSON_OBJECT: "JSON_OBJECT"> | < JSON_TYPE: "JSON_TYPE"> | < JSON_DEPTH: "JSON_DEPTH"> +| < JSON_LENGTH: "JSON_LENGTH"> | < JSON_OBJECTAGG: "JSON_OBJECTAGG"> | < JSON_QUERY: "JSON_QUERY" > | < K: "K" > diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index e5679b2..298a5c0 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -162,10 +162,12 @@ import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NULL; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_TRUE; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.ITEM; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_API_COMMON_SYNTAX; +import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_API_COMMON_SYNTAX_WITHOUT_PATH; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_ARRAY; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_ARRAYAGG; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_DEPTH; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_EXISTS; +import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_LENGTH; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_OBJECT; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_OBJECTAGG; import static org.apache.calcite.sql.fun.SqlStdOperatorTable.JSON_PRETTY; @@ -454,7 +456,9 @@ public class RexImpTable { defineMethod(JSON_STRUCTURED_VALUE_EXPRESSION, BuiltInMethod.JSON_STRUCTURED_VALUE_EXPRESSION.method, NullPolicy.STRICT); defineMethod(JSON_API_COMMON_SYNTAX, BuiltInMethod.JSON_API_COMMON_SYNTAX.method, - NullPolicy.NONE); + NullPolicy.NONE); + defineMethod(JSON_API_COMMON_SYNTAX_WITHOUT_PATH, + BuiltInMethod.JSON_API_COMMON_SYNTAX_WITHOUT_PATH.method, NullPolicy.NONE); defineMethod(JSON_EXISTS, BuiltInMethod.JSON_EXISTS.method, NullPolicy.NONE); defineMethod(JSON_VALUE_ANY, BuiltInMethod.JSON_VALUE_ANY.method, NullPolicy.NONE); defineMethod(JSON_QUERY, BuiltInMethod.JSON_QUERY.method, NullPolicy.NONE); @@ -463,6 +467,7 @@ public class RexImpTable { defineMethod(JSON_TYPE, BuiltInMethod.JSON_TYPE.method, NullPolicy.NONE); defineMethod(JSON_DEPTH, BuiltInMethod.JSON_DEPTH.method, NullPolicy.NONE); defineMethod(JSON_PRETTY, BuiltInMethod.JSON_PRETTY.method, NullPolicy.NONE); + defineMethod(JSON_LENGTH, BuiltInMethod.JSON_LENGTH.method, NullPolicy.NONE); aggMap.put(JSON_OBJECTAGG.with(SqlJsonConstructorNullClause.ABSENT_ON_NULL), JsonObjectAggImplementor .supplierFor(BuiltInMethod.JSON_OBJECTAGG_ADD.method)); diff --git a/core/src/main/java/org/apache/calcite/runtime/.SqlFunctions.java.swp b/core/src/main/java/org/apache/calcite/runtime/.SqlFunctions.java.swp new file mode 100644 index 0000000..d1376b8 Binary files /dev/null and b/core/src/main/java/org/apache/calcite/runtime/.SqlFunctions.java.swp differ diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java index e356e84..53b2480 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -43,6 +43,9 @@ public interface CalciteResource { @BaseMessage("APPLY operator is not allowed under the current SQL conformance level") ExInst<CalciteException> applyNotAllowed(); + @BaseMessage("JSON path expression must be specified after the JSON value expression") + ExInst<CalciteException> jsonPathMustBeSpecified(); + @BaseMessage("Illegal {0} literal {1}: {2}") ExInst<CalciteException> illegalLiteral(String a0, String a1, String a2); @@ -862,14 +865,17 @@ public interface CalciteResource { @BaseMessage("While executing SQL [{0}] on JDBC sub-schema") ExInst<RuntimeException> exceptionWhilePerformingQueryOnJdbcSubSchema(String sql); - @BaseMessage("Unknown JSON type in JSON_TYPE function, and the object is: ''{0}''") - ExInst<CalciteException> unknownObjectOfJsonType(String value); + @BaseMessage("Not a valid input for JSON_TYPE: ''{0}''") + ExInst<CalciteException> invalidInputForJsonType(String value); - @BaseMessage("Unknown JSON depth in JSON_DEPTH function, and the object is: ''{0}''") - ExInst<CalciteException> unknownObjectOfJsonDepth(String value); + @BaseMessage("Not a valid input for JSON_DEPTH: ''{0}''") + ExInst<CalciteException> invalidInputForJsonDepth(String value); @BaseMessage("Cannot serialize object to JSON, and the object is: ''{0}''") ExInst<CalciteException> exceptionWhileSerializingToJson(String value); + + @BaseMessage("Not a valid input for JSON_LENGTH: ''{0}''") + ExInst<CalciteException> invalidInputForJsonLength(String value); } // End CalciteResource.java diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index 50079cb..c508ea0 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -2460,6 +2460,10 @@ public class SqlFunctions { return input; } + public static PathContext jsonApiCommonSyntax(Object input) { + return jsonApiCommonSyntax(input, "strict $"); + } + public static PathContext jsonApiCommonSyntax(Object input, String pathSpec) { try { Matcher matcher = JSON_PATH_BASE.matcher(pathSpec); @@ -2755,12 +2759,12 @@ public class SqlFunctions { result = "unknown"; } if (result.equals("unknown")) { - throw RESOURCE.unknownObjectOfJsonType(o.toString()).ex(); + throw RESOURCE.invalidInputForJsonType(o.toString()).ex(); } else { return result; } } catch (Exception ex) { - throw RESOURCE.unknownObjectOfJsonType(o.toString()).ex(); + throw RESOURCE.invalidInputForJsonType(o.toString()).ex(); } } @@ -2774,7 +2778,7 @@ public class SqlFunctions { } return result; } catch (Exception ex) { - throw RESOURCE.unknownObjectOfJsonDepth(o.toString()).ex(); + throw RESOURCE.invalidInputForJsonDepth(o.toString()).ex(); } } @@ -2806,6 +2810,49 @@ public class SqlFunctions { return depth; } + public static Integer jsonLength(Object input) { + final Integer result; + final Object value; + try { + if (!isJsonPathContext(input)) { + throw RESOURCE.invalidInputForJsonLength( + input.toString()).ex(); + } + PathContext context = (PathContext) input; + if (context.exc != null) { + throw toUnchecked(context.exc); + } + value = context.pathReturned; + + if (value == null) { + result = null; + } else { + if (value instanceof Collection) { + result = ((Collection) value).size(); + } else if (value instanceof Map) { + result = ((LinkedHashMap) value).size(); + } else if (isScalarObject(value)) { + result = 1; + } else { + result = 0; + } + } + } catch (Exception ex) { + throw RESOURCE.invalidInputForJsonLength( + input.toString()).ex(); + } + return result; + } + + public static boolean isJsonPathContext(Object input) { + try { + PathContext context = (PathContext) input; + return context != null; + } catch (Exception e) { + return false; + } + } + public static boolean isJsonValue(String input) { try { dejsonize(input); @@ -2857,6 +2904,12 @@ public class SqlFunctions { public final Object pathReturned; public final Exception exc; + private PathContext(Object pathReturned, Exception exc) { + this.mode = PathMode.NONE; + this.pathReturned = pathReturned; + this.exc = exc; + } + private PathContext(PathMode mode, Object pathReturned, Exception exc) { this.mode = mode; this.pathReturned = pathReturned; @@ -2898,7 +2951,8 @@ public class SqlFunctions { public enum PathMode { LAX, STRICT, - UNKNOWN + UNKNOWN, + NONE } /** Enumerates over the cartesian product of the given lists, returning diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonApiCommonSyntaxOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonApiCommonSyntaxOperator.java index de50336..72e4f80 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonApiCommonSyntaxOperator.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonApiCommonSyntaxOperator.java @@ -33,16 +33,31 @@ import org.apache.calcite.sql.validate.SqlValidator; */ public class SqlJsonApiCommonSyntaxOperator extends SqlSpecialOperator { - public SqlJsonApiCommonSyntaxOperator() { - super("JSON_API_COMMON_SYNTAX", SqlKind.JSON_API_COMMON_SYNTAX, 100, true, + // If true, the syntax must contain a JSON path expression, e.g. '{'foo':'bar'}', 'lax $.foo'; + // otherwise the syntax can be specified within JSON text only, e.g. '{'foo':'bar'}'. + private final boolean hasPath; + + public SqlJsonApiCommonSyntaxOperator(String name, boolean hasPath) { + super(name, SqlKind.JSON_API_COMMON_SYNTAX, 100, true, ReturnTypes.explicit(SqlTypeName.ANY), null, - OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.STRING)); + hasPath ? OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.STRING) + : OperandTypes.family(SqlTypeFamily.ANY) + ); + this.hasPath = hasPath; } @Override protected void checkOperandCount(SqlValidator validator, SqlOperandTypeChecker argType, SqlCall call) { - if (call.operandCount() != 2) { - throw new UnsupportedOperationException("json passing syntax is not yet supported"); + if (hasPath) { + if (call.operandCount() < 2) { + throw new UnsupportedOperationException( + "JSON API common syntax requires at least 2 parameters"); + } + } else { + if (call.operandCount() < 1) { + throw new UnsupportedOperationException( + "JSON API common syntax requires at least 1 parameter"); + } } } @@ -50,9 +65,12 @@ public class SqlJsonApiCommonSyntaxOperator extends SqlSpecialOperator { int rightPrec) { SqlWriter.Frame frame = writer.startList(SqlWriter.FrameTypeEnum.SIMPLE); call.operand(0).unparse(writer, 0, 0); - writer.sep(",", true); - call.operand(1).unparse(writer, 0, 0); - if (call.operandCount() > 2) { + if (hasPath) { + writer.sep(",", true); + call.operand(1).unparse(writer, 0, 0); + } + if (hasPath && call.operandCount() > 2 + || !hasPath && call.operandCount() > 1) { writer.keyword("PASSING"); for (int i = 2; i < call.getOperandList().size(); i += 2) { call.operand(i).unparse(writer, 0, 0); diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java new file mode 100644 index 0000000..8140efd --- /dev/null +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.sql.fun; + +import org.apache.calcite.sql.SqlCall; +import org.apache.calcite.sql.SqlFunction; +import org.apache.calcite.sql.SqlFunctionCategory; +import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlLiteral; +import org.apache.calcite.sql.SqlNode; +import org.apache.calcite.sql.SqlWriter; +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.SqlTypeTransforms; + +/** + * The <code>JSON_LENGTH</code> function. + */ +public class SqlJsonLengthFunction extends SqlFunction { + public SqlJsonLengthFunction() { + super("JSON_LENGTH", SqlKind.OTHER_FUNCTION, + ReturnTypes.cascade(ReturnTypes.INTEGER, + SqlTypeTransforms.FORCE_NULLABLE), + null, + OperandTypes.ANY, + SqlFunctionCategory.SYSTEM); + } + + @Override public String getSignatureTemplate(int operandsCount) { + assert operandsCount == 1 || operandsCount == 2; + if (operandsCount == 1) { + return "{0}({1})"; + } + return "{0}({1} {2})"; + } + + @Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec, + int rightPrec) { + final SqlWriter.Frame frame = writer.startFunCall(getName()); + call.operand(0).unparse(writer, 0, 0); + writer.endFunCall(frame); + } + + @Override public SqlCall createCall(SqlLiteral functionQualifier, + SqlParserPos pos, SqlNode... operands) { + return super.createCall(functionQualifier, pos, operands); + } +} + +// End SqlJsonLengthFunction.java diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java index 50e6eb1..5e89b88 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java @@ -1290,7 +1290,10 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { true); public static final SqlJsonApiCommonSyntaxOperator JSON_API_COMMON_SYNTAX = - new SqlJsonApiCommonSyntaxOperator(); + new SqlJsonApiCommonSyntaxOperator("JSON_API_COMMON_SYNTAX", true); + + public static final SqlJsonApiCommonSyntaxOperator JSON_API_COMMON_SYNTAX_WITHOUT_PATH = + new SqlJsonApiCommonSyntaxOperator("JSON_API_COMMON_SYNTAX_WITHOUT_PATH", false); public static final SqlFunction JSON_EXISTS = new SqlJsonExistsFunction(); @@ -1311,6 +1314,8 @@ public class SqlStdOperatorTable extends ReflectiveSqlOperatorTable { public static final SqlFunction JSON_DEPTH = new SqlJsonDepthFunction(); + public static final SqlFunction JSON_LENGTH = new SqlJsonLengthFunction(); + public static final SqlJsonObjectAggAggFunction JSON_OBJECTAGG = new SqlJsonObjectAggAggFunction(SqlKind.JSON_OBJECTAGG, SqlJsonConstructorNullClause.NULL_ON_NULL); diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index 962c3bf..e0add52 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -275,6 +275,8 @@ public enum BuiltInMethod { "jsonStructuredValueExpression", Object.class), JSON_API_COMMON_SYNTAX(SqlFunctions.class, "jsonApiCommonSyntax", Object.class, String.class), + JSON_API_COMMON_SYNTAX_WITHOUT_PATH(SqlFunctions.class, "jsonApiCommonSyntax", + Object.class), JSON_EXISTS(SqlFunctions.class, "jsonExists", Object.class), JSON_VALUE_ANY(SqlFunctions.class, "jsonValueAny", Object.class, SqlJsonValueEmptyOrErrorBehavior.class, Object.class, @@ -288,6 +290,7 @@ public enum BuiltInMethod { JSON_TYPE(SqlFunctions.class, "jsonType", Object.class), JSON_DEPTH(SqlFunctions.class, "jsonDepth", Object.class), JSON_PRETTY(SqlFunctions.class, "jsonPretty", Object.class), + JSON_LENGTH(SqlFunctions.class, "jsonLength", Object.class), JSON_OBJECTAGG_ADD(SqlFunctions.class, "jsonObjectAggAdd", Map.class, String.class, Object.class, SqlJsonConstructorNullClause.class), JSON_ARRAY(SqlFunctions.class, "jsonArray", diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index bd31c82..d92b76a 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -21,6 +21,7 @@ BangEqualNotAllowed=Bang equal ''!='' is not allowed under the current SQL confo PercentRemainderNotAllowed=Percent remainder ''%'' is not allowed under the current SQL conformance level LimitStartCountNotAllowed=''LIMIT start, count'' is not allowed under the current SQL conformance level ApplyNotAllowed=APPLY operator is not allowed under the current SQL conformance level +JsonPathMustBeSpecified=JSON path expression must be specified after the JSON value expression IllegalLiteral=Illegal {0} literal {1}: {2} IdentifierTooLong=Length of identifier ''{0}'' must be less than or equal to {1,number,#} characters BadFormat=not in format ''{0}'' @@ -280,7 +281,8 @@ NullKeyOfJsonObjectNotAllowed=Null key of JSON object is not allowed QueryExecutionTimeoutReached=Timeout of ''{0}'' ms for query execution is reached. Query execution started at ''{1}'' AmbiguousSortOrderInJsonArrayAggFunc=Including both WITHIN GROUP(...) and inside ORDER BY in a single JSON_ARRAYAGG call is not allowed ExceptionWhilePerformingQueryOnJdbcSubSchema = While executing SQL [{0}] on JDBC sub-schema -UnknownObjectOfJsonType=Unknown JSON type in JSON_TYPE function, and the object is: ''{0}'' -UnknownObjectOfJsonDepth=Unknown JSON depth in JSON_DEPTH function, and the object is: ''{0}'' +InvalidInputForJsonType=Not a valid input for JSON_TYPE: ''{0}'' +InvalidInputForJsonDepth=Not a valid input for JSON_DEPTH: ''{0}'' ExceptionWhileSerializingToJson=Cannot serialize object to JSON, and the object is: ''{0}'' +InvalidInputForJsonLength=Not a valid input for JSON_LENGTH: ''{0}'' # End CalciteResource.properties diff --git a/core/src/test/codegen/config.fmpp b/core/src/test/codegen/config.fmpp index d9ae98b..79e0315 100644 --- a/core/src/test/codegen/config.fmpp +++ b/core/src/test/codegen/config.fmpp @@ -145,6 +145,7 @@ data: { "JSON" "JSON_TYPE" "JSON_DEPTH" + "JSON_LENGTH" "JSON_PRETTY" "K" "KEY" diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 378c405..a6176fb 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -3230,6 +3230,15 @@ public class RelToSqlConverterTest { sql(query).ok(expected); } + @Test public void testJsonLength() { + String query = "select json_length(\"product_name\", 'lax $'), " + + "json_length(\"product_name\") from \"product\""; + final String expected = "SELECT JSON_LENGTH(\"product_name\" FORMAT JSON, 'lax $'), " + + "JSON_LENGTH(\"product_name\" FORMAT JSON)\n" + + "FROM \"foodmart\".\"product\""; + sql(query).ok(expected); + } + /** Fluid interface to run tests. */ static class Sql { private final SchemaPlus schema; diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java index b53df4d..5e00000 100644 --- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java +++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java @@ -8429,6 +8429,17 @@ public class SqlParserTest { "JSON_DEPTH('{\"foo\": \"100\"}' FORMAT JSON)"); } + @Test public void testJsonLength() { + checkExp("json_length('{\"foo\": \"bar\"}')", + "JSON_LENGTH('{\"foo\": \"bar\"}' FORMAT JSON)"); + checkExp("json_length('{\"foo\": \"bar\"}', 'lax $')", + "JSON_LENGTH('{\"foo\": \"bar\"}' FORMAT JSON, 'lax $')"); + checkExp("json_length('{\"foo\": \"bar\"}', 'strict $')", + "JSON_LENGTH('{\"foo\": \"bar\"}' FORMAT JSON, 'strict $')"); + checkExp("json_length('{\"foo\": \"bar\"}', 'invalid $')", + "JSON_LENGTH('{\"foo\": \"bar\"}' FORMAT JSON, 'invalid $')"); + } + @Test public void testJsonObjectAgg() { checkExp("json_objectagg(k_column: v_column)", "JSON_OBJECTAGG(KEY `K_COLUMN` VALUE `V_COLUMN` NULL ON NULL)"); diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java index d42ab3c..56e9ef3 100644 --- a/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java +++ b/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java @@ -4534,7 +4534,7 @@ public abstract class SqlOperatorBaseTest { tester.checkString("json_depth('{}')", "1", "INTEGER"); tester.checkString("json_depth('[]')", - "1", "INTEGER"); + "1", "INTEGER"); tester.checkString("json_depth('null')", null, "INTEGER"); tester.checkString("json_depth(cast(null as varchar(1)))", @@ -4542,13 +4542,63 @@ public abstract class SqlOperatorBaseTest { tester.checkString("json_depth('[10, true]')", "2", "INTEGER"); tester.checkString("json_depth('[[], {}]')", - "2", "INTEGER"); + "2", "INTEGER"); tester.checkString("json_depth('{\"a\": [10, true]}')", "3", "INTEGER"); tester.checkString("json_depth('[10, {\"a\": [[1,2]]}]')", "5", "INTEGER"); } + @Test public void testJsonLength() { + // no path context + tester.checkString("json_length('{}')", + "0", "INTEGER"); + tester.checkString("json_length('[]')", + "0", "INTEGER"); + tester.checkString("json_length('{\"foo\":100}')", + "1", "INTEGER"); + tester.checkString("json_length('{\"a\": 1, \"b\": {\"c\": 30}}')", + "2", "INTEGER"); + tester.checkString("json_length('[1, 2, {\"a\": 3}]')", + "3", "INTEGER"); + + // lax test + tester.checkString("json_length('{}', 'lax $')", + "0", "INTEGER"); + tester.checkString("json_length('[]', 'lax $')", + "0", "INTEGER"); + tester.checkString("json_length('{\"foo\":100}', 'lax $')", + "1", "INTEGER"); + tester.checkString("json_length('{\"a\": 1, \"b\": {\"c\": 30}}', 'lax $')", + "2", "INTEGER"); + tester.checkString("json_length('[1, 2, {\"a\": 3}]', 'lax $')", + "3", "INTEGER"); + tester.checkString("json_length('{\"a\": 1, \"b\": {\"c\": 30}}', 'lax $.b')", + "1", "INTEGER"); + tester.checkString("json_length('{\"foo\":100}', 'lax $.foo1')", + null, "INTEGER"); + + // strict test + tester.checkString("json_length('{}', 'strict $')", + "0", "INTEGER"); + tester.checkString("json_length('[]', 'strict $')", + "0", "INTEGER"); + tester.checkString("json_length('{\"foo\":100}', 'strict $')", + "1", "INTEGER"); + tester.checkString("json_length('{\"a\": 1, \"b\": {\"c\": 30}}', 'strict $')", + "2", "INTEGER"); + tester.checkString("json_length('[1, 2, {\"a\": 3}]', 'strict $')", + "3", "INTEGER"); + tester.checkString("json_length('{\"a\": 1, \"b\": {\"c\": 30}}', 'strict $.b')", + "1", "INTEGER"); + + // catch error test + tester.checkFails("json_length('{\"foo\":100}', 'invalid $.foo')", + "(?s).*Illegal jsonpath spec.*", true); + tester.checkFails("json_length('{\"foo\":100}', 'strict $.foo1')", + "(?s).*No results for path.*", true); + } + @Test public void testJsonObjectAgg() { checkAggType(tester, "json_objectagg('foo': 'bar')", "VARCHAR(2000) NOT NULL"); tester.checkFails("^json_objectagg(100: 'bar')^", diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java index e39a81a..cfd34c9 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -6795,7 +6795,6 @@ public class JdbcTest { .returns("EXPR$0=[250, 500, 1000]\n"); } - @Ignore @Test public void testJsonType() { CalciteAssert.that() .query("SELECT JSON_TYPE(v) AS c1\n" @@ -6807,16 +6806,26 @@ public class JdbcTest { .returns("C1=OBJECT; C2=ARRAY; C3=INTEGER; C4=BOOLEAN\n"); } - @Ignore @Test public void testJsonDepth() { CalciteAssert.that() - .query("SELECT JSON_DEPTH(v) AS c1\n" - + ",JSON_DEPTH(JSON_VALUE(v, 'lax $.b' ERROR ON ERROR)) AS c2\n" - + ",JSON_DEPTH(JSON_VALUE(v, 'strict $.a[0]' ERROR ON ERROR)) AS c3\n" - + ",JSON_DEPTH(JSON_VALUE(v, 'strict $.a[1]' ERROR ON ERROR)) AS c4\n" - + "FROM (VALUES ('{\"a\": [10, true],\"b\": \"[10, true]\"}')) AS t(v)\n" + .query("SELECT JSON_DEPTH(v) AS c1\n" + + ",JSON_DEPTH(JSON_VALUE(v, 'lax $.b' ERROR ON ERROR)) AS c2\n" + + ",JSON_DEPTH(JSON_VALUE(v, 'strict $.a[0]' ERROR ON ERROR)) AS c3\n" + + ",JSON_DEPTH(JSON_VALUE(v, 'strict $.a[1]' ERROR ON ERROR)) AS c4\n" + + "FROM (VALUES ('{\"a\": [10, true],\"b\": \"[10, true]\"}')) AS t(v)\n" + + "limit 10") + .returns("C1=3; C2=2; C3=1; C4=1\n"); + } + + @Test public void testJsonLength() { + CalciteAssert.that() + .query("SELECT JSON_LENGTH(v) AS c1\n" + + ",JSON_LENGTH(v, 'lax $.a') AS c2\n" + + ",JSON_LENGTH(v, 'strict $.a[0]') AS c3\n" + + ",JSON_LENGTH(v, 'strict $.a[1]') AS c4\n" + + "FROM (VALUES ('{\"a\": [10, true]}')) AS t(v)\n" + "limit 10") - .returns("C1=3; C2=2; C3=1; C4=1\n"); + .returns("C1=1; C2=2; C3=1; C4=1\n"); } @Test diff --git a/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java b/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java index 3912a3d..9db52ee 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java @@ -495,6 +495,26 @@ public class SqlJsonFunctionsTest { } @Test + public void testJsonLength() { + assertJsonLength( + SqlFunctions.PathContext + .withReturned(SqlFunctions.PathMode.LAX, Collections.singletonList("bar")), + is(1)); + assertJsonLength( + SqlFunctions.PathContext + .withReturned(SqlFunctions.PathMode.LAX, null), + nullValue()); + assertJsonLength( + SqlFunctions.PathContext + .withReturned(SqlFunctions.PathMode.STRICT, Collections.singletonList("bar")), + is(1)); + assertJsonLength( + SqlFunctions.PathContext + .withReturned(SqlFunctions.PathMode.LAX, "bar"), + is(1)); + } + + @Test public void testJsonObjectAggAdd() { Map<String, Object> map = new HashMap<>(); Map<String, Object> expected = new HashMap<>(); @@ -667,6 +687,22 @@ public class SqlJsonFunctionsTest { matcher); } + private void assertJsonLength(Object input, + Matcher<? super Integer> matcher) { + assertThat( + invocationDesc(BuiltInMethod.JSON_LENGTH.getMethodName(), input), + SqlFunctions.jsonLength(input), + matcher); + } + + private void assertJsonLengthFailed(Object input, + Matcher<? super Throwable> matcher) { + assertFailed( + invocationDesc(BuiltInMethod.JSON_LENGTH.getMethodName(), input), + () -> SqlFunctions.jsonLength(input), + matcher); + } + private void assertDejsonize(String input, Matcher<Object> matcher) { assertThat(invocationDesc(BuiltInMethod.DEJSONIZE.getMethodName(), input), diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java index d13788b..57776b2 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java @@ -8729,6 +8729,7 @@ public class SqlValidatorTest extends SqlValidatorTestCase { + "DOT -\n" + "ITEM -\n" + "JSON_API_COMMON_SYNTAX -\n" + + "JSON_API_COMMON_SYNTAX_WITHOUT_PATH -\n" + "JSON_STRUCTURED_VALUE_EXPRESSION -\n" + "JSON_VALUE_EXPRESSION -\n" + "NEXT_VALUE -\n" @@ -10815,6 +10816,14 @@ public class SqlValidatorTest extends SqlValidatorTestCase { "(.*)JSON_VALUE_EXPRESSION(.*)"); } + @Test public void testJsonLength() { + checkExp("json_length('{\"foo\":\"bar\"}')"); + checkExp("json_length('{\"foo\":\"bar\"}', 'lax $')"); + checkExpType("json_length('{\"foo\":\"bar\"}')", "INTEGER"); + checkExpType("json_length('{\"foo\":\"bar\"}', 'lax $')", "INTEGER"); + checkExpType("json_length('{\"foo\":\"bar\"}', 'strict $')", "INTEGER"); + } + @Test public void testJsonObjectAgg() { check("select json_objectagg(ename: empno) from emp"); checkFails("select ^json_objectagg(empno: ename)^ from emp", diff --git a/server/src/main/codegen/config.fmpp b/server/src/main/codegen/config.fmpp index 0a110fd..1a53d33 100644 --- a/server/src/main/codegen/config.fmpp +++ b/server/src/main/codegen/config.fmpp @@ -153,6 +153,7 @@ data: { "JSON" "JSON_TYPE" "JSON_DEPTH" + "JSON_LENGTH" "JSON_PRETTY" "K" "KEY" diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 591a54b..a322b48 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -566,6 +566,7 @@ JSON, **JSON_ARRAYAGG**, JSON_DEPTH, **JSON_EXISTS**, +JSON_LENGTH, **JSON_OBJECT**, **JSON_OBJECTAGG**, JSON_PRETTY, @@ -2007,6 +2008,7 @@ Note: | JSON_TYPE(value) | Returns a string indicating the type of a JSON **value**. This can be an object, an array, or a scalar type | JSON_DEPTH(value) | Returns a integer indicating the depth of a JSON **value**. This can be an object, an array, or a scalar type | JSON_PRETTY(value) | Returns a pretty-printing of JSON **value**. +| JSON_LENGTH(value) | Returns a integer indicating the length of a JSON **value**. This can be an object, an array, or a scalar type * JSON_TYPE @@ -2046,6 +2048,25 @@ Result: | ------ | ----- | ------- | ------- | | 3 | 2 | 1 | 1 | +* JSON_LENGTH + +Example SQL: + +```SQL +SELECT JSON_LENGTH(v) AS c1 +,JSON_LENGTH(v, 'lax $.a') AS c2 +,JSON_LENGTH(v, 'strict $.a[0]') AS c3 +,JSON_LENGTH(v, 'strict $.a[1]') AS c4 +FROM (VALUES ('{"a": [10, true]}')) AS t(v) +LIMIT 10; +``` + +Result: + +| c1 | c2 | c3 | c4 | +| ------ | ----- | ------- | ------- | +| 1 | 2 | 1 | 1 | + ## User-defined functions Calcite is extensible. You can define each kind of function using user code.