-
Notifications
You must be signed in to change notification settings - Fork 181
Support spath command to extract all fields #4822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/permissive
Are you sure you want to change the base?
Changes from 5 commits
972a480
08703dd
3020586
0b7ad9f
28cbf06
48f5633
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -688,7 +688,50 @@ public RelNode visitParse(Parse node, CalcitePlanContext context) { | |
|
|
||
| @Override | ||
| public RelNode visitSpath(SPath node, CalcitePlanContext context) { | ||
| return visitEval(node.rewriteAsEval(), context); | ||
| if (node.getPath() != null) { | ||
| return visitEval(node.rewriteAsEval(), context); | ||
| } else { | ||
| return spathWithoutPath(node, context); | ||
| } | ||
| } | ||
|
|
||
| private RelNode spathWithoutPath(SPath node, CalcitePlanContext context) { | ||
| visitChildren(node, context); | ||
|
|
||
| // 1. Extract all fields from JSON in `inField` and merge with existing dynamic fields. | ||
| // _MAP = MAP_APPEND(_MAP, JSON_EXTRACT_ALL(inField)) | ||
| RexNode inField = QualifiedNameResolver.resolveFieldOrThrow(1, 0, node.getInField(), context); | ||
| RexNode map = context.rexBuilder.makeCall(BuiltinFunctionName.JSON_EXTRACT_ALL, inField); | ||
| if (context.fieldBuilder.isDynamicFieldsExist()) { | ||
| map = | ||
| context.rexBuilder.makeCall( | ||
| BuiltinFunctionName.MAP_APPEND, context.fieldBuilder.getDynamicFieldsMap(), map); | ||
| } | ||
|
|
||
| // 2. Merge dynamic fields with static fields. | ||
| // static_field1 = APPEND(static_field1, _MAP[static_field1]), static_attr2 = ... | ||
| List<String> staticFieldNames = context.fieldBuilder.getStaticFieldNames(); | ||
| List<RexNode> fields = new ArrayList<>(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mentioned in standup this morning that I think we're making this more complex by working directly on the I'd like to consider putting the bulk of this logic in a data class that can start supplying a higher-level interface for most field operations. Can incrementally move more commands to use it over time.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is very special logic for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If spath command is special enough for this, I imagine other commands would have their own special cases later. It would still be worth finding an abstraction here in my view. To me it seems like this is one of the main operations of adding dynamic fields to that structure & would eventually be reusable as more dynamic commands are visited.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expected commands which would add dynamic fields are:
There might be a chance we could reuse current logic in the future, but I think we should generalize when we implement that. I don't have very good idea to generalize some part right now. Let me know if you already have clear picture which part should be generalized. |
||
| for (String fieldName : staticFieldNames) { | ||
| RexNode field = context.fieldBuilder.staticField(fieldName); | ||
| RexNode appended = | ||
| context.rexBuilder.makeCall( | ||
| BuiltinFunctionName.INTERNAL_APPEND, | ||
| field, | ||
| context.rexBuilder.createItemAccess(map, fieldName)); | ||
| fields.add(context.relBuilder.alias(appended, fieldName)); | ||
| } | ||
|
|
||
| // 3. Dedupe dynamic fields with static fields. | ||
| // _MAP = MAP_REMOVE(_MAP, [static_attr1, static_attr2, ...]) | ||
| RexNode fieldNameArray = context.rexBuilder.createStringArrayLiteral(staticFieldNames); | ||
| RexNode dedupedMap = | ||
| context.rexBuilder.makeCall(BuiltinFunctionName.MAP_REMOVE, map, fieldNameArray); | ||
| fields.add(context.relBuilder.alias(dedupedMap, DynamicFieldsConstants.DYNAMIC_FIELDS_MAP)); | ||
|
|
||
| context.relBuilder.project(fields); | ||
|
|
||
| return context.relBuilder.peek(); | ||
| } | ||
|
|
||
| // might need to remove fields in _MAP | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,14 +8,32 @@ | |
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| /** Core logic for `mvappend` command to collect elements from list of args */ | ||
| public class MVAppendCore { | ||
| /** | ||
| * Core logic for `mvappend` and internal `append` function to collect elements from list of args. | ||
| */ | ||
| public class AppendCore { | ||
|
|
||
| /** | ||
| * Collect non-null elements from `args`. If an item is a list, it will collect non-null elements | ||
| * of the list. See {@ref MVAppendFunctionImplTest} for detailed behavior. | ||
| * of the list. See {@ref AppendFunctionImplTest} for detailed behavior. | ||
| */ | ||
| public static Object collectElements(Object... args) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: Can we make this typing any more specific than
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We cannot since input/output could be any type. |
||
| List<Object> elements = collectElementsToList(args); | ||
|
|
||
| if (elements.isEmpty()) { | ||
| return null; | ||
| } else if (elements.size() == 1) { | ||
| // return the element in case of single element | ||
| return elements.get(0); | ||
| } else { | ||
| return elements; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Collect non-null elements from `args`. If an item is a list, it will collect non-null elements. | ||
| */ | ||
| public static List<Object> collectElements(Object... args) { | ||
| public static List<Object> collectElementsToList(Object... args) { | ||
| List<Object> elements = new ArrayList<>(); | ||
|
|
||
| for (Object arg : args) { | ||
|
|
@@ -28,7 +46,7 @@ public static List<Object> collectElements(Object... args) { | |
| } | ||
| } | ||
|
|
||
| return elements.isEmpty() ? null : elements; | ||
| return elements; | ||
| } | ||
|
|
||
| private static void addListElements(List<?> list, List<Object> elements) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.expression.function.CollectionUDF; | ||
|
|
||
| import java.util.List; | ||
| import org.apache.calcite.adapter.enumerable.NotNullImplementor; | ||
| import org.apache.calcite.adapter.enumerable.NullPolicy; | ||
| import org.apache.calcite.adapter.enumerable.RexToLixTranslator; | ||
| import org.apache.calcite.linq4j.tree.Expression; | ||
| import org.apache.calcite.linq4j.tree.Expressions; | ||
| import org.apache.calcite.linq4j.tree.Types; | ||
| import org.apache.calcite.rel.type.RelDataTypeFactory; | ||
| import org.apache.calcite.rex.RexCall; | ||
| import org.apache.calcite.sql.type.SqlReturnTypeInference; | ||
| import org.apache.calcite.sql.type.SqlTypeName; | ||
| import org.opensearch.sql.expression.function.ImplementorUDF; | ||
| import org.opensearch.sql.expression.function.UDFOperandMetadata; | ||
|
|
||
| /** | ||
| * Internal append function that appends all elements from arguments to create an array. Returns | ||
| * null if there is no element. Returns the scalar value if there is single element. Otherwise, | ||
| * returns a list containing all the elements from inputs. | ||
| */ | ||
| public class AppendFunctionImpl extends ImplementorUDF { | ||
|
|
||
| public AppendFunctionImpl() { | ||
| super(new AppendImplementor(), NullPolicy.ALL); | ||
| } | ||
|
|
||
| @Override | ||
| public SqlReturnTypeInference getReturnTypeInference() { | ||
| return sqlOperatorBinding -> { | ||
| RelDataTypeFactory typeFactory = sqlOperatorBinding.getTypeFactory(); | ||
|
|
||
| if (sqlOperatorBinding.getOperandCount() == 0) { | ||
| return typeFactory.createSqlType(SqlTypeName.NULL); | ||
| } | ||
|
|
||
| // Return type is ANY as it could return scalar value (in case of single item) or array | ||
| return typeFactory.createSqlType(SqlTypeName.ANY); | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public UDFOperandMetadata getOperandMetadata() { | ||
| return null; | ||
| } | ||
|
|
||
| public static class AppendImplementor implements NotNullImplementor { | ||
| @Override | ||
| public Expression implement( | ||
| RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) { | ||
| return Expressions.call( | ||
| Types.lookupMethod(AppendFunctionImpl.class, "append", Object[].class), | ||
| Expressions.newArrayInit(Object.class, translatedOperands)); | ||
| } | ||
| } | ||
|
|
||
| public static Object append(Object... args) { | ||
| return AppendCore.collectElements(args); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.expression.function.CollectionUDF; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| public class AppendFunctionImplTest { | ||
|
|
||
| @Test | ||
| public void testAppendWithNoArguments() { | ||
| Object result = AppendFunctionImpl.append(); | ||
| assertNull(result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithSingleElement() { | ||
| Object result = AppendFunctionImpl.append(42); | ||
| assertEquals(42, result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithMultipleElements() { | ||
| Object result = AppendFunctionImpl.append(1, 2, 3); | ||
| assertEquals(Arrays.asList(1, 2, 3), result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithNullValues() { | ||
| Object result = AppendFunctionImpl.append(null, 1, null); | ||
| assertEquals(1, result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithAllNulls() { | ||
| Object result = AppendFunctionImpl.append(null, null); | ||
| assertNull(result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithArrayFlattening() { | ||
| List<Integer> array1 = Arrays.asList(1, 2); | ||
| List<Integer> array2 = Arrays.asList(3, 4); | ||
| Object result = AppendFunctionImpl.append(array1, array2); | ||
| assertEquals(Arrays.asList(1, 2, 3, 4), result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithMixedTypes() { | ||
| List<Integer> array = Arrays.asList(1, 2); | ||
| Object result = AppendFunctionImpl.append(array, 3, "hello"); | ||
| assertEquals(Arrays.asList(1, 2, 3, "hello"), result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithArrayAndNulls() { | ||
| List<Integer> array = Arrays.asList(1, 2); | ||
| Object result = AppendFunctionImpl.append(null, array, null, 3); | ||
| assertEquals(Arrays.asList(1, 2, 3), result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithSingleNull() { | ||
| Object result = AppendFunctionImpl.append((Object) null); | ||
| assertNull(result); | ||
| } | ||
|
|
||
| @Test | ||
| public void testAppendWithEmptyArray() { | ||
| List<Object> emptyArray = Arrays.asList(); | ||
| Object result = AppendFunctionImpl.append(emptyArray, 1); | ||
| assertEquals(1, result); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.