http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
new file mode 100644
index 0000000..af548da
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindNotEqualDirective.java
@@ -0,0 +1,56 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.velocity.context.InternalContextAdapter;
+
+/**
+ * A custom Velocity directive to create a PreparedStatement parameter text 
for "<>?".
+ * If null value is encountered, generated text will look like "IS NOT NULL". 
Usage in
+ * Velocity template is "WHERE SOME_COLUMN #bindNotEqual($xyz)".
+ * 
+ * @since 1.1
+ */
+public class BindNotEqualDirective extends BindDirective {
+
+    @Override
+    public String getName() {
+        return "bindNotEqual";
+    }
+
+    @Override
+    protected void render(
+            InternalContextAdapter context,
+            Writer writer,
+            ParameterBinding binding) throws IOException {
+
+        if (binding.getValue() != null) {
+            bind(context, binding);
+            writer.write("<> ?");
+        }
+        else {
+            writer.write("IS NOT NULL");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
new file mode 100644
index 0000000..8394673
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectEqualDirective.java
@@ -0,0 +1,164 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.sql.Types;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.parser.node.Node;
+
+/**
+ * A custom Velocity directive to create a set of SQL conditions to match an 
ObjectId of
+ * an object. Usage in Velocity template is "WHERE #bindObjectEqual($object)" 
or "WHERE
+ * #bindObjectEqual($object $columns $idValues)".
+ * 
+ * @since 3.0
+ */
+public class BindObjectEqualDirective extends BindDirective {
+
+    @Override
+    public String getName() {
+        return "bindObjectEqual";
+    }
+
+    @Override
+    public boolean render(InternalContextAdapter context, Writer writer, Node 
node)
+            throws IOException, ResourceNotFoundException, ParseErrorException,
+            MethodInvocationException {
+
+        Object object = getChild(context, node, 0);
+        Map idMap = toIdMap(object);
+
+        Object sqlColumns = getChild(context, node, 1);
+        Object idColumns = getChild(context, node, 2);
+
+        if (idMap == null) {
+            // assume null object, and bind all null values
+
+            if (sqlColumns == null || idColumns == null) {
+                throw new ParseErrorException("Invalid parameters. "
+                        + "Either object has to be set "
+                        + "or sqlColumns and idColumns or both.");
+            }
+
+            idMap = Collections.EMPTY_MAP;
+        }
+        else if (sqlColumns == null || idColumns == null) {
+            // infer SQL columns from ID columns
+            sqlColumns = idMap.keySet().toArray();
+            idColumns = sqlColumns;
+        }
+
+        Object[] sqlColumnsArray = toArray(sqlColumns);
+        Object[] idColumnsArray = toArray(idColumns);
+
+        if (sqlColumnsArray.length != idColumnsArray.length) {
+            throw new ParseErrorException(
+                    "SQL columns and ID columns arrays have different sizes.");
+        }
+
+        for (int i = 0; i < sqlColumnsArray.length; i++) {
+
+            Object value = idMap.get(idColumnsArray[i]);
+
+            int jdbcType = (value != null) ? 
TypesMapping.getSqlTypeByJava(value
+                    .getClass()) : Types.INTEGER;
+
+            renderColumn(context, writer, sqlColumnsArray[i], i);
+            writer.write(' ');
+            render(context, writer, new ParameterBinding(value, jdbcType, -1));
+        }
+
+        return true;
+    }
+
+    protected Object[] toArray(Object columns) {
+        if (columns instanceof Collection) {
+            return ((Collection) columns).toArray();
+        }
+        else if (columns.getClass().isArray()) {
+            return (Object[]) columns;
+        }
+        else {
+            return new Object[] {
+                columns
+            };
+        }
+    }
+
+    protected Map toIdMap(Object object) throws ParseErrorException {
+        if (object instanceof Persistent) {
+            return ((Persistent) object).getObjectId().getIdSnapshot();
+        }
+        else if (object instanceof ObjectId) {
+            return ((ObjectId) object).getIdSnapshot();
+        }
+        else if(object instanceof Map) {
+            return (Map) object;
+        }
+        else if (object != null) {
+            throw new ParseErrorException(
+                    "Invalid object parameter, expected Persistent or ObjectId 
or null: "
+                            + object);
+        }
+        else {
+            return null;
+        }
+    }
+
+    protected void renderColumn(
+            InternalContextAdapter context,
+            Writer writer,
+            Object columnName,
+            int columnIndex) throws IOException {
+
+        if (columnIndex > 0) {
+            writer.write(" AND ");
+        }
+
+        writer.write(columnName.toString());
+    }
+
+    @Override
+    protected void render(
+            InternalContextAdapter context,
+            Writer writer,
+            ParameterBinding binding) throws IOException {
+
+        if (binding.getValue() != null) {
+            bind(context, binding);
+            writer.write("= ?");
+        }
+        else {
+            writer.write("IS NULL");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
new file mode 100644
index 0000000..6c95852
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/BindObjectNotEqualDirective.java
@@ -0,0 +1,70 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.velocity.context.InternalContextAdapter;
+
+/**
+ * A custom Velocity directive to create a set of SQL conditions to check 
unequality of an
+ * ObjectId of an object. Usage in Velocity template is "WHERE
+ * #bindObjectNotEqual($object)" or "WHERE #bindObjectNotEqual($object $columns
+ * $idValues)".
+ * 
+ * @since 3.0
+ */
+public class BindObjectNotEqualDirective extends BindObjectEqualDirective {
+
+    @Override
+    public String getName() {
+        return "bindObjectNotEqual";
+    }
+
+    @Override
+    protected void renderColumn(
+            InternalContextAdapter context,
+            Writer writer,
+            Object columnName,
+            int columnIndex) throws IOException {
+
+        if (columnIndex > 0) {
+            writer.write(" OR ");
+        }
+
+        writer.write(columnName.toString());
+    }
+
+    @Override
+    protected void render(
+            InternalContextAdapter context,
+            Writer writer,
+            ParameterBinding binding) throws IOException {
+
+        if (binding.getValue() != null) {
+            bind(context, binding);
+            writer.write("<> ?");
+        }
+        else {
+            writer.write("IS NOT NULL");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java
new file mode 100644
index 0000000..acd0f8c
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChainDirective.java
@@ -0,0 +1,112 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.parser.node.ASTDirective;
+import org.apache.velocity.runtime.parser.node.Node;
+
+/**
+ * A custom Velocity directive to conditionally join a number of {@link 
ChunkDirective chunks}.
+ * Usage of chain is the following:
+ * 
+ * <pre>
+ * #chain(operator) - e.g. #chain(' AND ')
+ * #chain(operator prefix) - e.g. #chain(' AND ' 'WHERE ')</pre>
+ * 
+ * <p><code>operator</code> (e.g. AND, OR, etc.) is used to join chunks that 
are included
+ * in a chain. <code>prefix</code> is inserted if a chain contains at least 
one chunk.
+ * </p>
+ * 
+ * @since 1.1
+ */
+public class ChainDirective extends Directive {
+
+    @Override
+    public String getName() {
+        return "chain";
+    }
+
+    @Override
+    public int getType() {
+        return BLOCK;
+    }
+
+    @Override
+    public boolean render(InternalContextAdapter context, Writer writer, Node 
node)
+        throws
+            IOException,
+            ResourceNotFoundException,
+            ParseErrorException,
+            MethodInvocationException {
+
+        int size = node.jjtGetNumChildren();
+        if (size == 0) {
+            return true;
+        }
+
+        // BLOCK is the last child
+        Node block = node.jjtGetChild(node.jjtGetNumChildren() - 1);
+        String join = (size > 1) ? (String) node.jjtGetChild(0).value(context) 
: "";
+        String prefix = (size > 2) ? (String) 
node.jjtGetChild(1).value(context) : "";
+
+        // if there is a conditional prefix, use a separate buffer for children
+        StringWriter childWriter = new StringWriter(30);
+
+        int len = block.jjtGetNumChildren();
+        int includedChunks = 0;
+        for (int i = 0; i < len; i++) {
+            Node child = block.jjtGetChild(i);
+
+            // if this is a "chunk", evaluate its expression and prepend join 
if included...
+            if (child instanceof ASTDirective
+                && "chunk".equals(((ASTDirective) child).getDirectiveName())) {
+
+                if (child.jjtGetNumChildren() < 2
+                    || child.jjtGetChild(0).value(context) != null) {
+
+                    if (includedChunks > 0) {
+                        childWriter.write(join);
+                    }
+
+                    includedChunks++;
+                }
+            }
+
+            child.render(context, childWriter);
+        }
+
+        if (includedChunks > 0) {
+            childWriter.flush();
+            writer.write(prefix);
+            writer.write(childWriter.toString());
+        }
+
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java
new file mode 100644
index 0000000..5ff0a5e
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ChunkDirective.java
@@ -0,0 +1,75 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.parser.node.Node;
+
+/**
+ * A custom Velocity directive to describe a conditional chunk of a {@link 
ChainDirective chain}.
+ * Usage of chunk is the following:
+ * 
+ * <pre>
+ * #chunk()...#end - e.g. #chunk()A = 5#end
+ * #chunk($paramKey)...#end - e.g. #chunk($a)A = $a#end
+ * </pre>
+ * <p>
+ * If context contains paramKey and it's value isn't null, chunk is included 
in the
+ * chain, and if it is not the first chunk, it is prefixed with chain join 
(OR/AND).
+ * If context doesn't contain paramKey or it's value is null, chunk is skipped.
+ * @since 1.1
+ */
+public class ChunkDirective extends Directive {
+
+    @Override
+    public String getName() {
+        return "chunk";
+    }
+
+    @Override
+    public int getType() {
+        return BLOCK;
+    }
+
+    @Override
+    public boolean render(InternalContextAdapter context, Writer writer, Node 
node)
+            throws IOException, ResourceNotFoundException, ParseErrorException,
+            MethodInvocationException {
+
+        // first child is an expression, second is BLOCK
+        if (node.jjtGetNumChildren() > 1 && node.jjtGetChild(0).value(context) 
== null) {
+            // skip this chunk
+            return false;
+        }
+
+        // BLOCK is the last child
+        Node block = node.jjtGetChild(node.jjtGetNumChildren() - 1);
+        block.render(context, writer);
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
new file mode 100644
index 0000000..5973188
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
@@ -0,0 +1,202 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.util.Util;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.MethodInvocationException;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.directive.Directive;
+import org.apache.velocity.runtime.parser.node.Node;
+
+/**
+ * A custom Velocity directive to describe a ResultSet column. There are the
+ * following possible invocation formats inside the template:
+ * 
+ * <pre>
+ *       #result(column_name) - e.g. #result('ARTIST_ID')
+ *       #result(column_name java_type) - e.g. #result('ARTIST_ID' 'String')
+ *       #result(column_name java_type column_alias) - e.g. 
#result('ARTIST_ID' 'String' 'ID')
+ *       #result(column_name java_type column_alias data_row_key) - e.g. 
#result('ARTIST_ID' 'String' 'ID' 'toArtist.ID')
+ * </pre>
+ * 
+ * <p>
+ * 'data_row_key' is needed if SQL 'column_alias' is not appropriate as a
+ * DataRow key on the Cayenne side. One common case when this happens is when a
+ * DataRow retrieved from a query is mapped using joint prefetch keys. In this
+ * case DataRow must use DB_PATH expressions for joint column keys, and their
+ * format is incompatible with most databases alias format.
+ * </p>
+ * <p>
+ * Most common Java types used in JDBC can be specified without a package. This
+ * includes all numeric types, primitives, String, SQL dates, BigDecimal and
+ * BigInteger.
+ * </p>
+ * 
+ * @since 1.1
+ */
+public class ResultDirective extends Directive {
+
+       private static final Map<String, String> typesGuess;
+
+       static {
+               // init default types
+               typesGuess = new HashMap<>();
+
+               // primitives
+               typesGuess.put("long", Long.class.getName());
+               typesGuess.put("double", Double.class.getName());
+               typesGuess.put("byte", Byte.class.getName());
+               typesGuess.put("boolean", Boolean.class.getName());
+               typesGuess.put("float", Float.class.getName());
+               typesGuess.put("short", Short.class.getName());
+               typesGuess.put("int", Integer.class.getName());
+
+               // numeric
+               typesGuess.put("Long", Long.class.getName());
+               typesGuess.put("Double", Double.class.getName());
+               typesGuess.put("Byte", Byte.class.getName());
+               typesGuess.put("Boolean", Boolean.class.getName());
+               typesGuess.put("Float", Float.class.getName());
+               typesGuess.put("Short", Short.class.getName());
+               typesGuess.put("Integer", Integer.class.getName());
+
+               // other
+               typesGuess.put("String", String.class.getName());
+               typesGuess.put("Date", Date.class.getName());
+               typesGuess.put("Time", Time.class.getName());
+               typesGuess.put("Timestamp", Timestamp.class.getName());
+               typesGuess.put("BigDecimal", BigDecimal.class.getName());
+               typesGuess.put("BigInteger", BigInteger.class.getName());
+       }
+
+       @Override
+       public String getName() {
+               return "result";
+       }
+
+       @Override
+       public int getType() {
+               return LINE;
+       }
+
+       @Override
+       public boolean render(InternalContextAdapter context, Writer writer, 
Node node) throws IOException,
+                       ResourceNotFoundException, ParseErrorException, 
MethodInvocationException {
+
+               String column = getChildAsString(context, node, 0);
+               if (column == null) {
+                       throw new ParseErrorException("Column name expected at 
line " + node.getLine() + ", column "
+                                       + node.getColumn());
+               }
+
+               String alias = getChildAsString(context, node, 2);
+               String dataRowKey = getChildAsString(context, node, 3);
+
+               // determine what we want to name this column in a resulting 
DataRow...
+               String label = (!Util.isEmptyString(dataRowKey)) ? dataRowKey : 
(!Util.isEmptyString(alias)) ? alias : null;
+
+               ColumnDescriptor columnDescriptor = new ColumnDescriptor();
+               columnDescriptor.setName(column);
+               columnDescriptor.setDataRowKey(label);
+
+               String type = getChildAsString(context, node, 1);
+               if (type != null) {
+                       columnDescriptor.setJavaClass(guessType(type));
+               }
+
+               // TODO: andrus 6/27/2007 - this is an unofficial jdbcType 
parameter
+               // that is added
+               // temporarily pending CAY-813 implementation for the sake of 
EJBQL
+               // query...
+               Object jdbcType = getChild(context, node, 4);
+               if (jdbcType instanceof Number) {
+                       columnDescriptor.setJdbcType(((Number) 
jdbcType).intValue());
+               }
+
+               writer.write(column);
+
+               // append column alias if needed.
+
+               // Note that if table aliases are used, this logic will result 
in SQL
+               // like
+               // "t0.ARTIST_NAME AS ARTIST_NAME". Doing extra regex matching 
to handle
+               // this
+               // won't probably buy us much.
+               if (!Util.isEmptyString(alias) && !alias.equals(column)) {
+                       writer.write(" AS ");
+                       writer.write(alias);
+               }
+
+               bindResult(context, columnDescriptor);
+               return true;
+       }
+
+       protected Object getChild(InternalContextAdapter context, Node node, 
int i) throws MethodInvocationException {
+               return (i >= 0 && i < node.jjtGetNumChildren()) ? 
node.jjtGetChild(i).value(context) : null;
+       }
+
+       /**
+        * Returns a directive argument at a given index converted to String.
+        * 
+        * @since 1.2
+        */
+       protected String getChildAsString(InternalContextAdapter context, Node 
node, int i)
+                       throws MethodInvocationException {
+               Object value = getChild(context, node, i);
+               return (value != null) ? value.toString() : null;
+       }
+
+       /**
+        * Converts "short" type notation to the fully qualified class name. 
Right
+        * now supports all major standard SQL types, including primitives. All
+        * other types are expected to be fully qualified, and are not 
converted.
+        */
+       protected String guessType(String type) {
+               String guessed = typesGuess.get(type);
+               return guessed != null ? guessed : type;
+       }
+
+       /**
+        * Adds value to the list of result columns in the context.
+        */
+       protected void bindResult(InternalContextAdapter context, 
ColumnDescriptor columnDescriptor) {
+
+               Collection<Object> resultColumns = (Collection<Object>) 
context.getInternalUserContext().get(
+                               
VelocitySQLTemplateProcessor.RESULT_COLUMNS_LIST_KEY);
+
+               if (resultColumns != null) {
+                       resultColumns.add(columnDescriptor);
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java
new file mode 100644
index 0000000..085e2be
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/SQLTemplateResourceManager.java
@@ -0,0 +1,106 @@
+/*****************************************************************
+ *   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.velocity;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Map;
+
+import org.apache.commons.collections.ExtendedProperties;
+import org.apache.commons.collections.map.LRUMap;
+import org.apache.velocity.Template;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.ResourceManager;
+import org.apache.velocity.runtime.resource.loader.ResourceLoader;
+
+/**
+ * An implementation of the Velocity ResourceManager and ResourceLoader that
+ * creates templates from in-memory Strings.
+ * 
+ * @since 1.1
+ */
+// class must be public since it is instantiated by Velocity via reflection.
+public class SQLTemplateResourceManager
+    extends ResourceLoader
+    implements ResourceManager {
+
+    protected Map<String, Template> templateCache;
+
+    public void initialize(RuntimeServices rs) {
+        super.rsvc = rs;
+        this.templateCache = new LRUMap(100);
+    }
+
+    public void clearCache() {
+        templateCache.clear();
+    }
+
+    /**
+     * Returns a Velocity Resource which is a Template for the given SQL.
+     */
+    public Resource getResource(String resourceName, int resourceType, String 
encoding)
+        throws ResourceNotFoundException, ParseErrorException {
+
+        synchronized (templateCache) {
+            Template resource = templateCache.get(resourceName);
+
+            if (resource == null) {
+                resource = new Template();
+                resource.setRuntimeServices(rsvc);
+                resource.setResourceLoader(this);
+                resource.setName(resourceName);
+                resource.setEncoding(encoding);
+                resource.process();
+
+                templateCache.put(resourceName, resource);
+            }
+
+            return resource;
+        }
+    }
+
+    public String getLoaderNameForResource(String resourceName) {
+        return getClass().getName();
+    }
+
+    @Override
+    public long getLastModified(Resource resource) {
+        return -1;
+    }
+
+    @Override
+    public InputStream getResourceStream(String source)
+        throws ResourceNotFoundException {
+        return new ByteArrayInputStream(source.getBytes());
+    }
+
+    @Override
+    public void init(ExtendedProperties configuration) {
+
+    }
+
+    @Override
+    public boolean isSourceModified(Resource resource) {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java
new file mode 100644
index 0000000..c199bdb
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java
@@ -0,0 +1,35 @@
+/*****************************************************************
+ *   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.velocity;
+
+import org.apache.cayenne.access.jdbc.SQLTemplateProcessor;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+
+/**
+ * @since 4.1
+ */
+public class VelocityModule implements Module {
+
+    @Override
+    public void configure(Binder binder) {
+        
binder.bind(SQLTemplateProcessor.class).to(VelocitySQLTemplateProcessor.class);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
new file mode 100644
index 0000000..a39cffb
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
@@ -0,0 +1,209 @@
+/*****************************************************************
+ *   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.velocity;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.jdbc.SQLStatement;
+import org.apache.cayenne.access.jdbc.SQLTemplateProcessor;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.exp.ExpressionException;
+import org.apache.cayenne.template.SQLTemplateRenderingUtils;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.context.InternalContextAdapterImpl;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeInstance;
+import org.apache.velocity.runtime.log.NullLogChute;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.ASTReference;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.runtime.visitor.BaseVisitor;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Processor for SQL velocity templates.
+ * 
+ * @see org.apache.cayenne.query.SQLTemplate
+ * @since 4.0
+ */
+public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
+
+       private final class PositionalParamMapper extends BaseVisitor {
+
+               private int i;
+               private List<Object> positionalParams;
+               private Map<String, Object> params;
+
+               PositionalParamMapper(List<Object> positionalParams, 
Map<String, Object> params) {
+                       this.positionalParams = positionalParams;
+                       this.params = params;
+               }
+
+               @Override
+               public Object visit(ASTReference node, Object data) {
+
+                       // strip off leading "$"
+                       String paramName = 
node.getFirstToken().image.substring(1);
+
+                       // only consider the first instance of each named 
parameter
+                       if (!params.containsKey(paramName)) {
+
+                               if (i >= positionalParams.size()) {
+                                       throw new ExpressionException("Too few 
parameters to bind template: " + positionalParams.size());
+                               }
+
+                               params.put(paramName, positionalParams.get(i));
+                               i++;
+                       }
+
+                       return data;
+               }
+
+               void onFinish() {
+                       if (i < positionalParams.size()) {
+                               throw new ExpressionException("Too many 
parameters to bind template. Expected: " + i + ", actual: "
+                                               + positionalParams.size());
+                       }
+               }
+       }
+
+       static final String BINDINGS_LIST_KEY = "bindings";
+       static final String RESULT_COLUMNS_LIST_KEY = "resultColumns";
+       static final String HELPER_KEY = "helper";
+
+       protected RuntimeInstance velocityRuntime;
+       protected SQLTemplateRenderingUtils renderingUtils;
+
+       public VelocitySQLTemplateProcessor() {
+               this.renderingUtils = new SQLTemplateRenderingUtils();
+               this.velocityRuntime = new RuntimeInstance();
+
+               // set null logger
+               
velocityRuntime.addProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new 
NullLogChute());
+
+               velocityRuntime
+                               
.addProperty(RuntimeConstants.RESOURCE_MANAGER_CLASS, 
SQLTemplateResourceManager.class.getName());
+               velocityRuntime.addProperty("userdirective", 
BindDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
BindEqualDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
BindNotEqualDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
BindObjectEqualDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
BindObjectNotEqualDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
ResultDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
ChainDirective.class.getName());
+               velocityRuntime.addProperty("userdirective", 
ChunkDirective.class.getName());
+               try {
+                       velocityRuntime.init();
+               } catch (Exception ex) {
+                       throw new CayenneRuntimeException("Error setting up 
Velocity RuntimeInstance.", ex);
+               }
+
+       }
+
+       /**
+        * Builds and returns a SQLStatement based on SQL template and a set of
+        * parameters. During rendering, VelocityContext exposes the following 
as
+        * variables: all parameters in the map, {@link 
SQLTemplateRenderingUtils}
+        * as a "helper" variable and SQLStatement object as "statement" 
variable.
+        */
+       @Override
+       public SQLStatement processTemplate(String template, Map<String, ?> 
parameters) {
+               // have to make a copy of parameter map since we are gonna 
modify it..
+               Map<String, Object> internalParameters = (parameters != null && 
!parameters.isEmpty()) ? new HashMap<>(
+                               parameters) : new HashMap<String, Object>(5);
+
+               SimpleNode parsedTemplate = parse(template);
+               return processTemplate(template, parsedTemplate, 
internalParameters);
+       }
+
+       @Override
+       public SQLStatement processTemplate(String template, List<Object> 
positionalParameters) {
+
+               SimpleNode parsedTemplate = parse(template);
+
+               Map<String, Object> internalParameters = new HashMap<>();
+
+               PositionalParamMapper visitor = new 
PositionalParamMapper(positionalParameters, internalParameters);
+               parsedTemplate.jjtAccept(visitor, null);
+               visitor.onFinish();
+
+               return processTemplate(template, parsedTemplate, 
internalParameters);
+       }
+
+       SQLStatement processTemplate(String template, SimpleNode 
parsedTemplate, Map<String, Object> parameters) {
+               List<ParameterBinding> bindings = new ArrayList<>();
+               List<ColumnDescriptor> results = new ArrayList<>();
+               parameters.put(BINDINGS_LIST_KEY, bindings);
+               parameters.put(RESULT_COLUMNS_LIST_KEY, results);
+               parameters.put(HELPER_KEY, renderingUtils);
+
+               String sql;
+               try {
+                       sql = buildStatement(new VelocityContext(parameters), 
template, parsedTemplate);
+               } catch (Exception e) {
+                       throw new CayenneRuntimeException("Error processing 
Velocity template", e);
+               }
+
+               ParameterBinding[] bindingsArray = new 
ParameterBinding[bindings.size()];
+               bindings.toArray(bindingsArray);
+
+               ColumnDescriptor[] resultsArray = new 
ColumnDescriptor[results.size()];
+               results.toArray(resultsArray);
+
+               return new SQLStatement(sql, resultsArray, bindingsArray);
+       }
+
+       String buildStatement(VelocityContext context, String template, 
SimpleNode parsedTemplate) throws Exception {
+
+               // ... not sure what InternalContextAdapter is for...
+               InternalContextAdapterImpl ica = new 
InternalContextAdapterImpl(context);
+               ica.pushCurrentTemplateName(template);
+
+               StringWriter out = new StringWriter(template.length());
+               try {
+                       parsedTemplate.init(ica, velocityRuntime);
+                       parsedTemplate.render(ica, out);
+                       return out.toString();
+               } finally {
+                       ica.popCurrentTemplateName();
+               }
+       }
+
+       private SimpleNode parse(String template) {
+
+               SimpleNode nodeTree;
+               try {
+                       nodeTree = velocityRuntime.parse(new 
StringReader(template), template);
+               } catch (ParseException pex) {
+                       throw new CayenneRuntimeException("Error parsing 
template '%s' : %s", template, pex.getMessage());
+               }
+
+               if (nodeTree == null) {
+                       throw new CayenneRuntimeException("Error parsing 
template %s", template);
+               }
+
+               return nodeTree;
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.java
new file mode 100644
index 0000000..b51a5a4
--- /dev/null
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityServerModuleProvider.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.velocity;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.cayenne.configuration.server.CayenneServerModuleProvider;
+import org.apache.cayenne.configuration.server.ServerModule;
+import org.apache.cayenne.di.Module;
+
+/**
+ * @since 4.1
+ */
+public class VelocityServerModuleProvider implements 
CayenneServerModuleProvider {
+
+    @Override
+    public Module module() {
+        return new VelocityModule();
+    }
+
+    @Override
+    public Class<? extends Module> moduleType() {
+        return VelocityModule.class;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Collection<Class<? extends Module>> overrides() {
+        Collection modules = Collections.singletonList(ServerModule.class);
+        return modules;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider
 
b/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider
new file mode 100644
index 0000000..7228c91
--- /dev/null
+++ 
b/cayenne-velocity/src/main/resources/META-INF/services/org.apache.cayenne.configuration.server.CayenneServerModuleProvider
@@ -0,0 +1,20 @@
+##################################################################
+#   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.
+##################################################################
+
+org.apache.cayenne.velocity.VelocityServerModuleProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java
 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java
new file mode 100644
index 0000000..9ef2229
--- /dev/null
+++ 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/SQLTemplateResourceManagerTest.java
@@ -0,0 +1,77 @@
+/*****************************************************************
+ *   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.velocity;
+
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.Reader;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.runtime.resource.ResourceManager;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SQLTemplateResourceManagerTest {
+
+       private SQLTemplateResourceManager rm;
+
+       @Before
+       public void before() throws Exception {
+
+               RuntimeServices rs = mock(RuntimeServices.class);
+               when(rs.parse(any(Reader.class), anyString(), 
anyBoolean())).thenReturn(new SimpleNode(1));
+               when(rs.parse(any(Reader.class), anyString())).thenReturn(new 
SimpleNode(1));
+
+               this.rm = new SQLTemplateResourceManager();
+               rm.initialize(rs);
+       }
+
+       @Test
+       public void testFetResource() throws Exception {
+
+               Resource resource = rm.getResource("abc", 
ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT);
+
+               assertTrue(resource instanceof Template);
+
+               // must be cached...
+               assertSame(resource,
+                               rm.getResource("abc", 
ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT));
+
+               // new resource must be different
+               assertNotSame(resource,
+                               rm.getResource("xyz", 
ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT));
+
+               // after clearing cache, resource must be refreshed
+               rm.clearCache();
+               assertNotSame(resource,
+                               rm.getResource("abc", 
ResourceManager.RESOURCE_TEMPLATE, RuntimeConstants.ENCODING_DEFAULT));
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
new file mode 100644
index 0000000..504179b
--- /dev/null
+++ 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
@@ -0,0 +1,234 @@
+/*****************************************************************
+ *   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.velocity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Types;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.DataObject;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.access.jdbc.SQLStatement;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.junit.Before;
+import org.junit.Test;
+
+public class VelocitySQLTemplateProcessorTest {
+
+       private VelocitySQLTemplateProcessor processor;
+
+       @Before
+       public void before() {
+               processor = new VelocitySQLTemplateProcessor();
+       }
+
+       @Test
+       public void testProcessTemplateUnchanged1() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals(sqlTemplate, compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+       }
+
+       @Test
+       public void testProcessTemplateUnchanged2() throws Exception {
+               String sqlTemplate = "SELECT a.b as XYZ FROM $SYSTEM_TABLE";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals(sqlTemplate, compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+       }
+
+       @Test
+       public void testProcessTemplateSimpleDynamicContent() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE $a";
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", "VALUE_OF_A");
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals("SELECT * FROM ME WHERE VALUE_OF_A", 
compiled.getSql());
+
+               // bindings are not populated, since no "bind" macro is used.
+               assertEquals(0, compiled.getBindings().length);
+       }
+
+       @Test
+       public void testProcessTemplateBind() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE "
+                               + "COLUMN1 = #bind($a 'VARCHAR') AND COLUMN2 = 
#bind($b 'INTEGER')";
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", "VALUE_OF_A");
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN1 = ? AND COLUMN2 = 
?", compiled.getSql());
+               assertEquals(2, compiled.getBindings().length);
+               assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]);
+               assertBindingValue(null, compiled.getBindings()[1]);
+       }
+
+       @Test
+       public void testProcessTemplateBindGuessVarchar() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = 
#bind($a)";
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", "VALUE_OF_A");
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals(1, compiled.getBindings().length);
+               assertBindingType(Types.VARCHAR, compiled.getBindings()[0]);
+       }
+
+       @Test
+       public void testProcessTemplateBindGuessInteger() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = 
#bind($a)";
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", 4);
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals(1, compiled.getBindings().length);
+               assertBindingType(Types.INTEGER, compiled.getBindings()[0]);
+       }
+
+       @Test
+       public void testProcessTemplateBindEqual() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE COLUMN 
#bindEqual($a 'VARCHAR')";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN IS NULL", 
compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", "VALUE_OF_A");
+
+               compiled = processor.processTemplate(sqlTemplate, map);
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN = ?", 
compiled.getSql());
+               assertEquals(1, compiled.getBindings().length);
+               assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]);
+       }
+
+       @Test
+       public void testProcessTemplateBindNotEqual() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE COLUMN 
#bindNotEqual($a 'VARCHAR')";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN IS NOT NULL", 
compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", "VALUE_OF_A");
+
+               compiled = processor.processTemplate(sqlTemplate, map);
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN <> ?", 
compiled.getSql());
+               assertEquals(1, compiled.getBindings().length);
+               assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]);
+       }
+
+       @Test
+       public void testProcessTemplateID() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = 
#bind($helper.cayenneExp($a, 'db:ID_COLUMN'))";
+
+               DataObject dataObject = new CayenneDataObject();
+               dataObject.setObjectId(new ObjectId("T", "ID_COLUMN", 5));
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", dataObject);
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN1 = ?", 
compiled.getSql());
+               assertEquals(1, compiled.getBindings().length);
+               assertBindingValue(new Integer(5), compiled.getBindings()[0]);
+       }
+
+       @Test
+       public void testProcessTemplateNotEqualID() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE "
+                               + "COLUMN1 #bindNotEqual($helper.cayenneExp($a, 
'db:ID_COLUMN1')) "
+                               + "AND COLUMN2 
#bindNotEqual($helper.cayenneExp($a, 'db:ID_COLUMN2'))";
+
+               Map<String, Object> idMap = new HashMap<>();
+               idMap.put("ID_COLUMN1", new Integer(3));
+               idMap.put("ID_COLUMN2", "aaa");
+               ObjectId id = new ObjectId("T", idMap);
+               DataObject dataObject = new CayenneDataObject();
+               dataObject.setObjectId(id);
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", dataObject);
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN1 <> ? AND COLUMN2 
<> ?", compiled.getSql());
+               assertEquals(2, compiled.getBindings().length);
+               assertBindingValue(new Integer(3), compiled.getBindings()[0]);
+               assertBindingValue("aaa", compiled.getBindings()[1]);
+       }
+
+       @Test
+       public void testProcessTemplateConditions() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME #if($a) WHERE COLUMN1 > 
#bind($a)#end";
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("a", "VALUE_OF_A");
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
map);
+
+               assertEquals("SELECT * FROM ME  WHERE COLUMN1 > ?", 
compiled.getSql());
+               assertEquals(1, compiled.getBindings().length);
+               assertBindingValue("VALUE_OF_A", compiled.getBindings()[0]);
+
+               compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT * FROM ME ", compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+       }
+
+       @Test
+       public void testProcessTemplateBindCollection() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME WHERE COLUMN IN 
(#bind($list 'VARCHAR'))";
+
+               Map<String, Object> map = Collections.<String, Object> 
singletonMap("list", Arrays.asList("a", "b", "c"));
+               SQLStatement compiled = new 
VelocitySQLTemplateProcessor().processTemplate(sqlTemplate, map);
+
+               assertEquals("SELECT * FROM ME WHERE COLUMN IN (?,?,?)", 
compiled.getSql());
+               assertEquals(3, compiled.getBindings().length);
+
+               compiled = processor.processTemplate(sqlTemplate, map);
+               assertBindingValue("a", compiled.getBindings()[0]);
+               assertBindingValue("b", compiled.getBindings()[1]);
+               assertBindingValue("c", compiled.getBindings()[2]);
+       }
+
+       private void assertBindingValue(Object expectedValue, Object binding) {
+               assertTrue("Not a binding!", binding instanceof 
ParameterBinding);
+               assertEquals(expectedValue, ((ParameterBinding) 
binding).getValue());
+       }
+
+       private void assertBindingType(Integer expectedType, Object binding) {
+               assertTrue("Not a binding!", binding instanceof 
ParameterBinding);
+               assertEquals(expectedType, ((ParameterBinding) 
binding).getJdbcType());
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java
 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java
new file mode 100644
index 0000000..c864918
--- /dev/null
+++ 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_ChainTest.java
@@ -0,0 +1,184 @@
+/*****************************************************************
+ *   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.velocity;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.access.jdbc.SQLStatement;
+import org.junit.Before;
+import org.junit.Test;
+
+public class VelocitySQLTemplateProcessor_ChainTest {
+
+       private VelocitySQLTemplateProcessor processor;
+
+       @Before
+       public void before() {
+               processor = new VelocitySQLTemplateProcessor();
+       }
+
+       @Test
+       public void testProcessTemplateNoChunks() throws Exception {
+               // whatever is inside the chain, it should render as empty if 
there
+               // is no chunks...
+
+               SQLStatement compiled = processor.processTemplate("#chain(' AND 
') #end",
+                               Collections.<String, Object> emptyMap());
+               assertEquals("", compiled.getSql());
+
+               compiled = processor.processTemplate("#chain(' AND ') garbage 
#end", Collections.<String, Object> emptyMap());
+               assertEquals("", compiled.getSql());
+
+               compiled = processor.processTemplate("#chain(' AND ' 'PREFIX') 
#end", Collections.<String, Object> emptyMap());
+
+               assertEquals("", compiled.getSql());
+               compiled = processor.processTemplate("#chain(' AND ' 'PREFIX') 
garbage #end",
+                               Collections.<String, Object> emptyMap());
+
+               assertEquals("", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateFullChain() throws Exception {
+               String template = "#chain(' OR ')" + "#chunk($a)$a#end" + 
"#chunk($b)$b#end" + "#chunk($c)$c#end" + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", "[A]");
+               map.put("b", "[B]");
+               map.put("c", "[C]");
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("[A] OR [B] OR [C]", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateFullChainAndPrefix() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", "[A]");
+               map.put("b", "[B]");
+               map.put("c", "[C]");
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE [A] OR [B] OR [C]", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplatePartialChainMiddle() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", "[A]");
+               map.put("c", "[C]");
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE [A] OR [C]", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplatePartialChainStart() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("b", "[B]");
+               map.put("c", "[C]");
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE [B] OR [C]", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplatePartialChainEnd() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", "[A]");
+               map.put("b", "[B]");
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE [A] OR [B]", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateChainWithGarbage() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + " some other stuff" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", "[A]");
+               map.put("c", "[C]");
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE [A] some other stuff OR [C]", 
compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateChainUnconditionalChunks() throws 
Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + "#chunk()C1#end" 
+ "#chunk()C2#end" + "#chunk()C3#end" + "#end";
+
+               SQLStatement compiled = processor.processTemplate(template, 
Collections.<String, Object> emptyMap());
+               assertEquals("WHERE C1 OR C2 OR C3", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateEmptyChain() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               SQLStatement compiled = processor.processTemplate(template, 
Collections.<String, Object> emptyMap());
+               assertEquals("", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateWithFalseOrZero1() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)[A]#end" + "#chunk($b)[B]#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", false);
+               map.put("b", 0);
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE [A] OR [B]", compiled.getSql());
+       }
+
+       @Test
+       public void testProcessTemplateWithFalseOrZero2() throws Exception {
+               String template = "#chain(' OR ' 'WHERE ')" + 
"#chunk($a)$a#end" + "#chunk($b)$b#end" + "#chunk($c)$c#end"
+                               + "#end";
+
+               Map<String, Object> map = new HashMap<>();
+               map.put("a", false);
+               map.put("b", 0);
+
+               SQLStatement compiled = processor.processTemplate(template, 
map);
+               assertEquals("WHERE false OR 0", compiled.getSql());
+       }
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java
----------------------------------------------------------------------
diff --git 
a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java
 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java
new file mode 100644
index 0000000..74a4d22
--- /dev/null
+++ 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor_SelectTest.java
@@ -0,0 +1,109 @@
+/*****************************************************************
+ *   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.velocity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Collections;
+
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.jdbc.SQLStatement;
+import org.junit.Before;
+import org.junit.Test;
+
+public class VelocitySQLTemplateProcessor_SelectTest {
+
+       private VelocitySQLTemplateProcessor processor;
+
+       @Before
+       public void before() {
+               processor = new VelocitySQLTemplateProcessor();
+       }
+
+       @Test
+       public void testProcessTemplateUnchanged() throws Exception {
+               String sqlTemplate = "SELECT * FROM ME";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals(sqlTemplate, compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+               assertEquals(0, compiled.getResultColumns().length);
+       }
+
+       @Test
+       public void testProcessSelectTemplate1() throws Exception {
+               String sqlTemplate = "SELECT #result('A') FROM ME";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT A FROM ME", compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+               assertEquals(1, compiled.getResultColumns().length);
+               assertEquals("A", compiled.getResultColumns()[0].getName());
+               assertNull(compiled.getResultColumns()[0].getJavaClass());
+       }
+
+       @Test
+       public void testProcessSelectTemplate2() throws Exception {
+               String sqlTemplate = "SELECT #result('A' 'String') FROM ME";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT A FROM ME", compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+
+               assertEquals(1, compiled.getResultColumns().length);
+               assertEquals("A", compiled.getResultColumns()[0].getName());
+               assertEquals("java.lang.String", 
compiled.getResultColumns()[0].getJavaClass());
+       }
+
+       @Test
+       public void testProcessSelectTemplate3() throws Exception {
+               String sqlTemplate = "SELECT #result('A' 'String' 'B') FROM ME";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT A AS B FROM ME", compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+
+               assertEquals(1, compiled.getResultColumns().length);
+               ColumnDescriptor column = compiled.getResultColumns()[0];
+               assertEquals("A", column.getName());
+               assertEquals("B", column.getDataRowKey());
+               assertEquals("java.lang.String", column.getJavaClass());
+       }
+
+       @Test
+       public void testProcessSelectTemplate4() throws Exception {
+               String sqlTemplate = "SELECT #result('A'), #result('B'), 
#result('C') FROM ME";
+
+               SQLStatement compiled = processor.processTemplate(sqlTemplate, 
Collections.<String, Object> emptyMap());
+
+               assertEquals("SELECT A, B, C FROM ME", compiled.getSql());
+               assertEquals(0, compiled.getBindings().length);
+
+               assertEquals(3, compiled.getResultColumns().length);
+               assertEquals("A", compiled.getResultColumns()[0].getName());
+               assertEquals("B", compiled.getResultColumns()[1].getName());
+               assertEquals("C", compiled.getResultColumns()[2].getName());
+       }
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/docs/doc/pom.xml
----------------------------------------------------------------------
diff --git a/docs/doc/pom.xml b/docs/doc/pom.xml
index 14c569c..d3daa0a 100644
--- a/docs/doc/pom.xml
+++ b/docs/doc/pom.xml
@@ -46,11 +46,6 @@
                        <artifactId>cayenne-modeler</artifactId>
                        <version>${project.version}</version>
                </dependency>
-               
-               <dependency>
-                       <groupId>org.apache.velocity</groupId>
-                       <artifactId>velocity</artifactId>
-               </dependency>
 
                <dependency>
                        <groupId>commons-collections</groupId>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/modeler/cayenne-modeler/pom.xml
----------------------------------------------------------------------
diff --git a/modeler/cayenne-modeler/pom.xml b/modeler/cayenne-modeler/pom.xml
index 6f9fc2e..bbc4645 100644
--- a/modeler/cayenne-modeler/pom.xml
+++ b/modeler/cayenne-modeler/pom.xml
@@ -32,15 +32,6 @@
        <packaging>jar</packaging>
 
        <dependencies>
-               <dependency>
-                       <groupId>junit</groupId>
-                       <artifactId>junit</artifactId>
-               </dependency>
-
-               <dependency>
-                       <groupId>commons-collections</groupId>
-                       <artifactId>commons-collections</artifactId>
-               </dependency>
 
                <dependency>
                        <groupId>org.slf4j</groupId>
@@ -48,11 +39,6 @@
                </dependency>
 
                <dependency>
-                       <groupId>org.apache.velocity</groupId>
-                       <artifactId>velocity</artifactId>
-               </dependency>
-
-               <dependency>
                        <groupId>com.jgoodies</groupId>
                        <artifactId>forms</artifactId>
                </dependency>
@@ -93,8 +79,14 @@
                </dependency>
 
                <dependency>
+                       <groupId>jgraph</groupId>
+                       <artifactId>jgraph</artifactId>
+               </dependency>
+
+               <dependency>
                        <groupId>com.mockrunner</groupId>
                        <artifactId>mockrunner-jdbc</artifactId>
+                       <scope>test</scope>
                        <exclusions>
                                <exclusion>
                                        <!-- this one have old Xerces 
dependency that clashes with JDK's one -->
@@ -105,8 +97,9 @@
                </dependency>
 
                <dependency>
-                       <groupId>jgraph</groupId>
-                       <artifactId>jgraph</artifactId>
+                       <groupId>junit</groupId>
+                       <artifactId>junit</artifactId>
+                       <scope>test</scope>
                </dependency>
        </dependencies>
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/modeler/cayenne-wocompat/pom.xml
----------------------------------------------------------------------
diff --git a/modeler/cayenne-wocompat/pom.xml b/modeler/cayenne-wocompat/pom.xml
index d3544f9..3ad0344 100644
--- a/modeler/cayenne-wocompat/pom.xml
+++ b/modeler/cayenne-wocompat/pom.xml
@@ -29,36 +29,40 @@
        <packaging>jar</packaging>
 
        <dependencies>
+
+               <dependency>
+                       <groupId>org.apache.cayenne</groupId>
+                       <artifactId>cayenne-server</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.cayenne</groupId>
+                       <artifactId>cayenne-dbsync</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>commons-lang</groupId>
+                       <artifactId>commons-lang</artifactId>
+               </dependency>
+
                <dependency>
                        <groupId>junit</groupId>
                        <artifactId>junit</artifactId>
+                       <scope>test</scope>
                </dependency>
-               
                <dependency>
                        <groupId>org.apache.cayenne.build-tools</groupId>
                        <artifactId>cayenne-test-utilities</artifactId>
                        <version>${project.version}</version>
                        <scope>test</scope>
                </dependency>
-
                <dependency>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-simple</artifactId>
                        <scope>test</scope>
                </dependency>
 
-               <dependency>
-                       <groupId>org.apache.cayenne</groupId>
-                       <artifactId>cayenne-server</artifactId>
-                       <version>${project.version}</version>
-               </dependency>
-
-               <dependency>
-                       <groupId>org.apache.cayenne</groupId>
-                       <artifactId>cayenne-dbsync</artifactId>
-                       <version>${project.version}</version>
-               </dependency>
-       </dependencies>
+    </dependencies>
 
     <profiles>
         <profile>

http://git-wip-us.apache.org/repos/asf/cayenne/blob/f734851f/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 92f39f0..e3be55c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,7 @@
                <module>cayenne-project</module>
                <module>cayenne-project-compatibility</module>
                <module>cayenne-server</module>
+               <module>cayenne-velocity</module>
                <module>eventbridges</module>
                <module>itests</module>
                <module>maven-plugins</module>
@@ -319,7 +320,11 @@
                                <artifactId>commons-collections</artifactId>
                                <version>3.2.1</version>
                        </dependency>
-
+                       <dependency>
+                               <groupId>commons-lang</groupId>
+                               <artifactId>commons-lang</artifactId>
+                               <version>2.4</version>
+                       </dependency>
                        <dependency>
                 <groupId>org.apache.commons</groupId>
                 <artifactId>commons-dbcp2</artifactId>

Reply via email to