/*
* Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial agreement.
*/
package io.crate.analyze.expressions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.crate.action.sql.Option;
import io.crate.action.sql.SessionContext;
import io.crate.analyze.ParamTypeHints;
import io.crate.analyze.relations.AnalyzedRelation;
import io.crate.analyze.relations.FullQualifedNameFieldProvider;
import io.crate.analyze.relations.TableRelation;
import io.crate.analyze.symbol.Field;
import io.crate.analyze.symbol.Function;
import io.crate.analyze.symbol.Literal;
import io.crate.analyze.symbol.Symbol;
import io.crate.metadata.*;
import io.crate.metadata.table.TableInfo;
import io.crate.sql.parser.SqlParser;
import io.crate.sql.tree.*;
import io.crate.test.integration.CrateUnitTest;
import io.crate.testing.DummyRelation;
import io.crate.testing.SqlExpressions;
import io.crate.testing.T3;
import io.crate.types.DataTypes;
import org.junit.Before;
import org.junit.Test;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import static io.crate.testing.SymbolMatchers.isField;
import static io.crate.testing.TestingHelpers.getFunctions;
import static io.crate.testing.TestingHelpers.isSQL;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Additional tests for the ExpressionAnalyzer.
* Most of the remaining stuff is tested via {@link io.crate.analyze.SelectStatementAnalyzerTest} and other *AnalyzerTest classes.
*/
public class ExpressionAnalyzerTest extends CrateUnitTest {
private ImmutableMap<QualifiedName, AnalyzedRelation> dummySources;
private ExpressionAnalysisContext context;
private ParamTypeHints paramTypeHints;
private Functions functions;
@Before
public void prepare() throws Exception {
paramTypeHints = ParamTypeHints.EMPTY;
DummyRelation dummyRelation = new DummyRelation("obj.x", "myObj.x", "myObj.x.AbC");
dummySources = ImmutableMap.of(new QualifiedName("foo"), dummyRelation);
context = new ExpressionAnalysisContext();
functions = getFunctions();
}
@Test
public void testUnsupportedExpressionCurrentDate() throws Exception {
expectedException.expect(UnsupportedOperationException.class);
expectedException.expectMessage("Unsupported expression current_time");
ExpressionAnalyzer expressionAnalyzer = new ExpressionAnalyzer(
functions, SessionContext.SYSTEM_SESSION, paramTypeHints, new FullQualifedNameFieldProvider(dummySources), null);
ExpressionAnalysisContext expressionAnalysisContext = new ExpressionAnalysisContext();
expressionAnalyzer.convert(SqlParser.createExpression("current_time"), expressionAnalysisContext);
}
@Test
public void testQuotedSubscriptExpression() throws Exception {
ExpressionAnalyzer expressionAnalyzer = new ExpressionAnalyzer(
functions,
new SessionContext(0, EnumSet.of(Option.ALLOW_QUOTED_SUBSCRIPT), null, null),
paramTypeHints,
new FullQualifedNameFieldProvider(dummySources),
null);
ExpressionAnalysisContext expressionAnalysisContext = new ExpressionAnalysisContext();
Field field1 = (Field) expressionAnalyzer.convert(SqlParser.createExpression("obj['x']"), expressionAnalysisContext);
Field field2 = (Field) expressionAnalyzer.convert(SqlParser.createExpression("\"obj['x']\""), expressionAnalysisContext);
assertEquals(field1, field2);
Field field3 = (Field) expressionAnalyzer.convert(SqlParser.createExpression("\"myObj['x']\""), expressionAnalysisContext);
assertEquals("myObj['x']", field3.path().toString());
Field field4 = (Field) expressionAnalyzer.convert(SqlParser.createExpression("\"myObj['x']['AbC']\""), expressionAnalysisContext);
assertEquals("myObj['x']['AbC']", field4.path().toString());
}
@Test
public void testSubscriptSplitPatternMatcher() throws Exception {
assertEquals("\"foo\".\"bar\"['x']['y']", ExpressionAnalyzer.getQuotedSubscriptLiteral("foo.bar['x']['y']"));
assertEquals("\"foo\"['x']['y']", ExpressionAnalyzer.getQuotedSubscriptLiteral("foo['x']['y']"));
assertEquals("\"foo\"['x']", ExpressionAnalyzer.getQuotedSubscriptLiteral("foo['x']"));
assertEquals("\"myFoo\"['xY']", ExpressionAnalyzer.getQuotedSubscriptLiteral("myFoo['xY']"));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral("foo"));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral("foo.."));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral(".foo."));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral("foo.['x']"));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral("obj"));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral("obj.x"));
assertNull(ExpressionAnalyzer.getQuotedSubscriptLiteral("obj[x][y]"));
}
@Test
public void testAnalyzeSubscriptFunctionCall() throws Exception {
// Test when use subscript function is used explicitly then it's handled (and validated)
// the same way it's handled when the subscript operator `[]` is used
ExpressionAnalyzer expressionAnalyzer = new ExpressionAnalyzer(
functions,
new SessionContext(0, EnumSet.of(Option.ALLOW_QUOTED_SUBSCRIPT), null, null),
paramTypeHints,
new FullQualifedNameFieldProvider(dummySources),
null);
ExpressionAnalysisContext expressionAnalysisContext = new ExpressionAnalysisContext();
FunctionCall subscriptFunctionCall = new FunctionCall(
new QualifiedName("subscript"),
ImmutableList.of(
new ArrayLiteral(ImmutableList.of(new StringLiteral("obj"))),
new LongLiteral("1")));
Function function = (Function) expressionAnalyzer.convert(subscriptFunctionCall, expressionAnalysisContext);
assertEquals("subscript(_array(Literal{obj, type=string}),Literal{1, type=integer})", function.toString());
}
@Test
public void testInSelfJoinCaseFunctionsThatLookTheSameMustNotReuseFunctionAllocation() throws Exception {
TableInfo tableInfo = mock(TableInfo.class);
when(tableInfo.getReference(new ColumnIdent("id"))).thenReturn(
new Reference(new ReferenceIdent(new TableIdent("doc", "t"), "id"), RowGranularity.DOC, DataTypes.INTEGER));
when(tableInfo.ident()).thenReturn(new TableIdent("doc", "t"));
TableRelation tr1 = new TableRelation(tableInfo);
TableRelation tr2 = new TableRelation(tableInfo);
Map<QualifiedName, AnalyzedRelation> sources = ImmutableMap.of(
new QualifiedName("t1"), tr1,
new QualifiedName("t2"), tr2
);
ExpressionAnalyzer expressionAnalyzer = new ExpressionAnalyzer(
functions, SessionContext.SYSTEM_SESSION, paramTypeHints, new FullQualifedNameFieldProvider(sources), null);
Function andFunction = (Function) expressionAnalyzer.convert(
SqlParser.createExpression("not t1.id = 1 and not t2.id = 1"), context);
Field t1Id = ((Field) ((Function) ((Function) andFunction.arguments().get(0)).arguments().get(0)).arguments().get(0));
Field t2Id = ((Field) ((Function) ((Function) andFunction.arguments().get(1)).arguments().get(0)).arguments().get(0));
assertTrue(t1Id.relation() != t2Id.relation());
}
@Test
public void testSwapFunctionLeftSide() throws Exception {
SqlExpressions expressions = new SqlExpressions(T3.SOURCES);
Function cmp = (Function) expressions.normalize(expressions.asSymbol("8 + 5 > t1.x"));
// the comparison was swapped so the field is on the left side
assertThat(cmp.info().ident().name(), is("op_<"));
assertThat(cmp.arguments().get(0), isField("x"));
}
@Test
public void testBetweenIsRewrittenToLteAndGte() throws Exception {
SqlExpressions expressions = new SqlExpressions(T3.SOURCES);
Symbol symbol = expressions.asSymbol("10 between 1 and 10");
assertThat(symbol, isSQL("((10 >= 1) AND (10 <= 10))"));
}
@Test
public void testBetweenNullIsRewrittenToLteAndGte() throws Exception {
SqlExpressions expressions = new SqlExpressions(T3.SOURCES);
Symbol symbol = expressions.asSymbol("10 between 1 and NULL");
assertThat(symbol, isSQL("((10 >= 1) AND (10 <= NULL))"));
}
@Test
public void testNonDeterministicFunctionsAlwaysNew() throws Exception {
ExpressionAnalysisContext localContext = new ExpressionAnalysisContext();
FunctionInfo info1 = new FunctionInfo(
new FunctionIdent("inc", Collections.singletonList(DataTypes.BOOLEAN)),
DataTypes.INTEGER,
FunctionInfo.Type.SCALAR,
FunctionInfo.NO_FEATURES
);
Function fn1 = localContext.allocateFunction(info1, Collections.singletonList(Literal.BOOLEAN_FALSE));
Function fn2 = localContext.allocateFunction(info1, Collections.singletonList(Literal.BOOLEAN_FALSE));
Function fn3 = localContext.allocateFunction(info1, Collections.singletonList(Literal.BOOLEAN_TRUE));
// different instances
assertThat(fn1, allOf(
not(sameInstance(fn2)),
not(sameInstance(fn3))
));
// but equal
assertThat(fn1, is(equalTo(fn2)));
assertThat(fn1, is(not(equalTo(fn3))));
}
}