You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2014/11/02 21:03:06 UTC

git commit: CAY-1966 SQLTemplate/SQLSelect positional parameter binding

Repository: cayenne
Updated Branches:
  refs/heads/master 2584967e7 -> 14e9dc3bd


CAY-1966 SQLTemplate/SQLSelect positional parameter binding

    * positional parameter bindings in SQLtemplate


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

Branch: refs/heads/master
Commit: 14e9dc3bdad37e841cfa6bf00f17f80f6eab86b1
Parents: 2584967
Author: aadamchik <aa...@apache.org>
Authored: Sun Nov 2 22:59:04 2014 +0300
Committer: aadamchik <aa...@apache.org>
Committed: Sun Nov 2 22:59:04 2014 +0300

----------------------------------------------------------------------
 .../apache/cayenne/query/SQLTemplateTest.java   |   5 +-
 .../cayenne/access/jdbc/SQLTemplateAction.java  |  53 +++--
 .../access/jdbc/SQLTemplateProcessor.java       |   7 +
 .../org/apache/cayenne/query/SQLSelect.java     |  14 +-
 .../org/apache/cayenne/query/SQLTemplate.java   |  48 ++++-
 .../apache/cayenne/velocity/BindDirective.java  | 211 +++++++++----------
 .../cayenne/velocity/VelocityParamSequence.java |  68 ++++++
 .../velocity/VelocitySQLTemplateProcessor.java  | 101 +++++++--
 .../org/apache/cayenne/query/SQLTemplateIT.java |  71 ++++++-
 .../apache/cayenne/query/SQLTemplateTest.java   |  41 +++-
 10 files changed, 458 insertions(+), 161 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
----------------------------------------------------------------------
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java b/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
index b727d83..d432fe1 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
@@ -23,7 +23,6 @@ import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 
 import java.util.Collections;
-import java.util.Map;
 
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.remote.hessian.service.HessianUtil;
@@ -45,9 +44,7 @@ public class SQLTemplateTest {
 
 		// set immutable parameters ... query must recast them to mutable
 		// version
-		@SuppressWarnings("unchecked")
-		Map<String, Object>[] parameters = new Map[] { Collections.EMPTY_MAP };
-		o.setParameters(parameters);
+		o.setParams(Collections.<String, Object> emptyMap());
 
 		HessianUtil.cloneViaClientServerSerialization(o, new EntityResolver());
 	}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
index 2e0d767..9c76fe9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
@@ -101,16 +101,54 @@ public class SQLTemplateAction implements SQLAction {
 		}
 
 		boolean loggable = dataNode.getJdbcEventLogger().isLoggable();
