/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */
package org.teiid.olingo;

import static org.junit.Assert.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.sql.Time;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.TimeZone;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.olingo.commons.api.edm.provider.CsdlSchema;
import org.apache.olingo.commons.api.edmx.EdmxReference;
import org.apache.olingo.commons.core.Encoder;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.ODataHttpHandler;
import org.apache.olingo.server.api.ServiceMetadata;
import org.apache.olingo.server.core.OData4Impl;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.teiid.core.util.TimestampWithTimezone;
import org.teiid.metadata.MetadataStore;
import org.teiid.odata.api.Client;
import org.teiid.odata.api.CountResponse;
import org.teiid.odata.api.OperationResponse;
import org.teiid.odata.api.SQLParameter;
import org.teiid.olingo.service.EntityCollectionResponse;
import org.teiid.olingo.service.ODataSchemaBuilder;
import org.teiid.olingo.service.TeiidEdmProvider;
import org.teiid.olingo.service.TeiidServiceHandler;
import org.teiid.query.metadata.TransformationMetadata;
import org.teiid.query.parser.ParseInfo;
import org.teiid.query.parser.QueryParser;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Query;
import org.teiid.query.unittest.RealMetadataFactory;
import org.teiid.query.unittest.RealMetadataFactory.DDLHolder;

@SuppressWarnings("nls")
public class TestODataSQLBuilder {
    static String DEFAULT_DDL =
            "CREATE FOREIGN TABLE G1 (\n" +
            "    e1 string, \n" +
            "    e2 integer PRIMARY KEY, \n" +
            "    e3 double\n" +
            ");\n" +
            "\n" +
            "CREATE FOREIGN TABLE G2 (\n" +
            "    e1 string, \n" +
            "    e2 integer PRIMARY KEY, \n" +
            "    CONSTRAINT FK0 FOREIGN KEY (e2) REFERENCES G1 (e2)\n" +
            ") OPTIONS (UPDATABLE 'true');\n" +
            "\n"+
            "CREATE FOREIGN TABLE G3 (\n" +
            "    e1 string, \n" +
            "    e2 integer,\n" +
            "    e3 string[],\n"+
            "    CONSTRAINT PK PRIMARY KEY (e1,e2)\n" +
            ") OPTIONS (UPDATABLE 'true')" +
            "\n" +
            "CREATE FOREIGN TABLE G4 (\n" +
            "    e1 string PRIMARY KEY, \n" +
            "    e2 integer,\n" +
            "    CONSTRAINT FKX FOREIGN KEY (e2) REFERENCES G1(e2)\n" +
            ") OPTIONS (UPDATABLE 'true');" +
            "\n"+
            "CREATE FOREIGN TABLE G5 (\n" +
            "    e1 string OPTIONS (SELECTABLE 'FALSE'), \n" +
            "    e2 integer PRIMARY KEY, \n" +
            "    e3 double\n" +
            ");\n" +            
            "CREATE FOREIGN PROCEDURE getCustomers(" +
            "   IN p2 timestamp, "+
            "   IN p3 decimal"+
            "   ) RETURNS integer;\n" +
            "CREATE FOREIGN TABLE SimpleTable(\n" + 
            "    intkey integer PRIMARY KEY,\n" + 
            "    intnum integer,\n" + 
            "    stringkey varchar(20),\n" + 
            "    stringval varchar(20),\n" + 
            "    booleanval boolean,\n" + 
            "    decimalval decimal(20, 10),\n" + 
            "    timeval time,\n" + 
            "    dateval date,\n" + 
            "    timestampval timestamp,\n" + 
            "    clobval clob);"+
            "CREATE FOREIGN TABLE Customers("+
            "   id integer PRIMARY KEY, " +
            "   name varchar(10));\n" + 
            "CREATE FOREIGN TABLE Orders("+
            "   id integer PRIMARY KEY, " +
            "   customerid integer, " +
            "   place varchar(10), "+
            "   FOREIGN KEY (customerid) REFERENCES Customers(id));" +
            "CREATE FOREIGN TABLE EmployeeEntity (\n" + 
            "  EmployeeID integer primary key,\n" + 
            "  Delegate integer," +
            "  DeputyDelegate integer," +
            " CONSTRAINT delegates FOREIGN KEY (Delegate) REFERENCES EmployeeEntity(EmployeeID)," +
            " CONSTRAINT deputyDelegates FOREIGN KEY (DeputyDelegate) REFERENCES EmployeeEntity(EmployeeID)"
            + ");\n";
    
    static class QueryState extends BaseState {
        List<SQLParameter> parameters;
        ArgumentCaptor<Query> arg1;
        ArgumentCaptor<EntityCollectionResponse> arg6;        
    }
    
    static class UpdateState extends BaseState {
        ArgumentCaptor<Command> commandArg;
    }
    
    static class BaseState {
        Client client;
        String response;
		Integer status;
    }
    
    static class ProcedureState extends BaseState {
        String arg1;
        List arg2;
        Integer arg3;
        OperationResponse arg4;        
    }    
    
    private QueryState setup(String url) throws Exception {    
        return (QueryState)setup(DEFAULT_DDL, url, "GET", null, new QueryState());
    }
       
