diff --git a/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/RequestParser.java b/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/RequestParser.java index 7788f02cc8..7551916e35 100644 --- a/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/RequestParser.java +++ b/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/RequestParser.java @@ -35,6 +35,7 @@ import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.NotImplementedException; @@ -747,6 +748,7 @@ public Map generateNameMapping( List tables @AllArgsConstructor + @Getter public static class Filters { public final Map>> literalFilters; diff --git a/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/Rest.java b/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/Rest.java index 5428bc47db..31ad3ddc7f 100644 --- a/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/Rest.java +++ b/plugins/rest-interface/src/main/java/org/polypheny/db/restapi/Rest.java @@ -311,11 +311,11 @@ AlgBuilder tableScans( AlgBuilder algBuilder, RexBuilder rexBuilder, List filters( Statement statement, AlgBuilder algBuilder, RexBuilder rexBuilder, Filters filters, HttpServletRequest req ) { - if ( filters.literalFilters != null ) { + if ( filters.getLiteralFilters() != null ) { if ( req != null && log.isDebugEnabled() ) { log.debug( "Starting to process filters. Session ID: {}.", req.getSession().getId() ); } @@ -336,15 +336,15 @@ List filters( Statement statement, AlgBuilder algBuilder, RexBuilder re Map filterMap = new HashMap<>(); filtersRows.forEach( ( r ) -> filterMap.put( r.getName(), r ) ); int index = 0; - for ( RequestColumn column : filters.literalFilters.keySet() ) { - for ( Pair filterOperationPair : filters.literalFilters.get( column ) ) { - AlgDataTypeField typeField = filterMap.get( column.getColumn().name ); + for ( RequestColumn column : filters.getLiteralFilters().keySet() ) { + for ( Pair filterOperationPair : filters.getLiteralFilters().get( column ) ) { + AlgDataTypeField typeField = filterMap.get( column.getColumn().getName() ); RexNode inputRef = rexBuilder.makeInputRef( baseNodeForFilters, typeField.getIndex() ); - PolyValue param = filterOperationPair.right; + PolyValue param = filterOperationPair.getRight(); statement.getDataContext().addParameterValues( index, typeField.getType(), ImmutableList.of( param ) ); RexNode rightHandSide = rexBuilder.makeDynamicParam( typeField.getType(), index ); index++; - RexNode call = rexBuilder.makeCall( filterOperationPair.left, inputRef, rightHandSide ); + RexNode call = rexBuilder.makeCall( filterOperationPair.getLeft(), inputRef, rightHandSide ); filterNodes.add( call ); } } @@ -365,11 +365,12 @@ List filters( Statement statement, AlgBuilder algBuilder, RexBuilder re } + @VisibleForTesting List valuesColumnNames( List>> values ) { List valueColumnNames = new ArrayList<>(); List> rowsToInsert = values.get( 0 ); for ( Pair insertValue : rowsToInsert ) { - valueColumnNames.add( insertValue.left.getColumn().name ); + valueColumnNames.add( insertValue.getLeft().getColumn().getName() ); } return valueColumnNames; @@ -417,6 +418,7 @@ List> valuesLiteral( AlgBuilder algBuilder, RexBuilder rexBuild } + @VisibleForTesting AlgBuilder initialProjection( AlgBuilder algBuilder, RexBuilder rexBuilder, List columns ) { AlgNode baseNode = algBuilder.peek(); List inputRefs = new ArrayList<>(); @@ -433,6 +435,7 @@ AlgBuilder initialProjection( AlgBuilder algBuilder, RexBuilder rexBuilder, List } + @VisibleForTesting AlgBuilder finalProjection( AlgBuilder algBuilder, RexBuilder rexBuilder, List columns ) { AlgNode baseNode = algBuilder.peek(); List inputRefs = new ArrayList<>(); @@ -451,6 +454,7 @@ AlgBuilder finalProjection( AlgBuilder algBuilder, RexBuilder rexBuilder, List requestColumns, List groupings ) { AlgNode baseNodeForAggregation = algBuilder.peek(); @@ -504,6 +508,7 @@ AlgBuilder aggregates( AlgBuilder algBuilder, RexBuilder rexBuilder, List> sorts, int limit, int offset ) { if ( (sorts == null || sorts.isEmpty()) && (limit >= 0 || offset >= 0) ) { algBuilder = algBuilder.limit( offset, limit ); @@ -538,6 +543,7 @@ private Transaction getTransaction() { } + @VisibleForTesting String executeAndTransformPolyAlg( AlgRoot algRoot, final Statement statement, final Context ctx ) { RestResult restResult; try { diff --git a/plugins/rest-interface/src/test/java/org/polypheny/db/restapi/RestTest.java b/plugins/rest-interface/src/test/java/org/polypheny/db/restapi/RestTest.java new file mode 100644 index 0000000000..1a00b25fb8 --- /dev/null +++ b/plugins/rest-interface/src/test/java/org/polypheny/db/restapi/RestTest.java @@ -0,0 +1,371 @@ +/* + * Copyright 2019-2024 The Polypheny Project + * + * Licensed 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.polypheny.db.restapi; + +import io.javalin.http.Context; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.polypheny.db.algebra.AlgNode; +import org.polypheny.db.algebra.AlgRoot; +import org.polypheny.db.algebra.core.JoinAlgType; +import org.polypheny.db.algebra.operators.OperatorName; +import org.polypheny.db.catalog.entity.logical.LogicalColumn; +import org.polypheny.db.catalog.entity.logical.LogicalTable; +import org.polypheny.db.languages.OperatorRegistry; +import org.polypheny.db.processing.QueryProcessor; +import org.polypheny.db.rex.RexBuilder; +import org.polypheny.db.rex.RexIndexRef; +import org.polypheny.db.rex.RexLiteral; +import org.polypheny.db.rex.RexNode; +import org.polypheny.db.tools.AlgBuilder; +import org.polypheny.db.transaction.Statement; +import org.polypheny.db.type.entity.PolyValue; +import org.polypheny.db.util.Pair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class RestTest { + private Rest rest; + private AlgBuilder algBuilder; + private RexBuilder rexBuilder; + + private void setupMocks() { + algBuilder = mock(AlgBuilder.class); + rexBuilder = mock(RexBuilder.class); + } + + + @Test + @DisplayName("Table scans with single table") + public void tableScansWithSingleTable() { + setupMocks(); + LogicalTable logicalTable = mock(LogicalTable.class); + + when(logicalTable.getNamespaceName()).thenReturn("namespace"); + when(logicalTable.getName()).thenReturn("table"); + when(algBuilder.relScan(anyString(), anyString())).thenReturn(algBuilder); + + List tables = Collections.singletonList(logicalTable); + + Rest rest = new Rest(null, 0, 0); + rest.tableScans(algBuilder, rexBuilder, tables); + + verify(algBuilder).relScan("namespace", "table"); + verify(algBuilder, never()).join((JoinAlgType) any(), (Iterable) any()); + } + + + @Test + @DisplayName("Table scans with multiple tables") + public void tableScansWithMultipleTables() { + setupMocks(); + LogicalTable logicalTable1 = mock(LogicalTable.class); + LogicalTable logicalTable2 = mock(LogicalTable.class); + RexLiteral rexLiteral = mock(RexLiteral.class); + + when(logicalTable1.getNamespaceName()).thenReturn("namespace1"); + when(logicalTable1.getName()).thenReturn("table1"); + when(logicalTable2.getNamespaceName()).thenReturn("namespace2"); + when(logicalTable2.getName()).thenReturn("table2"); + when(algBuilder.relScan(anyString(), anyString())).thenReturn(algBuilder); + when(algBuilder.join((JoinAlgType) any(), (Iterable) any())).thenReturn(algBuilder); + when(rexBuilder.makeLiteral(true)).thenReturn(rexLiteral); + + List tables = Arrays.asList(logicalTable1, logicalTable2); + + Rest rest = new Rest(null, 0, 0); + rest.tableScans(algBuilder, rexBuilder, tables); + + verify(algBuilder).relScan("namespace1", "table1"); + verify(algBuilder).relScan("namespace2", "table2"); + verify(algBuilder).join(JoinAlgType.INNER, rexBuilder.makeLiteral(true)); + } + + + @Test + @DisplayName("Values column names with non-empty values") + public void valuesColumnNamesWithNonEmptyValues() { + rest = new Rest(null, 0, 0); + RequestColumn requestColumn1 = mock(RequestColumn.class); + RequestColumn requestColumn2 = mock(RequestColumn.class); + LogicalColumn column1 = mock(LogicalColumn.class); + LogicalColumn column2 = mock(LogicalColumn.class); + Pair pair1 = mock(Pair.class); + Pair pair2 = mock(Pair.class); + when(requestColumn1.getColumn()).thenReturn(column1); + when(requestColumn2.getColumn()).thenReturn(column2); + when(column1.getName()).thenReturn("column1"); + when(column2.getName()).thenReturn("column2"); + List>> values = mock(List.class); + List> value = Arrays.asList(pair1, pair2); + when(pair1.getLeft()).thenReturn(requestColumn1); + when(pair2.getLeft()).thenReturn(requestColumn2); + when(values.get(0)).thenReturn(value); + + List result = rest.valuesColumnNames(values); + + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.contains("column1")); + assertTrue(result.contains("column2")); + } + + + @Test + @DisplayName("Values column names with empty values") + public void valuesColumnNamesWithEmptyValues() { + rest = new Rest(null, 0, 0); + List>> values = mock(List.class); + List> value = new ArrayList<>(); + when(values.get(0)).thenReturn(value); + + List result = rest.valuesColumnNames(values); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + + @Test + @DisplayName("Values column names with null values") + public void valuesColumnNamesWithNullValues() { + rest = new Rest(null, 0, 0); + List>> values = null; + + assertThrows(NullPointerException.class, () -> rest.valuesColumnNames(values)); + } + + + @Test + @DisplayName("Initial projection with non-empty columns") + public void initialProjectionWithNonEmptyColumns() { + setupMocks(); + RequestColumn requestColumn1 = mock(RequestColumn.class); + RequestColumn requestColumn2 = mock(RequestColumn.class); + AlgNode algNode = mock(AlgNode.class); + RexIndexRef rexNode1 = mock(RexIndexRef.class); + RexIndexRef rexNode2 = mock(RexIndexRef.class); + + when(algBuilder.peek()).thenReturn(algNode); + when(rexBuilder.makeInputRef(algNode, requestColumn1.getScanIndex())).thenReturn(rexNode1); + when(rexBuilder.makeInputRef(algNode, requestColumn2.getScanIndex())).thenReturn(rexNode2); + when(algBuilder.project(Arrays.asList(rexNode1, rexNode2))).thenReturn(algBuilder); + + List columns = Arrays.asList(requestColumn1, requestColumn2); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.initialProjection(algBuilder, rexBuilder, columns); + + verify(algBuilder).project(anyList()); + } + + + @Test + @DisplayName("Initial projection with empty columns") + public void initialProjectionWithEmptyColumns() { + setupMocks(); + List columns = new ArrayList<>(); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.initialProjection(algBuilder, rexBuilder, columns); + + verify(algBuilder).project(Collections.emptyList()); + } + + + @Test + @DisplayName("Initial projection with null columns") + public void initialProjectionWithNullColumns() { + setupMocks(); + Rest rest = new Rest(null, 0, 0); + + assertThrows(NullPointerException.class, () -> rest.initialProjection(algBuilder, rexBuilder, null)); + } + + + @Test + @DisplayName("Final projection with explicit columns") + public void finalProjectionWithExplicitColumns() { + setupMocks(); + RequestColumn requestColumn1 = mock(RequestColumn.class); + RequestColumn requestColumn2 = mock(RequestColumn.class); + AlgNode algNode = mock(AlgNode.class); + RexIndexRef rexNode1 = mock(RexIndexRef.class); + RexIndexRef rexNode2 = mock(RexIndexRef.class); + + when(algBuilder.peek()).thenReturn(algNode); + when(rexBuilder.makeInputRef(algNode, requestColumn1.getLogicalIndex())).thenReturn(rexNode1); + when(rexBuilder.makeInputRef(algNode, requestColumn2.getLogicalIndex())).thenReturn(rexNode2); + when(requestColumn1.isExplicit()).thenReturn(true); + when(requestColumn2.isExplicit()).thenReturn(true); + when(algBuilder.project(Arrays.asList(rexNode1, rexNode2), Arrays.asList("alias1", "alias2"), true)).thenReturn(algBuilder); + + List columns = Arrays.asList(requestColumn1, requestColumn2); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.finalProjection(algBuilder, rexBuilder, columns); + + verify(algBuilder).project(anyList(), anyList(), eq(true)); + } + + + @Test + @DisplayName("Final projection with non-explicit columns") + public void finalProjectionWithNonExplicitColumns() { + setupMocks(); + RequestColumn requestColumn1 = mock(RequestColumn.class); + RequestColumn requestColumn2 = mock(RequestColumn.class); + + when(requestColumn1.isExplicit()).thenReturn(false); + when(requestColumn2.isExplicit()).thenReturn(false); + when(algBuilder.project(Collections.emptyList(), Collections.emptyList(), true)).thenReturn(algBuilder); + + List columns = Arrays.asList(requestColumn1, requestColumn2); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.finalProjection(algBuilder, rexBuilder, columns); + + verify(algBuilder).project(Collections.emptyList(), Collections.emptyList(), true); + assertEquals(algBuilder, result); + } + + + @Test + @DisplayName("Final projection with empty columns") + public void finalProjectionWithEmptyColumns() { + setupMocks(); + List columns = new ArrayList<>(); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.finalProjection(algBuilder, rexBuilder, columns); + + verify(algBuilder).project(Collections.emptyList(), Collections.emptyList(), true); + } + + + @Test + @DisplayName("Final projection with null columns") + public void finalProjectionWithNullColumns() { + setupMocks(); + Rest rest = new Rest(null, 0, 0); + + assertThrows(NullPointerException.class, () -> rest.finalProjection(algBuilder, rexBuilder, null)); + } + + + @Test + @DisplayName("Aggregates with empty request columns and groupings") + public void aggregatesWithEmptyRequestColumnsAndGroupings() { + setupMocks(); + List requestColumns = new ArrayList<>(); + List groupings = new ArrayList<>(); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.aggregates(algBuilder, rexBuilder, requestColumns, groupings); + + verify(algBuilder, never()).aggregate((AlgBuilder.GroupKey) any(), (AlgBuilder.AggCall) any()); + assertEquals(algBuilder, result); + } + + + @Test + @DisplayName("Aggregates with null request columns and groupings") + public void aggregatesWithNullRequestColumnsAndGroupings() { + setupMocks(); + Rest rest = new Rest(null, 0, 0); + + assertThrows(NullPointerException.class, () -> rest.aggregates(algBuilder, rexBuilder, null, null)); + } + + + @Test + @DisplayName("Sort with null sorts and positive limit and offset") + public void sortWithNullSortsAndPositiveLimitAndOffset() { + setupMocks(); + when(algBuilder.limit(anyInt(), anyInt())).thenReturn(algBuilder); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.sort(algBuilder, rexBuilder, null, 10, 5); + + verify(algBuilder).limit(5, 10); + assertEquals(algBuilder, result); + } + + + @Test + @DisplayName("Sort with empty sorts and positive limit and offset") + public void sortWithEmptySortsAndPositiveLimitAndOffset() { + setupMocks(); + when(algBuilder.limit(anyInt(), anyInt())).thenReturn(algBuilder); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.sort(algBuilder, rexBuilder, new ArrayList<>(), 10, 5); + + verify(algBuilder).limit(5, 10); + assertEquals(algBuilder, result); + } + + + @Test + @DisplayName("Sort with non-empty sorts") + public void sortWithNonEmptySorts() { + setupMocks(); + RequestColumn requestColumn = mock(RequestColumn.class); + AlgNode algNode = mock(AlgNode.class); + RexIndexRef inputRef = mock(RexIndexRef.class); + RexNode innerNode = mock(RexNode.class); + RexNode sortingNode = mock(RexNode.class); + when(algBuilder.peek()).thenReturn(algNode); + when(requestColumn.getLogicalIndex()).thenReturn(0); + when(rexBuilder.makeInputRef(algNode, 0)).thenReturn(inputRef); + when(rexBuilder.makeCall(OperatorRegistry.get(OperatorName.DESC), inputRef)).thenReturn(innerNode); + when(rexBuilder.makeCall(OperatorRegistry.get(OperatorName.NULLS_FIRST), innerNode)).thenReturn(sortingNode); + when(algBuilder.sortLimit(anyInt(), anyInt(), anyList())).thenReturn(algBuilder); + + List> sorts = Arrays.asList(new Pair<>(requestColumn, true)); + + Rest rest = new Rest(null, 0, 0); + AlgBuilder result = rest.sort(algBuilder, rexBuilder, sorts, 10, 5); + + verify(algBuilder).sortLimit(5, 10, Arrays.asList(sortingNode)); + assertEquals(algBuilder, result); + } + + + @Test + @DisplayName("Execute and transform with query preparation and execution failure") + public void executeAndTransformWithQueryFailure() { + AlgRoot algRoot = mock(AlgRoot.class); + Statement statement = mock(Statement.class); + Context ctx = mock(Context.class); + QueryProcessor queryProcessor = mock(QueryProcessor.class); + + when(statement.getQueryProcessor()).thenReturn(queryProcessor); + when(queryProcessor.prepareQuery(algRoot, true)).thenThrow(new RuntimeException()); + + Rest rest = new Rest(null, 0, 0); + + assertThrows(RuntimeException.class, () -> rest.executeAndTransformPolyAlg(algRoot, statement, ctx)); + } +}