+		List<Number> counts = new ArrayList<Number>();
+
+		// bind either positional or named parameters;
+		// for legacy reasons named parameters are processed as a batch.. this
+		// should go away after 4.0; newer positional parameter only support a
+		// single set of values.
+		if (query.getPositionalParams().isEmpty()) {
+			runWithNamedParametersBatch(connection, callback, template, counts, loggable);
+		} else {
+			runWithPositionalParameters(connection, callback, template, counts, loggable);
+		}
+
+		// notify of combined counts of all queries inside SQLTemplate
+		// multiplied by the
+		// number of parameter sets...
+		int[] ints = new int[counts.size()];
+		for (int i = 0; i < ints.length; i++) {
+			ints[i] = counts.get(i).intValue();
+		}
+
+		callback.nextBatchCount(query, ints);
+	}
+
+	private void runWithPositionalParameters(Connection connection, OperationObserver callback, String template,
+			Collection<Number> counts, boolean loggable) throws Exception {
+
+		SQLStatement compiled = dataNode.getSqlTemplateProcessor().processTemplate(template,
+				query.getPositionalParams());
+
+		if (loggable) {
+			dataNode.getJdbcEventLogger().logQuery(compiled.getSql(), Arrays.asList(compiled.getBindings()));
+		}
+
+		execute(connection, callback, compiled, counts);
+	}
+
+	@SuppressWarnings("deprecation")
+	private void runWithNamedParametersBatch(Connection connection, OperationObserver callback, String template,
+			Collection<Number> counts, boolean loggable) throws Exception {
+
 		int size = query.parametersSize();
 
 		// zero size indicates a one-shot query with no parameters
 		// so fake a single entry batch...
 		int batchSize = (size > 0) ? size : 1;
 
-		List<Number> counts = new ArrayList<Number>(batchSize);
-
 		// for now supporting deprecated batch parameters...
-		@SuppressWarnings({ "deprecation", "unchecked" })
+		@SuppressWarnings("unchecked")
 		Iterator<Map<String, ?>> it = (size > 0) ? query.parametersIterator() : IteratorUtils
 				.singletonIterator(Collections.emptyMap());
 		for (int i = 0; i < batchSize; i++) {
@@ -125,15 +163,6 @@ public class SQLTemplateAction implements SQLAction {
 			execute(connection, callback, compiled, counts);
 		}
 
-		// notify of combined counts of all queries inside SQLTemplate
-		// multiplied by the
-		// number of parameter sets...
-		int[] ints = new int[counts.size()];
-		for (int i = 0; i < ints.length; i++) {
-			ints[i] = counts.get(i).intValue();
-		}
-
-		callback.nextBatchCount(query, ints);
 	}
 
 	protected void execute(Connection connection, OperationObserver callback, SQLStatement compiled,

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
index 3873494..fc6ef62 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateProcessor.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.access.jdbc;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -30,4 +31,10 @@ public interface SQLTemplateProcessor {
 	 * of parameters.
 	 */
 	SQLStatement processTemplate(String template, Map<String, ?> parameters);
+
+	/**
+	 * Builds and returns a SQLStatement based on SQL template String and a list
+	 * of positional parameters.
+	 */
+	SQLStatement processTemplate(String template, List<Object> positionalParameters);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
index 7f36267..fc413fa 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLSelect.java
@@ -89,7 +89,7 @@ public class SQLSelect<T> extends IndirectQuery implements Select<T> {
 	protected StringBuilder sqlBuffer;
 	protected QueryCacheStrategy cacheStrategy;
 	protected String[] cacheGroups;
-	protected Map<String, Object> parameters;
+	protected Map<String, Object> params;
 	protected CapsStrategy columnNameCaps;
 	protected int limit;
 	protected int offset;
@@ -103,7 +103,7 @@ public class SQLSelect<T> extends IndirectQuery implements Select<T> {
 	public SQLSelect(Class<T> persistentType, String sql) {
 		this.persistentType = persistentType;
 		this.sqlBuffer = sql != null ? new StringBuilder(sql) : new StringBuilder();
-		this.parameters = new HashMap<String, Object>();
+		this.params = new HashMap<String, Object>();
 		this.limit = QueryMetadata.FETCH_LIMIT_DEFAULT;
 		this.offset = QueryMetadata.FETCH_OFFSET_DEFAULT;
 		this.pageSize = QueryMetadata.PAGE_SIZE_DEFAULT;
@@ -148,7 +148,7 @@ public class SQLSelect<T> extends IndirectQuery implements Select<T> {
 	}
 
 	public SQLSelect<T> params(String name, Object value) {
-		parameters.put(name, value);
+		params.put(name, value);
 		this.replacementQuery = null;
 		return this;
 	}
@@ -162,12 +162,12 @@ public class SQLSelect<T> extends IndirectQuery implements Select<T> {
 	}
 
 	/**
-	 * Returns mutable map of parameters that will be bound to SQL. A caller is
-	 * free to add/remove parameters from the returned map as needed.
+	 * Returns a mutable map of parameters that will be bound to SQL. A caller
+	 * is free to add/remove parameters from the returned map as needed.
 	 * Alternatively one may use chained {@link #params(String, Object)}
 	 */
 	public Map<String, Object> getParams() {
-		return parameters;
+		return params;
 	}
 
 	@Override
@@ -195,7 +195,7 @@ public class SQLSelect<T> extends IndirectQuery implements Select<T> {
 		template.setDefaultTemplate(getSql());
 		template.setCacheGroups(cacheGroups);
 		template.setCacheStrategy(cacheStrategy);
-		template.setParams(parameters);
+		template.setParams(params);
 		template.setColumnNamesCapitalization(columnNameCaps);
 		template.setFetchLimit(limit);
 		template.setFetchOffset(offset);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
index 3d3514b..dd4b176 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SQLTemplate.java
@@ -19,10 +19,12 @@
 
 package org.apache.cayenne.query;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeSet;
 
@@ -82,6 +84,7 @@ public class SQLTemplate extends AbstractQuery implements ParameterizedQuery, XM
 	protected String defaultTemplate;
 	protected Map<String, String> templates;
 	protected Map<String, ?>[] parameters;
+	protected List<Object> positionalParams;
 	protected CapsStrategy columnNamesCapitalization;
 	protected SQLResult result;
 	private String dataNodeName;
@@ -323,15 +326,42 @@ public class SQLTemplate extends AbstractQuery implements ParameterizedQuery, XM
 	}
 
 	/**
-	 * Initializes parameters map of this query.
+	 * Initializes named parameter of this query. Note that calling this method
+	 * will reset any positional parameters.
 	 * 
 	 * @since 4.0
 	 */
 	@SuppressWarnings("unchecked")
-	public void setParams(Map<String, ?> parameters) {
+	public void setParams(Map<String, ?> params) {
+
+		// since named parameters are specified, resetting positional
+		// parameters
+		this.positionalParams = null;
+
 		// calling a deprecated method until we can remove multi-parameter-batch
 		// deprecation.
-		setParameters(parameters);
+		setParameters(params);
+	}
+
+	/**
+	 * Initializes positional parameters of the query. This is a positional
+	 * style of binding. Names of variables in the expression are ignored and
+	 * parameters are bound in order they are found in the expression. E.g. if
+	 * the same name is mentioned twice, it can be bound to two different
+	 * values. If declared and provided parameters counts are mismatched, an
+	 * exception will be thrown.
+	 * <p>
+	 * Note that calling this method will reset any previously set *named*
+	 * parameters.
+	 * 
+	 * @since 4.0
+	 */
+	public void setParamsArray(Object... params) {
+		// since positional parameters are specified, resetting named
+		// parameters
+		this.parameters = null;
+
+		this.positionalParams = params != null ? Arrays.asList(params) : null;
 	}
 
 	/**
@@ -552,7 +582,7 @@ public class SQLTemplate extends AbstractQuery implements ParameterizedQuery, XM
 	}
 
 	/**
-	 * Returns a map of parameters.
+	 * Returns a map of named parameters that will be bound to SQL.
 	 * 
 	 * @since 4.0
 	 */
@@ -562,6 +592,15 @@ public class SQLTemplate extends AbstractQuery implements ParameterizedQuery, XM
 	}
 
 	/**
+	 * Returns a list of positional parameters that will be bound to SQL.
+	 * 
+	 * @since 4.0
+	 */
+	public List<Object> getPositionalParams() {
+		return positionalParams != null ? positionalParams : Collections.emptyList();
+	}
+
+	/**
 	 * Utility method to get the first set of parameters, since most queries
 	 * will only have one.
 	 * 
@@ -581,6 +620,7 @@ public class SQLTemplate extends AbstractQuery implements ParameterizedQuery, XM
 	 *             batches of parameters are superseded by the use of
 	 *             {@link QueryChain}.
 	 */
+	@SuppressWarnings("unchecked")
 	@Deprecated
 	public void setParameters(Map<String, ?>... parameters) {
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
index 6e7e83e..a2b50a2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/BindDirective.java
@@ -35,8 +35,8 @@ import org.apache.velocity.runtime.directive.Directive;
 import org.apache.velocity.runtime.parser.node.Node;
 
 /**
- * A custom Velocity directive to create a PreparedStatement parameter text. There are the
- * following possible invocation formats inside the template:
+ * A custom Velocity directive to create a PreparedStatement parameter text.
+ * There are the following possible invocation formats inside the template:
  * 
  * <pre>
  * #bind(value) - e.g. #bind($xyz)
@@ -51,16 +51,17 @@ import org.apache.velocity.runtime.parser.node.Node;
  * </p>
  * <p>
  * <code>"WHERE SOME_COLUMN &gt; #bind($xyz)"</code> produces
- * <code>"WHERE SOME_COLUMN &gt; ?"</code> and also places the value of the "xyz" parameter
- * in the context "bindings" collection.
+ * <code>"WHERE SOME_COLUMN &gt; ?"</code> and also places the value of the
+ * "xyz" parameter in the context "bindings" collection.
  * </p>
  * <p>
  * <strong>Binding ID column of a DataObject value:</strong>
  * </p>
  * <p>
  * <code>"WHERE ID_COL1 = #bind($helper.cayenneExp($xyz, 'db:ID_COL2')) 
- * AND ID_COL2 = #bind($helper.cayenneExp($xyz, 'db:ID_COL2'))"</code> produces <code>"WHERE ID_COL1 = ? AND ID_COL2 = ?"</code> and also places the
- * values of id columns of the DataObject parameter "xyz" in the context "bindings"
+ * AND ID_COL2 = #bind($helper.cayenneExp($xyz, 'db:ID_COL2'))"</code> produces
+ * <code>"WHERE ID_COL1 = ? AND ID_COL2 = ?"</code> and also places the values
+ * of id columns of the DataObject parameter "xyz" in the context "bindings"
  * collection.
  * </p>
  * 
@@ -68,109 +69,97 @@ import org.apache.velocity.runtime.parser.node.Node;
  */
 public class BindDirective extends Directive {
 
-    @Override
-    public String getName() {
-        return "bind";
-    }
-
-    @Override
-    public int getType() {
-        return LINE;
-    }
-
-    /**
-     * Extracts the value of the object property to render and passes control to
-     * {@link #render(InternalContextAdapter, Writer, ParameterBinding)} to do the actual
-     * rendering.
-     */
-    @Override
-    public boolean render(InternalContextAdapter context, Writer writer, Node node)
-            throws IOException, ResourceNotFoundException, ParseErrorException,
-            MethodInvocationException {
-
-        Object value = getChild(context, node, 0);
-        Object type = getChild(context, node, 1);
-        int scale = ConversionUtil.toInt(getChild(context, node, 2), -1);
-        String typeString = type != null ? type.toString() : null;
-
-        if (value instanceof Collection) {
-            Iterator<?> it = ((Collection) value).iterator();
-            while (it.hasNext()) {
-                render(context, writer, node, it.next(), typeString, scale);
-
-                if (it.hasNext()) {
-                    writer.write(',');
-                }
-            }
-        }
-        else {
-            render(context, writer, node, value, typeString, scale);
-        }
-
-        return true;
-    }
-
-    /**
-     * @since 3.0
-     */
-    protected void render(
-            InternalContextAdapter context,
-            Writer writer,
-            Node node,
-            Object value,
-            String typeString,
-            int scale) throws IOException, ParseErrorException {
-
-        int jdbcType = TypesMapping.NOT_DEFINED;
-        if (typeString != null) {
-            jdbcType = TypesMapping.getSqlTypeByName(typeString);
-        }
-        else if (value != null) {
-            jdbcType = TypesMapping.getSqlTypeByJava(value.getClass());
-        } else {
-            // value is null, set JDBC type to NULL
-        	jdbcType = TypesMapping.getSqlTypeByName(TypesMapping.SQL_NULL);
-        }
-
-        if (jdbcType == TypesMapping.NOT_DEFINED) {
-            throw new ParseErrorException("Can't determine JDBC type of binding ("
-                    + value
-                    + ", "
-                    + typeString
-                    + ") at line "
-                    + node.getLine()
-                    + ", column "
-                    + node.getColumn());
-        }
-
-        render(context, writer, new ParameterBinding(value, jdbcType, scale));
-    }
-
-    protected void render(
-            InternalContextAdapter context,
-            Writer writer,
-            ParameterBinding binding) throws IOException {
-
-        bind(context, binding);
-        writer.write('?');
-    }
-
-    protected Object getChild(InternalContextAdapter context, Node node, int i)
-            throws MethodInvocationException {
-        return (i >= 0 && i < node.jjtGetNumChildren()) ? node.jjtGetChild(i).value(
-                context) : null;
-    }
-
-    /**
-     * Adds value to the list of bindings in the context.
-     */
-    protected void bind(InternalContextAdapter context, ParameterBinding binding) {
-
-        Collection bindings = (Collection) context.getInternalUserContext().get(
-                VelocitySQLTemplateProcessor.BINDINGS_LIST_KEY);
-
-        if (bindings != null) {
-            bindings.add(binding);
-        }
-    }
+	@Override
+	public String getName() {
+		return "bind";
+	}
+
+	@Override
+	public int getType() {
+		return LINE;
+	}
+
+	/**
+	 * Extracts the value of the object property to render and passes control to
+	 * {@link #render(InternalContextAdapter, Writer, ParameterBinding)} to do
+	 * the actual rendering.
+	 */
+	@Override
+	public boolean render(InternalContextAdapter context, Writer writer, Node node) throws IOException,
+			ResourceNotFoundException, ParseErrorException, MethodInvocationException {
+
+		Object value = getChild(context, node, 0);
+		Object type = getChild(context, node, 1);
+		int scale = ConversionUtil.toInt(getChild(context, node, 2), -1);
+		String typeString = type != null ? type.toString() : null;
+
+		if (value instanceof Collection) {
+			Iterator<?> it = ((Collection) value).iterator();
+			while (it.hasNext()) {
+				render(context, writer, node, it.next(), typeString, scale);
+
+				if (it.hasNext()) {
+					writer.write(',');
+				}
+			}
+		} else {
+			render(context, writer, node, value, typeString, scale);
+		}
+
+		return true;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	protected void render(InternalContextAdapter context, Writer writer, Node node, Object value, String typeString,
+			int scale) throws IOException, ParseErrorException {
+
+		int jdbcType = TypesMapping.NOT_DEFINED;
+		if (typeString != null) {
+			jdbcType = TypesMapping.getSqlTypeByName(typeString);
+		} else if (value != null) {
+			jdbcType = TypesMapping.getSqlTypeByJava(value.getClass());
+		} else {
+			// value is null, set JDBC type to NULL
+			jdbcType = TypesMapping.getSqlTypeByName(TypesMapping.SQL_NULL);
+		}
+
+		if (jdbcType == TypesMapping.NOT_DEFINED) {
+			throw new ParseErrorException("Can't determine JDBC type of binding (" + value + ", " + typeString
+					+ ") at line " + node.getLine() + ", column " + node.getColumn());
+		}
+
+		render(context, writer, new ParameterBinding(value, jdbcType, scale));
+	}
+
+	protected void render(InternalContextAdapter context, Writer writer, ParameterBinding binding) throws IOException {
+
+		bind(context, binding);
+		writer.write('?');
+	}
+
+	protected Object getChild(InternalContextAdapter context, Node node, int i) throws MethodInvocationException {
+		Object child = (i >= 0 && i < node.jjtGetNumChildren()) ? node.jjtGetChild(i).value(context) : null;
+
+		// unwrap postional parameters
+		if (child instanceof VelocityParamSequence) {
+			child = ((VelocityParamSequence) child).next();
+		}
+
+		return child;
+	}
+
+	/**
+	 * Adds value to the list of bindings in the context.
+	 */
+	protected void bind(InternalContextAdapter context, ParameterBinding binding) {
+
+		Collection bindings = (Collection) context.getInternalUserContext().get(
+				VelocitySQLTemplateProcessor.BINDINGS_LIST_KEY);
+
+		if (bindings != null) {
+			bindings.add(binding);
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
new file mode 100644
index 0000000..089e7f4
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocityParamSequence.java
@@ -0,0 +1,68 @@
+/*****************************************************************
+ *   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.util.ArrayList;
+import java.util.List;
+
+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.Renderable;
+
+/**
+ * A parameter value container that helps to may a single velocity variable to a
+ * sequence of positional parameters.
+ * 
+ * @since 4.0
+ */
+class VelocityParamSequence implements Renderable {
+
+	private List<Object> parameters;
+	private int index;
+
+	VelocityParamSequence() {
+		this.parameters = new ArrayList<Object>();
+	}
+
+	void add(Object parameter) {
+		parameters.add(parameter);
+	}
+
+	Object next() {
+		return parameters.get(index++);
+	}
+
+	@Override
+	public boolean render(InternalContextAdapter context, Writer writer) throws IOException, MethodInvocationException,
+			ParseErrorException, ResourceNotFoundException {
+
+		// rewind the list regardless of whether we produce any output
+		Object next = next();
+
+		if (context.getAllowRendering()) {
+			writer.write(String.valueOf(next));
+		}
+		return true;
+	}
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
index 89e7952..be5641d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessor.java
@@ -31,13 +31,16 @@ import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.ParameterBinding;
 import org.apache.cayenne.access.jdbc.SQLStatement;
 import org.apache.cayenne.access.jdbc.SQLTemplateProcessor;
+import org.apache.cayenne.exp.ExpressionException;
 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;
 
 /**
  * Processor for SQL velocity templates.
@@ -47,6 +50,45 @@ import org.apache.velocity.runtime.parser.node.SimpleNode;
  */
 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) {
+
+			if (i >= positionalParams.size()) {
+				throw new ExpressionException("Too few parameters to bind template: " + positionalParams.size());
+			}
+
+			// strip off leading "$"
+			String paramName = node.getFirstToken().image.substring(1);
+			VelocityParamSequence sequence = (VelocityParamSequence) params.get(paramName);
+			if (sequence == null) {
+				sequence = new VelocityParamSequence();
+				params.put(paramName, sequence);
+			}
+
+			sequence.add(positionalParams.get(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";
@@ -91,15 +133,34 @@ public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
 		Map<String, Object> internalParameters = (parameters != null && !parameters.isEmpty()) ? new HashMap<String, Object>(
 				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<String, Object>();
+
+		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<ParameterBinding>();
 		List<ColumnDescriptor> results = new ArrayList<ColumnDescriptor>();
-		internalParameters.put(BINDINGS_LIST_KEY, bindings);
-		internalParameters.put(RESULT_COLUMNS_LIST_KEY, results);
-		internalParameters.put(HELPER_KEY, renderingUtils);
+		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(internalParameters), template);
+			sql = buildStatement(new VelocityContext(parameters), template, parsedTemplate);
 		} catch (Exception e) {
 			throw new CayenneRuntimeException("Error processing Velocity template", e);
 		}
@@ -113,12 +174,24 @@ public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
 		return new SQLStatement(sql, resultsArray, bindingsArray);
 	}
 
-	String buildStatement(VelocityContext context, String template) throws Exception {
-		// Note: this method is a reworked version of
-		// org.apache.velocity.app.Velocity.evaluate(..)
-		// cleaned up to avoid using any Velocity singletons
+	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 = null;
 
 		try {
@@ -131,16 +204,6 @@ public class VelocitySQLTemplateProcessor implements SQLTemplateProcessor {
 			throw new CayenneRuntimeException("Error parsing template " + template);
 		}
 
-		// ... not sure what InternalContextAdapter is for...
-		InternalContextAdapterImpl ica = new InternalContextAdapterImpl(context);
-		ica.pushCurrentTemplateName(template);
-
-		try {
-			nodeTree.init(ica, velocityRuntime);
-			nodeTree.render(ica, out);
-			return out.toString();
-		} finally {
-			ica.popCurrentTemplateName();
-		}
+		return nodeTree;
 	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
index 3d2b989..5696eba 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.query;
 
+import java.sql.SQLException;
+import java.sql.Types;
 import java.util.List;
 
 import org.apache.cayenne.CayenneRuntimeException;
@@ -27,9 +29,10 @@ import org.apache.cayenne.access.DataContext;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.testmap.Painting;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.apache.cayenne.util.Util;
 
 @UseServerRuntime(ServerCase.TESTMAP_PROJECT)
 public class SQLTemplateIT extends ServerCase {
@@ -40,6 +43,8 @@ public class SQLTemplateIT extends ServerCase {
 	@Inject
 	private DBHelper dbHelper;
 
+	private TableHelper tPainting;
+
 	@Override
 	protected void setUpAfterInjection() throws Exception {
 		dbHelper.deleteAll("PAINTING_INFO");
@@ -47,6 +52,10 @@ public class SQLTemplateIT extends ServerCase {
 		dbHelper.deleteAll("ARTIST_EXHIBIT");
 		dbHelper.deleteAll("ARTIST_GROUP");
 		dbHelper.deleteAll("ARTIST");
+
+		tPainting = new TableHelper(dbHelper, "PAINTING");
+		tPainting.setColumns("PAINTING_ID", "ARTIST_ID", "PAINTING_TITLE", "ESTIMATED_PRICE").setColumnTypes(
+				Types.INTEGER, Types.BIGINT, Types.VARCHAR, Types.DECIMAL);
 	}
 
 	public void testSQLTemplateForDataMap() {
@@ -83,4 +92,64 @@ public class SQLTemplateIT extends ServerCase {
 		assertTrue("If fetchingDataRows is false and ObjectEntity not set, shoulb be thrown exception",
 				gotRuntimeException);
 	}
+
+	public void testSQLTemplate_PositionalParams() throws SQLException {
+
+		String sql = "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ESTIMATED_PRICE) "
+				+ "VALUES ($b, '$n', #bind($c 'INTEGER'))";
+
+		SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+		q1.setParamsArray(76, "The Fiddler", 10005);
+		context.performNonSelectingQuery(q1);
+
+		assertEquals("The Fiddler", tPainting.getString("PAINTING_TITLE"));
+		assertEquals(76, tPainting.getInt("PAINTING_ID"));
+		assertEquals(10005.d, tPainting.getDouble("ESTIMATED_PRICE"), 0.001);
+	}
+
+	public void testSQLTemplate_PositionalParams_RepeatingVars() throws SQLException {
+
+		String sql = "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ESTIMATED_PRICE) "
+				+ "VALUES ($b, '$n', #bind($b 'INTEGER'))";
+
+		SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+		q1.setParamsArray(11, "The Fiddler", 4567);
+		context.performNonSelectingQuery(q1);
+
+		assertEquals("The Fiddler", tPainting.getString("PAINTING_TITLE"));
+		assertEquals(11, tPainting.getInt("PAINTING_ID"));
+		assertEquals(4567.d, tPainting.getDouble("ESTIMATED_PRICE"), 0.001);
+	}
+
+	public void testSQLTemplate_PositionalParams_ToFewParams() throws SQLException {
+
+		String sql = "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ESTIMATED_PRICE) "
+				+ "VALUES ($b, '$n', #bind($b 'INTEGER'))";
+
+		SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+		q1.setParamsArray(11, "The Fiddler");
+
+		try {
+			context.performNonSelectingQuery(q1);
+			fail("Exception not thrown on parameter length mismatch");
+		} catch (CayenneRuntimeException e) {
+			// expected
+		}
+	}
+
+	public void testSQLTemplate_PositionalParams_ToManyParams() throws SQLException {
+
+		String sql = "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ESTIMATED_PRICE) "
+				+ "VALUES ($b, '$n', #bind($b 'INTEGER'))";
+
+		SQLTemplate q1 = new SQLTemplate(Painting.class, sql);
+		q1.setParamsArray(11, "The Fiddler", 2345, 333);
+
+		try {
+			context.performNonSelectingQuery(q1);
+			fail("Exception not thrown on parameter length mismatch");
+		} catch (CayenneRuntimeException e) {
+			// expected
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/14e9dc3b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
index dc5df3f..a88a6c8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateTest.java
@@ -19,12 +19,13 @@
 package org.apache.cayenne.query;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -37,7 +38,6 @@ public class SQLTemplateTest {
 	public void testSetParams() throws Exception {
 		SQLTemplate query = new SQLTemplate();
 
-		assertNotNull(query.getParams());
 		assertTrue(query.getParams().isEmpty());
 
 		Map<String, Object> params = new HashMap<String, Object>();
@@ -47,11 +47,46 @@ public class SQLTemplateTest {
 		assertEquals(params, query.getParams());
 
 		query.setParams(null);
-		assertNotNull(query.getParams());
 		assertTrue(query.getParams().isEmpty());
 	}
 
 	@Test
+	public void testSetParamsArray() throws Exception {
+		SQLTemplate query = new SQLTemplate();
+
+		assertTrue(query.getPositionalParams().isEmpty());
+
+		query.setParamsArray("N", "m");
+		assertEquals(Arrays.asList("N", "m"), query.getPositionalParams());
+
+		query.setParamsArray();
+		assertTrue(query.getPositionalParams().isEmpty());
+	}
+
+	@Test
+	public void testSetParams_MixingStyles() throws Exception {
+
+		SQLTemplate query = new SQLTemplate();
+
+		assertTrue(query.getParams().isEmpty());
+		assertTrue(query.getPositionalParams().isEmpty());
+
+		Map<String, Object> params = Collections.<String, Object> singletonMap("a", "b");
+		query.setParams(params);
+		assertEquals(params, query.getParams());
+		assertTrue(query.getPositionalParams().isEmpty());
+
+		query.setParamsArray("D", "G");
+		assertEquals(Arrays.asList("D", "G"), query.getPositionalParams());
+		assertTrue(query.getParams().isEmpty());
+
+		// even resetting named to null should result in resetting positional
+		query.setParams(null);
+		assertTrue(query.getParams().isEmpty());
+		assertTrue(query.getPositionalParams().isEmpty());
+	}
+
+	@Test
 	public void testGetDefaultTemplate() {
 		SQLTemplate query = new SQLTemplate();
 		query.setDefaultTemplate("AAA # BBB");