    private BaseState setup(String ddl, String url, String method, ServletInputStream stream, BaseState state) throws Exception {
        Client client = Mockito.mock(Client.class);

        DDLHolder model = new DDLHolder("PM1", ddl);
        TransformationMetadata metadata = RealMetadataFactory.fromDDL("vdb", model);
        MetadataStore store = metadata.getMetadataStore();
        //TranslationUtility utility = new TranslationUtility(metadata);

        OData odata = OData4Impl.newInstance();
        org.teiid.metadata.Schema teiidSchema = store.getSchema("PM1");
        CsdlSchema schema = ODataSchemaBuilder.buildMetadata("vdb", teiidSchema);
        TeiidEdmProvider edmProvider = new TeiidEdmProvider("baseuri", schema, "x");
        
        ServiceMetadata serviceMetadata = odata.createServiceMetadata(edmProvider, Collections.<EdmxReference> emptyList());
        ODataHttpHandler handler = odata.createHandler(serviceMetadata);

        Hashtable<String, String> headers = new Hashtable<String, String>();
        headers.put("Content-Type", "application/json");

        Mockito.stub(client.getMetadataStore()).toReturn(store);        
        Mockito.stub(client.executeCount(Mockito.any(Query.class), Mockito.anyListOf(SQLParameter.class))).toReturn(new CountResponse() {
            @Override
            public int getCount() {
                return 10;
            }
        });

        HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
        Mockito.stub(request.getHeaderNames()).toReturn(headers.keys());
        Mockito.stub(request.getHeaders("Content-Type")).toReturn(headers.elements());
        Mockito.stub(request.getMethod()).toReturn(method);
        
        String requestURL = url;
        String queryString = "";
        int idx = url.indexOf("?");
        if (idx != -1) {
            requestURL = url.substring(0, idx);
            queryString = url.substring(idx+1);
        }
        Mockito.stub(request.getRequestURL()).toReturn(new StringBuffer(requestURL));
        Mockito.stub(request.getQueryString()).toReturn(queryString);
        Mockito.stub(request.getServletPath()).toReturn("");
        Mockito.stub(request.getContextPath()).toReturn("/odata4/vdb/PM1");
        Mockito.stub(request.getInputStream()).toReturn(stream);

        final StringBuffer sb = new StringBuffer();
        ServletOutputStream out = new ServletOutputStream() {
            @Override
            public void write(int b) throws IOException {
                sb.append((char)b);
            }
            @Override
            public boolean isReady() {
                return true;
            }
            @Override
            public void setWriteListener(WriteListener writeListener) {
            }
        };
        HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
        Mockito.stub(response.getOutputStream()).toReturn(out);

        try {
            TeiidServiceHandler tsh = new TeiidServiceHandler("PM1");
            tsh.setPrepared(false);
            TeiidServiceHandler.setClient(client);
            handler.register(tsh);
            handler.process(request, response);
        } finally {
            TeiidServiceHandler.setClient(null);
        }
        ArgumentCaptor<Integer> statusCapture = ArgumentCaptor.forClass(Integer.class);
        Mockito.verify(response).setStatus(statusCapture.capture());
        state.client = client;        
        state.response = sb.toString();
        state.status = statusCapture.getValue();
        return state;
    }

    public QueryState helpTest(String url, String sqlExpected) throws Exception {
        return helpTest(url, sqlExpected, null, null, false);
    }
    public QueryState helpTest(String url, String sqlExpected, Integer skip, Integer top, Boolean count) throws Exception {
        QueryState state = setup(url);
        Client client = state.client;
        
        ArgumentCaptor<Query> arg1 = ArgumentCaptor.forClass(Query.class);
        ArgumentCaptor<EntityCollectionResponse> arg6 = ArgumentCaptor.forClass(EntityCollectionResponse.class);
        List<SQLParameter> parameters = new ArrayList<SQLParameter>();
        
        if (sqlExpected != null) {
            Query actualCommand = (Query) QueryParser.getQueryParser().parseCommand(sqlExpected, new ParseInfo());
            Mockito.verify(client).executeSQL(arg1.capture(),
                    Mockito.eq(parameters), Mockito.eq(count),
                    (Integer) Mockito.eq(skip), (Integer) Mockito.eq(top),
                    (String) Mockito.eq(null), Mockito.anyInt(),
                    arg6.capture());
            Assert.assertEquals(actualCommand.toString(), arg1.getValue().toString());
        }
        
        state.parameters = parameters;
        state.arg1 = arg1;
        state.arg6 = arg6;
        
        return state;
    }

    @Test
    public void testSimpleEntitySet() throws Exception {
        helpTest("/odata4/vdb/PM1/G1", "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 ORDER BY g0.e2");
    }

    @Test
    public void testSimpleEntitySetWithKey() throws Exception {
        helpTest("/odata4/vdb/PM1/G1(1)", "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 = 1 ORDER BY g0.e2");
    }
    
    @Test
    public void testSimpleEntitySetWithKeyWithAlias() throws Exception {
        helpTest("/odata4/vdb/PM1/G1(@e2)?@e2=1", "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 = 1 ORDER BY g0.e2");
    }    

    @Test
    public void testSimpleEntityID() throws Exception {
        helpTest("/odata4/vdb/PM1/$entity?$id="+Encoder.encode("http://host/odata4/vdb/PM1/G1(1)"), 
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 = 1 ORDER BY g0.e2");
    }

    @Test
    public void testEntitySet$Select() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$select=e1", "SELECT g0.e1, g0.e2 FROM PM1.G1 AS g0 ORDER BY g0.e2");
    }

    @Test
    public void testEntitySet$SelectBad() throws Exception {
        QueryState state = helpTest("/odata4/vdb/PM1/G1?$select=e1,x", null);
        Assert.assertEquals("{\"error\":{\"code\":null,\"message\":\"The property 'x', used in a query expression, is not defined in type 'G1'.\"}}", state.response);
    }

