You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2017/01/05 12:33:22 UTC

[1/3] cayenne git commit: CAY-2191 and CAY-2188 - expressions in Property class - explicit type in Property class and factory methods - string and math functions from JPA standard with support for specific DBs - factory methods for new function call

Repository: cayenne
Updated Branches:
  refs/heads/master 50d4fbf50 -> 803166c03


http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLengthTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLengthTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLengthTest.java
new file mode 100644
index 0000000..dde6dc0
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLengthTest.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.0
+ */
+public class ASTLengthTest {
+
+    @Test
+    public void evaluateNode() throws Exception {
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTLength lower = new ASTLength(path);
+
+        Artist a = new Artist();
+        a.setArtistName("123456789");
+
+        Object res = lower.evaluateNode(a);
+        assertTrue(res instanceof Integer);
+        assertEquals(9, res);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLocateTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLocateTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLocateTest.java
new file mode 100644
index 0000000..9c4090c
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLocateTest.java
@@ -0,0 +1,53 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0
+ */
+public class ASTLocateTest {
+
+    @Test
+    public void testEvaluateLocate() throws Exception {
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTScalar substr = new ASTScalar("678");
+        ASTScalar offset = new ASTScalar((Integer)5);
+        ASTLocate exp = new ASTLocate(substr, path, offset);
+
+        Artist a = new Artist();
+        a.setArtistName("1267834567890abc");
+
+        Object res = exp.evaluateNode(a);
+        assertTrue(res instanceof Integer);
+        assertEquals(9, res);
+
+        a.setArtistName("abcdefgh");
+        res = exp.evaluateNode(a);
+        assertTrue(res instanceof Integer);
+        assertEquals(0, res);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLowerTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLowerTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLowerTest.java
new file mode 100644
index 0000000..887e5b5
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTLowerTest.java
@@ -0,0 +1,46 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0
+ */
+public class ASTLowerTest {
+
+    @Test
+    public void testEvaluateLower() throws Exception {
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTLower lower = new ASTLower(path);
+
+        Artist a = new Artist();
+        a.setArtistName("AbCdEfG02X");
+
+        Object res = lower.evaluateNode(a);
+        assertTrue(res instanceof String);
+        assertEquals("abcdefg02x", res);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTModTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTModTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTModTest.java
new file mode 100644
index 0000000..fb014f6
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTModTest.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.table_primitives.TablePrimitives;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.0
+ */
+public class ASTModTest {
+
+    @Test
+    public void evaluateNode() throws Exception {
+        ASTObjPath path = new ASTObjPath("intColumn");
+        ASTMod mod = new ASTMod(path, new ASTScalar(3.0));
+
+        TablePrimitives a = new TablePrimitives();
+        a.setIntColumn(10);
+
+        Object res = mod.evaluateNode(a);
+        assertTrue(res instanceof Double);
+        assertEquals(1.0, res);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSqrtTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSqrtTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSqrtTest.java
new file mode 100644
index 0000000..50dbf2f
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSqrtTest.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.table_primitives.TablePrimitives;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.0
+ */
+public class ASTSqrtTest {
+    @Test
+    public void evaluateNode() throws Exception {
+        ASTObjPath path = new ASTObjPath("intColumn");
+        ASTSqrt abs = new ASTSqrt(path);
+
+        TablePrimitives a = new TablePrimitives();
+        a.setIntColumn(9);
+
+        Object res = abs.evaluateNode(a);
+        assertTrue(res instanceof Double);
+        assertEquals(3.0, res);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSubstringTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSubstringTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSubstringTest.java
new file mode 100644
index 0000000..ac51846
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTSubstringTest.java
@@ -0,0 +1,48 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0
+ */
+public class ASTSubstringTest {
+
+    @Test
+    public void testEvaluateSubstring() throws Exception {
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTScalar offset = new ASTScalar((Integer)2);
+        ASTScalar length = new ASTScalar((Integer)8);
+        ASTSubstring exp = new ASTSubstring(path, offset, length);
+
+        Artist a = new Artist();
+        a.setArtistName("1234567890xyz");
+
+        Object res = exp.evaluateNode(a);
+        assertTrue(res instanceof String);
+        assertEquals("34567890", res);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTTrimTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTTrimTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTTrimTest.java
new file mode 100644
index 0000000..6a45370
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTTrimTest.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0
+ */
+public class ASTTrimTest {
+
+    @Test
+    public void testEvaluateTrim() throws Exception {
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTTrim trim = new ASTTrim(path);
+
+        Artist a = new Artist();
+        a.setArtistName("   \t\r\ntestArtist   \t\r\n");
+        Object res = trim.evaluateNode(a);
+        assertTrue(res instanceof String);
+        assertEquals("testArtist", res);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTUpperTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTUpperTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTUpperTest.java
new file mode 100644
index 0000000..6918041
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTUpperTest.java
@@ -0,0 +1,46 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0
+ */
+public class ASTUpperTest {
+
+    @Test
+    public void testEvaluateUpper() throws Exception {
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTUpper upper = new ASTUpper(path);
+
+        Artist a = new Artist();
+        a.setArtistName("abcDEFx981");
+
+        Object res = upper.evaluateNode(a);
+        assertTrue(res instanceof String);
+        assertEquals("ABCDEFX981", res);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
index c10056f..92d54e7 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
@@ -33,12 +33,17 @@ import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.ResultIteratorCallback;
 import org.apache.cayenne.access.DataContext;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.FunctionExpressionFactory;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
@@ -50,7 +55,8 @@ public class ObjectSelect_RunIT extends ServerCase {
 	@Inject
 	private DBHelper dbHelper;
 
-	protected void createArtistsDataSet() throws Exception {
+	@Before
+	public void createArtistsDataSet() throws Exception {
 		TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
 		tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
 
@@ -61,11 +67,15 @@ public class ObjectSelect_RunIT extends ServerCase {
 		}
 	}
 
+	@After
+	public void clearArtistsDataSet() throws Exception {
+		TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
+		tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
+		tArtist.deleteAll();
+	}
+
 	@Test
 	public void test_SelectObjects() throws Exception {
-
-		createArtistsDataSet();
-
 		List<Artist> result = ObjectSelect.query(Artist.class).select(context);
 		assertEquals(20, result.size());
 		assertThat(result.get(0), instanceOf(Artist.class));
@@ -77,8 +87,6 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_Iterate() throws Exception {
-		createArtistsDataSet();
-
 		final int[] count = new int[1];
 		ObjectSelect.query(Artist.class).iterate(context, new ResultIteratorCallback<Artist>() {
 
@@ -94,8 +102,6 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_Iterator() throws Exception {
-		createArtistsDataSet();
-
 		try (ResultIterator<Artist> it = ObjectSelect.query(Artist.class).iterator(context)) {
 			int count = 0;
 
@@ -109,8 +115,6 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_BatchIterator() throws Exception {
-		createArtistsDataSet();
-
 		try (ResultBatchIterator<Artist> it = ObjectSelect.query(Artist.class).batchIterator(context, 5);) {
 			int count = 0;
 
@@ -125,9 +129,6 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_SelectDataRows() throws Exception {
-
-		createArtistsDataSet();
-
 		List<DataRow> result = ObjectSelect.dataRowQuery(Artist.class).select(context);
 		assertEquals(20, result.size());
 		assertThat(result.get(0), instanceOf(DataRow.class));
@@ -139,8 +140,6 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_SelectOne() throws Exception {
-		createArtistsDataSet();
-
 		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist13")).selectOne(context);
 		assertNotNull(a);
 		assertEquals("artist13", a.getArtistName());
@@ -148,20 +147,17 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_SelectOne_NoMatch() throws Exception {
-		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist13")).selectOne(context);
+		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist33")).selectOne(context);
 		assertNull(a);
 	}
 
 	@Test(expected = CayenneRuntimeException.class)
 	public void test_SelectOne_MoreThanOneMatch() throws Exception {
-		createArtistsDataSet();
 		ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.like("artist%")).selectOne(context);
 	}
 
 	@Test
 	public void test_SelectFirst() throws Exception {
-		createArtistsDataSet();
-
 		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist13")).selectFirst(context);
 		assertNotNull(a);
 		assertEquals("artist13", a.getArtistName());
@@ -169,8 +165,6 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_SelectFirstByContext() throws Exception {
-		createArtistsDataSet();
-
 		ObjectSelect<Artist> q = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist13"));
 		Artist a = context.selectFirst(q);
 		assertNotNull(a);
@@ -179,16 +173,34 @@ public class ObjectSelect_RunIT extends ServerCase {
 
 	@Test
 	public void test_SelectFirst_NoMatch() throws Exception {
-		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist13")).selectFirst(context);
+		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("artist33")).selectFirst(context);
 		assertNull(a);
 	}
 
 	@Test
 	public void test_SelectFirst_MoreThanOneMatch() throws Exception {
-		createArtistsDataSet();
+		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.like("artist%"))
+				.orderBy("db:ARTIST_ID").selectFirst(context);
+		assertNotNull(a);
+		assertEquals("artist1", a.getArtistName());
+	}
 
-		Artist a = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.like("artist%")).orderBy("db:ARTIST_ID")
-				.selectFirst(context);
+	@Test
+	public void test_SelectFirst_TrimInWhere() throws Exception {
+		Expression exp = FunctionExpressionFactory.trimExp(Artist.ARTIST_NAME.path());
+		Property<String> trimmedName = Property.create("trimmed", exp, String.class);
+		Artist a = ObjectSelect.query(Artist.class).where(trimmedName.likeIgnoreCase("artist%"))
+				.orderBy("db:ARTIST_ID").selectFirst(context);
+		assertNotNull(a);
+		assertEquals("artist1", a.getArtistName());
+	}
+
+	@Test
+	public void test_SelectFirst_SubstringInWhere() throws Exception {
+		Expression exp = FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.path(), 2, 3);
+		Property<String> substrName = Property.create("substr", exp, String.class);
+		Artist a = ObjectSelect.query(Artist.class).where(substrName.eq("rti"))
+				.orderBy("db:ARTIST_ID").selectFirst(context);
 		assertNotNull(a);
 		assertEquals("artist1", a.getArtistName());
 	}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/main/java/org/apache/cayenne/gen/StringUtils.java
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/main/java/org/apache/cayenne/gen/StringUtils.java b/cayenne-tools/src/main/java/org/apache/cayenne/gen/StringUtils.java
index 5473142..cc27627 100644
--- a/cayenne-tools/src/main/java/org/apache/cayenne/gen/StringUtils.java
+++ b/cayenne-tools/src/main/java/org/apache/cayenne/gen/StringUtils.java
@@ -162,4 +162,28 @@ public class StringUtils {
             return str + "s";
         }
     }
+
+    /**
+     * <p>
+     * Strip generic definition from string
+     * </p>
+     * <p>For example: List&gt;Integer&lt; == List</p>
+     * @since 4.0
+     */
+    public String stripGeneric(String str) {
+        if(str == null) {
+            return null;
+        }
+        int start = str.indexOf('<');
+        if(start == -1) {
+            return str;
+        }
+        int end = str.lastIndexOf('>');
+        if(end == -1) {
+            return str;
+        } else if(end == str.length() - 1) {
+            return str.substring(0, start);
+        }
+        return str.substring(0, start) + str.substring(end+1);
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/main/resources/templates/v1_2/client-superclass.vm
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/main/resources/templates/v1_2/client-superclass.vm b/cayenne-tools/src/main/resources/templates/v1_2/client-superclass.vm
index f7c1c9d..f8c9cbe 100644
--- a/cayenne-tools/src/main/resources/templates/v1_2/client-superclass.vm
+++ b/cayenne-tools/src/main/resources/templates/v1_2/client-superclass.vm
@@ -76,19 +76,21 @@ public abstract class ${superClassName} extends ${baseClassName} {
 #end
 ## Create Properties
 #foreach( $attr in ${object.DeclaredAttributes} )
-    public static final Property<$importUtils.formatJavaType(${attr.Type}, false)> ${stringUtils.capitalizedAsConstant($attr.Name)} = new Property<>("${attr.Name}");
+    #set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($attr.Name)} = Property.create("${attr.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #foreach( $rel in ${object.DeclaredRelationships} )
 #if( $rel.ToMany )
 #if ( ${rel.CollectionType} == "java.util.Map")
     #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($entityUtils.getMapKeyType($rel)), $importUtils.formatJavaType($rel.TargetEntity.ClientClassName)>" )
-    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #else
     #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($rel.TargetEntity.ClientClassName)>" )
-    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #else
-    public static final Property<$importUtils.formatJavaType(${rel.TargetEntity.ClientClassName})> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    #set( $type = "$importUtils.formatJavaType(${rel.TargetEntity.ClassName})" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #end
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/main/resources/templates/v1_2/embeddable-singleclass.vm
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/main/resources/templates/v1_2/embeddable-singleclass.vm b/cayenne-tools/src/main/resources/templates/v1_2/embeddable-singleclass.vm
index b40c9ff..a30dcec 100644
--- a/cayenne-tools/src/main/resources/templates/v1_2/embeddable-singleclass.vm
+++ b/cayenne-tools/src/main/resources/templates/v1_2/embeddable-singleclass.vm
@@ -57,7 +57,8 @@ public abstract class ${subClassName} extends ${baseClassName} {
 #end
 ## Create Properties
 #foreach( $attr in ${object.Attributes} )
-    public static final Property<$importUtils.formatJavaType(${attr.Type}, false)> ${stringUtils.capitalizedAsConstant($attr.Name)} = new Property<$importUtils.formatJavaType(${attr.Type}, false)>("${attr.Name}");
+    #set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($attr.Name)} = Property.create("${attr.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 
     // special properties injected by Cayenne

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/main/resources/templates/v1_2/embeddable-superclass.vm
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/main/resources/templates/v1_2/embeddable-superclass.vm b/cayenne-tools/src/main/resources/templates/v1_2/embeddable-superclass.vm
index f4de6c7..95e39dc 100644
--- a/cayenne-tools/src/main/resources/templates/v1_2/embeddable-superclass.vm
+++ b/cayenne-tools/src/main/resources/templates/v1_2/embeddable-superclass.vm
@@ -63,7 +63,8 @@ public abstract class ${superClassName} extends ${baseClassName} {
 #end
 ## Create Properties
 #foreach( $attr in ${object.Attributes} )
-    public static final Property<$importUtils.formatJavaType(${attr.Type}, false)> ${stringUtils.capitalizedAsConstant($attr.Name)} = new Property<>("${attr.Name}");
+    #set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($attr.Name)} = Property.create("${attr.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 
     // special properties injected by Cayenne

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/main/resources/templates/v1_2/singleclass.vm
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/main/resources/templates/v1_2/singleclass.vm b/cayenne-tools/src/main/resources/templates/v1_2/singleclass.vm
index 2d83cfd..53c6aa7 100644
--- a/cayenne-tools/src/main/resources/templates/v1_2/singleclass.vm
+++ b/cayenne-tools/src/main/resources/templates/v1_2/singleclass.vm
@@ -72,19 +72,21 @@ public#if("true" == "${object.isAbstract()}") abstract#end class ${subClassName}
 
 ## Create Properties
 #foreach( $attr in ${object.DeclaredAttributes} )
-    public static final Property<$importUtils.formatJavaType(${attr.Type}, false)> ${stringUtils.capitalizedAsConstant($attr.Name)} = new Property<>("${attr.Name}");
+    #set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($attr.Name)} = Property.create("${attr.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #foreach( $rel in ${object.DeclaredRelationships} )
 #if( $rel.ToMany )
 #if ( ${rel.CollectionType} == "java.util.Map")
     #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($entityUtils.getMapKeyType($rel)), $importUtils.formatJavaType($rel.TargetEntity.ClassName)>" )
-    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #else
     #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($rel.TargetEntity.ClassName)>" )
-    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #else
-    public static final Property<$importUtils.formatJavaType(${rel.TargetEntity.ClassName})> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    #set( $type = "$importUtils.formatJavaType(${rel.TargetEntity.ClassName})" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #end
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/main/resources/templates/v1_2/superclass.vm
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/main/resources/templates/v1_2/superclass.vm b/cayenne-tools/src/main/resources/templates/v1_2/superclass.vm
index 9934a96..7857189 100644
--- a/cayenne-tools/src/main/resources/templates/v1_2/superclass.vm
+++ b/cayenne-tools/src/main/resources/templates/v1_2/superclass.vm
@@ -77,19 +77,21 @@ public abstract class ${superClassName} extends ${baseClassName} {
 
 ## Create Properties
 #foreach( $attr in ${object.DeclaredAttributes} )
-    public static final Property<$importUtils.formatJavaType(${attr.Type}, false)> ${stringUtils.capitalizedAsConstant($attr.Name)} = new Property<>("${attr.Name}");
+    #set ( $type = "$importUtils.formatJavaType(${attr.Type}, false)")
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($attr.Name)} = Property.create("${attr.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #foreach( $rel in ${object.DeclaredRelationships} )
 #if( $rel.ToMany )
 #if ( ${rel.CollectionType} == "java.util.Map")
     #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($entityUtils.getMapKeyType($rel)), $importUtils.formatJavaType($rel.TargetEntity.ClassName)>" )
-    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #else
     #set( $type = "$importUtils.formatJavaType($rel.CollectionType)<$importUtils.formatJavaType($rel.TargetEntity.ClassName)>" )
-    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #else
-    public static final Property<$importUtils.formatJavaType(${rel.TargetEntity.ClassName})> ${stringUtils.capitalizedAsConstant($rel.Name)} = new Property<>("${rel.Name}");
+    #set( $type = "$importUtils.formatJavaType(${rel.TargetEntity.ClassName})" )
+    public static final Property<$type> ${stringUtils.capitalizedAsConstant($rel.Name)} = Property.create("${rel.Name}", ${stringUtils.stripGeneric($type)}.class);
 #end
 #end
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-tools/src/test/java/org/apache/cayenne/gen/StringUtilsTest.java
----------------------------------------------------------------------
diff --git a/cayenne-tools/src/test/java/org/apache/cayenne/gen/StringUtilsTest.java b/cayenne-tools/src/test/java/org/apache/cayenne/gen/StringUtilsTest.java
index 1fe187e..a11ba2f 100644
--- a/cayenne-tools/src/test/java/org/apache/cayenne/gen/StringUtilsTest.java
+++ b/cayenne-tools/src/test/java/org/apache/cayenne/gen/StringUtilsTest.java
@@ -98,4 +98,13 @@ public class StringUtilsTest {
         assertEquals(expected, stringUtils.capitalizedAsConstant("abCe"));
     }
 
+    @Test
+    public void testStripGeneric() throws Exception {
+        assertEquals("List", stringUtils.stripGeneric("List"));
+        assertEquals("List", stringUtils.stripGeneric("List<Integer>"));
+        assertEquals("List", stringUtils.stripGeneric("List<List<Map<Integer,List<String>>>>"));
+        assertEquals("List123", stringUtils.stripGeneric("List<List<Map<Integer,List<String>>>>123"));
+        assertEquals("List<Integer", stringUtils.stripGeneric("List<Integer"));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/plugins/maven-cayenne-plugin/src/test/java/org/apache/cayenne/tools/CayenneGeneratorMojoTest.java
----------------------------------------------------------------------
diff --git a/plugins/maven-cayenne-plugin/src/test/java/org/apache/cayenne/tools/CayenneGeneratorMojoTest.java b/plugins/maven-cayenne-plugin/src/test/java/org/apache/cayenne/tools/CayenneGeneratorMojoTest.java
index bd8068f..734e93c 100644
--- a/plugins/maven-cayenne-plugin/src/test/java/org/apache/cayenne/tools/CayenneGeneratorMojoTest.java
+++ b/plugins/maven-cayenne-plugin/src/test/java/org/apache/cayenne/tools/CayenneGeneratorMojoTest.java
@@ -70,7 +70,7 @@ public class CayenneGeneratorMojoTest extends AbstractMojoTestCase {
         assertFalse(excludedEntity.exists());
 
         String content = FileUtils.readFileToString(superTestEntity);
-        assertTrue(content.contains("public static final Property<List<TestRelEntity>> ADDITIONAL_REL = new Property<>(\"additionalRel\");"));
+        assertTrue(content.contains("public static final Property<List<TestRelEntity>> ADDITIONAL_REL = Property.create(\"additionalRel\", List.class);"));
         assertTrue(content.contains("public void addToAdditionalRel(TestRelEntity obj)"));
         assertTrue(content.contains("public void removeFromAdditionalRel(TestRelEntity obj)"));
 


[3/3] cayenne git commit: CAY-2191 and CAY-2188 - expressions in Property class - explicit type in Property class and factory methods - string and math functions from JPA standard with support for specific DBs - factory methods for new function call

Posted by nt...@apache.org.
CAY-2191 and CAY-2188
 - expressions in Property class
 - explicit type in Property class and factory methods
 - string and math functions from JPA standard with support for specific DBs
 - factory methods for new function call expressions in FunctionExpressionFactory class

Limitations:
 - no parser for new expression
 - usage is limited for where clause in select queries


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/803166c0
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/803166c0
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/803166c0

Branch: refs/heads/master
Commit: 803166c03395194975d9ac32f0e9c8c634dfde74
Parents: 50d4fbf
Author: Nikita Timofeev <st...@gmail.com>
Authored: Thu Jan 5 15:32:28 2017 +0300
Committer: Nikita Timofeev <st...@gmail.com>
Committed: Thu Jan 5 15:32:28 2017 +0300

----------------------------------------------------------------------
 .../select/DataObjectMatchTranslator.java       |   2 +-
 .../translator/select/QualifierTranslator.java  | 131 ++++++--
 .../cayenne/dba/db2/DB2QualifierTranslator.java |  35 +++
 .../dba/derby/DerbyQualifierTranslator.java     |  65 +++-
 .../firebird/FirebirdQualifierTranslator.java   |  70 +++++
 .../cayenne/dba/frontbase/FrontBaseAdapter.java |  10 +
 .../frontbase/FrontBaseQualifierTranslator.java |  91 ++++++
 .../cayenne/dba/ingres/IngresAdapter.java       |   2 +-
 .../dba/ingres/IngresQualifierTranslator.java   |  59 ++++
 .../dba/oracle/OracleQualifierTranslator.java   |  68 ++++
 .../postgres/PostgresQualifierTranslator.java   |  81 +++--
 .../cayenne/dba/sqlite/SQLiteAdapter.java       |  10 +
 .../dba/sqlite/SQLiteQualifierTranslator.java   | 102 ++++++
 .../SQLServerTrimmingQualifierTranslator.java   |  45 +++
 .../cayenne/dba/sybase/SybaseAdapter.java       |  10 +
 .../dba/sybase/SybaseQualifierTranslator.java   |  78 +++++
 .../java/org/apache/cayenne/exp/Expression.java |   7 +-
 .../apache/cayenne/exp/ExpressionFactory.java   | 312 +++++++++++++++++--
 .../cayenne/exp/FunctionExpressionFactory.java  | 288 +++++++++++++++++
 .../java/org/apache/cayenne/exp/Property.java   | 181 ++++++++---
 .../org/apache/cayenne/exp/parser/ASTAbs.java   |  48 +++
 .../apache/cayenne/exp/parser/ASTBetween.java   |   2 +-
 .../apache/cayenne/exp/parser/ASTConcat.java    |  51 +++
 .../org/apache/cayenne/exp/parser/ASTEqual.java |   2 +-
 .../cayenne/exp/parser/ASTFunctionCall.java     |  64 ++++
 .../apache/cayenne/exp/parser/ASTGreater.java   |   2 +-
 .../cayenne/exp/parser/ASTGreaterOrEqual.java   |   2 +-
 .../org/apache/cayenne/exp/parser/ASTIn.java    |   2 +-
 .../apache/cayenne/exp/parser/ASTLength.java    |  51 +++
 .../org/apache/cayenne/exp/parser/ASTLess.java  |   2 +-
 .../cayenne/exp/parser/ASTLessOrEqual.java      |   2 +-
 .../org/apache/cayenne/exp/parser/ASTLike.java  |   4 +-
 .../cayenne/exp/parser/ASTLikeIgnoreCase.java   |   4 +-
 .../apache/cayenne/exp/parser/ASTLocate.java    |  64 ++++
 .../org/apache/cayenne/exp/parser/ASTLower.java |  52 ++++
 .../org/apache/cayenne/exp/parser/ASTMod.java   |  52 ++++
 .../cayenne/exp/parser/ASTNotBetween.java       |   2 +-
 .../apache/cayenne/exp/parser/ASTNotEqual.java  |   2 +-
 .../org/apache/cayenne/exp/parser/ASTNotIn.java |   2 +-
 .../apache/cayenne/exp/parser/ASTNotLike.java   |   4 +-
 .../exp/parser/ASTNotLikeIgnoreCase.java        |   4 +-
 .../org/apache/cayenne/exp/parser/ASTSqrt.java  |  48 +++
 .../apache/cayenne/exp/parser/ASTSubstring.java |  64 ++++
 .../org/apache/cayenne/exp/parser/ASTTrim.java  |  62 ++++
 .../org/apache/cayenne/exp/parser/ASTUpper.java |  51 +++
 .../apache/cayenne/exp/parser/SimpleNode.java   |   6 +-
 .../org/apache/cayenne/util/ConversionUtil.java |  79 +++--
 .../select/QualifierTranslatorIT.java           |  20 ++
 .../exp/FunctionExpressionFactoryTest.java      | 195 ++++++++++++
 .../org/apache/cayenne/exp/PropertyTest.java    |   9 +
 .../apache/cayenne/exp/parser/ASTAbsTest.java   |  45 +++
 .../cayenne/exp/parser/ASTConcatTest.java       |  50 +++
 .../exp/parser/ASTFunctionCallMathIT.java       |  83 +++++
 .../exp/parser/ASTFunctionCallStringIT.java     | 155 +++++++++
 .../cayenne/exp/parser/ASTLengthTest.java       |  45 +++
 .../cayenne/exp/parser/ASTLocateTest.java       |  53 ++++
 .../apache/cayenne/exp/parser/ASTLowerTest.java |  46 +++
 .../apache/cayenne/exp/parser/ASTModTest.java   |  45 +++
 .../apache/cayenne/exp/parser/ASTSqrtTest.java  |  44 +++
 .../cayenne/exp/parser/ASTSubstringTest.java    |  48 +++
 .../apache/cayenne/exp/parser/ASTTrimTest.java  |  45 +++
 .../apache/cayenne/exp/parser/ASTUpperTest.java |  46 +++
 .../cayenne/query/ObjectSelect_RunIT.java       |  62 ++--
 .../org/apache/cayenne/gen/StringUtils.java     |  24 ++
 .../templates/v1_2/client-superclass.vm         |  10 +-
 .../templates/v1_2/embeddable-singleclass.vm    |   3 +-
 .../templates/v1_2/embeddable-superclass.vm     |   3 +-
 .../resources/templates/v1_2/singleclass.vm     |  10 +-
 .../main/resources/templates/v1_2/superclass.vm |  10 +-
 .../org/apache/cayenne/gen/StringUtilsTest.java |   9 +
 .../cayenne/tools/CayenneGeneratorMojoTest.java |   2 +-
 71 files changed, 3210 insertions(+), 227 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
index 653881d..07f53f1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DataObjectMatchTranslator.java
@@ -74,7 +74,7 @@ public class DataObjectMatchTranslator {
 		if (rel.isToMany() || !rel.isToPK()) {
 
 			// match on target PK
-			DbEntity ent = (DbEntity) rel.getTargetEntity();
+			DbEntity ent = rel.getTargetEntity();
 
 			// index by name
 			for (DbAttribute pkAttr : ent.getPrimaryKeys()) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index c5fd13a..1d0108d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -26,9 +26,11 @@ import java.util.List;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
+import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.TraversalHandler;
 import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTObjPath;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.exp.parser.SimpleNode;
@@ -189,6 +191,7 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 		objectMatchTranslator.reset();
 	}
 
+	@Override
 	public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {
 
 		if (!hasMoreChildren) {
@@ -351,7 +354,18 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 		return ">>";
 	}
 
+	@Override
 	public void startNode(Expression node, Expression parentNode) {
+		boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
+
+		if(node.getType() == Expression.FUNCTION_CALL) {
+			appendFunction((ASTFunctionCall)node);
+			if(parenthesisNeeded) {
+				out.append("(");
+			}
+			return;
+		}
+
 		int count = node.getOperandCount();
 
 		if (count == 2) {
@@ -359,7 +373,7 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 			detectObjectMatch(node);
 		}
 
-		if (parenthesisNeeded(node, parentNode)) {
+		if (parenthesisNeeded) {
 			out.append('(');
 		}
 
@@ -367,21 +381,17 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 			// not all databases handle true/false
 			if (node.getType() == Expression.TRUE) {
 				out.append("1 = 1");
-			}
-			if (node.getType() == Expression.FALSE) {
+			} else if (node.getType() == Expression.FALSE) {
 				out.append("1 = 0");
 			}
 		}
 
 		if (count == 1) {
-			if (node.getType() == Expression.NEGATIVE)
+			if (node.getType() == Expression.NEGATIVE) {
 				out.append('-');
-			// ignore POSITIVE - it is a NOOP
-			// else if(node.getType() == Expression.POSITIVE)
-			// qualBuf.append('+');
-			else if (node.getType() == Expression.NOT)
+			} else if (node.getType() == Expression.NOT) {
 				out.append("NOT ");
-			else if (node.getType() == Expression.BITWISE_NOT) {
+			} else if (node.getType() == Expression.BITWISE_NOT) {
 				out.append(operandForBitwiseNot());
 			}
 		} else if ((node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE)
@@ -394,12 +404,11 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	/**
 	 * @since 1.1
 	 */
+	@Override
 	public void endNode(Expression node, Expression parentNode) {
 
 		try {
-			// check if we need to use objectMatchTranslator to finish building
-			// the
-			// expression
+			// check if we need to use objectMatchTranslator to finish building the expression
 			if (node.getOperandCount() == 2 && matchingObject) {
 				appendObjectMatch();
 			}
@@ -417,26 +426,44 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 				appendLikeEscapeCharacter((PatternMatchNode) node);
 			}
 
+			// clean up trailing comma in function argument list
+			if(node.getType() == Expression.FUNCTION_CALL) {
+				clearLastFunctionArgDivider((ASTFunctionCall)node);
+			}
+
 			// closing LIKE parenthesis
 			if (parenthesisNeeded) {
 				out.append(')');
 			}
+
+			// if inside function call, put comma between arguments
+			if(parentNode != null && parentNode.getType() == Expression.FUNCTION_CALL) {
+				appendFunctionArgDivider((ASTFunctionCall) parentNode);
+			}
 		} catch (IOException ioex) {
 			throw new CayenneRuntimeException("Error appending content", ioex);
 		}
 	}
 
+	@Override
 	public void objectNode(Object leaf, Expression parentNode) {
 
 		try {
-			if (parentNode.getType() == Expression.OBJ_PATH) {
-				appendObjPath(parentNode);
-			} else if (parentNode.getType() == Expression.DB_PATH) {
-				appendDbPath(parentNode);
-			} else if (parentNode.getType() == Expression.LIST) {
-				appendList(parentNode, paramsDbType(parentNode));
-			} else {
-				appendLiteral(leaf, paramsDbType(parentNode), parentNode);
+			switch (parentNode.getType()) {
+				case Expression.OBJ_PATH:
+					appendObjPath(parentNode);
+					break;
+				case Expression.DB_PATH:
+					appendDbPath(parentNode);
+					break;
+				case Expression.LIST:
+					appendList(parentNode, paramsDbType(parentNode));
+					break;
+				case Expression.FUNCTION_CALL:
+					appendFunctionArg(leaf, (ASTFunctionCall)parentNode);
+					break;
+				default:
+					appendLiteral(leaf, paramsDbType(parentNode), parentNode);
 			}
 		} catch (IOException ioex) {
 			throw new CayenneRuntimeException("Error appending content", ioex);
@@ -444,24 +471,28 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	}
 
 	protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
-		if (parentNode == null)
+		if (parentNode == null) {
 			return false;
+		}
 
-		// only unary expressions can go w/o parenthesis
-		if (node.getOperandCount() > 1)
+		if (node.getType() == Expression.FUNCTION_CALL) {
 			return true;
+		}
 
-		if (node.getType() == Expression.OBJ_PATH)
-			return false;
+		// only unary expressions can go w/o parenthesis
+		if (node.getOperandCount() > 1) {
+			return true;
+		}
 
-		if (node.getType() == Expression.DB_PATH)
+		if (node.getType() == Expression.OBJ_PATH || node.getType() == Expression.DB_PATH) {
 			return false;
+		}
 
 		return true;
 	}
 
 	private final void appendList(Expression listExpr, DbAttribute paramDesc) throws IOException {
-		Iterator<?> it = null;
+		Iterator<?> it;
 		Object list = listExpr.getOperand(0);
 		if (list instanceof List) {
 			it = ((List<?>) list).iterator();
@@ -474,10 +505,11 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 
 		// process first element outside the loop
 		// (unroll loop to avoid condition checking
-		if (it.hasNext())
+		if (it.hasNext()) {
 			appendLiteral(it.next(), paramDesc, listExpr);
-		else
+		} else {
 			return;
+		}
 
 		while (it.hasNext()) {
 			out.append(", ");
@@ -514,6 +546,47 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers
 	}
 
 	/**
+	 * Append function name to result SQL
+	 * Override this method to rename or skip function if generic name isn't supported on target DB.
+	 * @since 4.0
+	 */
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		out.append(functionExpression.getFunctionName());
+	}
+
+	/**
+	 * Append scalar argument of a function call
+	 * Used only for values stored in ASTScalar other
+	 * expressions appended in objectNode() method
+	 *
+	 * @since 4.0
+	 */
+	protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
+		// Create fake DbAttribute to pass argument info down to bind it to SQL prepared statement
+		DbAttribute dbAttrForArg = new DbAttribute();
+		dbAttrForArg.setType(TypesMapping.getSqlTypeByJava(value.getClass()));
+		super.appendLiteral(value, dbAttrForArg, functionExpression);
+		appendFunctionArgDivider(functionExpression);
+	}
+
+	/**
+	 * Append divider between function arguments.
+	 * In overriding methods can be replaced e.g. for " || " for CONCAT operation
+	 * @since 4.0
+	 */
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		out.append(", ");
+	}
+
+	/**
+	 * Clear last divider as we currently don't now position of argument until parent element is ended.
+	 * @since 4.0
+	 */
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		out.delete(out.length() - 2, out.length());
+	}
+
+	/**
 	 * Class to translate DB Entity qualifiers annotation to Obj-entity
 	 * qualifiers annotation This is done by changing all Obj-paths to Db-paths
 	 * and rejecting all original Db-paths

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
index 67f22ca..731c052 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2QualifierTranslator.java
@@ -28,6 +28,7 @@ import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.parser.ASTEqual;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTNotEqual;
 import org.apache.cayenne.exp.parser.SimpleNode;
 import org.apache.cayenne.map.DbAttribute;
@@ -114,4 +115,38 @@ public class DB2QualifierTranslator extends TrimmingQualifierTranslator {
 			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
 		}
 	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		if(!"CONCAT".equals(functionExpression.getFunctionName())) {
+			super.appendFunction(functionExpression);
+		}
+	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.append(" || ");
+		} else {
+			super.appendFunctionArgDivider(functionExpression);
+		}
+	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.delete(out.length() - " || ".length(), out.length());
+		} else {
+			super.clearLastFunctionArgDivider(functionExpression);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
index 1b59288..84c5889 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyQualifierTranslator.java
@@ -18,12 +18,14 @@
  ****************************************************************/
 package org.apache.cayenne.dba.derby;
 
+import java.io.IOException;
 import java.sql.Types;
 
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.parser.ASTEqual;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTNotEqual;
 import org.apache.cayenne.exp.parser.SimpleNode;
 import org.apache.cayenne.map.DbAttribute;
@@ -52,9 +54,70 @@ public class DerbyQualifierTranslator extends TrimmingQualifierTranslator {
 
 			out.append("CAST(");
 			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
-			out.append(" AS VARCHAR(" + size + "))");
+			out.append(" AS VARCHAR(").append(size).append("))");
 		} else {
 			super.processColumnWithQuoteSqlIdentifiers(dbAttr, pathExp);
 		}
 	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		if("SUBSTRING".equals(functionExpression.getFunctionName())) {
+			out.append("SUBSTR");
+		} else if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.append("");
+		} else {
+			super.appendFunction(functionExpression);
+		}
+	}
+
+	/**
+	 * A little bit ugly code that wraps String scalars to CAST(? AS VARCHAR(length))
+	 * because otherwise derby don't know what type will be at the placeholder and
+	 * use LONG VARCHAR that isn't comparable what leads to statement preparation failure.
+	 *
+	 * @since 4.0
+	 */
+	protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			if(value instanceof String) {
+				out.append("CAST(");
+			}
+			super.appendFunctionArg(value, functionExpression);
+			if(value instanceof String) {
+				clearLastFunctionArgDivider(functionExpression);
+				out.append(" AS VARCHAR(").append(((String)value).length()).append("))");
+				appendFunctionArgDivider(functionExpression);
+			}
+		} else {
+			super.appendFunctionArg(value, functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.append(" || ");
+		} else {
+			super.appendFunctionArgDivider(functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.delete(out.length() - 4, out.length());
+		} else {
+			super.clearLastFunctionArgDivider(functionExpression);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
index f41bdf2..9965b12 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdQualifierTranslator.java
@@ -23,8 +23,12 @@ import org.apache.cayenne.access.translator.select.QualifierTranslator;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.dba.oracle.OracleQualifierTranslator;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 
 public class FirebirdQualifierTranslator extends QualifierTranslator {
+
+	private int substringArg = 0;
+
 	public FirebirdQualifierTranslator(QueryAssembler queryAssembler) {
 		super(queryAssembler);
 	}
@@ -39,4 +43,70 @@ public class FirebirdQualifierTranslator extends QualifierTranslator {
 		rootNode = rootNode.transform(new OracleQualifierTranslator.INTrimmer());
 		rootNode.traverse(this);
 	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		switch (functionExpression.getFunctionName()) {
+			case "CONCAT":
+				// noop
+				break;
+			case "LENGTH":
+				out.append("CHAR_LENGTH");
+				break;
+			case "LOCATE":
+				out.append("POSITION");
+				break;
+			case "SUBSTRING":
+				substringArg = 0;
+			default:
+				super.appendFunction(functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		switch (functionExpression.getFunctionName()) {
+			case "CONCAT":
+				out.append(" || ");
+				break;
+			case "SUBSTRING":
+				switch (substringArg++) {
+					case 0:
+						out.append(" FROM ");
+						break;
+					case 1:
+						out.append(" FOR ");
+						break;
+				}
+				break;
+			default:
+				super.appendFunctionArgDivider(functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		switch (functionExpression.getFunctionName()) {
+			case "CONCAT":
+				out.delete(out.length() - 4, out.length());
+				break;
+			case "SUBSTRING":
+				// no offset arg
+				if(substringArg == 2) {
+					out.delete(out.length() - " FOR ".length(), out.length());
+				}
+				break;
+			default:
+				super.clearLastFunctionArgDivider(functionExpression);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
index f50aa7b..d6131bb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
@@ -20,6 +20,8 @@
 package org.apache.cayenne.dba.frontbase;
 
 import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.translator.select.QualifierTranslator;
+import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -83,6 +85,14 @@ public class FrontBaseAdapter extends JdbcAdapter {
 		return new FrontBaseSelectTranslator(query, this, entityResolver);
 	}
 
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
+		return new FrontBaseQualifierTranslator(queryAssembler);
+	}
+
 	@Override
 	public String tableTypeForTable() {
 		return "BASE TABLE";

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java
new file mode 100644
index 0000000..0a3d99e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseQualifierTranslator.java
@@ -0,0 +1,91 @@
+/*****************************************************************
+ *   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.cayenne.dba.frontbase;
+
+import org.apache.cayenne.access.translator.select.QualifierTranslator;
+import org.apache.cayenne.access.translator.select.QueryAssembler;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
+
+/**
+ * @since 4.0
+ */
+public class FrontBaseQualifierTranslator extends QualifierTranslator {
+
+    private int substringArg = 0;
+
+    public FrontBaseQualifierTranslator(QueryAssembler queryAssembler) {
+        super(queryAssembler);
+    }
+
+    @Override
+    protected void appendFunction(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "CONCAT":
+                // noop
+                break;
+            case "LOCATE":
+                out.append("POSITION");
+                break;
+            case "SUBSTRING":
+                substringArg = 0;
+            default:
+                super.appendFunction(functionExpression);
+        }
+    }
+
+    @Override
+    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "CONCAT":
+                out.append(" | ");
+                break;
+            case "SUBSTRING":
+                // SUBSTRING (str FROM offset FOR length)
+                switch (substringArg++) {
+                    case 0:
+                        out.append(" FROM ");
+                        break;
+                    case 1:
+                        out.append(" FOR ");
+                        break;
+                }
+                break;
+            default:
+                super.appendFunctionArgDivider(functionExpression);
+        }
+    }
+
+    @Override
+    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "CONCAT":
+                out.delete(out.length() - 3, out.length());
+                break;
+            case "SUBSTRING":
+                // no offset arg
+                if(substringArg == 2) {
+                    out.delete(out.length() - " FOR ".length(), out.length());
+                }
+                break;
+            default:
+                super.clearLastFunctionArgDivider(functionExpression);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
index dc94bae..0ac444f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
@@ -83,7 +83,7 @@ public class IngresAdapter extends JdbcAdapter {
 
 	@Override
 	public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
-		return new TrimmingQualifierTranslator(queryAssembler, IngresAdapter.TRIM_FUNCTION);
+		return new IngresQualifierTranslator(queryAssembler);
 	}
 
 	@Override

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java
new file mode 100644
index 0000000..949155b
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresQualifierTranslator.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.cayenne.dba.ingres;
+
+import org.apache.cayenne.access.translator.select.QueryAssembler;
+import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
+
+/**
+ * @since 4.0
+ */
+class IngresQualifierTranslator extends TrimmingQualifierTranslator {
+
+    IngresQualifierTranslator(QueryAssembler queryAssembler) {
+        super(queryAssembler, IngresAdapter.TRIM_FUNCTION);
+    }
+
+    @Override
+    protected void appendFunction(ASTFunctionCall functionExpression) {
+        if(!"CONCAT".equals(functionExpression.getFunctionName())) {
+            super.appendFunction(functionExpression);
+        }
+    }
+
+    @Override
+    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+        if("CONCAT".equals(functionExpression.getFunctionName())) {
+            out.append(" + ");
+        } else {
+            super.appendFunctionArgDivider(functionExpression);
+        }
+    }
+
+    @Override
+    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+        if("CONCAT".equals(functionExpression.getFunctionName())) {
+            out.delete(out.length() - " + ".length(), out.length());
+        } else {
+            super.clearLastFunctionArgDivider(functionExpression);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
index 3d14765..28b9f74 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleQualifierTranslator.java
@@ -21,11 +21,13 @@ package org.apache.cayenne.dba.oracle;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.ASTIn;
 import org.apache.cayenne.exp.parser.ASTList;
 import org.apache.cayenne.exp.parser.ASTNegate;
 import org.apache.cayenne.exp.parser.ASTNotIn;
 import org.apache.cayenne.exp.parser.ASTPath;
+import org.apache.cayenne.exp.parser.Node;
 import org.apache.commons.collections.Transformer;
 
 import java.util.ArrayList;
@@ -93,4 +95,70 @@ public class OracleQualifierTranslator extends TrimmingQualifierTranslator {
 			return input;
 		}
 	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	public void endNode(Expression node, Expression parentNode) {
+		super.endNode(node, parentNode);
+		if(node.getType() == Expression.FUNCTION_CALL) {
+			if("LOCATE".equals(((ASTFunctionCall)node).getFunctionName())) {
+				// order of args in INSTR is different, so swap them back
+				swapNodeChildren((ASTFunctionCall)node, 0, 1);
+			}
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			// CONCAT(x, y, z) -> (x || y || z)
+		} else if("SUBSTRING".equals(functionExpression.getFunctionName())) {
+			out.append("SUBSTR");
+		} else if("LOCATE".equals(functionExpression.getFunctionName())) {
+			// LOCATE(substr, str) -> INSTR(str, substr)
+			out.append("INSTR");
+			swapNodeChildren(functionExpression, 0, 1);
+		} else {
+			super.appendFunction(functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.append(" || ");
+		} else {
+			super.appendFunctionArgDivider(functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	@Override
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("CONCAT".equals(functionExpression.getFunctionName())) {
+			out.delete(out.length() - " || ".length(), out.length());
+		} else {
+			super.clearLastFunctionArgDivider(functionExpression);
+		}
+	}
+
+	/**
+	 * @since 4.0
+	 */
+	private void swapNodeChildren(Node node, int i, int j) {
+		Node ni = node.jjtGetChild(i);
+		Node nj = node.jjtGetChild(j);
+		node.jjtAddChild(ni, j);
+		node.jjtAddChild(nj, i);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
index 8b8e731..42937bb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresQualifierTranslator.java
@@ -25,6 +25,7 @@ import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 
 /**
@@ -40,20 +41,15 @@ public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
 
 	@Override
 	public void startNode(Expression node, Expression parentNode) {
-
-		if (node.getOperandCount() == 2) {
+		// super implementation has special handling of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+		// Postgres uses ILIKE
+		boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
+		if (likeIgnoreCase) {
 			// binary nodes are the only ones that currently require this
 			detectObjectMatch(node);
-
 			if (parenthesisNeeded(node, parentNode)) {
 				out.append('(');
 			}
-
-			// super implementation has special handling
-			// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-			// Postgres uses ILIKE
-			// ...
-
 		} else {
 			super.startNode(node, parentNode);
 		}
@@ -61,12 +57,12 @@ public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
 
 	@Override
 	public void endNode(Expression node, Expression parentNode) {
-		if (node.getOperandCount() == 2) {
+		// super implementation has special handling of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
+		// Postgres uses ILIKE
+		boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
 
+		if (likeIgnoreCase) {
 			try {
-				// check if we need to use objectMatchTranslator to finish
-				// building the
-				// expression
 				if (matchingObject) {
 					appendObjectMatch();
 				}
@@ -78,11 +74,6 @@ public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
 				if (parenthesisNeeded(node, parentNode)) {
 					out.append(')');
 				}
-
-				// super implementation has special handling
-				// of LIKE_IGNORE_CASE and NOT_LIKE_IGNORE_CASE
-				// Postgres uses ILIKE
-				// ...
 			} catch (IOException ioex) {
 				throw new CayenneRuntimeException("Error appending content", ioex);
 			}
@@ -99,17 +90,15 @@ public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
 
 		try {
 			// use ILIKE
-
 			switch (node.getType()) {
-
-			case Expression.LIKE_IGNORE_CASE:
-				finishedChildNodeAppendExpression(node, " ILIKE ");
-				break;
-			case Expression.NOT_LIKE_IGNORE_CASE:
-				finishedChildNodeAppendExpression(node, " NOT ILIKE ");
-				break;
-			default:
-				super.finishedChild(node, childIndex, hasMoreChildren);
+				case Expression.LIKE_IGNORE_CASE:
+					finishedChildNodeAppendExpression(node, " ILIKE ");
+					break;
+				case Expression.NOT_LIKE_IGNORE_CASE:
+					finishedChildNodeAppendExpression(node, " NOT ILIKE ");
+					break;
+				default:
+					super.finishedChild(node, childIndex, hasMoreChildren);
 			}
 		} catch (IOException ioex) {
 			throw new CayenneRuntimeException("Error appending content", ioex);
@@ -124,4 +113,40 @@ public class PostgresQualifierTranslator extends TrimmingQualifierTranslator {
 			objectMatchTranslator.setExpression(node);
 		}
 	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		if("LOCATE".equals(functionExpression.getFunctionName())) {
+			out.append("POSITION");
+		} else {
+			super.appendFunction(functionExpression);
+		}
+	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("LOCATE".equals(functionExpression.getFunctionName())) {
+			out.append(" in ");
+		} else {
+			super.appendFunctionArgDivider(functionExpression);
+		}
+	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("LOCATE".equals(functionExpression.getFunctionName())) {
+			out.delete(out.length() - " in ".length(), out.length());
+		} else {
+			super.clearLastFunctionArgDivider(functionExpression);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
index 839b548..b390ebc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
@@ -19,6 +19,8 @@
 package org.apache.cayenne.dba.sqlite;
 
 import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.select.QualifierTranslator;
+import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
@@ -81,6 +83,14 @@ public class SQLiteAdapter extends JdbcAdapter {
         map.registerType(new SQLiteCalendarType(Calendar.class));
     }
 
+    /**
+     * @since 4.0
+     */
+    @Override
+    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
+        return new SQLiteQualifierTranslator(queryAssembler);
+    }
+
     @Override
     public String createFkConstraint(DbRelationship rel) {
         return null;

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java
new file mode 100644
index 0000000..bc2bb2e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteQualifierTranslator.java
@@ -0,0 +1,102 @@
+/*****************************************************************
+ *   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.cayenne.dba.sqlite;
+
+import org.apache.cayenne.access.translator.select.QualifierTranslator;
+import org.apache.cayenne.access.translator.select.QueryAssembler;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
+import org.apache.cayenne.exp.parser.Node;
+
+/**
+ * @since 4.0
+ */
+public class SQLiteQualifierTranslator extends QualifierTranslator {
+
+    public SQLiteQualifierTranslator(QueryAssembler queryAssembler) {
+        super(queryAssembler);
+    }
+
+    @Override
+    public void endNode(Expression node, Expression parentNode) {
+        super.endNode(node, parentNode);
+        if(node.getType() == Expression.FUNCTION_CALL) {
+            if("LOCATE".equals(((ASTFunctionCall)node).getFunctionName())) {
+                // order of args in INSTR is different, so swap them back
+                swapNodeChildren((ASTFunctionCall)node, 0, 1);
+            }
+        }
+    }
+
+    @Override
+    protected void appendFunction(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "MOD":
+            case "CONCAT":
+                // noop
+                break;
+            case "SUBSTRING":
+                out.append("SUBSTR");
+                break;
+            case "LOCATE":
+                // LOCATE(substr, str) -> INSTR(str, substr)
+                out.append("INSTR");
+                swapNodeChildren(functionExpression, 0, 1);
+                break;
+            default:
+                super.appendFunction(functionExpression);
+        }
+    }
+
+    @Override
+    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "MOD":
+                out.append(" % ");
+                break;
+            case "CONCAT":
+                out.append(" || ");
+                break;
+            default:
+                super.appendFunctionArgDivider(functionExpression);
+        }
+    }
+
+    @Override
+    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "MOD":
+                out.delete(out.length() - 3, out.length());
+                break;
+            case "CONCAT":
+                out.delete(out.length() - 4, out.length());
+                break;
+            default:
+                super.clearLastFunctionArgDivider(functionExpression);
+        }
+    }
+
+    private void swapNodeChildren(Node node, int i, int j) {
+        Node ni = node.jjtGetChild(i);
+        Node nj = node.jjtGetChild(j);
+        node.jjtAddChild(ni, j);
+        node.jjtAddChild(nj, i);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
index fd9a191..9a4e9be 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerTrimmingQualifierTranslator.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.dba.sqlserver;
 import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.TrimmingQualifierTranslator;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
 import org.apache.cayenne.map.DbAttribute;
 
 import java.sql.Types;
@@ -112,4 +113,48 @@ class SQLServerTrimmingQualifierTranslator extends TrimmingQualifierTranslator {
 
 		return expressionStack.get(index);
 	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void appendFunction(ASTFunctionCall functionExpression) {
+		switch (functionExpression.getFunctionName()) {
+			case "LENGTH":
+				out.append("LEN");
+				break;
+			case "LOCATE":
+				out.append("CHARINDEX");
+				break;
+			case "MOD":
+				// noop
+				break;
+			default:
+				super.appendFunction(functionExpression);
+		}
+	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("MOD".equals(functionExpression.getFunctionName())) {
+			out.append(" % ");
+		} else {
+			super.appendFunctionArgDivider(functionExpression);
+		}
+	}
+
+    /**
+     * @since 4.0
+     */
+	@Override
+	protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+		if("MOD".equals(functionExpression.getFunctionName())) {
+			out.delete(out.length() - " % ".length(), out.length());
+		} else {
+			super.clearLastFunctionArgDivider(functionExpression);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
index d5893ac..28e948c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
@@ -26,6 +26,8 @@ import java.util.List;
 
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
+import org.apache.cayenne.access.translator.select.QualifierTranslator;
+import org.apache.cayenne.access.translator.select.QueryAssembler;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
 import org.apache.cayenne.access.types.ByteArrayType;
 import org.apache.cayenne.access.types.ByteType;
@@ -81,6 +83,14 @@ public class SybaseAdapter extends JdbcAdapter {
 	}
 
     /**
+     * @since 4.0
+     */
+    @Override
+    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
+        return new SybaseQualifierTranslator(queryAssembler);
+    }
+
+    /**
      * Returns word "go".
      * 
      * @since 1.0.4

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
new file mode 100644
index 0000000..1ad939e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseQualifierTranslator.java
@@ -0,0 +1,78 @@
+/*****************************************************************
+ *   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.cayenne.dba.sybase;
+
+import org.apache.cayenne.access.translator.select.QualifierTranslator;
+import org.apache.cayenne.access.translator.select.QueryAssembler;
+import org.apache.cayenne.exp.parser.ASTFunctionCall;
+
+/**
+ * @since 4.0
+ */
+public class SybaseQualifierTranslator extends QualifierTranslator {
+
+    public SybaseQualifierTranslator(QueryAssembler queryAssembler) {
+        super(queryAssembler);
+    }
+
+    @Override
+    protected void appendFunction(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "MOD":
+            case "CONCAT":
+                // noop
+                break;
+            case "LENGTH":
+                out.append("LEN");
+                break;
+            case "LOCATE":
+                out.append("CHARINDEX");
+                break;
+            default:
+                super.appendFunction(functionExpression);
+        }
+    }
+
+    @Override
+    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "MOD":
+                out.append(" % ");
+                break;
+            case "CONCAT":
+                out.append(" + ");
+                break;
+            default:
+                super.appendFunctionArgDivider(functionExpression);
+        }
+    }
+
+    @Override
+    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
+        switch (functionExpression.getFunctionName()) {
+            case "MOD":
+            case "CONCAT":
+                out.delete(out.length() - 3, out.length());
+                break;
+            default:
+                super.clearLastFunctionArgDivider(functionExpression);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
index 11bfdb6..6d9024c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
@@ -151,6 +151,11 @@ public abstract class Expression implements Serializable, XMLSerializable {
 	 */
 	public static final int BITWISE_RIGHT_SHIFT = 44;
 
+	/**
+	 * @since 4.0
+	 */
+	public static final int FUNCTION_CALL = 45;
+
 	protected int type;
 
 	/**
@@ -569,7 +574,7 @@ public abstract class Expression implements Serializable, XMLSerializable {
 		for (int i = 0; i < count; i++) {
 			Object child = getOperand(i);
 
-			if (child instanceof Expression) {
+			if (child instanceof Expression && !(child instanceof ASTScalar)) {
 				Expression childExp = (Expression) child;
 				childExp.traverse(this, visitor);
 			} else {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
index 16380e4..22dec9f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java
@@ -385,21 +385,54 @@ public class ExpressionFactory {
 	 * A convenience method to create an OBJ_PATH "equal to" expression.
 	 */
 	public static Expression matchExp(String pathSpec, Object value) {
-		return new ASTEqual(new ASTObjPath(pathSpec), value);
+		return matchExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#matchExp(String, Object)
+	 */
+	static Expression matchExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTEqual((SimpleNode)exp, value);
 	}
 
 	/**
 	 * A convenience method to create an OBJ_PATH "not equal to" expression.
 	 */
 	public static Expression noMatchExp(String pathSpec, Object value) {
-		return new ASTNotEqual(new ASTObjPath(pathSpec), value);
+		return noMatchExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#noMatchExp(String, Object)
+	 */
+	static Expression noMatchExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotEqual((SimpleNode)exp, value);
 	}
 
 	/**
 	 * A convenience method to create an OBJ_PATH "less than" expression.
 	 */
 	public static Expression lessExp(String pathSpec, Object value) {
-		return new ASTLess(new ASTObjPath(pathSpec), value);
+		return lessExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#lessExp(String, Object)
+	 */
+	static Expression lessExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTLess((SimpleNode)exp, value);
 	}
 
 	/**
@@ -416,7 +449,18 @@ public class ExpressionFactory {
 	 * expression.
 	 */
 	public static Expression lessOrEqualExp(String pathSpec, Object value) {
-		return new ASTLessOrEqual(new ASTObjPath(pathSpec), value);
+		return lessOrEqualExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#lessOrEqualExp(String, Object)
+	 */
+	static Expression lessOrEqualExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTLessOrEqual((SimpleNode)exp, value);
 	}
 
 	/**
@@ -433,7 +477,18 @@ public class ExpressionFactory {
 	 * A convenience method to create an OBJ_PATH "greater than" expression.
 	 */
 	public static Expression greaterExp(String pathSpec, Object value) {
-		return new ASTGreater(new ASTObjPath(pathSpec), value);
+		return greaterExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#greaterExp(String, Object)
+	 */
+	static Expression greaterExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTGreater((SimpleNode)exp, value);
 	}
 
 	/**
@@ -450,7 +505,18 @@ public class ExpressionFactory {
 	 * expression.
 	 */
 	public static Expression greaterOrEqualExp(String pathSpec, Object value) {
-		return new ASTGreaterOrEqual(new ASTObjPath(pathSpec), value);
+		return greaterOrEqualExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#greaterOrEqualExp(String, Object)
+	 */
+	static Expression greaterOrEqualExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTGreaterOrEqual((SimpleNode)exp, value);
 	}
 
 	/**
@@ -468,10 +534,21 @@ public class ExpressionFactory {
 	 * empty collection.
 	 */
 	public static Expression inExp(String pathSpec, Object... values) {
+		return inExp(new ASTObjPath(pathSpec), values);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#inExp(String, Object[])
+	 */
+	static Expression inExp(Expression exp, Object... values) {
 		if (values.length == 0) {
 			return new ASTFalse();
 		}
-		return new ASTIn(new ASTObjPath(pathSpec), new ASTList(values));
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTIn((SimpleNode)exp, new ASTList(values));
 	}
 
 	/**
@@ -490,10 +567,21 @@ public class ExpressionFactory {
 	 * empty collection.
 	 */
 	public static Expression inExp(String pathSpec, Collection<?> values) {
+		return inExp(new ASTObjPath(pathSpec), values);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#inExp(String, Collection)
+	 */
+	static Expression inExp(Expression exp, Collection<?> values) {
 		if (values.isEmpty()) {
 			return new ASTFalse();
 		}
-		return new ASTIn(new ASTObjPath(pathSpec), new ASTList(values));
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTIn((SimpleNode)exp, new ASTList(values));
 	}
 
 	/**
@@ -512,10 +600,21 @@ public class ExpressionFactory {
 	 * empty collection.
 	 */
 	public static Expression notInExp(String pathSpec, Collection<?> values) {
+		return notInExp(new ASTObjPath(pathSpec), values);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notInExp(String, Collection)
+	 */
+	static Expression notInExp(Expression exp, Collection<?> values) {
 		if (values.isEmpty()) {
-			return new ASTTrue();
+			return new ASTFalse();
+		}
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
 		}
-		return new ASTNotIn(new ASTObjPath(pathSpec), new ASTList(values));
+		return new ASTNotIn((SimpleNode)exp, new ASTList(values));
 	}
 
 	/**
@@ -538,10 +637,21 @@ public class ExpressionFactory {
 	 * @since 1.0.6
 	 */
 	public static Expression notInExp(String pathSpec, Object... values) {
+		return notInExp(new ASTObjPath(pathSpec), values);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notInExp(String, Object[])
+	 */
+	static Expression notInExp(Expression exp, Object... values) {
 		if (values.length == 0) {
 			return new ASTTrue();
 		}
-		return new ASTNotIn(new ASTObjPath(pathSpec), new ASTList(values));
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotIn((SimpleNode)exp, new ASTList(values));
 	}
 
 	/**
@@ -561,7 +671,18 @@ public class ExpressionFactory {
 	 * A convenience shortcut for building BETWEEN expressions.
 	 */
 	public static Expression betweenExp(String pathSpec, Object value1, Object value2) {
-		return new ASTBetween(new ASTObjPath(pathSpec), value1, value2);
+		return betweenExp(new ASTObjPath(pathSpec), value1, value2);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#betweenExp(String, Object, Object)
+	 */
+	static Expression betweenExp(Expression exp, Object value1, Object value2) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTBetween((SimpleNode)exp, value1, value2);
 	}
 
 	/**
@@ -577,7 +698,18 @@ public class ExpressionFactory {
 	 * A convenience shortcut for building NOT_BETWEEN expressions.
 	 */
 	public static Expression notBetweenExp(String pathSpec, Object value1, Object value2) {
-		return new ASTNotBetween(new ASTObjPath(pathSpec), value1, value2);
+		return notBetweenExp(new ASTObjPath(pathSpec), value1, value2);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notBetweenExp(String, Object, Object)
+	 */
+	static Expression notBetweenExp(Expression exp, Object value1, Object value2) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotBetween((SimpleNode)exp, value1, value2);
 	}
 
 	/**
@@ -597,6 +729,14 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#likeExp(String, Object)
+	 */
+	static Expression likeExp(Expression exp, Object value) {
+		return likeExpInternal(exp, value, (char) 0);
+	}
+
+	/**
 	 * <p>
 	 * A convenience shortcut for building LIKE expression.
 	 * </p>
@@ -612,8 +752,23 @@ public class ExpressionFactory {
 		return likeExpInternal(pathSpec, value, escapeChar);
 	}
 
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#likeExp(String, Object)
+	 */
+	static Expression likeExp(Expression exp, Object value, char escapeChar) {
+		return likeExpInternal(exp, value, escapeChar);
+	}
+
 	static ASTLike likeExpInternal(String pathSpec, Object value, char escapeChar) {
-		return new ASTLike(new ASTObjPath(pathSpec), value, escapeChar);
+		return likeExpInternal(new ASTObjPath(pathSpec), value, escapeChar);
+	}
+
+	static ASTLike likeExpInternal(Expression expression, Object value, char escapeChar) {
+		if(!(expression instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTLike((SimpleNode) expression, value, escapeChar);
 	}
 
 	/**
@@ -645,7 +800,18 @@ public class ExpressionFactory {
 	 * A convenience shortcut for building NOT_LIKE expression.
 	 */
 	public static Expression notLikeExp(String pathSpec, Object value) {
-		return new ASTNotLike(new ASTObjPath(pathSpec), value);
+		return notLikeExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notLikeExp(String, Object)
+	 */
+	static Expression notLikeExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotLike((SimpleNode)exp, value);
 	}
 
 	/**
@@ -661,7 +827,18 @@ public class ExpressionFactory {
 	 * @since 3.0.1
 	 */
 	public static Expression notLikeExp(String pathSpec, Object value, char escapeChar) {
-		return new ASTNotLike(new ASTObjPath(pathSpec), value, escapeChar);
+		return notLikeExp(new ASTObjPath(pathSpec), value, escapeChar);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notLikeExp(String, Object)
+	 */
+	static Expression notLikeExp(Expression exp, Object value, char escapeChar) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotLike((SimpleNode)exp, value, escapeChar);
 	}
 
 	/**
@@ -697,6 +874,14 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#likeIgnoreCaseExp(String, Object)
+	 */
+	static Expression likeIgnoreCaseExp(Expression exp, Object value) {
+		return likeIgnoreCaseExp(exp, value, (char) 0);
+	}
+
+	/**
 	 * <p>
 	 * A convenience shortcut for building LIKE_IGNORE_CASE expression.
 	 * </p>
@@ -713,7 +898,14 @@ public class ExpressionFactory {
 	}
 
 	static ASTLikeIgnoreCase likeIgnoreCaseExpInternal(String pathSpec, Object value, char escapeChar) {
-		return new ASTLikeIgnoreCase(new ASTObjPath(pathSpec), value, escapeChar);
+		return likeIgnoreCaseExp(new ASTObjPath(pathSpec), value, escapeChar);
+	}
+
+	static ASTLikeIgnoreCase likeIgnoreCaseExp(Expression exp, Object value, char escapeChar) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTLikeIgnoreCase((SimpleNode) exp, value, escapeChar);
 	}
 
 	/**
@@ -745,7 +937,18 @@ public class ExpressionFactory {
 	 * A convenience shortcut for building NOT_LIKE_IGNORE_CASE expression.
 	 */
 	public static Expression notLikeIgnoreCaseExp(String pathSpec, Object value) {
-		return new ASTNotLikeIgnoreCase(new ASTObjPath(pathSpec), value);
+		return notLikeIgnoreCaseExp(new ASTObjPath(pathSpec), value);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notLikeIgnoreCaseExp(String, Object)
+	 */
+	static Expression notLikeIgnoreCaseExp(Expression exp, Object value) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotLikeIgnoreCase((SimpleNode)exp, value);
 	}
 
 	/**
@@ -761,7 +964,18 @@ public class ExpressionFactory {
 	 * @since 3.0.1
 	 */
 	public static Expression notLikeIgnoreCaseExp(String pathSpec, Object value, char escapeChar) {
-		return new ASTNotLikeIgnoreCase(new ASTObjPath(pathSpec), value, escapeChar);
+		return notLikeIgnoreCaseExp(new ASTObjPath(pathSpec), value, escapeChar);
+	}
+
+	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#notLikeIgnoreCaseExp(String, Object, char)
+	 */
+	static Expression notLikeIgnoreCaseExp(Expression exp, Object value, char escapeChar) {
+		if(!(exp instanceof SimpleNode)) {
+			throw new IllegalArgumentException("exp should be instance of SimpleNode");
+		}
+		return new ASTNotLikeIgnoreCase((SimpleNode)exp, value, escapeChar);
 	}
 
 	/**
@@ -801,6 +1015,16 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#containsExp(String, String)
+	 */
+	static Expression containsExp(Expression exp, String value) {
+		ASTLike like = likeExpInternal(exp, value, (char) 0);
+		LikeExpressionHelper.toContains(like);
+		return like;
+	}
+
+	/**
 	 * @return An expression for a database "LIKE" query with the value
 	 *         converted to a pattern matching the beginning of the String.
 	 * @since 4.0
@@ -812,6 +1036,16 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#startsWithExp(String, String)
+	 */
+	static Expression startsWithExp(Expression exp, String value) {
+		ASTLike like = likeExpInternal(exp, value, (char) 0);
+		LikeExpressionHelper.toStartsWith(like);
+		return like;
+	}
+
+	/**
 	 * @return An expression for a database "LIKE" query with the value
 	 *         converted to a pattern matching the beginning of the String.
 	 * @since 4.0
@@ -823,6 +1057,16 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#endsWithExp(String, String)
+	 */
+	static Expression endsWithExp(Expression exp, String value) {
+		ASTLike like = likeExpInternal(exp, value, (char) 0);
+		LikeExpressionHelper.toEndsWith(like);
+		return like;
+	}
+
+	/**
 	 * Same as {@link #containsExp(String, String)} only using case-insensitive
 	 * comparison.
 	 * 
@@ -835,6 +1079,16 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#containsIgnoreCaseExp(String, String)
+	 */
+	static Expression containsIgnoreCaseExp(Expression exp, String value) {
+		ASTLikeIgnoreCase like = likeIgnoreCaseExp(exp, value, (char) 0);
+		LikeExpressionHelper.toContains(like);
+		return like;
+	}
+
+	/**
 	 * Same as {@link #startsWithExp(String, String)} only using
 	 * case-insensitive comparison.
 	 * 
@@ -847,6 +1101,16 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#startsWithIgnoreCaseExp(String, String)
+	 */
+	static Expression startsWithIgnoreCaseExp(Expression exp, String value) {
+		ASTLikeIgnoreCase like = likeIgnoreCaseExp(exp, value, (char) 0);
+		LikeExpressionHelper.toStartsWith(like);
+		return like;
+	}
+
+	/**
 	 * Same as {@link #endsWithExp(String, String)} only using case-insensitive
 	 * comparison.
 	 * 
@@ -859,6 +1123,16 @@ public class ExpressionFactory {
 	}
 
 	/**
+	 * @since 4.0
+	 * @see ExpressionFactory#endsWithIgnoreCaseExp(String, String)
+	 */
+	static Expression endsWithIgnoreCaseExp(Expression exp, String value) {
+		ASTLikeIgnoreCase like = likeIgnoreCaseExp(exp, value, (char) 0);
+		LikeExpressionHelper.toEndsWith(like);
+		return like;
+	}
+
+	/**
 	 * @param pathSpec a String "obj:" path.
 	 * @since 4.0
 	 * @return a new "obj:" path expression for the specified String path.

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java
new file mode 100644
index 0000000..cfecdc7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java
@@ -0,0 +1,288 @@
+/*****************************************************************
+ *   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.cayenne.exp;
+
+import org.apache.cayenne.exp.parser.ASTAbs;
+import org.apache.cayenne.exp.parser.ASTConcat;
+import org.apache.cayenne.exp.parser.ASTLength;
+import org.apache.cayenne.exp.parser.ASTLocate;
+import org.apache.cayenne.exp.parser.ASTLower;
+import org.apache.cayenne.exp.parser.ASTMod;
+import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.exp.parser.ASTScalar;
+import org.apache.cayenne.exp.parser.ASTSqrt;
+import org.apache.cayenne.exp.parser.ASTSubstring;
+import org.apache.cayenne.exp.parser.ASTTrim;
+import org.apache.cayenne.exp.parser.ASTUpper;
+
+/**
+ * Collection of factory methods to create function call expressions.
+ *
+ * @since 4.0
+ */
+public class FunctionExpressionFactory {
+
+    /**
+     * Call SUBSTRING(string, offset, length) function
+     *
+     * @param exp expression that must evaluate to string
+     * @param offset start offset of substring
+     * @param length length of subtring
+     * @return SUBSTRING() call expression
+     */
+    public static Expression substringExp(Expression exp, int offset, int length) {
+        return substringExp(exp, new ASTScalar((Integer)offset), new ASTScalar((Integer)length));
+    }
+
+    /**
+     * Call SUBSTRING(string, offset, length) function
+     *
+     * @param path Object path value
+     * @param offset start offset of substring
+     * @param length length of subtring
+     * @return SUBSTRING() call expression
+     */
+    public static Expression substringExp(String path, int offset, int length) {
+        return substringExp(new ASTObjPath(path), new ASTScalar((Integer)offset), new ASTScalar((Integer)length));
+    }
+
+    /**
+     * Call SUBSTRING(string, offset, length) function
+     *
+     * @param exp expression that must evaluate to string
+     * @param offset start offset of substring must evaluate to int
+     * @param length length of subtring must evaluate to int
+     * @return SUBSTRING() call expression
+     */
+    public static Expression substringExp(Expression exp, Expression offset, Expression length) {
+        return new ASTSubstring(exp, offset, length);
+    }
+
+    /**
+     * @param exp string expression to trim
+     * @return TRIM() call expression
+     */
+    public static Expression trimExp(Expression exp) {
+        return new ASTTrim(exp);
+    }
+
+    /**
+     * @param path object path value
+     * @return TRIM() call expression
+     */
+    public static Expression trimExp(String path) {
+        return new ASTTrim(new ASTObjPath(path));
+    }
+
+    /**
+     * @param exp string expression
+     * @return LOWER() call expression
+     */
+    public static Expression lowerExp(Expression exp) {
+        return new ASTLower(exp);
+    }
+
+    /**
+     * @param path object path value
+     * @return LOWER() call expression
+     */
+    public static Expression lowerExp(String path) {
+        return new ASTLower(new ASTObjPath(path));
+    }
+
+    /**
+     * @param exp string expression
+     * @return UPPER() call expression
+     */
+    public static Expression upperExp(Expression exp) {
+        return new ASTUpper(exp);
+    }
+
+    /**
+     * @param path object path value
+     * @return UPPER() call expression
+     */
+    public static Expression upperExp(String path) {
+        return new ASTUpper(new ASTObjPath(path));
+    }
+
+    /**
+     * @param exp string expression
+     * @return LENGTH() call expression
+     */
+    public static Expression lengthExp(Expression exp) {
+        return new ASTLength(exp);
+    }
+
+    /**
+     * @param path object path value
+     * @return LENGTH() call expression
+     */
+    public static Expression lengthExp(String path) {
+        return new ASTLength(new ASTObjPath(path));
+    }
+
+    /**
+     * Call LOCATE(substring, string) function that return position
+     * of substring in string or 0 if it is not found.
+     *
+     * @param substring object path value
+     * @param exp string expression
+     * @return LOCATE() call expression
+     */
+    public static Expression locateExp(String substring, Expression exp) {
+        return locateExp(new ASTScalar(substring), exp);
+    }
+
+    /**
+     * Call LOCATE(substring, string) function that return position
+     * of substring in string or 0 if it is not found.
+     *
+     * @param substring object path value
+     * @param path object path
+     * @return LOCATE() call expression
+     */
+    public static Expression locateExp(String substring, String path) {
+        return locateExp(new ASTScalar(substring), new ASTObjPath(path));
+    }
+
+    /**
+     * Call LOCATE(substring, string) function that return position
+     * of substring in string or 0 if it is not found.
+     *
+     * @param substring string expression
+     * @param exp string expression
+     * @return LOCATE() call expression
+     */
+    public static Expression locateExp(Expression substring, Expression exp) {
+        return new ASTLocate(substring, exp);
+    }
+
+    /**
+     * @param exp numeric expression
+     * @return ABS() call expression
+     */
+    public static Expression absExp(Expression exp) {
+        return new ASTAbs(exp);
+    }
+
+    /**
+     * @param path object path value
+     * @return ABS() call expression
+     */
+    public static Expression absExp(String path) {
+        return new ASTAbs(new ASTObjPath(path));
+    }
+
+    /**
+     * @param exp numeric expression
+     * @return SQRT() call expression
+     */
+    public static Expression sqrtExp(Expression exp) {
+        return new ASTSqrt(exp);
+    }
+
+    /**
+     * @param path object path value
+     * @return SQRT() call expression
+     */
+    public static Expression sqrtExp(String path) {
+        return new ASTSqrt(new ASTObjPath(path));
+    }
+
+    /**
+     * @param exp numeric expression
+     * @param number divisor
+     * @return MOD() call expression
+     */
+    public static Expression modExp(Expression exp, Number number) {
+        return modExp(exp, new ASTScalar(number));
+    }
+
+    /**
+     * @param path object path value
+     * @param number divisor
+     * @return MOD() call expression
+     */
+    public static Expression modExp(String path, Number number) {
+        return modExp(new ASTObjPath(path), new ASTScalar(number));
+    }
+
+    /**
+     * @param exp object path value
+     * @param number numeric expression
+     * @return MOD() call expression
+     */
+    public static Expression modExp(Expression exp, Expression number) {
+        return new ASTMod(exp, number);
+    }
+
+    /**
+     * Factory method for expression to call CONCAT(string1, string2, ...) function
+     * Can be used like:
+     *  Expression concat = concatExp(SomeClass.POPERTY_1.getPath(), SomeClass.PROPERTY_2.getPath());
+     *
+     * SQL generation note:
+     * - if DB supports CONCAT function with vararg then it will be used
+     * - if DB supports CONCAT function with two args but also supports concat operator, then operator (eg ||) will be used
+     * - if DB supports only CONCAT function with two args then it will be used what can lead to SQL exception if
+     * used with more than two arguments
+     *
+     * Currently only known DB with limited concatenation functionality is Openbase.
+     *
+     * @param expressions array of expressions
+     * @return CONCAT() call expression
+     */
+    public static Expression concatExp(Expression... expressions) {
+        if(expressions == null || expressions.length == 0) {
+            return new ASTConcat();
+        }
+
+        return new ASTConcat(expressions);
+    }
+
+    /**
+     * Factory method for expression to call CONCAT(string1, string2, ...) function
+     * Can be used like:
+     *  Expression concat = concatExp("property1", "property2");
+     *
+     * SQL generation note:
+     * - if DB supports CONCAT function with vararg then it will be used
+     * - if DB supports CONCAT function with two args but also supports concat operator, then operator (eg ||) will be used
+     * - if DB supports only CONCAT function with two args then it will be used what can lead to SQL exception if
+     * used with more than two arguments
+     *
+     * Currently only known DB with limited concatenation functionality is Openbase.
+     *
+     * @param paths array of paths
+     * @return CONCAT() call expression
+     */
+    public static Expression concatExp(String... paths) {
+        if(paths == null || paths.length == 0) {
+            return new ASTConcat();
+        }
+
+        Expression[] expressions = new Expression[paths.length];
+        for(int i=0; i<paths.length; i++) {
+            expressions[i] = new ASTObjPath(paths[i]);
+        }
+        return new ASTConcat(expressions);
+    }
+}


[2/3] cayenne git commit: CAY-2191 and CAY-2188 - expressions in Property class - explicit type in Property class and factory methods - string and math functions from JPA standard with support for specific DBs - factory methods for new function call

Posted by nt...@apache.org.
http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
index a340b50..fe963b1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.exp;
 
-import org.apache.cayenne.exp.parser.ASTObjPath;
 import org.apache.cayenne.query.Ordering;
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.SortOrder;
@@ -33,16 +32,19 @@ import java.util.List;
  * A property in a DataObject.
  * </p>
  * <p>
- * <p>
- * Used to construct Expressions quickly and with type-safety, and to construct
- * Orderings
+ * Used to construct Expressions quickly and with type-safety, and to construct Orderings
  * </p>
  * <p>
- * <p>
  * Instances of this class are immutable
+ * Construct via factory methods Property.create(..)
  * </p>
  *
  * @param <E> The type this property returns.
+ *
+ * @see Property#create(String, Class)
+ * @see Property#create(Expression, Class)
+ * @see Property#create(String, Expression, Class)
+ *
  * @since 4.0
  */
 public class Property<E> {
@@ -53,10 +55,67 @@ public class Property<E> {
     private final String name;
 
     /**
+     * Expression provider for the property
+     * @since 4.0
+     */
+    private final ExpressionProvider expressionProvider;
+
+    /**
+     * Explicit type of the property
+     * @since 4.0
+     */
+    private final Class<? super E> type;
+
+    /**
      * Constructs a new property with the given name.
+     *
+     * @param name name of the property (usually it's obj path)
+     *
+     * @see Property#create(String, Class)
+     * @deprecated use factory method Property.create("propertyName", PropertyType.class)
+     */
+    public Property(final String name) {
+        this(name, null);
+    }
+
+    /**
+     * Constructs a new property with the given name and type.
+     *
+     * @param name of the property (usually it's obj path)
+     * @param type of the property
+     *
+     * @see Property#create(String, Class)
+     */
+    protected Property(final String name, final Class<? super E> type) {
+        this.name = name;
+        expressionProvider = new ExpressionProvider() {
+            @Override
+            public Expression get() {
+                return ExpressionFactory.pathExp(name);
+            }
+        };
+        this.type = type;
+    }
+
+    /**
+     * Constructs a new property with the given name and expression
+     *
+     * @param name of the property (will be used as alias for the expression)
+     * @param expression expression for property
+     * @param type of the property
+     * @since 4.0
+     *
+     * @see Property#create(String, Expression, Class)
      */
-    public Property(String name) {
+    protected Property(final String name, final Expression expression, final Class<? super E> type) {
         this.name = name;
+        expressionProvider = new ExpressionProvider() {
+            @Override
+            public Expression get() {
+                return expression.deepCopy();
+            }
+        };
+        this.type = type;
     }
 
     /**
@@ -66,6 +125,13 @@ public class Property<E> {
         return name;
     }
 
+    /**
+     * @since 4.0
+     */
+    public Expression getExpression() {
+        return expressionProvider.get();
+    }
+
     @Override
     public int hashCode() {
         return getName().hashCode();
@@ -82,7 +148,7 @@ public class Property<E> {
      * @return a newly created Property object.
      */
     public Property<Object> dot(String property) {
-        return new Property<Object>(getName() + "." + property);
+        return create(getName() + "." + property, null);
     }
 
     /**
@@ -91,7 +157,7 @@ public class Property<E> {
      * @return a newly created Property object.
      */
     public <T> Property<T> dot(Property<T> property) {
-        return new Property<T>(getName() + "." + property.getName());
+        return create(getName() + "." + property.getName(), property.getType());
     }
 
     /**
@@ -100,7 +166,7 @@ public class Property<E> {
      * as "outer" attributes make no sense.
      */
     public Property<E> outer() {
-        return isOuter() ? this : new Property<E>(name + "+");
+        return isOuter() ? this : create(name + "+", type);
     }
 
     private boolean isOuter() {
@@ -109,46 +175,48 @@ public class Property<E> {
 
     /**
      * Converts this property to a path expression.
+     * This method is equivalent of getExpression() which is preferred as more generic.
      *
      * @return a newly created expression.
+     * @see Property#getExpression()
      */
     public Expression path() {
-        return ExpressionFactory.pathExp(getName());
+        return getExpression();
     }
 
     /**
      * @return An expression representing null.
      */
     public Expression isNull() {
-        return ExpressionFactory.matchExp(getName(), null);
+        return ExpressionFactory.matchExp(getExpression(), null);
     }
 
     /**
      * @return An expression representing a non-null value.
      */
     public Expression isNotNull() {
-        return ExpressionFactory.matchExp(getName(), null).notExp();
+        return ExpressionFactory.matchExp(getExpression(), null).notExp();
     }
 
     /**
      * @return An expression representing equality to TRUE.
      */
     public Expression isTrue() {
-        return ExpressionFactory.matchExp(getName(), Boolean.TRUE);
+        return ExpressionFactory.matchExp(getExpression(), Boolean.TRUE);
     }
 
     /**
      * @return An expression representing equality to FALSE.
      */
     public Expression isFalse() {
-        return ExpressionFactory.matchExp(getName(), Boolean.FALSE);
+        return ExpressionFactory.matchExp(getExpression(), Boolean.FALSE);
     }
 
     /**
      * @return An expression representing equality to a value.
      */
     public Expression eq(E value) {
-        return ExpressionFactory.matchExp(getName(), value);
+        return ExpressionFactory.matchExp(getExpression(), value);
     }
 
     /**
@@ -156,14 +224,14 @@ public class Property<E> {
      * (columns).
      */
     public Expression eq(Property<?> value) {
-        return ExpressionFactory.matchExp(getName(), new ASTObjPath(value.getName()));
+        return ExpressionFactory.matchExp(getExpression(), value.getExpression());
     }
 
     /**
      * @return An expression representing inequality to a value.
      */
     public Expression ne(E value) {
-        return ExpressionFactory.noMatchExp(getName(), value);
+        return ExpressionFactory.noMatchExp(getExpression(), value);
     }
 
     /**
@@ -171,7 +239,7 @@ public class Property<E> {
      * (columns).
      */
     public Expression ne(Property<?> value) {
-        return ExpressionFactory.noMatchExp(getName(), new ASTObjPath(value.getName()));
+        return ExpressionFactory.noMatchExp(getExpression(), value.getExpression());
     }
 
     /**
@@ -183,7 +251,7 @@ public class Property<E> {
      * @return An expression for a Database "LIKE" query.
      */
     public Expression like(String pattern) {
-        return ExpressionFactory.likeExp(getName(), pattern);
+        return ExpressionFactory.likeExp(getExpression(), pattern);
     }
 
     /**
@@ -194,28 +262,28 @@ public class Property<E> {
      * @return An expression for a Database "LIKE" query.
      */
     public Expression like(String pattern, char escapeChar) {
-        return ExpressionFactory.likeExp(getName(), pattern, escapeChar);
+        return ExpressionFactory.likeExp(getExpression(), pattern, escapeChar);
     }
 
     /**
      * @return An expression for a case insensitive "LIKE" query.
      */
     public Expression likeIgnoreCase(String pattern) {
-        return ExpressionFactory.likeIgnoreCaseExp(getName(), pattern);
+        return ExpressionFactory.likeIgnoreCaseExp(getExpression(), pattern);
     }
 
     /**
      * @return An expression for a Database "NOT LIKE" query.
      */
     public Expression nlike(String value) {
-        return ExpressionFactory.notLikeExp(getName(), value);
+        return ExpressionFactory.notLikeExp(getExpression(), value);
     }
 
     /**
      * @return An expression for a case insensitive "NOT LIKE" query.
      */
     public Expression nlikeIgnoreCase(String value) {
-        return ExpressionFactory.notLikeIgnoreCaseExp(getName(), value);
+        return ExpressionFactory.notLikeIgnoreCaseExp(getExpression(), value);
     }
 
     /**
@@ -228,7 +296,7 @@ public class Property<E> {
      * @return a newly created expression.
      */
     public Expression contains(String substring) {
-        return ExpressionFactory.containsExp(getName(), substring);
+        return ExpressionFactory.containsExp(getExpression(), substring);
     }
 
     /**
@@ -241,7 +309,7 @@ public class Property<E> {
      * @return a newly created expression.
      */
     public Expression startsWith(String value) {
-        return ExpressionFactory.startsWithExp(getName(), value);
+        return ExpressionFactory.startsWithExp(getExpression(), value);
     }
 
     /**
@@ -254,7 +322,7 @@ public class Property<E> {
      * @return a newly created expression.
      */
     public Expression endsWith(String value) {
-        return ExpressionFactory.endsWithExp(getName(), value);
+        return ExpressionFactory.endsWithExp(getExpression(), value);
     }
 
     /**
@@ -262,7 +330,7 @@ public class Property<E> {
      * comparison.
      */
     public Expression containsIgnoreCase(String value) {
-        return ExpressionFactory.containsIgnoreCaseExp(getName(), value);
+        return ExpressionFactory.containsIgnoreCaseExp(getExpression(), value);
     }
 
     /**
@@ -270,7 +338,7 @@ public class Property<E> {
      * comparison.
      */
     public Expression startsWithIgnoreCase(String value) {
-        return ExpressionFactory.startsWithIgnoreCaseExp(getName(), value);
+        return ExpressionFactory.startsWithIgnoreCaseExp(getExpression(), value);
     }
 
     /**
@@ -278,7 +346,7 @@ public class Property<E> {
      * comparison.
      */
     public Expression endsWithIgnoreCase(String value) {
-        return ExpressionFactory.endsWithIgnoreCaseExp(getName(), value);
+        return ExpressionFactory.endsWithIgnoreCaseExp(getExpression(), value);
     }
 
     /**
@@ -288,7 +356,7 @@ public class Property<E> {
      * bound inclusive
      */
     public Expression between(E lower, E upper) {
-        return ExpressionFactory.betweenExp(getName(), lower, upper);
+        return ExpressionFactory.betweenExp(getExpression(), lower, upper);
     }
 
     /**
@@ -305,7 +373,7 @@ public class Property<E> {
             System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
         }
 
-        return ExpressionFactory.inExp(getName(), values);
+        return ExpressionFactory.inExp(getExpression(), values);
     }
 
     /**
@@ -323,14 +391,14 @@ public class Property<E> {
             System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
         }
 
-        return ExpressionFactory.notInExp(getName(), values);
+        return ExpressionFactory.notInExp(getExpression(), values);
     }
 
     /**
      * @return An expression for finding objects with values in the given set.
      */
     public Expression in(Collection<E> values) {
-        return ExpressionFactory.inExp(getName(), values);
+        return ExpressionFactory.inExp(getExpression(), values);
     }
 
     /**
@@ -338,14 +406,14 @@ public class Property<E> {
      * set.
      */
     public Expression nin(Collection<E> values) {
-        return ExpressionFactory.notInExp(getName(), values);
+        return ExpressionFactory.notInExp(getExpression(), values);
     }
 
     /**
      * @return A greater than Expression.
      */
     public Expression gt(E value) {
-        return ExpressionFactory.greaterExp(getName(), value);
+        return ExpressionFactory.greaterExp(getExpression(), value);
     }
 
     /**
@@ -353,14 +421,14 @@ public class Property<E> {
      * (columns).
      */
     public Expression gt(Property<?> value) {
-        return ExpressionFactory.greaterExp(getName(), new ASTObjPath(value.getName()));
+        return ExpressionFactory.greaterExp(getExpression(), value.getExpression());
     }
 
     /**
      * @return A greater than or equal to Expression.
      */
     public Expression gte(E value) {
-        return ExpressionFactory.greaterOrEqualExp(getName(), value);
+        return ExpressionFactory.greaterOrEqualExp(getExpression(), value);
     }
 
     /**
@@ -368,14 +436,14 @@ public class Property<E> {
      * attributes (columns).
      */
     public Expression gte(Property<?> value) {
-        return ExpressionFactory.greaterOrEqualExp(getName(), new ASTObjPath(value.getName()));
+        return ExpressionFactory.greaterOrEqualExp(getExpression(), value.getExpression());
     }
 
     /**
      * @return A less than Expression.
      */
     public Expression lt(E value) {
-        return ExpressionFactory.lessExp(getName(), value);
+        return ExpressionFactory.lessExp(getExpression(), value);
     }
 
     /**
@@ -383,14 +451,14 @@ public class Property<E> {
      * (columns).
      */
     public Expression lt(Property<?> value) {
-        return ExpressionFactory.lessExp(getName(), new ASTObjPath(value.getName()));
+        return ExpressionFactory.lessExp(getExpression(), value.getExpression());
     }
 
     /**
      * @return A less than or equal to Expression.
      */
     public Expression lte(E value) {
-        return ExpressionFactory.lessOrEqualExp(getName(), value);
+        return ExpressionFactory.lessOrEqualExp(getExpression(), value);
     }
 
     /**
@@ -398,7 +466,7 @@ public class Property<E> {
      * attributes (columns).
      */
     public Expression lte(Property<?> value) {
-        return ExpressionFactory.lessOrEqualExp(getName(), new ASTObjPath(value.getName()));
+        return ExpressionFactory.lessOrEqualExp(getExpression(), value.getExpression());
     }
 
     /**
@@ -471,7 +539,7 @@ public class Property<E> {
      * prefetch semantics.
      */
     public PrefetchTreeNode joint() {
-        return PrefetchTreeNode.withPath(name, PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+        return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
     }
 
     /**
@@ -480,7 +548,7 @@ public class Property<E> {
      * "disjoint" prefetch semantics.
      */
     public PrefetchTreeNode disjoint() {
-        return PrefetchTreeNode.withPath(name, PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+        return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
     }
 
     /**
@@ -489,7 +557,7 @@ public class Property<E> {
      * "disjoint by id" prefetch semantics.
      */
     public PrefetchTreeNode disjointById() {
-        return PrefetchTreeNode.withPath(name, PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
+        return PrefetchTreeNode.withPath(getName(), PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
     }
 
     /**
@@ -534,4 +602,27 @@ public class Property<E> {
         }
     }
 
+    public Property<E> alias(String alias) {
+        return new Property<>(alias, this.getExpression(), this.getType());
+    }
+
+    public Class<? super E> getType() {
+        return type;
+    }
+
+    public static <T> Property<T> create(String name, Class<? super T> type) {
+        return new Property<>(name, type);
+    }
+
+    public static <T> Property<T> create(Expression expression, Class<? super T> type) {
+        return new Property<>(expression.expName().toLowerCase(), expression, type);
+    }
+
+    public static <T> Property<T> create(String name, Expression expression, Class<? super T> type) {
+        return new Property<>(name, expression, type);
+    }
+
+    private interface ExpressionProvider {
+        Expression get();
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAbs.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAbs.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAbs.java
new file mode 100644
index 0000000..47b06dc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAbs.java
@@ -0,0 +1,48 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTAbs extends ASTFunctionCall {
+
+    ASTAbs(int id) {
+        super(id, "ABS");
+    }
+
+    public ASTAbs(Expression expression) {
+        super("ABS", expression);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        double n = ConversionUtil.toDouble(evaluateChild(0, o), 0.0);
+        return Math.abs(n);
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTAbs(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
index 539991c..38d2cba 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTBetween.java
@@ -38,7 +38,7 @@ public class ASTBetween extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTBETWEEN);
 	}
 
-	public ASTBetween(ASTPath path, Object value1, Object value2) {
+	public ASTBetween(SimpleNode path, Object value1, Object value2) {
 		super(ExpressionParserTreeConstants.JJTBETWEEN);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value1), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTConcat.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTConcat.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTConcat.java
new file mode 100644
index 0000000..efb11ad
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTConcat.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTConcat extends ASTFunctionCall {
+
+    ASTConcat(int id) {
+        super(id, "CONCAT");
+    }
+
+    public ASTConcat(Expression... expressions) {
+        super("CONCAT", expressions);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        StringBuilder sb = new StringBuilder();
+        for(int i=0; i<getOperandCount(); i++) {
+            sb.append(ConversionUtil.toString(evaluateChild(i, o)));
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTConcat(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
index 579545d..e2194d0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
@@ -51,7 +51,7 @@ public class ASTEqual extends ConditionNode implements ValueInjector {
 	/**
 	 * Creates "Equal To" expression.
 	 */
-	public ASTEqual(ASTPath path, Object value) {
+	public ASTEqual(SimpleNode path, Object value) {
 		super(ExpressionParserTreeConstants.JJTEQUAL);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFunctionCall.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFunctionCall.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFunctionCall.java
new file mode 100644
index 0000000..9c3aa07
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFunctionCall.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+
+/**
+ * @since 4.0
+ */
+public abstract class ASTFunctionCall extends SimpleNode {
+
+    private String functionName;
+
+    ASTFunctionCall(int id, String functionName) {
+        super(id);
+        this.functionName = functionName;
+    }
+
+    public ASTFunctionCall(String functionName, Object... nodes) {
+        this(0, functionName);
+        this.functionName = functionName;
+        int len = nodes.length;
+        for (int i = 0; i < len; i++) {
+            jjtAddChild(wrapChild(nodes[i]), i);
+        }
+
+        connectChildren();
+    }
+
+    @Override
+    public int getType() {
+        return Expression.FUNCTION_CALL;
+    }
+
+    public String getFunctionName() {
+        return functionName;
+    }
+
+    /**
+     * TODO what should this method return?
+     */
+    @Override
+    protected String getExpressionOperator(int index) {
+        return functionName;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
index f990e24..c7eb1f1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreater.java
@@ -39,7 +39,7 @@ public class ASTGreater extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTGREATER);
 	}
 
-	public ASTGreater(ASTPath path, Object value) {
+	public ASTGreater(SimpleNode path, Object value) {
 		super(ExpressionParserTreeConstants.JJTGREATER);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
index 22d9e97..a970c12 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTGreaterOrEqual.java
@@ -41,7 +41,7 @@ public class ASTGreaterOrEqual extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTGREATEROREQUAL);
 	}
 
-	public ASTGreaterOrEqual(ASTPath path, Object value) {
+	public ASTGreaterOrEqual(SimpleNode path, Object value) {
 		super(ExpressionParserTreeConstants.JJTGREATEROREQUAL);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
index 10d7dc7..44ced07 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java
@@ -43,7 +43,7 @@ public class ASTIn extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTIN);
 	}
 
-	public ASTIn(ASTPath path, ASTList list) {
+	public ASTIn(SimpleNode path, ASTList list) {
 		super(ExpressionParserTreeConstants.JJTIN);
 		jjtAddChild(path, 0);
 		jjtAddChild(list, 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLength.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLength.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLength.java
new file mode 100644
index 0000000..5d1eea8
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLength.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTLength extends ASTFunctionCall {
+
+    ASTLength(int id) {
+        super(id, "LENGTH");
+    }
+
+    public ASTLength(Expression expression) {
+        super("LENGTH", expression);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        String s1 = ConversionUtil.toString(evaluateChild(0, o));
+        if (s1 == null) {
+            return null;
+        }
+        return s1.length();
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTLength(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
index 5a44f62..5880f93 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLess.java
@@ -41,7 +41,7 @@ public class ASTLess extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTLESS);
 	}
 
-	public ASTLess(ASTPath path, Object value) {
+	public ASTLess(SimpleNode path, Object value) {
 		super(ExpressionParserTreeConstants.JJTLESS);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
index 9065631..d686797 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLessOrEqual.java
@@ -41,7 +41,7 @@ public class ASTLessOrEqual extends ConditionNode {
 		super(ExpressionParserTreeConstants.JJTLESSOREQUAL);
 	}
 
-	public ASTLessOrEqual(ASTPath path, Object value) {
+	public ASTLessOrEqual(SimpleNode path, Object value) {
 		super(ExpressionParserTreeConstants.JJTLESSOREQUAL);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
index 0799ccf..31afa92 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLike.java
@@ -41,14 +41,14 @@ public class ASTLike extends PatternMatchNode {
 		super(ExpressionParserTreeConstants.JJTLIKE, false);
 	}
 
-	public ASTLike(ASTPath path, Object pattern) {
+	public ASTLike(SimpleNode path, Object pattern) {
 		super(ExpressionParserTreeConstants.JJTLIKE, false);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(pattern), 1);
 		connectChildren();
 	}
 
-	public ASTLike(ASTPath path, Object pattern, char escapeChar) {
+	public ASTLike(SimpleNode path, Object pattern, char escapeChar) {
 		super(ExpressionParserTreeConstants.JJTLIKE, false, escapeChar);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(pattern), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
index 44cd9f2..c8be6a0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLikeIgnoreCase.java
@@ -42,14 +42,14 @@ public class ASTLikeIgnoreCase extends IgnoreCaseNode {
 		super(ExpressionParserTreeConstants.JJTLIKEIGNORECASE, true);
 	}
 
-	public ASTLikeIgnoreCase(ASTPath path, Object pattern) {
+	public ASTLikeIgnoreCase(SimpleNode path, Object pattern) {
 		super(ExpressionParserTreeConstants.JJTLIKEIGNORECASE, true);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(pattern), 1);
 		connectChildren();
 	}
 
-	public ASTLikeIgnoreCase(ASTPath path, Object pattern, char escapeChar) {
+	public ASTLikeIgnoreCase(SimpleNode path, Object pattern, char escapeChar) {
 		super(ExpressionParserTreeConstants.JJTLIKEIGNORECASE, true, escapeChar);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(pattern), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLocate.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLocate.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLocate.java
new file mode 100644
index 0000000..1c537e5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLocate.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTLocate extends ASTFunctionCall {
+
+    ASTLocate(int id) {
+        super(id, "LOCATE");
+    }
+
+    public ASTLocate(Expression substring, Expression path) {
+        super("LOCATE", substring, path);
+    }
+
+    public ASTLocate(Expression substring, Expression path, Expression offset) {
+        super("LOCATE", substring, path, offset);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        int len = jjtGetNumChildren();
+        if (len < 2) {
+            return 0L;
+        }
+
+        String substr = ConversionUtil.toString(evaluateChild(0, o));
+        String str = ConversionUtil.toString(evaluateChild(1, o));
+        int offset = 0;
+        if(len > 2) {
+            offset = ConversionUtil.toInt(evaluateChild(2, o), 0);
+        }
+
+        // +1 to comply with SQL
+        return str.indexOf(substr, offset) + 1;
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTLocate(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLower.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLower.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLower.java
new file mode 100644
index 0000000..b63319a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTLower.java
@@ -0,0 +1,52 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTLower extends ASTFunctionCall {
+
+
+    ASTLower(int id) {
+        super(id, "LOWER");
+    }
+
+    public ASTLower(Expression expression) {
+        super("LOWER", expression);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        String s1 = ConversionUtil.toString(evaluateChild(0, o));
+        if (s1 == null) {
+            return null;
+        }
+        return s1.toLowerCase();
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTLower(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMod.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMod.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMod.java
new file mode 100644
index 0000000..f951cb7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMod.java
@@ -0,0 +1,52 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTMod extends ASTFunctionCall {
+
+    ASTMod(int id) {
+        super(id, "MOD");
+    }
+
+    public ASTMod(Expression expression, Expression divisor) {
+        super("MOD", expression, divisor);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        double x = ConversionUtil.toDouble(evaluateChild(0, o), 0.0);
+        double y = ConversionUtil.toDouble(evaluateChild(1, o), 0.0);
+        if(y == 0.0) {
+            return 0.0;
+        }
+        return x % y;
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTMod(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
index 9b0b013..000e5ee 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotBetween.java
@@ -34,7 +34,7 @@ public class ASTNotBetween extends ConditionNode {
         super(ExpressionParserTreeConstants.JJTNOTBETWEEN);
     }
 
-    public ASTNotBetween(ASTPath path, Object value1, Object value2) {
+    public ASTNotBetween(SimpleNode path, Object value1, Object value2) {
         super(ExpressionParserTreeConstants.JJTNOTBETWEEN);
         jjtAddChild(path, 0);
         jjtAddChild(new ASTScalar(value1), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
index 8a83a86..1ec687b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotEqual.java
@@ -38,7 +38,7 @@ public class ASTNotEqual extends ConditionNode {
     /**
      * Creates "Not Equal To" expression.
      */
-    public ASTNotEqual(ASTPath path, Object value) {
+    public ASTNotEqual(SimpleNode path, Object value) {
         super(ExpressionParserTreeConstants.JJTNOTEQUAL);
         jjtAddChild(path, 0);
         jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
index 373280b..ea82c38 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java
@@ -36,7 +36,7 @@ public class ASTNotIn extends ConditionNode {
         super(ExpressionParserTreeConstants.JJTNOTIN);
     }
 
-    public ASTNotIn(ASTPath path, ASTList list) {
+    public ASTNotIn(SimpleNode path, ASTList list) {
         super(ExpressionParserTreeConstants.JJTNOTIN);
         jjtAddChild(path, 0);
         jjtAddChild(list, 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
index 2278143..da0fd76 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLike.java
@@ -37,14 +37,14 @@ public class ASTNotLike extends PatternMatchNode {
         super(ExpressionParserTreeConstants.JJTNOTLIKE, false);
     }
 
-    public ASTNotLike(ASTPath path, Object value) {
+    public ASTNotLike(SimpleNode path, Object value) {
         super(ExpressionParserTreeConstants.JJTNOTLIKE, false);
         jjtAddChild(path, 0);
         jjtAddChild(new ASTScalar(value), 1);
         connectChildren();
     }
 
-    public ASTNotLike(ASTPath path, Object value, char escapeChar) {
+    public ASTNotLike(SimpleNode path, Object value, char escapeChar) {
         super(ExpressionParserTreeConstants.JJTNOTLIKE, false, escapeChar);
         jjtAddChild(path, 0);
         jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
index 8317775..f6f6d11 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTNotLikeIgnoreCase.java
@@ -38,14 +38,14 @@ public class ASTNotLikeIgnoreCase extends IgnoreCaseNode {
 		super(ExpressionParserTreeConstants.JJTNOTLIKEIGNORECASE, true);
 	}
 
-	public ASTNotLikeIgnoreCase(ASTPath path, Object value) {
+	public ASTNotLikeIgnoreCase(SimpleNode path, Object value) {
 		super(ExpressionParserTreeConstants.JJTNOTLIKEIGNORECASE, true);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);
 		connectChildren();
 	}
 
-	public ASTNotLikeIgnoreCase(ASTPath path, Object value, char escapeChar) {
+	public ASTNotLikeIgnoreCase(SimpleNode path, Object value, char escapeChar) {
 		super(ExpressionParserTreeConstants.JJTNOTLIKEIGNORECASE, true, escapeChar);
 		jjtAddChild(path, 0);
 		jjtAddChild(new ASTScalar(value), 1);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSqrt.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSqrt.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSqrt.java
new file mode 100644
index 0000000..d311444
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSqrt.java
@@ -0,0 +1,48 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTSqrt extends ASTFunctionCall {
+
+    ASTSqrt(int id) {
+        super(id, "SQRT");
+    }
+
+    public ASTSqrt(Expression expression) {
+        super("SQRT", expression);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        double n = ConversionUtil.toDouble(evaluateChild(0, o), 0.0);
+        return Math.sqrt(n);
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTSqrt(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubstring.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubstring.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubstring.java
new file mode 100644
index 0000000..c6ae864
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTSubstring.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTSubstring extends ASTFunctionCall {
+
+
+    ASTSubstring(int id) {
+        super(id, "SUBSTRING");
+    }
+
+    public ASTSubstring(Expression path, Expression length, Expression offset) {
+        super("SUBSTRING", path, length, offset);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        int len = jjtGetNumChildren();
+        if (len != 3) {
+            return null;
+        }
+
+        String s1 = ConversionUtil.toString(evaluateChild(0, o));
+        if (s1 == null) {
+            return null;
+        }
+
+        int offset = ConversionUtil.toInt(evaluateChild(1, o), 0);
+        int length = ConversionUtil.toInt(evaluateChild(2, o), 0);
+        if(length == 0) {
+            return null;
+        }
+
+        return s1.substring(offset, offset + length);
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTSubstring(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrim.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrim.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrim.java
new file mode 100644
index 0000000..2a0077a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTTrim.java
@@ -0,0 +1,62 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTTrim extends ASTFunctionCall {
+
+    ASTTrim(int id) {
+        super(id, "TRIM");
+    }
+
+    public ASTTrim(Expression path) {
+        super("TRIM", path);
+    }
+
+    @Override
+    protected String getExpressionOperator(int index) {
+        return null;
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        String s1 = ConversionUtil.toString(evaluateChild(0, o));
+        if (s1 == null) {
+            return null;
+        }
+
+        return s1.trim();
+    }
+
+    @Override
+    public int getType() {
+        return Expression.FUNCTION_CALL;
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTTrim(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTUpper.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTUpper.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTUpper.java
new file mode 100644
index 0000000..fad6026
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTUpper.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.util.ConversionUtil;
+
+/**
+ * @since 4.0
+ */
+public class ASTUpper extends ASTFunctionCall {
+
+    ASTUpper(int id) {
+        super(id, "UPPER");
+    }
+
+    public ASTUpper(Expression expression) {
+        super("UPPER", expression);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        String s1 = ConversionUtil.toString(evaluateChild(0, o));
+        if (s1 == null) {
+            return null;
+        }
+        return s1.toUpperCase();
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTUpper(id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java
index 458afb4..cbe9e16 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java
@@ -326,10 +326,8 @@ public abstract class SimpleNode extends Expression implements Node {
 	}
 
 	protected Node wrapChild(Object child) {
-		// when child is null, there's no way of telling whether this is a
-		// scalar or
-		// not... fuzzy... maybe we should stop using this method - it is too
-		// generic
+		// when child is null, there's no way of telling whether this is a scalar or not... fuzzy...
+		// maybe we should stop using this method - it is too generic
 		return (child instanceof Node || child == null) ? (Node) child : new ASTScalar(child);
 	}
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/main/java/org/apache/cayenne/util/ConversionUtil.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ConversionUtil.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ConversionUtil.java
index 81729de..69f00aa 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/ConversionUtil.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ConversionUtil.java
@@ -30,20 +30,17 @@ import org.apache.cayenne.exp.ExpressionException;
  * 
  * @since 1.1
  */
-public class ConversionUtil {
+public final class ConversionUtil {
 
     public static int toInt(Object object, int defaultValue) {
         if (object == null) {
             return defaultValue;
-        }
-        else if (object instanceof Number) {
+        } else if (object instanceof Number) {
             return ((Number) object).intValue();
-        }
-        else if (object instanceof String) {
+        } else if (object instanceof String) {
             try {
                 return Integer.parseInt((String) object);
-            }
-            catch (NumberFormatException ex) {
+            } catch (NumberFormatException ex) {
                 return defaultValue;
             }
         }
@@ -57,15 +54,31 @@ public class ConversionUtil {
     public static long toLong(Object object, long defaultValue) {
         if (object == null) {
             return defaultValue;
-        }
-        else if (object instanceof Number) {
+        } else if (object instanceof Number) {
             return ((Number) object).longValue();
-        }
-        else if (object instanceof String) {
+        } else if (object instanceof String) {
             try {
                 return Long.parseLong((String) object);
+            } catch (NumberFormatException ex) {
+                return defaultValue;
             }
-            catch (NumberFormatException ex) {
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * @since 4.0
+     */
+    public static double toDouble(Object object, double defaultValue) {
+        if (object == null) {
+            return defaultValue;
+        } else if (object instanceof Number) {
+            return ((Number) object).doubleValue();
+        } else if (object instanceof String) {
+            try {
+                return Double.parseDouble((String) object);
+            } catch (NumberFormatException ex) {
                 return defaultValue;
             }
         }
@@ -89,14 +102,11 @@ public class ConversionUtil {
 
         if (object == null) {
             return null;
-        }
-        else if (object instanceof BigDecimal) {
+        } else if (object instanceof BigDecimal) {
             return (BigDecimal) object;
-        }
-        else if (object instanceof BigInteger) {
+        } else if (object instanceof BigInteger) {
             return new BigDecimal((BigInteger) object);
-        }
-        else if (object instanceof Number) {
+        } else if (object instanceof Number) {
             return new BigDecimal(((Number) object).doubleValue());
         }
 
@@ -109,20 +119,15 @@ public class ConversionUtil {
     public static Comparable toComparable(Object object) {
         if (object == null) {
             return null;
-        }
-        else if (object instanceof Comparable) {
+        } else if (object instanceof Comparable) {
             return (Comparable) object;
-        }
-        else if (object instanceof StringBuilder) {
+        } else if (object instanceof StringBuilder) {
             return object.toString();
-        }
-        else if (object instanceof StringBuffer) {
+        } else if (object instanceof StringBuffer) {
             return object.toString();
-        }
-        else if (object instanceof char[]) {
+        } else if (object instanceof char[]) {
             return new String((char[]) object);
-        }
-        else {
+        } else {
             throw new ClassCastException(
                 "Invalid Comparable class:" + object.getClass().getName());
         }
@@ -134,17 +139,13 @@ public class ConversionUtil {
     public static String toString(Object object) {
         if (object == null) {
             return null;
-        }
-        else if (object instanceof String) {
+        } else if (object instanceof String) {
             return (String) object;
-        }
-        else if (object instanceof StringBuffer) {
+        } else if (object instanceof StringBuffer) {
             return object.toString();
-        }
-        else if (object instanceof char[]) {
+        } else if (object instanceof char[]) {
             return new String((char[]) object);
-        }
-        else {
+        } else {
             throw new ClassCastException(
                 "Invalid class for String conversion:" + object.getClass().getName());
         }
@@ -156,11 +157,9 @@ public class ConversionUtil {
     public static Object toUpperCase(Object object) {
         if ((object instanceof String) || (object instanceof StringBuffer)) {
             return object.toString().toUpperCase();
-        }
-        else if (object instanceof char[]) {
+        } else if (object instanceof char[]) {
             return new String((char[]) object).toUpperCase();
-        }
-        else {
+        } else {
             return object;
         }
     }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
index 5671830..9db81a1 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
@@ -29,8 +29,11 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.FunctionExpressionFactory;
+import org.apache.cayenne.exp.Property;
 import org.apache.cayenne.query.MockQuery;
 import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.testdo.testmap.Exhibit;
 import org.apache.cayenne.testdo.testmap.Gallery;
 import org.apache.cayenne.testdo.testmap.Painting;
@@ -132,6 +135,23 @@ public class QualifierTranslatorIT extends ServerCase {
 		doExpressionTest(Exhibit.class, e2, "(ta.GALLERY_ID = ?) OR (ta.GALLERY_ID = ?)");
 	}
 
+	@Test
+	public void testTrim() throws Exception {
+		Expression exp = FunctionExpressionFactory.trimExp(Artist.ARTIST_NAME.path());
+		Property<String> property = Property.create("trimmedName", exp, String.class);
+
+		doExpressionTest(Artist.class, property.like("P%"), "TRIM(ta.ARTIST_NAME) LIKE ?");
+	}
+
+	@Test
+	public void testConcat() throws Exception {
+		Expression exp = FunctionExpressionFactory.concatExp("artistName", "dateOfBirth");
+
+		Property<String> property = Property.create("concatNameAndDate", exp, String.class);
+
+		doExpressionTest(Artist.class, property.like("P%"), "CONCAT(ta.ARTIST_NAME, ta.DATE_OF_BIRTH) LIKE ?");
+	}
+
 	private void doExpressionTest(Class<?> queryType, String qualifier, String expectedSQL) throws Exception {
 		doExpressionTest(queryType, ExpressionFactory.exp(qualifier), expectedSQL);
 	}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java
new file mode 100644
index 0000000..ab3443e
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java
@@ -0,0 +1,195 @@
+/*****************************************************************
+ *   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.cayenne.exp;
+
+import org.apache.cayenne.exp.parser.ASTAbs;
+import org.apache.cayenne.exp.parser.ASTConcat;
+import org.apache.cayenne.exp.parser.ASTLength;
+import org.apache.cayenne.exp.parser.ASTLocate;
+import org.apache.cayenne.exp.parser.ASTLower;
+import org.apache.cayenne.exp.parser.ASTMod;
+import org.apache.cayenne.exp.parser.ASTScalar;
+import org.apache.cayenne.exp.parser.ASTSqrt;
+import org.apache.cayenne.exp.parser.ASTSubstring;
+import org.apache.cayenne.exp.parser.ASTTrim;
+import org.apache.cayenne.exp.parser.ASTUpper;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.0
+ */
+public class FunctionExpressionFactoryTest {
+
+    @Test
+    public void substringExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.path(), 10, 15);
+        Expression exp2 = FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.getName(), 10, 15);
+        Expression exp3 = FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.path(), new ASTScalar(10), new ASTScalar(15));
+
+        assertTrue(exp1 instanceof ASTSubstring);
+
+        assertEquals(3, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+        assertEquals(10, exp1.getOperand(1));
+        assertEquals(15, exp1.getOperand(2));
+
+        assertEquals(exp1, exp2);
+        assertEquals(exp2, exp3);
+    }
+
+    @Test
+    public void trimExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.trimExp(Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.trimExp(Artist.ARTIST_NAME.getName());
+
+        assertTrue(exp1 instanceof ASTTrim);
+
+        assertEquals(1, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+
+        assertEquals(exp1, exp2);
+    }
+
+    @Test
+    public void lowerExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.lowerExp(Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.lowerExp(Artist.ARTIST_NAME.getName());
+
+        assertTrue(exp1 instanceof ASTLower);
+
+        assertEquals(1, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+
+        assertEquals(exp1, exp2);
+    }
+
+    @Test
+    public void upperExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.upperExp(Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.upperExp(Artist.ARTIST_NAME.getName());
+
+        assertTrue(exp1 instanceof ASTUpper);
+
+        assertEquals(1, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+
+        assertEquals(exp1, exp2);
+    }
+
+    @Test
+    public void lengthExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.lengthExp(Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.lengthExp(Artist.ARTIST_NAME.getName());
+
+        assertTrue(exp1 instanceof ASTLength);
+
+        assertEquals(1, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+
+        assertEquals(exp1, exp2);
+    }
+
+
+    @Test
+    public void locateExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.locateExp("abc", Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.locateExp("abc", Artist.ARTIST_NAME.getName());
+        Expression exp3 = FunctionExpressionFactory.locateExp(new ASTScalar("abc"), Artist.ARTIST_NAME.path());
+
+        assertTrue(exp1 instanceof ASTLocate);
+
+        assertEquals(2, exp1.getOperandCount());
+        assertEquals("abc", exp1.getOperand(0));
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(1));
+
+        assertEquals(exp1, exp2);
+        assertEquals(exp2, exp3);
+    }
+
+
+    @Test
+    public void absExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.absExp(Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.absExp(Artist.ARTIST_NAME.getName());
+
+        assertTrue(exp1 instanceof ASTAbs);
+
+        assertEquals(1, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+
+        assertEquals(exp1, exp2);
+    }
+
+    @Test
+    public void sqrtExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.sqrtExp(Artist.ARTIST_NAME.path());
+        Expression exp2 = FunctionExpressionFactory.sqrtExp(Artist.ARTIST_NAME.getName());
+
+        assertTrue(exp1 instanceof ASTSqrt);
+
+        assertEquals(1, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+
+        assertEquals(exp1, exp2);
+    }
+
+
+    @Test
+    public void modExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.modExp(Artist.ARTIST_NAME.path(), 10);
+        Expression exp2 = FunctionExpressionFactory.modExp(Artist.ARTIST_NAME.getName(), 10);
+        Expression exp3 = FunctionExpressionFactory.modExp(Artist.ARTIST_NAME.path(), new ASTScalar(10));
+
+        assertTrue(exp1 instanceof ASTMod);
+
+        assertEquals(2, exp1.getOperandCount());
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+        assertEquals(10, exp1.getOperand(1));
+
+        assertEquals(exp1, exp2);
+        assertEquals(exp2, exp3);
+    }
+
+
+    @Test
+    public void concatExp() throws Exception {
+        Expression exp1 = FunctionExpressionFactory.concatExp(Artist.ARTIST_NAME.path(), new ASTScalar("abc"), Artist.DATE_OF_BIRTH.path());
+        assertTrue(exp1 instanceof ASTConcat);
+        assertEquals(3, exp1.getOperandCount());
+
+        assertEquals(Artist.ARTIST_NAME.path(), exp1.getOperand(0));
+        assertEquals("abc", exp1.getOperand(1));
+        assertEquals(Artist.DATE_OF_BIRTH.path(), exp1.getOperand(2));
+
+
+        Expression exp2 = FunctionExpressionFactory.concatExp(Artist.ARTIST_NAME.getName(), Artist.DATE_OF_BIRTH.getName(), Artist.PAINTING_ARRAY.getName());
+        assertTrue(exp2 instanceof ASTConcat);
+        assertEquals(3, exp2.getOperandCount());
+
+        assertEquals(Artist.ARTIST_NAME.path(), exp2.getOperand(0));
+        assertEquals(Artist.DATE_OF_BIRTH.path(), exp2.getOperand(1));
+        assertEquals(Artist.PAINTING_ARRAY.path(), exp2.getOperand(2));
+    }
+
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java
index 5984a94..757c4d6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/PropertyTest.java
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertTrue;
 import java.util.Arrays;
 import java.util.List;
 
+import org.apache.cayenne.exp.parser.ASTObjPath;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.reflect.TstJavaBean;
 import org.junit.Test;
@@ -223,4 +224,12 @@ public class PropertyTest {
         assertEquals("prop like \"%a#_!bc%\"", e.toString());
         assertEquals('#', ((PatternMatchNode) e).getEscapeChar());
     }
+
+    @Test
+    public void testExpressionConstructor() {
+        Property<Integer> p = Property.create("testPath", new ASTObjPath("test.path"), Integer.class);
+        assertEquals("testPath", p.getName());
+        Expression ex = p.getExpression();
+        assertEquals("test.path", ex.toString());
+    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTAbsTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTAbsTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTAbsTest.java
new file mode 100644
index 0000000..a8ec060
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTAbsTest.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.table_primitives.TablePrimitives;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.0
+ */
+public class ASTAbsTest {
+
+    @Test
+    public void evaluateNode() throws Exception {
+        ASTObjPath path = new ASTObjPath("intColumn");
+        ASTAbs abs = new ASTAbs(path);
+
+        TablePrimitives a = new TablePrimitives();
+        a.setIntColumn(-10);
+
+        Object res = abs.evaluateNode(a);
+        assertTrue(res instanceof Double);
+        assertEquals(10.0, res);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTConcatTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTConcatTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTConcatTest.java
new file mode 100644
index 0000000..0374aaa
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTConcatTest.java
@@ -0,0 +1,50 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.0
+ */
+public class ASTConcatTest {
+
+    @Test
+    public void testEvaluateConcat() throws Exception {
+
+        ASTObjPath path = new ASTObjPath("artistName");
+        ASTScalar scalar = new ASTScalar(" ");
+        ASTScalar scalar1 = new ASTScalar("test");
+
+        ASTConcat concat = new ASTConcat(path, scalar, scalar1);
+
+        Artist a = new Artist();
+        a.setArtistName("name");
+
+        Object res = concat.evaluateNode(a);
+        assertTrue(res instanceof String);
+        assertEquals("name test", res);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallMathIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallMathIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallMathIT.java
new file mode 100644
index 0000000..192a1ad
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallMathIT.java
@@ -0,0 +1,83 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.table_primitives.TablePrimitives;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * @since 4.0
+ */
+@UseServerRuntime(CayenneProjects.TABLE_PRIMITIVES_PROJECT)
+public class ASTFunctionCallMathIT extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    private TablePrimitives createPrimitives(int value) {
+        TablePrimitives primitives = context.newObject(TablePrimitives.class);
+        primitives.setIntColumn(value);
+        context.commitChanges();
+        return primitives;
+    }
+
+    @Test
+    public void testASTAbs() throws Exception {
+        TablePrimitives p1 = createPrimitives(-10);
+
+        ASTAbs exp = new ASTAbs(TablePrimitives.INT_COLUMN.path());
+        Property<Integer> intColumn = Property.create("intColumn", exp, Integer.class);
+
+        TablePrimitives p2 = ObjectSelect.query(TablePrimitives.class).where(intColumn.eq(10)).selectOne(context);
+        assertEquals(p1, p2);
+    }
+
+    @Test
+    public void testASTSqrt() throws Exception {
+        TablePrimitives p1 = createPrimitives(9);
+
+        ASTSqrt exp = new ASTSqrt(TablePrimitives.INT_COLUMN.path());
+        Property<Integer> intColumn = Property.create("intColumn", exp, Integer.class);
+
+        TablePrimitives p2 = ObjectSelect.query(TablePrimitives.class).where(intColumn.eq(3)).selectOne(context);
+        assertEquals(p1, p2);
+    }
+
+    @Test
+    public void testASTMod() throws Exception {
+        TablePrimitives p1 = createPrimitives(10);
+
+        ASTMod exp = new ASTMod(TablePrimitives.INT_COLUMN.path(), new ASTScalar((Integer)3));
+        Property<Integer> intColumn = Property.create("intColumn", exp, Integer.class);
+
+        TablePrimitives p2 = ObjectSelect.query(TablePrimitives.class).where(intColumn.eq(1)).selectOne(context);
+        assertEquals(p1, p2);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/803166c0/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
new file mode 100644
index 0000000..cbb7514
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
@@ -0,0 +1,155 @@
+/*****************************************************************
+ *   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.cayenne.exp.parser;
+
+import java.util.Date;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * @since 4.0
+ */
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ASTFunctionCallStringIT extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    private Artist createArtist(String name) throws Exception {
+        Artist a1 = context.newObject(Artist.class);
+        a1.setArtistName(name);
+        a1.setDateOfBirth(new Date());
+        context.commitChanges();
+        return a1;
+    }
+
+    @Test
+    public void testASTTrimInWhere() throws Exception {
+        Artist a1 = createArtist("  name  ");
+
+        ASTTrim exp = new ASTTrim(Artist.ARTIST_NAME.path());
+        Property<String> trimmedName = Property.create("trimmedName", exp, String.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(trimmedName.eq("name")).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+    @Test
+    public void testASTUpperInWhere() throws Exception {
+        Artist a1 = createArtist("name");
+
+        ASTUpper exp = new ASTUpper(Artist.ARTIST_NAME.path());
+        Property<String> upperName = Property.create("upperName", exp, String.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(upperName.eq("NAME")).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+    @Test
+    public void testASTLowerInWhere() throws Exception {
+        Artist a1 = createArtist("NAME");
+
+        ASTLower exp = new ASTLower(Artist.ARTIST_NAME.path());
+        Property<String> lowerName = Property.create("lowerName", exp, String.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(lowerName.eq("name")).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+    @Test
+    public void testASTSubstringInWhere() throws Exception {
+        Artist a1 = createArtist("1234567890xyz");
+
+        ASTSubstring exp = new ASTSubstring(Artist.ARTIST_NAME.path(), new ASTScalar((Integer)2), new ASTScalar((Integer)8));
+        Property<String> substrName = Property.create("substrName", exp, String.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(substrName.eq("23456789")).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+    @Test
+    public void testASTConcat() throws Exception {
+        Artist a1 = createArtist("Pablo");
+
+        ASTScalar scalar1 = new ASTScalar(" ");
+        ASTScalar scalar2 = new ASTScalar("Picasso");
+
+        ASTConcat exp = new ASTConcat(Artist.ARTIST_NAME.path(), scalar1, scalar2);
+        Property<String> concatName = Property.create("concatName", exp, String.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(concatName.eq("Pablo Picasso")).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+    @Test
+    public void testASTLength() throws Exception {
+        Artist a1 = createArtist("123456");
+
+        ASTLength exp = new ASTLength(Artist.ARTIST_NAME.path());
+        Property<Integer> nameLength = Property.create("nameLength", exp, Integer.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(nameLength.gt(5)).selectOne(context);
+        assertEquals(a1, a2);
+
+        Artist a3 = ObjectSelect.query(Artist.class).where(nameLength.lt(5)).selectOne(context);
+        assertEquals(null, a3);
+    }
+
+    @Test
+    public void testASTLocate() throws Exception {
+        Artist a1 = createArtist("1267834567890abc");
+
+        ASTScalar substr = new ASTScalar("678");
+//        ASTScalar offset = new ASTScalar((Integer)5); // not all DBs support offset parameter, so skip it
+        ASTLocate exp = new ASTLocate(substr, Artist.ARTIST_NAME.path());
+        Property<Integer> nameLoc = Property.create("nameLoc", exp, Integer.class);
+
+        Artist a2 = ObjectSelect.query(Artist.class).where(nameLoc.eq(3)).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+    @Test
+    public void testCombinedFunction() throws Exception {
+        Artist a1 = createArtist("absdefghij  klmnopq"); // substring with length 10 from 3 is "sdefghij  "
+
+        ASTSubstring substring = new ASTSubstring(
+                Artist.ARTIST_NAME.path(),
+                new ASTScalar((Integer)3),
+                new ASTScalar((Integer)10));
+        ASTTrim trim = new ASTTrim(substring);
+        ASTUpper upper = new ASTUpper(trim);
+        ASTConcat concat = new ASTConcat(upper, new ASTScalar(" "), new ASTScalar("test"));
+
+        Property<String> name = Property.create("substrName", concat, String.class);
+        Artist a2 = ObjectSelect.query(Artist.class).where(name.eq("SDEFGHIJ test")).selectOne(context);
+        assertEquals(a1, a2);
+    }
+
+}