    @Test
    public void testEntitySet$OrderBy() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$orderby=e1 desc,e2",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 ORDER BY g0.e1 DESC, g0.e2");
        helpTest("/odata4/vdb/PM1/G1?$orderby=e1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 ORDER BY g0.e1");
    }

    @Test
    public void testEntitySet$OrderByNotIn$Select() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$orderby=e2&$select=e1",
                "SELECT g0.e1, g0.e2 FROM PM1.G1 AS g0 ORDER BY g0.e2");
    }

    @Test
    public void testEntitySet$filter() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq '1'",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e1 = '1' ORDER BY g0.e2");
    }

    @Test
    public void test_$it_OverPrimitiveProperty() throws Exception {
        helpTest("/odata4/vdb/PM1/G3(e1='e1',e2=2)/e3?$filter=endswith($it,'.com')",
                "SELECT g0.e1, g0.e2, ARRAY_AGG(CAST(g1.col AS string) ORDER BY g0.e1, g0.e2) AS e3 FROM PM1.G3 AS g0, LATERAL(EXEC arrayiterate(g0.e3)) AS g1 WHERE (g0.e1 = 'e1') AND (g0.e2 = 2) AND (ENDSWITH('.com', CAST(g1.col AS string)) = TRUE) GROUP BY g0.e1, g0.e2");
    }
        
    @Test
    public void test$CountIsTrueEntitySet() throws Exception {
        String expected = "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 ORDER BY g0.e2";
        QueryState state = helpTest("/odata4/vdb/PM1/G1?$count=true", null);

        Mockito.verify(state.client).executeSQL(state.arg1.capture(),
                Mockito.eq(state.parameters), Mockito.eq(true), (Integer)Mockito.eq(null),
                (Integer)Mockito.eq(null), 
                (String) Mockito.eq(null), Mockito.anyInt(),
                state.arg6.capture());
        Assert.assertEquals(expected, state.arg1.getValue().toString());
    }

    @Test
    public void test$CountInEntitySet() throws Exception {
        String expected = "SELECT COUNT(*) FROM PM1.G1 AS g0";
        QueryState state = helpTest("/odata4/vdb/PM1/G1/$count", null);

        Mockito.verify(state.client).executeCount(state.arg1.capture(), Mockito.eq(state.parameters));
        Assert.assertEquals(expected, state.arg1.getValue().toString());
    }
    
    @Test
    public void test$CountAnd$Filter() throws Exception {
        String expected = "SELECT COUNT(*) FROM PM1.G1 AS g0 WHERE g0.e3 < 10";
        QueryState state = helpTest("/odata4/vdb/PM1/G1/$count?$filter="+Encoder.encode("e3 lt 10"), null);

        Mockito.verify(state.client).executeCount(state.arg1.capture(), Mockito.eq(state.parameters));
        Assert.assertEquals(expected, state.arg1.getValue().toString());
        
    }    

    @Test
    public void test$CountInNavigation() throws Exception {
        String expected = "SELECT COUNT(*) FROM PM1.G1 AS g0 INNER JOIN PM1.G4 AS g1 ON g0.e2 = g1.e2 WHERE g0.e2 = 1";
        QueryState state = helpTest("/odata4/vdb/PM1/G1(1)/G4_FKX/$count", null);
        Mockito.verify(state.client).executeCount(state.arg1.capture(), Mockito.eq(state.parameters));
        Assert.assertEquals(expected, state.arg1.getValue().toString());
    }
    
    @Test
    public void test$CountIn$FilterWithCount() throws Exception {
        String expected = "SELECT g0.e1, g0.e2 FROM PM1.G1 AS g0 WHERE "
                + "(SELECT COUNT(*) FROM PM1.G4 AS g1 WHERE g1.e2 = g0.e2) = 2 ORDER BY g0.e2";
        helpTest("/odata4/vdb/PM1/G1?$filter=G4_FKX/$count eq 2&$select=e1", expected);
    }
    
    @Test
    public void test$CountIn$FilterOnExpression() throws Exception {
        String expected = "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 "
                + "WHERE (SELECT COUNT(*) FROM PM1.G4 AS g1 WHERE g1.e2 = g0.e2) = 2 "
                + "ORDER BY g0.e2";
        helpTest("/odata4/vdb/PM1/G1?$filter="+Encoder.encode("G4_FKX/$count eq 2"), expected);
    }
    
    @Test
    public void test$CountIn$orderby() throws Exception {
        String expected = "SELECT g0.e1, g0.e2, g0.e3, (SELECT COUNT(*) FROM PM1.G4 AS g1 WHERE g1.e2 = g0.e2) "
                + "AS \"_orderByAlias\" FROM PM1.G1 AS g0 "
                + "ORDER BY \"_orderByAlias\"";
        helpTest("/odata4/vdb/PM1/G1?$orderby=G4_FKX/$count", expected);
    }    
        
    @Test
    public void test$CountIn$OrderBy() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$orderby=G4_FKX/$count", 
                "SELECT g0.e1, g0.e2, g0.e3, "
                + "(SELECT COUNT(*) FROM PM1.G4 AS g1 WHERE g1.e2 = g0.e2) AS \"_orderByAlias\" "
                + "FROM PM1.G1 AS g0 "
                + "ORDER BY \"_orderByAlias\"");
    }

    @Test
    public void testCanonicalQuery() throws Exception {
        // this should be same as "/odata4/vdb/PM1/G1('1')"
        helpTest("/odata4/vdb/PM1/G2(1)/FK0", 
                "SELECT g1.e1, g1.e2, g1.e3 FROM PM1.G2 AS g0 INNER JOIN PM1.G1 AS g1 "
                + "ON g1.e2 = g0.e2 WHERE g0.e2 = 1 ORDER BY g1.e2");
        
        helpTest("/odata4/vdb/PM1/G1(1)/G4_FKX('1')", 
                "SELECT g1.e1, g1.e2 FROM PM1.G4 AS g1 WHERE g1.e1 = '1' ORDER BY g1.e1");
    }

    @Test
    public void testAlias() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e2 eq @p1&@p1=1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 = 1 ORDER BY g0.e2");
    }
    
    @Test
    public void testAlias2InvalidType() throws Exception {
        QueryState state = helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq @p1&@p1=1", null);
        assertEquals(Integer.valueOf(400), state.status);
    }    

    @Test
    public void testAlias3() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq @p1&@p1='1'",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e1 = '1' ORDER BY g0.e2");
    }
    
    @Test
    public void testNoAlias() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq @p1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e1 is NULL ORDER BY g0.e2");
    }    
    
    @Test
    public void testNoAlias2() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e2 eq @p1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 is NULL ORDER BY g0.e2");
    }    
    
    @Test
    public void testAliasWithExpression() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq @p1&@p1=$root/G2(1)/e1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e1 = (SELECT g1.e1 FROM PM1.G2 AS g1 WHERE g1.e2 = 1) ORDER BY g0.e2");
    }    
    
    @Test
    public void testMultiEntitykey() throws Exception {
        helpTest("/odata4/vdb/PM1/G3(e1='1',e2=2)",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G3 AS g0 WHERE g0.e1 = '1' AND g0.e2 = 2 ORDER BY g0.e1, g0.e2");
    }

    private void te(String in, String expected) throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter="+in,
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE "+expected+" ORDER BY g0.e2");
    }

    private void tea(String in, String expected) throws Exception {
        helpTest("/odata4/vdb/PM1/SimpleTable?$select=intkey&$filter="+in,
                "SELECT g0.intkey FROM PM1.SimpleTable AS g0 WHERE "+expected+" ORDER BY g0.intkey");
    }    
    
    @Test
    public void testAnd() throws Exception {
        te("e1 eq '1' and e1 eq '2'", "(g0.e1 = '1') AND (g0.e1 = '2')");
        te("(e2 add 4) eq 3", "(g0.e2 + 4) = 3");
        tea("((intkey add intnum) sub decimalval) mod intkey eq 0", 
                "MOD(((g0.intkey + g0.intnum) - g0.decimalval), g0.intkey) = 0");
    }

    @Test
    public void testEq() throws Exception {
        te("e2 eq 1", "g0.e2 = 1");
        te("e3 eq 4.5", "g0.e3 = 4.5");
        te("e3 eq -4.5", "g0.e3 = -4.5");
        te("e1 eq null", "g0.e1 IS NULL");
        te("e1 eq 'foo'", "g0.e1 = 'foo'");
        tea("booleanval eq true", "g0.booleanval = TRUE");
        tea("booleanval eq false", "g0.booleanval = FALSE");
        
        Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        gmt.clear();
        gmt.set(Calendar.HOUR, 8);
        gmt.set(Calendar.MINUTE, 20);
        gmt.set(Calendar.SECOND, 02);
        
        SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss");
        sdf.setTimeZone(TimeZone.getDefault());
        String time = sdf.format(new Time(gmt.getTimeInMillis()));
        String expected = "g0.timeval = {t'"+time+"'}";
        tea("timeval eq 08:20:02", expected);
        
        
        gmt = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        gmt.clear();
        gmt.set(Calendar.YEAR, 2008);
        gmt.set(Calendar.MONTH, Calendar.OCTOBER);
        gmt.set(Calendar.DAY_OF_MONTH, 12);
        gmt.set(Calendar.HOUR, 8);
        gmt.set(Calendar.MINUTE, 20);
        gmt.set(Calendar.SECOND, 02);
        
        sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
        sdf.setTimeZone(TimeZone.getDefault());
        time = sdf.format(new Time(gmt.getTimeInMillis()));
        expected = "g0.timestampval = {ts '"+time+"'}";        
        tea("timestampval eq 2008-10-12T08:20:02Z", expected);
        
        gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT-04:00"));
        gmt.clear();
        gmt.set(Calendar.YEAR, 2008);
        gmt.set(Calendar.MONTH, Calendar.OCTOBER);
        gmt.set(Calendar.DAY_OF_MONTH, 12);
        gmt.set(Calendar.HOUR, 8);
        gmt.set(Calendar.MINUTE, 20);
        gmt.set(Calendar.SECOND, 02);
        gmt.set(Calendar.MILLISECOND, 235);
        
        sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
        sdf.setTimeZone(TimeZone.getDefault());
        time = sdf.format(new Time(gmt.getTimeInMillis()));
        expected = "g0.timestampval = {ts '"+time+"'}";        
        tea("timestampval eq 2008-10-12T08:20:02.235-04:00", expected);        
    }

    @Test
    public void testCast() throws Exception {
        te("e1 eq cast('foo',Edm.String)", "g0.e1 = CONVERT('foo', string)");
        te("e2 eq cast('32',Edm.Int32)", "g0.e2 = CONVERT('32', integer)");
    }

    @Test
    public void testConcat() throws Exception {
        te("e1 eq concat('foo','bar')", "g0.e1 = CONCAT2('foo', 'bar')");
    }

    @Test
    public void testEndsWith() throws Exception {
        te("endswith(e1,'foo')", "ENDSWITH('foo', g0.e1) = TRUE");
    }

    @Test
    public void testIndexOf() throws Exception {
        te("indexof(e1,'foo') eq 1", "(LOCATE('foo', g0.e1) - 1) = 1");
    }
    
    @Test
    public void testSubString() throws Exception {
        te("substring(e1,1) eq 'x'", "SUBSTRING(g0.e1, CASE 1 WHEN 1 < 0 THEN 1 ELSE (1 + 1) END) = 'x'");
        te("substring(e1,-1) eq 'x'", "SUBSTRING(g0.e1, CASE -1 WHEN -1 < 0 THEN -1 ELSE (-1 + 1) END) = 'x'");
        te("substring(e1,1,2) eq 'x'", "SUBSTRING(g0.e1, CASE 1 WHEN 1 < 0 THEN 1 ELSE (1 + 1) END, 2) = 'x'");
    }    

    @Test
    public void testLength() throws Exception {
        te("length(e1) eq 2", "LENGTH(g0.e1) = 2");
    }

    @Test
    public void testOperator() throws Exception {
        tea("not (booleanval)", "NOT (g0.booleanval)");
        tea("(intkey mul intkey) gt 5", "(g0.intkey * g0.intkey) > 5");
        tea("(intkey div 5) gt 5", "(g0.intkey / 5) > 5");
        tea("(intkey add 5) lt 5", "(g0.intkey + 5) < 5");
        tea("(intkey sub 5) ne 0", "(g0.intkey - 5) != 0");
        tea("(intkey mod 5) eq 0", "MOD(g0.intkey, 5) = 0");
        tea("(intkey mul -1) eq 0", "(g0.intkey * -1) = 0");
    }

    @Test
    public void testComparisions() throws Exception {
        tea("intkey gt intnum", "g0.intkey > g0.intnum");
        tea("intkey lt intnum", "g0.intkey < g0.intnum");
        tea("intkey ge intnum", "g0.intkey >= g0.intnum");
        tea("intkey le intnum", "g0.intkey <= g0.intnum");
        tea("intkey eq intnum", "g0.intkey = g0.intnum");
        tea("intkey ne intnum", "g0.intkey <> g0.intnum");
        te("e1 eq null", "g0.e1 IS NULL");
        te("e1 ne null", "g0.e1 IS NOT NULL");
    }

    @Test
    public void testStringMethods() throws Exception {
        //te("replace(x, y, z)", "REPLACE(x, y, z)");

        te("substring('foo',1) eq 'f'", "SUBSTRING('foo', CASE 1 WHEN 1 < 0 THEN 1 ELSE (1 + 1) END) = 'f'");
        te("substring('foo',1,2) eq 'f'", "SUBSTRING('foo', CASE 1 WHEN 1 < 0 THEN 1 ELSE (1 + 1) END, 2) = 'f'");
        te("tolower(e1) eq 'foo'", "LCASE(g0.e1) = 'foo'");
        te("toupper(e1) eq 'FOO'", "UCASE(g0.e1) = 'FOO'");
        te("trim('x') eq e1", "TRIM(' ' FROM 'x') = g0.e1");
        te("trim(e1) ne 'foo' and toupper(e1) eq 'bar'", "(TRIM(' ' FROM g0.e1) <> 'foo') AND (UCASE(g0.e1) = 'bar')");
        te("contains(e1,'foo')", "LOCATE('foo', g0.e1, 1) >= 1");
    }


    @Test
    public void testStartsWith() throws Exception {
        te("startswith(e1,'foo')", "LOCATE('foo', g0.e1, 1) = 1");
    }

    @Test
    public void testTimeMethods() throws Exception {
        tea("year(dateval) eq 2000", "YEAR(g0.dateval) = 2000");
        
        Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmt.clear();
        gmt.set(Calendar.YEAR, 2008);
        gmt.set(Calendar.MONTH, Calendar.OCTOBER);
        gmt.set(Calendar.DAY_OF_MONTH, 13);
        gmt.set(Calendar.HOUR, 8);
        gmt.set(Calendar.MINUTE, 20);
        gmt.set(Calendar.SECOND, 2);
        
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
        sdf.setTimeZone(TimeZone.getDefault());
        String time = sdf.format(new Time(gmt.getTimeInMillis()));
        String expected = "YEAR({ts'"+time+"'}) = 2008"; 
        
        te("year(2008-10-13T08:20:02Z) eq 2008", expected);
        tea("month(dateval) gt 1", "MONTH(g0.dateval) > 1");
        tea("day(dateval) ne 1", "DAYOFMONTH(g0.dateval) != 1");
        tea("hour(timeval) eq 12", "HOUR(g0.timeval) = 12");
        tea("minute(timeval) lt 5", "MINUTE(g0.timeval) < 5");
        tea("second(timeval) eq 3", "SECOND(g0.timeval) = 3");
    }

    @Test
    public void testRoundMethods() throws Exception {
        tea("round(decimalval) eq 0", "ROUND(g0.decimalval, 0) = 0");
        tea("floor(decimalval) eq 0", "FLOOR(g0.decimalval) = 0");
        tea("ceiling(decimalval) eq 1", "CEILING(g0.decimalval) = 1");
    }

    @Test
    public void testNavigationQuery() throws Exception {
        helpTest(
                "/odata4/vdb/PM1/G2(1)/FK0",
                "SELECT g1.e1, g1.e2, g1.e3 FROM PM1.G2 as g0 INNER JOIN PM1.G1 as g1 "
                + "ON g1.e2 = g0.e2 WHERE g0.e2 = 1 ORDER BY g1.e2");
    }

    @Test
    public void testNavigationQuery$Select() throws Exception {
        helpTest(
                "/odata4/vdb/PM1/G2(1)/FK0?$select=e1",
                "SELECT g1.e1, g1.e2 FROM PM1.G2 as g0 INNER JOIN PM1.G1 as g1 "
                + "ON g1.e2 = g0.e2 WHERE g0.e2 = 1 ORDER BY g1.e2");
    }

    @Test
    public void test$refCollection() throws Exception {
        helpTest("/odata4/vdb/PM1/G1(0)/G4_FKX/$ref",
                "SELECT g1.e1 FROM PM1.G1 AS g0 INNER JOIN PM1.G4 AS g1 ON g0.e2 = g1.e2 WHERE g0.e2 = 0 ORDER BY g1.e1");
    }

    @Test
    public void test$refEntity() throws Exception {
        helpTest(
                "/odata4/vdb/PM1/G2(1)/FK0/$ref",
                "SELECT g1.e2 FROM PM1.G2 as g0 INNER JOIN PM1.G1 as g1 "
                + "ON g1.e2 = g0.e2 WHERE g0.e2 = 1 ORDER BY g1.e2");
    }

    @Test
    public void testAddressingProperty() throws Exception {
        helpTest("/odata4/vdb/PM1/G2(1)/e1","SELECT g0.e1, g0.e2 FROM PM1.G2 AS g0 WHERE g0.e2 = 1 ORDER BY g0.e2");
    }

    @Test
    public void testAddressingPropertyValue() throws Exception {
        helpTest("/odata4/vdb/PM1/G1(1)/e1/$value","SELECT g0.e1, g0.e2 FROM PM1.G1 AS g0 WHERE g0.e2 = 1 ORDER BY g0.e2");
    }

    @Test
    public void test$filter() throws Exception {
        helpTest("/odata4/vdb/PM1/G2?$filter=e1 eq '1'",
                "SELECT g0.e1, g0.e2 FROM PM1.G2 AS g0 WHERE g0.e1 = '1' ORDER BY g0.e2");
        helpTest("/odata4/vdb/PM1/G2?$filter=contains(e1,'foo')",
                "SELECT g0.e1, g0.e2 FROM PM1.G2 AS g0 WHERE LOCATE('foo', g0.e1, 1) >= 1 ORDER BY g0.e2");
        helpTest("/odata4/vdb/PM1/G2?$filter=(4 add 5) mod (4 sub 1) eq 0",
                "SELECT g0.e1, g0.e2 FROM PM1.G2 AS g0 WHERE MOD((4 + 5), (4 - 1)) = 0 ORDER BY g0.e2");
    }
    
    @Test
    public void test$filterWithNavigation() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=G4_FKX/$count lt 2",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 "
                + "WHERE (SELECT COUNT(*) FROM PM1.G4 AS g1 WHERE g1.e2 = g0.e2) < 2 ORDER BY g0.e2");
        
        helpTest("/odata4/vdb/PM1/Customers?$filter=Orders_FK0/$count lt 2",
                "SELECT g0.id, g0.name FROM PM1.Customers AS g0 "
                + "WHERE (SELECT COUNT(*) FROM PM1.Orders AS g1 "
                + "WHERE g1.customerid = g0.id) < 2 ORDER BY g0.id");
    }    
    
    @Test
    public void testInsert() throws Exception {
        String payload = "{\n" +
                "  \"e1\":\"teiid\",\n" +
                "  \"e2\":1,\n" +
                "  \"e3\":2.0" +
                "}";
        helpInsert("/odata4/vdb/PM1/G1",
                "insert into PM1.G1 (e1, e2, e3) values('teiid', 1, 2.0)", 
                new StringServletInputStream(payload), "POST");
    }    
    
    @Test
    public void testUpdate() throws Exception {
        String payload = "{ \"e1\":\"teiid\", \"e3\":3.0}";
        helpInsert("/odata4/vdb/PM1/G1(1)",
                "INSERT INTO PM1.G1 (e1, e3) VALUES ('teiid', 3.0)",  
                new StringServletInputStream(payload),"PATCH");
    }    
    
    @Test
    public void testUpdateProperty() throws Exception {
        String payload = "{\"value\":4.0}";
        helpInsert("/odata4/vdb/PM1/G1(1)/e3",
                "UPDATE PM1.G1 SET e3 = 4.0 WHERE PM1.G1.e2 = 1",  
                new StringServletInputStream(payload),"PUT");
    }
    
    @Test
    public void testDeleteProperty() throws Exception {
        String payload = "{\"value\":4.0}";
        helpInsert("/odata4/vdb/PM1/G1(1)/e3",
                "UPDATE PM1.G1 SET e3 = null WHERE PM1.G1.e2 = 1",  
                new StringServletInputStream(payload),"DELETE");
    }    
    
    @Test
    public void testDelete() throws Exception {
        String payload = "";
        helpInsert("/odata4/vdb/PM1/G1(1)",
                "DELETE FROM PM1.G1 WHERE PM1.G1.e2 = 1",  
                new StringServletInputStream(payload),"DELETE");
    }    

    @Test
    public void testUpdateReference() throws Exception {
        String payload = "{\n" +
                "\"@odata.id\": \"/odata4/vdb/PM1/G1(9)\"\n" +
                "}";            

        helpInsert("/odata4/vdb/PM1/G2(1)/FK0/$ref",
                "UPDATE PM1.G2 SET PM1.G2.e2 = 9 WHERE PM1.G2.e2 = 1",  
                new StringServletInputStream(payload),"PUT");
    }
    
    @Test
    public void testAddReference() throws Exception {
        String payload = "{\n" +
                "\"@odata.id\": \"/odata4/vdb/PM1/G4('9')\"\n" +
                "}";            

        helpInsert("/odata4/vdb/PM1/G1(1)/G4_FKX/$ref",
                "UPDATE PM1.G4 SET e2 = 1 WHERE PM1.G4.e1 = '9'",  
                new StringServletInputStream(payload),"POST");
    }    

    @Test
    public void testDeleteReferenceNonCollection() throws Exception {
        String payload = "";            
        helpInsert("/odata4/vdb/PM1/G2(1)/FK0/$ref",
                "UPDATE PM1.G2 SET PM1.G2.e2 = null WHERE PM1.G2.e2 = 1",  
                new StringServletInputStream(payload),"DELETE");
    }
    
    @Test
    public void testDeleteReferenceCollectionValued() throws Exception {
        String payload = "";            

        String id = "$id=/odata4/vdb/PM1/G4('9')";
        helpInsert("/odata4/vdb/PM1/G1(1)/G4_FKX/$ref?"+id,
                "UPDATE PM1.G4 SET e2 = null WHERE PM1.G4.e1 = '9'",  
                new StringServletInputStream(payload),"DELETE");
    }    
    
    private UpdateState helpInsert(String url, String sqlExpected, 
            StringServletInputStream stream, String method) throws Exception{
        UpdateState state = (UpdateState)setup(DEFAULT_DDL, url, method, stream, new UpdateState());
        
        Client client = state.client;
        
        ArgumentCaptor<Command> arg1 = ArgumentCaptor.forClass(Command.class);
        ArgumentCaptor<List> arg2 = ArgumentCaptor.forClass(List.class);
        
        if (sqlExpected != null) {
            Command actualCommand = (Command) QueryParser.getQueryParser().parseCommand(sqlExpected, new ParseInfo());
            Mockito.verify(client).executeUpdate(arg1.capture(), arg2.capture());
            Assert.assertEquals(actualCommand.toString(), arg1.getValue().toString());
        }
        
        state.commandArg = arg1;
        
        return state;
    }

    static class StringServletInputStream extends ServletInputStream{
        ByteArrayInputStream stream;
        
        public StringServletInputStream(String content) {
            this.stream = new ByteArrayInputStream(content.getBytes());
        }
        @Override
        public int read() throws IOException {
            return this.stream.read();
        }
        @Override
        public boolean isFinished() {
            return false;
        }
        @Override
        public boolean isReady() {
            return true;
        }
        @Override
        public void setReadListener(ReadListener readListener) {
        }
    }
            
    @Test
    public void testNavigationalQuery() throws Exception {
        helpTest("/odata4/vdb/PM1/G2(0)/FK0", "SELECT g1.e1, g1.e2, g1.e3 FROM PM1.G2 AS g0 "
                + "INNER JOIN PM1.G1 AS g1 ON g1.e2 = g0.e2 WHERE g0.e2 = 0 ORDER BY g1.e2");

        //Canonical query
        helpTest("/odata4/vdb/PM1/G1(0)/G4_FKX('0')/e1", "SELECT g1.e1 FROM PM1.G4 AS g1 WHERE g1.e1 = '0' ORDER BY g1.e1");

        helpTest("/odata4/vdb/PM1/G1(0)/G4_FKX", "SELECT g1.e1, g1.e2 FROM PM1.G1 AS g0 "
                + "INNER JOIN PM1.G4 AS g1 ON g0.e2 = g1.e2 WHERE g0.e2 = 0 ORDER BY g1.e1");
    }
    
    @Test
    public void testSelfNavigation() throws Exception {
        helpTest("/odata4/vdb/PM1/EmployeeEntity(3)/deputyDelegates", "SELECT g1.EmployeeID, g1.Delegate, g1.DeputyDelegate "
                + "FROM PM1.EmployeeEntity AS g0 INNER JOIN PM1.EmployeeEntity AS g1 ON g1.EmployeeID = g0.DeputyDelegate WHERE g0.EmployeeID = 3 ORDER BY g1.EmployeeID");

        helpTest("/odata4/vdb/PM1/EmployeeEntity(3)/delegates", "SELECT g1.EmployeeID, g1.Delegate, g1.DeputyDelegate "
                + "FROM PM1.EmployeeEntity AS g0 INNER JOIN PM1.EmployeeEntity AS g1 ON g1.EmployeeID = g0.Delegate WHERE g0.EmployeeID = 3 ORDER BY g1.EmployeeID");
    }
    
    @Test
    public void test$RootOverPath() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq $root/G1(1)/e1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e1 = (SELECT g1.e1 FROM PM1.G1 AS g1 WHERE g1.e2 = 1) ORDER BY g0.e2");
    } 
    
    @Test
    public void test$RootOverPath1() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=$root/G1(1)/e1 eq e1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE (SELECT g1.e1 FROM PM1.G1 AS g1 WHERE g1.e2 = 1) = g0.e1 ORDER BY g0.e2");
    }
    
    @Test
    public void testAny() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter="+Encoder.encode("G4_FKX/any(ol:ol/e2 gt 10)"), 
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE EXISTS (SELECT 1 FROM PM1.G4 AS ol WHERE (g0.e2 = ol.e2) AND (ol.e2 > 10)) ORDER BY g0.e2");
    }
    
    @Test
    public void testAnyNoLambdaVariable() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter="+Encoder.encode("G4_FKX/any(ol:1 gt 10)"), 
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE 1 > 10 ORDER BY g0.e2");
    }

    @Test
    public void testAll() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter="+Encoder.encode("G4_FKX/all(ol:ol/e2 gt 10)"),
                "SELECT g0.e1, g0.e2, g0.e3 "
                + "FROM PM1.G1 AS g0 WHERE TRUE = ALL (SELECT ol.e2 > 10 FROM PM1.G4 AS ol "
                + "WHERE g0.e2 = ol.e2) ORDER BY g0.e2");        
    }
    
    @Test
    public void testAllRoot() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter="+Encoder.encode("G4_FKX/all(ol:ol/e2 gt length($root/G1(1)/e1))"),
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE TRUE = ALL (SELECT ol.e2 > LENGTH((SELECT g2.e1 FROM PM1.G1 AS g2 WHERE g2.e2 = 1)) FROM PM1.G4 AS ol WHERE g0.e2 = ol.e2) ORDER BY g0.e2");        
    }
    
    @Test
    public void testAllAnd() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter="+Encoder.encode("G4_FKX/all(ol:ol/e2 gt 10 and ol/e1 eq 'b')"),
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE TRUE = ALL (SELECT (ol.e2 > 10) AND (ol.e1 = 'b') FROM PM1.G4 AS ol WHERE g0.e2 = ol.e2) ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandSimple() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX",
                "SELECT g0.e1, g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE g0.e2 = g1.e2) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandSimple_OneToOne() throws Exception {
        helpTest("/odata4/vdb/PM1/G2?$expand=FK0",
                "SELECT g0.e1, g0.e2, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2, g1.e3) ORDER BY g1.e2) FROM PM1.G1 AS g1 WHERE g1.e2 = g0.e2) FROM PM1.G2 AS g0 ORDER BY g0.e2");   
    }    
    
    @Test
    public void testExpandSimpleWithSelect() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX&$select=e3",
                "SELECT g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE g0.e2 = g1.e2) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandWithNestedSelect() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($select=e2)&$select=e3",
                "SELECT g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE g0.e2 = g1.e2) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandFilter() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($filter=e2 eq 100)&$select=e3",
                "SELECT g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE (g0.e2 = g1.e2) AND (g1.e2 = 100)) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandFilter2() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($filter=e2 eq 1)&$select=e3",
                "SELECT g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE (g0.e2 = g1.e2) AND (g1.e2 = 1)) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }    
    
    @Test
    public void testExpandCompoundFilter() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($filter=e2 eq 100)&$select=e3&$filter=e2 ne 100",
                "SELECT g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE (g0.e2 = g1.e2) AND (g1.e2 = 100)) FROM PM1.G1 AS g0 WHERE g0.e2 <> 100 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandFilter$it() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($filter=$it/e2 eq e2)",
                "SELECT g0.e1, g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE (g0.e2 = g1.e2) AND (g0.e2 = g1.e2)) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandFilter$itNested() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($expand=FKX($filter=$it/e3 eq e2))",
                "SELECT g0.e1, g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2, /*+ MJ */ (SELECT ARRAY_AGG((g2.e1, g2.e2, g2.e3) ORDER BY g2.e2) FROM PM1.G1 AS g2 WHERE (g2.e2 = g1.e2) AND (g0.e3 = g2.e2))) ORDER BY g1.e1) FROM PM1.G4 AS g1 WHERE g0.e2 = g1.e2) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testExpandOrderby() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$expand=G4_FKX($orderby=e1 desc)",
                "SELECT g0.e1, g0.e2, g0.e3, /*+ MJ */ (SELECT ARRAY_AGG((g1.e1, g1.e2) ORDER BY g1.e1 DESC) FROM PM1.G4 AS g1 WHERE g0.e2 = g1.e2) FROM PM1.G1 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testSimpleCrossJoin() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)",
                "SELECT g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "ORDER BY g0.e2, g1.e2");        
    } 

    @Test
    public void testSimpleCrossJoinWithSkip() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)?$skip=1",
                "SELECT g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "ORDER BY g0.e2, g1.e2", 1, null, false);        
    } 
    
    @Test
    public void testSimpleCrossJoinWithTop() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)?$top=1",
                "SELECT g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "ORDER BY g0.e2, g1.e2", null, 1, false);        
    }     
    
    @Test
    public void testSimpleCrossJoinWithCount() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)?$count=true",
                "SELECT g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "ORDER BY g0.e2, g1.e2", null, null, true);        
    }    
    
    @Test
    public void testSimpleCrossJoinWithFilter() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)?$filter="+Encoder.encode("G1/e1 eq G2/e1"),
                "SELECT g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "WHERE g0.e1 = g1.e1 "
                + "ORDER BY g0.e2, g1.e2");        
    }
    
    @Test
    public void testSimpleCrossJoinWith$Orderby() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)?$orderby=G1/e1,G2/e2",
                "SELECT g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "ORDER BY g0.e1, g1.e2");        
    }
    
    @Ignore //OLINGO-904
    @Test
    public void testSimpleCrossJoinWith$expand() throws Exception {
        helpTest("/odata4/vdb/PM1/$crossjoin(G1,G2)?$expand=G1",
                "SELECT g0.e1, g0.e3, g0.e2, g1.e2 "
                + "FROM PM1.G1 AS g0, "
                + "PM1.G2 AS g1 "
                + "ORDER BY g0.e2, g1.e2");        
    }    
    
    @Test
    public void testSelectStarWithNonSelectableColumn() throws Exception {
        helpTest("/odata4/vdb/PM1/G5",
                "SELECT g0.e2, g0.e3 FROM PM1.G5 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testSelectStarWithNonSelectableColumn2() throws Exception {
        helpTest("/odata4/vdb/PM1/G5?$select=*",
                "SELECT g0.e2, g0.e3 FROM PM1.G5 AS g0 ORDER BY g0.e2");        
    }
    
    @Test
    public void testSelectStarWithNonSelectableColumn3() throws Exception {
        helpTest("/odata4/vdb/PM1/G5?$filter=e1 eq '1'",
                "SELECT g0.e2, g0.e3 FROM PM1.G5 AS g0 WHERE g0.e1 = '1' ORDER BY g0.e2");        
    }
    
    @Test
    public void testFilterOnNull() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e1 eq null",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e1 IS NULL ORDER BY g0.e2");        
    }     
    
    @Test
    public void testMultipleAirthamatic() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e2 eq 1 add 1 add 1",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 = ((1 + 1) + 1) ORDER BY g0.e2");   
    }     
    
    @Test
    public void testFloor() throws Exception {
        helpTest("/odata4/vdb/PM1/G1?$filter=e2 eq floor(4.2)",
                "SELECT g0.e1, g0.e2, g0.e3 FROM PM1.G1 AS g0 WHERE g0.e2 = FLOOR(4.2) ORDER BY g0.e2");   
    }
    
    @Test
    public void testMonthWithDatetimeoffser() throws Exception {
        helpTest("/odata4/vdb/PM1/SimpleTable?$filter="+Encoder.encode("month(2001-01-01T00:01:01.01Z) eq intkey"),
                "SELECT g0.intkey, g0.intnum, g0.stringkey, g0.stringval, g0.booleanval, g0.decimalval, "
                + "g0.timeval, g0.dateval, g0.timestampval, g0.clobval FROM "
                + "PM1.SimpleTable AS g0 WHERE MONTH({ts'2001-01-01 00:01:01.01'}) = g0.intkey ORDER BY g0.intkey");   
    }
    
    @Test
    public void testDate() throws Exception {
        helpTest("/odata4/vdb/PM1/SimpleTable?$select=intkey&$filter="+Encoder.encode("date(now()) eq dateval"),
                "SELECT g0.intkey FROM PM1.SimpleTable AS g0 "
                + "WHERE CONVERT(NOW(), date) = g0.dateval ORDER BY g0.intkey");   
    }

    @Test
    public void testTime() throws Exception {
        helpTest("/odata4/vdb/PM1/SimpleTable?$select=intkey&$filter="+
                Encoder.encode("timeval gt time(2001-01-01T00:01:01.01Z)"),
                "SELECT g0.intkey FROM PM1.SimpleTable AS g0 "
                + "WHERE g0.timeval > CONVERT({ts'2001-01-01 00:01:01.01'}, time) ORDER BY g0.intkey");   
    }
    
    @BeforeClass public static void oneTimeSetup() {
    	TimestampWithTimezone.resetCalendar(TimeZone.getTimeZone("GMT"));
    }
    
    @AfterClass public static void oneTimeTearDown() {
    	TimestampWithTimezone.resetCalendar(null);
    }
    
}
