cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aadamc...@apache.org
Subject [6/7] cayenne git commit: Refactoring SelectTranslator for better extensibility
Date Tue, 28 Apr 2015 10:45:11 GMT
Refactoring SelectTranslator for better extensibility

    * Removing checked exceptions from translator API... we no longer pretend to write to Appendable, and
      will write to StringBuilder instead
    * unifying ParameterBinding objects between query types
    * fixing a bug in SelecTranslator - iterators will not work when supress distinct is in effect
    * removing Connection from translators; SQLAction should take care of all connection ops


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

Branch: refs/heads/master
Commit: 81f4fb50b75895fee1c0a1b73b173ca97634abfa
Parents: a6b55f7
Author: aadamchik <aadamchik@apache.org>
Authored: Tue Apr 28 12:29:07 2015 +0200
Committer: aadamchik <aadamchik@apache.org>
Committed: Tue Apr 28 12:43:55 2015 +0200

----------------------------------------------------------------------
 .../CryptoBatchTranslatorFactoryDecorator.java  |   8 +-
 .../crypto/transformer/BindingsTransformer.java |   4 +-
 .../transformer/DefaultBindingsTransformer.java |   6 +-
 .../transformer/DefaultTransformerFactory.java  |   6 +-
 .../crypto/transformer/TransformerFactory.java  |   4 +-
 .../apache/cayenne/access/jdbc/BatchAction.java |  10 +-
 .../cayenne/access/jdbc/ParameterBinding.java   |  68 --
 .../access/jdbc/SQLParameterBinding.java        |  62 ++
 .../cayenne/access/jdbc/SQLStatement.java       |  10 +-
 .../cayenne/access/jdbc/SQLTemplateAction.java  |   2 +-
 .../cayenne/access/jdbc/SelectAction.java       | 344 ++++---
 .../access/translator/ParameterBinding.java     |  81 ++
 .../translator/batch/BatchParameterBinding.java |  79 --
 .../translator/batch/BatchTranslator.java       |   5 +-
 .../batch/DefaultBatchTranslator.java           |  11 +-
 .../translator/batch/DeleteBatchTranslator.java |  11 +-
 .../translator/batch/InsertBatchTranslator.java |  11 +-
 .../batch/SoftDeleteBatchTranslator.java        |  13 +-
 .../translator/batch/UpdateBatchTranslator.java |  11 +-
 .../select/DefaultSelectTranslator.java         | 630 ++++++++++++
 .../access/translator/select/JoinStack.java     | 349 ++++---
 .../translator/select/OrderingTranslator.java   | 166 ++--
 .../translator/select/QualifierTranslator.java  | 949 +++++++++----------
 .../translator/select/QueryAssembler.java       | 302 +++---
 .../translator/select/QueryAssemblerHelper.java | 885 +++++++++--------
 .../translator/select/SelectTranslator.java     | 631 +-----------
 .../select/TrimmingQualifierTranslator.java     | 113 ++-
 .../cayenne/dba/db2/DB2QualifierTranslator.java | 172 ++--
 .../dba/derby/DerbyQualifierTranslator.java     |  59 +-
 .../firebird/FirebirdQualifierTranslator.java   |  30 +-
 .../dba/frontbase/FrontBaseActionBuilder.java   |   6 +-
 .../frontbase/FrontBaseSelectTranslator.java    |  41 +-
 .../cayenne/dba/hsqldb/HSQLSelectAction.java    |  24 +-
 .../dba/hsqldb/HSQLSelectTranslator.java        |  60 +-
 .../cayenne/dba/ingres/IngresSelectAction.java  |   6 +-
 .../dba/ingres/IngresSelectTranslator.java      |  13 +-
 .../cayenne/dba/mysql/MySQLSelectAction.java    |  24 +-
 .../dba/mysql/MySQLSelectTranslator.java        |  61 +-
 .../dba/openbase/OpenBaseActionBuilder.java     |  26 +-
 .../cayenne/dba/openbase/OpenBaseJoinStack.java | 158 ++-
 .../openbase/OpenBaseQualifierTranslator.java   | 262 +++--
 .../dba/openbase/OpenBaseSelectTranslator.java  |  45 +-
 .../cayenne/dba/oracle/Oracle8JoinStack.java    | 146 ++-
 .../dba/oracle/Oracle8LOBBatchAction.java       |   8 +-
 .../dba/oracle/Oracle8LOBBatchTranslator.java   |  12 +-
 .../dba/oracle/Oracle8QualifierTranslator.java  |  38 +-
 .../cayenne/dba/oracle/Oracle8SelectAction.java |  16 +-
 .../dba/oracle/Oracle8SelectTranslator.java     |  35 +-
 .../dba/oracle/OracleQualifierTranslator.java   | 104 +-
 .../cayenne/dba/oracle/OracleSelectAction.java  |  24 +-
 .../dba/oracle/OracleSelectTranslator.java      |  89 +-
 .../postgres/PostgresQualifierTranslator.java   | 188 ++--
 .../dba/postgres/PostgresSelectAction.java      |  24 +-
 .../dba/postgres/PostgresSelectTranslator.java  |  15 +-
 .../dba/sqlserver/SQLServerSelectAction.java    |   6 +-
 .../sqlserver/SQLServerSelectTranslator.java    |  65 +-
 .../SQLServerTrimmingQualifierTranslator.java   | 164 ++--
 .../cayenne/log/CommonsJdbcEventLogger.java     | 819 ++++++++--------
 .../org/apache/cayenne/log/JdbcEventLogger.java |  21 +-
 .../apache/cayenne/log/NoopJdbcEventLogger.java | 142 +--
 .../merge/DefaultValueForNullProvider.java      |  10 +-
 .../apache/cayenne/velocity/BindDirective.java  |  10 +-
 .../cayenne/velocity/BindEqualDirective.java    |   4 +-
 .../cayenne/velocity/BindNotEqualDirective.java |   4 +-
 .../velocity/BindObjectEqualDirective.java      |   6 +-
 .../velocity/BindObjectNotEqualDirective.java   |   4 +-
 .../velocity/VelocitySQLTemplateProcessor.java  |   6 +-
 .../cayenne/access/jdbc/SQLStatementTest.java   |   2 +-
 .../batch/DefaultBatchTranslatorIT.java         |  25 +-
 .../select/DefaultSelectTranslatorIT.java       | 787 +++++++++++++++
 .../translator/select/OrderingTranslatorIT.java | 216 ++---
 .../select/QualifierTranslatorIT.java           | 197 ++--
 .../translator/select/QueryAssemblerIT.java     |  62 +-
 .../translator/select/SelectTranslatorIT.java   | 794 ----------------
 .../translator/select/TstQueryAssembler.java    |  64 +-
 .../apache/cayenne/merge/ValueForNullIT.java    |   6 +-
 .../VelocitySQLTemplateProcessorTest.java       |  10 +-
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |   1 +
 78 files changed, 4870 insertions(+), 5012 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
index cbb19f2..e86e564 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslatorFactoryDecorator.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.batch;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.crypto.transformer.BindingsTransformer;
@@ -65,16 +65,16 @@ public class CryptoBatchTranslatorFactoryDecorator implements BatchTranslatorFac
             }
 
             @Override
-            public BatchParameterBinding[] getBindings() {
+            public ParameterBinding[] getBindings() {
                 return delegateTranslator.getBindings();
             }
 
             @Override
-            public BatchParameterBinding[] updateBindings(BatchQueryRow row) {
+            public ParameterBinding[] updateBindings(BatchQueryRow row) {
 
                 ensureEncryptorCompiled();
 
-                BatchParameterBinding[] bindings = delegateTranslator.updateBindings(row);
+                ParameterBinding[] bindings = delegateTranslator.updateBindings(row);
 
                 if (encryptor != null) {
                     encryptor.transform(bindings);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
index 2d782ee..c6f9577 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java
@@ -18,12 +18,12 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 
 /**
  * @since 4.0
  */
 public interface BindingsTransformer {
 
-    void transform(BatchParameterBinding[] bindings);
+    void transform(ParameterBinding[] bindings);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
index 0ef8caf..db31eea 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.transformer;
 
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.crypto.transformer.bytes.BytesEncryptor;
 import org.apache.cayenne.crypto.transformer.value.ValueEncryptor;
 
@@ -38,12 +38,12 @@ public class DefaultBindingsTransformer implements BindingsTransformer {
     }
 
     @Override
-    public void transform(BatchParameterBinding[] bindings) {
+    public void transform(ParameterBinding[] bindings) {
 
         int len = positions.length;
 
         for (int i = 0; i < len; i++) {
-            BatchParameterBinding b = bindings[positions[i]];
+            ParameterBinding b = bindings[positions[i]];
             Object transformed = transformers[i].encrypt(encryptor, b.getValue());
             b.setValue(transformed);
         }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
index 3b3e73a..178a2f8 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java
@@ -23,7 +23,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.crypto.map.ColumnMapper;
 import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory;
 import org.apache.cayenne.crypto.transformer.value.ValueDecryptor;
@@ -90,7 +90,7 @@ public class DefaultTransformerFactory implements TransformerFactory {
     }
 
     @Override
-    public BindingsTransformer encryptor(BatchParameterBinding[] bindings) {
+    public BindingsTransformer encryptor(ParameterBinding[] bindings) {
         int len = bindings.length;
         List<Integer> cryptoColumns = null;
 
@@ -115,7 +115,7 @@ public class DefaultTransformerFactory implements TransformerFactory {
 
             for (int i = 0; i < dlen; i++) {
                 int pos = cryptoColumns.get(i);
-                BatchParameterBinding b = bindings[pos];
+                ParameterBinding b = bindings[pos];
                 positions[i] = pos;
                 transformers[i] = transformerFactory.encryptor(b.getAttribute());
             }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
index 0d6ed2b..9bc28a8 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.crypto.transformer;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 
 /**
  * A factory that creates encryption transformers used for processing batch
@@ -29,7 +29,7 @@ import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
  */
 public interface TransformerFactory {
 
-    BindingsTransformer encryptor(BatchParameterBinding[] bindings);
+    BindingsTransformer encryptor(ParameterBinding[] bindings);
 
     MapTransformer decryptor(ColumnDescriptor[] columns, Object sampleRow);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
index 7efe4e3..ca3d1ef 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
@@ -33,7 +33,7 @@ import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.OptimisticLockException;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
-import org.apache.cayenne.access.translator.batch.BatchParameterBinding;
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
@@ -53,10 +53,10 @@ public class BatchAction extends BaseSQLAction {
     protected BatchQuery query;
     protected RowDescriptor keyRowDescriptor;
 
-    private static void bind(DbAdapter adapter, PreparedStatement statement, BatchParameterBinding[] bindings)
+    private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
             throws SQLException, Exception {
 
-        for (BatchParameterBinding b : bindings) {
+        for (ParameterBinding b : bindings) {
             if (!b.isExcluded()) {
                 adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
                         .getAttribute().getScale());
@@ -114,7 +114,7 @@ public class BatchAction extends BaseSQLAction {
         try {
             for (BatchQueryRow row : query.getRows()) {
 
-                BatchParameterBinding[] bindings = translator.updateBindings(row);
+                ParameterBinding[] bindings = translator.updateBindings(row);
                 logger.logQueryParameters("batch bind", bindings);
                 bind(adapter, statement, bindings);
 
@@ -171,7 +171,7 @@ public class BatchAction extends BaseSQLAction {
         try {
             for (BatchQueryRow row : query.getRows()) {
 
-                BatchParameterBinding[] bindings = translator.updateBindings(row);
+                ParameterBinding[] bindings = translator.updateBindings(row);
                 logger.logQueryParameters("bind", bindings);
 
                 bind(adapter, statement, bindings);

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java
deleted file mode 100644
index 8eb5f2b..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ParameterBinding.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*****************************************************************
- *   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.access.jdbc;
-
-/**
- * Describes PreparedStatement parameter binding.
- * 
- * @since 1.1
- */
-public class ParameterBinding {
-
-    protected int jdbcType;
-    protected int scale;
-    protected Object value;
-
-    public ParameterBinding(Object value, int jdbcType, int scale) {
-        this.value = value;
-        this.jdbcType = jdbcType;
-        this.scale = scale;
-    }
-
-    public int getJdbcType() {
-        return jdbcType;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public int getScale() {
-        return scale;
-    }
-
-    public Object getValue() {
-        return value;
-    }
-
-    public void setJdbcType(int i) {
-        jdbcType = i;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public void setScale(int i) {
-        scale = i;
-    }
-
-    public void setValue(Object object) {
-        value = object;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java
new file mode 100644
index 0000000..f2b2369
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLParameterBinding.java
@@ -0,0 +1,62 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.jdbc;
+
+/**
+ * Describes PreparedStatement parameter binding.
+ * 
+ * @since 4.0
+ */
+public class SQLParameterBinding {
+
+	protected int jdbcType;
+	protected int scale;
+	protected Object value;
+
+	public SQLParameterBinding(Object value, int jdbcType, int scale) {
+		this.value = value;
+		this.jdbcType = jdbcType;
+		this.scale = scale;
+	}
+
+	public int getJdbcType() {
+		return jdbcType;
+	}
+
+	public int getScale() {
+		return scale;
+	}
+
+	public Object getValue() {
+		return value;
+	}
+
+	public void setJdbcType(int i) {
+		jdbcType = i;
+	}
+
+	public void setScale(int i) {
+		scale = i;
+	}
+
+	public void setValue(Object object) {
+		value = object;
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
index b13050a..33d385d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SQLStatement.java
@@ -28,13 +28,13 @@ package org.apache.cayenne.access.jdbc;
 public class SQLStatement {
 
     protected String sql;
-    protected ParameterBinding[] bindings;
+    protected SQLParameterBinding[] bindings;
     protected ColumnDescriptor[] resultColumns;
 
     public SQLStatement() {
     }
 
-    public SQLStatement(String sql, ParameterBinding[] bindings) {
+    public SQLStatement(String sql, SQLParameterBinding[] bindings) {
         this(sql, null, bindings);
     }
 
@@ -42,7 +42,7 @@ public class SQLStatement {
      * @since 1.2
      */
     public SQLStatement(String sql, ColumnDescriptor[] resultColumns,
-            ParameterBinding[] bindings) {
+            SQLParameterBinding[] bindings) {
 
         setSql(sql);
         setBindings(bindings);
@@ -63,7 +63,7 @@ public class SQLStatement {
         resultColumns = descriptors;
     }
 
-    public ParameterBinding[] getBindings() {
+    public SQLParameterBinding[] getBindings() {
         return bindings;
     }
 
@@ -71,7 +71,7 @@ public class SQLStatement {
         return sql;
     }
 
-    public void setBindings(ParameterBinding[] bindings) {
+    public void setBindings(SQLParameterBinding[] bindings) {
         this.bindings = bindings;
     }
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/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 9c76fe9..97a1067 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
@@ -343,7 +343,7 @@ public class SQLTemplateAction implements SQLAction {
 	/**
 	 * Binds parameters to the PreparedStatement.
 	 */
-	protected void bind(PreparedStatement preparedStatement, ParameterBinding[] bindings) throws SQLException,
+	protected void bind(PreparedStatement preparedStatement, SQLParameterBinding[] bindings) throws SQLException,
 			Exception {
 		// bind parameters
 		if (bindings.length > 0) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index f1e6419..00785c9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -30,7 +30,11 @@ import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
+import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.query.PrefetchProcessor;
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.QueryMetadata;
@@ -43,149 +47,199 @@ import org.apache.cayenne.query.SelectQuery;
  */
 public class SelectAction extends BaseSQLAction {
 
-    protected SelectQuery<?> query;
-
-    /**
-     * @since 4.0
-     */
-    public SelectAction(SelectQuery<?> query, DataNode dataNode) {
-        super(dataNode);
-        this.query = query;
-    }
-
-    protected SelectTranslator createTranslator(Connection connection) {
-        return new SelectTranslator(query, dataNode, connection);
-    }
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    @Override
-    public void performAction(Connection connection, OperationObserver observer) throws SQLException, Exception {
-
-        final long t1 = System.currentTimeMillis();
-
-        final SelectTranslator translator = createTranslator(connection);
-        PreparedStatement prepStmt = translator.createStatement();
-
-        // TODO: ugly... 'createSqlString' is already called inside
-        // 'createStatement', but calling it here again to store for logging
-        // purposes
-        final String sqlString = translator.createSqlString();
-
-        ResultSet rs;
-
-        // need to run in try-catch block to close statement properly if
-        // exception happens
-        try {
-            rs = prepStmt.executeQuery();
-        } catch (Exception ex) {
-            prepStmt.close();
-            throw ex;
-        }
-        QueryMetadata md = query.getMetaData(dataNode.getEntityResolver());
-        RowDescriptor descriptor = new RowDescriptorBuilder().setColumns(translator.getResultColumns()).getDescriptor(
-                dataNode.getAdapter().getExtendedTypes());
-        
-        RowReader<?> rowReader = dataNode.rowReader(descriptor, md, translator.getAttributeOverrides());
-
-        JDBCResultIterator workerIterator = new JDBCResultIterator(prepStmt, rs, rowReader);
-
-        ResultIterator it = workerIterator;
-
-        if (observer.isIteratedResult()) {
-            it = new ConnectionAwareResultIterator(it, connection) {
-                @Override
-                protected void doClose() {
-                    dataNode.getJdbcEventLogger().logSelectCount(rowCounter, System.currentTimeMillis() - t1, sqlString);
-                    super.doClose();
-                }
-            };
-        }
-
-        // wrap result iterator if distinct has to be suppressed
-        if (translator.isSuppressingDistinct()) {
-
-            // a joint prefetch warrants full row compare
-
-            final boolean[] compareFullRows = new boolean[1];
-
-            final PrefetchTreeNode rootPrefetch = md.getPrefetchTree();
-
-            if (rootPrefetch != null) {
-                rootPrefetch.traverse(new PrefetchProcessor() {
-
-                    public void finishPrefetch(PrefetchTreeNode node) {
-                    }
-
-                    public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-                        // continue to children only if we are at root
-                        return rootPrefetch == node;
-                    }
-
-                    public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
-                        // continue to children only if we are at root
-                        return rootPrefetch == node;
-                    }
-
-                    public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-                        // continue to children only if we are at root
-                        return rootPrefetch == node;
-                    }
-
-                    public boolean startJointPrefetch(PrefetchTreeNode node) {
-                        if (rootPrefetch != node) {
-                            compareFullRows[0] = true;
-                            return false;
-                        }
-
-                        return true;
-                    }
-
-                    public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-                        return true;
-                    }
-                });
-            }
-
-            it = new DistinctResultIterator(workerIterator, translator.getRootDbEntity(), compareFullRows[0]);
-        }
-
-        // wrap iterator in a fetch limit checker ... there are a few cases when
-        // in-memory
-        // fetch limit is a noop, however in a general case this is needed, as
-        // the SQL
-        // result count does not directly correspond to the number of objects
-        // returned
-        // from Cayenne.
-
-        int fetchLimit = query.getFetchLimit();
-        int offset = translator.isSuppressingDistinct() ? query.getFetchOffset() : getInMemoryOffset(query
-                .getFetchOffset());
-        if (fetchLimit > 0 || offset > 0) {
-            it = new LimitResultIterator(it, offset, fetchLimit);
-        }
-
-        // TODO: Should do something about closing ResultSet and
-        // PreparedStatement in this
-        // method, instead of relying on DefaultResultIterator to do that later
-
-        if (observer.isIteratedResult()) {
-            try {
-                observer.nextRows(translator.getQuery(), it);
-            } catch (Exception ex) {
-                it.close();
-                throw ex;
-            }
-        } else {
-            List<DataRow> resultRows;
-            try {
-                resultRows = it.allRows();
-            } finally {
-                it.close();
-            }
-
-            dataNode.getJdbcEventLogger().logSelectCount(resultRows.size(), System.currentTimeMillis() - t1, sqlString);
-
-            observer.nextRows(query, resultRows);
-        }
-    }
+	private static void bind(DbAdapter adapter, PreparedStatement statement, ParameterBinding[] bindings)
+			throws SQLException, Exception {
+
+		for (ParameterBinding b : bindings) {
+
+			if (b.isExcluded()) {
+				continue;
+			}
+
+			// null DbAttributes are a result of inferior qualifier
+			// processing (qualifier can't map parameters to DbAttributes
+			// and therefore only supports standard java types now) hence, a
+			// special moronic case here:
+			if (b.getAttribute() == null) {
+				statement.setObject(b.getStatementPosition(), b.getValue());
+			} else {
+				adapter.bindParameter(statement, b.getValue(), b.getStatementPosition(), b.getAttribute().getType(), b
+						.getAttribute().getScale());
+			}
+		}
+
+	}
+
+	protected SelectQuery<?> query;
+	protected QueryMetadata queryMetadata;
+
+	/**
+	 * @since 4.0
+	 */
+	public SelectAction(SelectQuery<?> query, DataNode dataNode) {
+		super(dataNode);
+		this.query = query;
+		this.queryMetadata = query.getMetaData(dataNode.getEntityResolver());
+	}
+
+	protected SelectTranslator createTranslator() {
+		return new DefaultSelectTranslator(query, dataNode.getAdapter(), dataNode.getEntityResolver());
+	}
+
+	@SuppressWarnings({ "unchecked", "rawtypes", "resource" })
+	@Override
+	public void performAction(Connection connection, OperationObserver observer) throws SQLException, Exception {
+
+		final long t1 = System.currentTimeMillis();
+
+		JdbcEventLogger logger = dataNode.getJdbcEventLogger();
+		SelectTranslator translator = createTranslator();
+		final String sql = translator.getSql();
+
+		ParameterBinding[] bindings = translator.getBindings();
+		PreparedStatement statement = connection.prepareStatement(sql);
+		bind(dataNode.getAdapter(), statement, bindings);
+
+		int fetchSize = queryMetadata.getStatementFetchSize();
+		if (fetchSize != 0) {
+			statement.setFetchSize(fetchSize);
+		}
+
+		logger.logQuery(sql, bindings, System.currentTimeMillis() - t1);
+
+		ResultSet rs;
+
+		// need to run in try-catch block to close statement properly if
+		// exception happens
+		try {
+			rs = statement.executeQuery();
+		} catch (Exception ex) {
+			statement.close();
+			throw ex;
+		}
+		RowDescriptor descriptor = new RowDescriptorBuilder().setColumns(translator.getResultColumns()).getDescriptor(
+				dataNode.getAdapter().getExtendedTypes());
+
+		RowReader<?> rowReader = dataNode.rowReader(descriptor, queryMetadata, translator.getAttributeOverrides());
+
+		ResultIterator it = new JDBCResultIterator(statement, rs, rowReader);
+		it = forIteratedResult(it, observer, connection, t1, sql);
+		it = forSuppressedDistinct(it, translator);
+		it = forFetchLimit(it, translator);
+
+		// TODO: Should do something about closing ResultSet and
+		// PreparedStatement in this method, instead of relying on
+		// DefaultResultIterator to do that later
+
+		if (observer.isIteratedResult()) {
+			try {
+				observer.nextRows(query, it);
+			} catch (Exception ex) {
+				it.close();
+				throw ex;
+			}
+		} else {
+			List<DataRow> resultRows;
+			try {
+				resultRows = it.allRows();
+			} finally {
+				it.close();
+			}
+
+			dataNode.getJdbcEventLogger().logSelectCount(resultRows.size(), System.currentTimeMillis() - t1, sql);
+
+			observer.nextRows(query, resultRows);
+		}
+	}
+
+	private <T> ResultIterator<T> forIteratedResult(ResultIterator<T> iterator, OperationObserver observer,
+			Connection connection, final long queryStartedAt, final String sql) {
+		if (!observer.isIteratedResult()) {
+			return iterator;
+		}
+
+		return new ConnectionAwareResultIterator<T>(iterator, connection) {
+			@Override
+			protected void doClose() {
+				dataNode.getJdbcEventLogger().logSelectCount(rowCounter, System.currentTimeMillis() - queryStartedAt,
+						sql);
+				super.doClose();
+			}
+		};
+	}
+
+	private <T> ResultIterator<T> forFetchLimit(ResultIterator<T> iterator, SelectTranslator translator) {
+		// wrap iterator in a fetch limit checker ... there are a few cases when
+		// in-memory fetch limit is a noop, however in a general case this is
+		// needed, as the SQL result count does not directly correspond to the
+		// number of objects returned from Cayenne.
+
+		int fetchLimit = query.getFetchLimit();
+		int offset = translator.isSuppressingDistinct() ? query.getFetchOffset() : getInMemoryOffset(query
+				.getFetchOffset());
+
+		if (fetchLimit > 0 || offset > 0) {
+			return new LimitResultIterator<T>(iterator, offset, fetchLimit);
+		} else {
+			return iterator;
+		}
+	}
+
+	private <T> ResultIterator<T> forSuppressedDistinct(ResultIterator<T> iterator, SelectTranslator translator) {
+		if (!translator.isSuppressingDistinct()) {
+			return iterator;
+		}
+
+		// wrap result iterator if distinct has to be suppressed
+
+		// a joint prefetch warrants full row compare
+
+		final boolean[] compareFullRows = new boolean[1];
+		final PrefetchTreeNode rootPrefetch = queryMetadata.getPrefetchTree();
+
+		if (rootPrefetch != null) {
+			rootPrefetch.traverse(new PrefetchProcessor() {
+
+				@Override
+				public void finishPrefetch(PrefetchTreeNode node) {
+				}
+
+				@Override
+				public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+					// continue to children only if we are at root
+					return rootPrefetch == node;
+				}
+
+				@Override
+				public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
+					// continue to children only if we are at root
+					return rootPrefetch == node;
+				}
+
+				@Override
+				public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+					// continue to children only if we are at root
+					return rootPrefetch == node;
+				}
+
+				@Override
+				public boolean startJointPrefetch(PrefetchTreeNode node) {
+					if (rootPrefetch != node) {
+						compareFullRows[0] = true;
+						return false;
+					}
+
+					return true;
+				}
+
+				@Override
+				public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+					return true;
+				}
+			});
+		}
+
+		return new DistinctResultIterator<T>(iterator, queryMetadata.getDbEntity(), compareFullRows[0]);
+	}
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
new file mode 100644
index 0000000..7a6a7e6
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/ParameterBinding.java
@@ -0,0 +1,81 @@
+/*****************************************************************
+ *   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.access.translator;
+
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * Describes a PreparedStatement parameter binding mapped to a DbAttribute.
+ * 
+ * @since 4.0
+ */
+public class ParameterBinding {
+
+	static final int EXCLUDED_POSITION = -1;
+
+	private DbAttribute attribute;
+	private Object value;
+	private int statementPosition;
+
+	public ParameterBinding(DbAttribute attribute) {
+		this.attribute = attribute;
+		this.statementPosition = EXCLUDED_POSITION;
+	}
+
+	public DbAttribute getAttribute() {
+		return attribute;
+	}
+
+	public Object getValue() {
+		return value;
+	}
+
+	public void setValue(Object value) {
+		this.value = value;
+	}
+
+	public int getStatementPosition() {
+		return statementPosition;
+	}
+
+	public void setStatementPosition(int statementPosition) {
+		this.statementPosition = statementPosition;
+	}
+
+	public boolean isExcluded() {
+		return statementPosition == EXCLUDED_POSITION;
+	}
+
+	/**
+	 * Marks the binding object as excluded for the current iteration.
+	 */
+	public void exclude() {
+		this.statementPosition = EXCLUDED_POSITION;
+		this.value = null;
+	}
+
+	/**
+	 * Sets the value of the binding and initializes statement position var,
+	 * thus "including" this binding in the current iteration.
+	 */
+	public void include(int statementPosition, Object value) {
+		this.statementPosition = statementPosition;
+		this.value = value;
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java
deleted file mode 100644
index a37a145..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchParameterBinding.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*****************************************************************
- *   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.access.translator.batch;
-
-import org.apache.cayenne.map.DbAttribute;
-
-/**
- * @since 4.0
- */
-public class BatchParameterBinding {
-
-    static final int EXCLUDED_POSITION = -1;
-
-    private DbAttribute attribute;
-    private Object value;
-    private int statementPosition;
-
-    public BatchParameterBinding(DbAttribute attribute) {
-        this.attribute = attribute;
-        this.statementPosition = EXCLUDED_POSITION;
-    }
-
-    public DbAttribute getAttribute() {
-        return attribute;
-    }
-
-    public Object getValue() {
-        return value;
-    }
-
-    public void setValue(Object value) {
-        this.value = value;
-    }
-
-    public int getStatementPosition() {
-        return statementPosition;
-    }
-
-    public void setStatementPosition(int statementPosition) {
-        this.statementPosition = statementPosition;
-    }
-
-    public boolean isExcluded() {
-        return statementPosition == EXCLUDED_POSITION;
-    }
-
-    /**
-     * Marks the binding object as excluded for the current iteration.
-     */
-    public void exclude() {
-        this.statementPosition = EXCLUDED_POSITION;
-        this.value = null;
-    }
-
-    /**
-     * Sets the value of the binding and initializes statement position var,
-     * thus "including" this binding in the current iteration.
-     */
-    public void include(int statementPosition, Object value) {
-        this.statementPosition = statementPosition;
-        this.value = value;
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
index 989fcf1..04a4902 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslator.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.access.translator.batch;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.query.BatchQueryRow;
 
 /**
@@ -36,7 +37,7 @@ public interface BatchTranslator {
     /**
      * Returns the widest possible array of bindings for this query.
      */
-    BatchParameterBinding[] getBindings();
+    ParameterBinding[] getBindings();
 
     /**
      * Updates internal bindings to be used with a given row, returning updated
@@ -46,5 +47,5 @@ public interface BatchTranslator {
      * parameter). Usually the returned array is actually the same object reused
      * for every iteration, only with changed object state.
      */
-    BatchParameterBinding[] updateBindings(BatchQueryRow row);
+    ParameterBinding[] updateBindings(BatchQueryRow row);
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
index 82f945e..732ec64 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
@@ -20,6 +20,7 @@ package org.apache.cayenne.access.translator.batch;
 
 import java.sql.Types;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -39,7 +40,7 @@ public abstract class DefaultBatchTranslator implements BatchTranslator {
 
     protected boolean translated;
     protected String sql;
-    protected BatchParameterBinding[] bindings;
+    protected ParameterBinding[] bindings;
 
     public DefaultBatchTranslator(BatchQuery query, DbAdapter adapter, String trimFunction) {
         this.query = query;
@@ -66,22 +67,22 @@ public abstract class DefaultBatchTranslator implements BatchTranslator {
     }
 
     @Override
-    public BatchParameterBinding[] getBindings() {
+    public ParameterBinding[] getBindings() {
         ensureTranslated();
         return bindings;
     }
     
     @Override
-    public BatchParameterBinding[] updateBindings(BatchQueryRow row) {
+    public ParameterBinding[] updateBindings(BatchQueryRow row) {
         ensureTranslated();
         return doUpdateBindings(row);
     }
 
     protected abstract String createSql();
 
-    protected abstract BatchParameterBinding[] createBindings();
+    protected abstract ParameterBinding[] createBindings();
     
-    protected abstract BatchParameterBinding[] doUpdateBindings(BatchQueryRow row);
+    protected abstract ParameterBinding[] doUpdateBindings(BatchQueryRow row);
 
     /**
      * Appends the name of the column to the query buffer. Subclasses use this

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
index bf91404..e6636da 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.access.translator.batch;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -71,23 +72,23 @@ public class DeleteBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
         List<DbAttribute> attributes = deleteBatch.getDbAttributes();
         int len = attributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[len];
+        ParameterBinding[] bindings = new ParameterBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute a = attributes.get(i);
-            bindings[i] = new BatchParameterBinding(a);
+            bindings[i] = new ParameterBinding(a);
         }
 
         return bindings;
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
 
         int len = bindings.length;
 
@@ -95,7 +96,7 @@ public class DeleteBatchTranslator extends DefaultBatchTranslator {
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             // skip null attributes... they are translated as "IS NULL"
             if (deleteBatch.isNull(b.getAttribute())) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
index 43ce612..3deb552 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.access.translator.batch;
 
 import java.util.List;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -81,15 +82,15 @@ public class InsertBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         List<DbAttribute> attributes = query.getDbAttributes();
         int len = attributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[len];
+        ParameterBinding[] bindings = new ParameterBinding[len];
 
         for (int i = 0; i < len; i++) {
             DbAttribute a = attributes.get(i);
-            bindings[i] = new BatchParameterBinding(a);
+            bindings[i] = new ParameterBinding(a);
 
             // include/exclude state depends on DbAttribute only and can be
             // precompiled here
@@ -106,12 +107,12 @@ public class InsertBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
         int len = bindings.length;
 
         for (int i = 0, j = 1; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             // exclusions are permanent
             if (!b.isExcluded()) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
index 7311cc5..4e793e5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.access.translator.batch;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -53,16 +54,16 @@ public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
 
-        BatchParameterBinding[] superBindings = super.createBindings();
+        ParameterBinding[] superBindings = super.createBindings();
 
         int slen = superBindings.length;
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[slen + 1];
+        ParameterBinding[] bindings = new ParameterBinding[slen + 1];
 
         DbAttribute deleteAttribute = query.getDbEntity().getAttribute(deletedFieldName);
-        bindings[0] = new BatchParameterBinding(deleteAttribute);
+        bindings[0] = new ParameterBinding(deleteAttribute);
         bindings[0].include(1, true);
         
         System.arraycopy(superBindings, 0, bindings, 1, slen);
@@ -71,7 +72,7 @@ public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
         int len = bindings.length;
 
         DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
@@ -79,7 +80,7 @@ public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
         // skip position 0... Otherwise follow super algorithm
         for (int i = 1, j = 2; i < len; i++) {
 
-            BatchParameterBinding b = bindings[i];
+            ParameterBinding b = bindings[i];
 
             // skip null attributes... they are translated as "IS NULL"
             if (deleteBatch.isNull(b.getAttribute())) {

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
index 61a49b3..a44a373 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
@@ -22,6 +22,7 @@ package org.apache.cayenne.access.translator.batch;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -78,7 +79,7 @@ public class UpdateBatchTranslator extends DefaultBatchTranslator {
     }
 
     @Override
-    protected BatchParameterBinding[] createBindings() {
+    protected ParameterBinding[] createBindings() {
         UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
 
         List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
@@ -87,23 +88,23 @@ public class UpdateBatchTranslator extends DefaultBatchTranslator {
         int ul = updatedDbAttributes.size();
         int ql = qualifierAttributes.size();
 
-        BatchParameterBinding[] bindings = new BatchParameterBinding[ul + ql];
+        ParameterBinding[] bindings = new ParameterBinding[ul + ql];
 
         for (int i = 0; i < ul; i++) {
             DbAttribute a = updatedDbAttributes.get(i);
-            bindings[i] = new BatchParameterBinding(a);
+            bindings[i] = new ParameterBinding(a);
         }
 
         for (int i = 0; i < ql; i++) {
             DbAttribute a = qualifierAttributes.get(i);
-            bindings[ul + i] = new BatchParameterBinding(a);
+            bindings[ul + i] = new ParameterBinding(a);
         }
 
         return bindings;
     }
 
     @Override
-    protected BatchParameterBinding[] doUpdateBindings(BatchQueryRow row) {
+    protected ParameterBinding[] doUpdateBindings(BatchQueryRow row) {
 
         UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
 

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
new file mode 100644
index 0000000..38b5763
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
@@ -0,0 +1,630 @@
+/*****************************************************************
+ *   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.access.translator.select;
+
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbJoin;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.JoinType;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.map.PathComponent;
+import org.apache.cayenne.query.PrefetchSelectQuery;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SelectQuery;
+import org.apache.cayenne.reflect.ArcProperty;
+import org.apache.cayenne.reflect.AttributeProperty;
+import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyVisitor;
+import org.apache.cayenne.reflect.ToManyProperty;
+import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.util.CayenneMapEntry;
+import org.apache.cayenne.util.EqualsBuilder;
+import org.apache.cayenne.util.HashCodeBuilder;
+
+/**
+ * @since 4.0
+ */
+public class DefaultSelectTranslator extends QueryAssembler implements SelectTranslator {
+
+	protected static final int[] UNSUPPORTED_DISTINCT_TYPES = { Types.BLOB, Types.CLOB, Types.NCLOB,
+			Types.LONGVARBINARY, Types.LONGVARCHAR, Types.LONGNVARCHAR };
+
+	protected static boolean isUnsupportedForDistinct(int type) {
+		for (int unsupportedDistinctType : UNSUPPORTED_DISTINCT_TYPES) {
+			if (unsupportedDistinctType == type) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	JoinStack joinStack;
+
+	public DefaultSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) {
+		super(query, adapter, entityResolver);
+	}
+
+	protected JoinStack getJoinStack() {
+		if (joinStack == null) {
+			joinStack = createJoinStack();
+		}
+		return joinStack;
+	}
+
+	List<ColumnDescriptor> resultColumns;
+	Map<ObjAttribute, ColumnDescriptor> attributeOverrides;
+	Map<ColumnDescriptor, ObjAttribute> defaultAttributesByColumn;
+
+	boolean suppressingDistinct;
+
+	/**
+	 * If set to <code>true</code>, indicates that distinct select query is
+	 * required no matter what the original query settings where. This flag can
+	 * be set when joins are created using "to-many" relationships.
+	 */
+	boolean forcingDistinct;
+
+	protected JoinStack createJoinStack() {
+		return new JoinStack(getAdapter(), queryMetadata.getDataMap(), this);
+	}
+
+	@Override
+	protected void doTranslate() {
+
+		DataMap dataMap = queryMetadata.getDataMap();
+		JoinStack joins = getJoinStack();
+
+		QuotingStrategy strategy = getAdapter().getQuotingStrategy();
+		forcingDistinct = false;
+
+		// build column list
+		this.resultColumns = buildResultColumns();
+
+		// build qualifier
+		QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this);
+		StringBuilder qualifierBuffer = qualifierTranslator.appendPart(new StringBuilder());
+
+		// build ORDER BY
+		OrderingTranslator orderingTranslator = new OrderingTranslator(this);
+		StringBuilder orderingBuffer = orderingTranslator.appendPart(new StringBuilder());
+
+		// assemble
+		StringBuilder queryBuf = new StringBuilder();
+		queryBuf.append("SELECT ");
+
+		// check if DISTINCT is appropriate
+		// side effect: "suppressingDistinct" flag may end up being flipped here
+		if (forcingDistinct || getSelectQuery().isDistinct()) {
+			suppressingDistinct = false;
+
+			for (ColumnDescriptor column : resultColumns) {
+				if (isUnsupportedForDistinct(column.getJdbcType())) {
+					suppressingDistinct = true;
+					break;
+				}
+			}
+
+			if (!suppressingDistinct) {
+				queryBuf.append(buildDistinctStatement() + " ");
+			}
+		}
+
+		// convert ColumnDescriptors to column names
+		List<String> selectColumnExpList = new ArrayList<String>();
+		for (ColumnDescriptor column : resultColumns) {
+			String fullName = strategy.quotedIdentifier(dataMap, column.getNamePrefix(), column.getName());
+			selectColumnExpList.add(fullName);
+		}
+
+		// append any column expressions used in the order by if this query
+		// uses the DISTINCT modifier
+		if (forcingDistinct || getSelectQuery().isDistinct()) {
+			List<String> orderByColumnList = orderingTranslator.getOrderByColumnList();
+			for (String orderByColumnExp : orderByColumnList) {
+				// Convert to ColumnDescriptors??
+				if (!selectColumnExpList.contains(orderByColumnExp)) {
+					selectColumnExpList.add(orderByColumnExp);
+				}
+			}
+		}
+
+		appendSelectColumns(queryBuf, selectColumnExpList);
+
+		// append from clause
+		queryBuf.append(" FROM ");
+
+		// append tables and joins
+		joins.appendRootWithQuoteSqlIdentifiers(queryBuf, getQueryMetadata().getDbEntity());
+
+		joins.appendJoins(queryBuf);
+		joins.appendQualifier(qualifierBuffer, qualifierBuffer.length() == 0);
+
+		// append qualifier
+		if (qualifierBuffer.length() > 0) {
+			queryBuf.append(" WHERE ");
+			queryBuf.append(qualifierBuffer);
+		}
+
+		// append prebuilt ordering
+		if (orderingBuffer.length() > 0) {
+			queryBuf.append(" ORDER BY ").append(orderingBuffer);
+		}
+
+		if (!isSuppressingDistinct()) {
+			appendLimitAndOffsetClauses(queryBuf);
+		}
+
+		this.sql = queryBuf.toString();
+	}
+
+	/**
+	 * Allows subclasses to insert their own dialect of DISTINCT statement to
+	 * improve performance.
+	 *
+	 * @return string representing the DISTINCT statement
+	 * @since 4.0
+	 */
+	protected String buildDistinctStatement() {
+		return "DISTINCT";
+	}
+
+	/**
+	 * @since 3.1
+	 */
+	protected void appendSelectColumns(StringBuilder buffer, List<String> selectColumnExpList) {
+
+		// append columns (unroll the loop's first element)
+		int columnCount = selectColumnExpList.size();
+		buffer.append(selectColumnExpList.get(0));
+
+		// assume there is at least 1 element
+		for (int i = 1; i < columnCount; i++) {
+			buffer.append(", ");
+			buffer.append(selectColumnExpList.get(i));
+		}
+	}
+
+	/**
+	 * Handles appending optional limit and offset clauses. This implementation
+	 * does nothing, deferring to subclasses to define the LIMIT/OFFSET clause
+	 * syntax.
+	 * 
+	 * @since 3.0
+	 */
+	protected void appendLimitAndOffsetClauses(StringBuilder buffer) {
+
+	}
+
+	@Override
+	public String getCurrentAlias() {
+		return getJoinStack().getCurrentAlias();
+	}
+
+	/**
+	 * Returns a list of ColumnDescriptors for the query columns.
+	 * 
+	 * @since 1.2
+	 */
+	public ColumnDescriptor[] getResultColumns() {
+		if (resultColumns == null || resultColumns.isEmpty()) {
+			return new ColumnDescriptor[0];
+		}
+
+		return resultColumns.toArray(new ColumnDescriptor[resultColumns.size()]);
+	}
+
+	/**
+	 * Returns a map of ColumnDescriptors keyed by ObjAttribute for columns that
+	 * may need to be reprocessed manually due to incompatible mappings along
+	 * the inheritance hierarchy.
+	 * 
+	 * @since 1.2
+	 */
+	public Map<ObjAttribute, ColumnDescriptor> getAttributeOverrides() {
+		if (attributeOverrides != null) {
+			return attributeOverrides;
+		} else {
+			return Collections.emptyMap();
+		}
+	}
+
+	/**
+	 * Returns true if SelectTranslator determined that a query requiring
+	 * DISTINCT can't be run with DISTINCT keyword for internal reasons. If this
+	 * method returns true, DataNode may need to do in-memory distinct
+	 * filtering.
+	 * 
+	 * @since 1.1
+	 */
+	public boolean isSuppressingDistinct() {
+		return suppressingDistinct;
+	}
+
+	private SelectQuery<?> getSelectQuery() {
+		return (SelectQuery<?>) getQuery();
+	}
+
+	protected List<ColumnDescriptor> buildResultColumns() {
+
+		this.defaultAttributesByColumn = new HashMap<ColumnDescriptor, ObjAttribute>();
+
+		List<ColumnDescriptor> columns = new ArrayList<ColumnDescriptor>();
+		SelectQuery<?> query = getSelectQuery();
+
+		if (query.getRoot() instanceof DbEntity) {
+			appendDbEntityColumns(columns, query);
+		} else if (getQueryMetadata().getPageSize() > 0) {
+			appendIdColumns(columns, query);
+		} else {
+			appendQueryColumns(columns, query);
+		}
+
+		return columns;
+	}
+
+	<T> List<ColumnDescriptor> appendDbEntityColumns(List<ColumnDescriptor> columns, SelectQuery<T> query) {
+
+		Set<ColumnTracker> attributes = new HashSet<ColumnTracker>();
+
+		DbEntity table = getQueryMetadata().getDbEntity();
+		for (DbAttribute dba : table.getAttributes()) {
+			appendColumn(columns, null, dba, attributes, null);
+		}
+
+		return columns;
+	}
+
+	/**
+	 * Appends columns needed for object SelectQuery to the provided columns
+	 * list.
+	 */
+	<T> List<ColumnDescriptor> appendQueryColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) {
+
+		final Set<ColumnTracker> attributes = new HashSet<ColumnTracker>();
+
+		// fetched attributes include attributes that are either:
+		//
+		// * class properties
+		// * PK
+		// * FK used in relationship
+		// * joined prefetch PK
+
+		ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
+		ObjEntity oe = descriptor.getEntity();
+
+		PropertyVisitor visitor = new PropertyVisitor() {
+
+			public boolean visitAttribute(AttributeProperty property) {
+				ObjAttribute oa = property.getAttribute();
+
+				resetJoinStack();
+				Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
+				while (dbPathIterator.hasNext()) {
+					Object pathPart = dbPathIterator.next();
+
+					if (pathPart == null) {
+						throw new CayenneRuntimeException("ObjAttribute has no component: " + oa.getName());
+					} else if (pathPart instanceof DbRelationship) {
+						DbRelationship rel = (DbRelationship) pathPart;
+						dbRelationshipAdded(rel, JoinType.LEFT_OUTER, null);
+					} else if (pathPart instanceof DbAttribute) {
+						DbAttribute dbAttr = (DbAttribute) pathPart;
+
+						appendColumn(columns, oa, dbAttr, attributes, null);
+					}
+				}
+				return true;
+			}
+
+			public boolean visitToMany(ToManyProperty property) {
+				visitRelationship(property);
+				return true;
+			}
+
+			public boolean visitToOne(ToOneProperty property) {
+				visitRelationship(property);
+				return true;
+			}
+
+			private void visitRelationship(ArcProperty property) {
+				resetJoinStack();
+
+				ObjRelationship rel = property.getRelationship();
+				DbRelationship dbRel = rel.getDbRelationships().get(0);
+
+				List<DbJoin> joins = dbRel.getJoins();
+				for (DbJoin join : joins) {
+					DbAttribute src = join.getSource();
+					appendColumn(columns, null, src, attributes, null);
+				}
+			}
+		};
+
+		descriptor.visitAllProperties(visitor);
+
+		// stack should be reset, because all root table attributes go with "t0"
+		// table alias
+		resetJoinStack();
+
+		// add remaining needed attrs from DbEntity
+		DbEntity table = getQueryMetadata().getDbEntity();
+		for (DbAttribute dba : table.getPrimaryKeys()) {
+			appendColumn(columns, null, dba, attributes, null);
+		}
+
+		// special handling of a disjoint query...
+
+		if (query instanceof PrefetchSelectQuery) {
+
+			// for each relationship path add PK of the target entity...
+			for (String path : ((PrefetchSelectQuery) query).getResultPaths()) {
+
+				ASTDbPath pathExp = (ASTDbPath) oe.translateToDbPath(ExpressionFactory.exp(path));
+
+				// add joins and find terminating element
+
+				resetJoinStack();
+
+				PathComponent<DbAttribute, DbRelationship> lastComponent = null;
+				for (PathComponent<DbAttribute, DbRelationship> component : table
+						.resolvePath(pathExp, getPathAliases())) {
+
+					if (component.getRelationship() != null) {
+						// do not invoke dbRelationshipAdded(), invoke
+						// pushJoin() instead. This is to prevent
+						// 'forcingDistinct' flipping to true, that will result
+						// in unneeded extra processing and sometimes in invalid
+						// results (see CAY-1979). Distinctness of each row is
+						// guaranteed by the prefetch query semantics - we
+						// include target ID in the result columns
+						getJoinStack().pushJoin(component.getRelationship(), component.getJoinType(), null);
+					}
+
+					lastComponent = component;
+				}
+
+				// process terminating element
+				if (lastComponent != null) {
+
+					DbRelationship relationship = lastComponent.getRelationship();
+
+					if (relationship != null) {
+
+						String labelPrefix = pathExp.getPath();
+						DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+
+						for (DbAttribute pk : targetEntity.getPrimaryKeys()) {
+
+							// note that we my select a source attribute, but
+							// label it as
+							// target for simplified snapshot processing
+							appendColumn(columns, null, pk, attributes, labelPrefix + '.' + pk.getName());
+						}
+					}
+				}
+			}
+		}
+
+		// handle joint prefetches directly attached to this query...
+		if (query.getPrefetchTree() != null) {
+
+			for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) {
+
+				// for each prefetch add all joins plus columns from the target
+				// entity
+				Expression prefetchExp = ExpressionFactory.exp(prefetch.getPath());
+				ASTDbPath dbPrefetch = (ASTDbPath) oe.translateToDbPath(prefetchExp);
+
+				resetJoinStack();
+				DbRelationship r = null;
+				for (PathComponent<DbAttribute, DbRelationship> component : table.resolvePath(dbPrefetch,
+						getPathAliases())) {
+					r = component.getRelationship();
+					dbRelationshipAdded(r, JoinType.LEFT_OUTER, null);
+				}
+
+				if (r == null) {
+					throw new CayenneRuntimeException("Invalid joint prefetch '" + prefetch + "' for entity: "
+							+ oe.getName());
+				}
+
+				// add columns from the target entity, including those that are
+				// matched
+				// against the FK of the source entity. This is needed to
+				// determine
+				// whether optional relationships are null
+
+				// go via target OE to make sure that Java types are mapped
+				// correctly...
+				ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(oe);
+				ObjEntity targetEntity = (ObjEntity) targetRel.getTargetEntity();
+
+				String labelPrefix = dbPrefetch.getPath();
+				for (ObjAttribute oa : targetEntity.getAttributes()) {
+					Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator();
+					while (dbPathIterator.hasNext()) {
+						Object pathPart = dbPathIterator.next();
+
+						if (pathPart == null) {
+							throw new CayenneRuntimeException("ObjAttribute has no component: " + oa.getName());
+						} else if (pathPart instanceof DbRelationship) {
+							DbRelationship rel = (DbRelationship) pathPart;
+							dbRelationshipAdded(rel, JoinType.INNER, null);
+						} else if (pathPart instanceof DbAttribute) {
+							DbAttribute attribute = (DbAttribute) pathPart;
+
+							appendColumn(columns, oa, attribute, attributes, labelPrefix + '.' + attribute.getName());
+						}
+					}
+				}
+
+				// append remaining target attributes such as keys
+				DbEntity targetDbEntity = (DbEntity) r.getTargetEntity();
+				for (DbAttribute attribute : targetDbEntity.getAttributes()) {
+					appendColumn(columns, null, attribute, attributes, labelPrefix + '.' + attribute.getName());
+				}
+			}
+		}
+
+		return columns;
+	}
+
+	<T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) {
+
+		Set<ColumnTracker> skipSet = new HashSet<ColumnTracker>();
+
+		ClassDescriptor descriptor = queryMetadata.getClassDescriptor();
+		ObjEntity oe = descriptor.getEntity();
+		DbEntity dbEntity = oe.getDbEntity();
+		for (ObjAttribute attribute : oe.getPrimaryKeys()) {
+
+			// synthetic objattributes can't reliably lookup their DbAttribute,
+			// so do it manually..
+			DbAttribute dbAttribute = dbEntity.getAttribute(attribute.getDbAttributeName());
+			appendColumn(columns, attribute, dbAttribute, skipSet, null);
+		}
+
+		return columns;
+	}
+
+	private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute,
+			Set<ColumnTracker> skipSet, String label) {
+
+		String alias = getCurrentAlias();
+		if (skipSet.add(new ColumnTracker(alias, attribute))) {
+
+			ColumnDescriptor column = (objAttribute != null) ? new ColumnDescriptor(objAttribute, attribute, alias)
+					: new ColumnDescriptor(attribute, alias);
+
+			if (label != null) {
+				column.setDataRowKey(label);
+			}
+
+			columns.add(column);
+
+			// TODO: andrus, 5/7/2006 - replace 'columns' collection with this
+			// map, as it
+			// is redundant
+			defaultAttributesByColumn.put(column, objAttribute);
+		} else if (objAttribute != null) {
+
+			// record ObjAttribute override
+			for (ColumnDescriptor column : columns) {
+				if (attribute.getName().equals(column.getName())) {
+
+					if (attributeOverrides == null) {
+						attributeOverrides = new HashMap<ObjAttribute, ColumnDescriptor>();
+					}
+
+					// kick out the original attribute
+					ObjAttribute original = defaultAttributesByColumn.remove(column);
+
+					if (original != null) {
+						attributeOverrides.put(original, column);
+					}
+
+					attributeOverrides.put(objAttribute, column);
+					column.setJavaClass(Void.TYPE.getName());
+
+					break;
+				}
+			}
+		}
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	@Override
+	public void resetJoinStack() {
+		getJoinStack().resetStack();
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	@Override
+	public void dbRelationshipAdded(DbRelationship relationship, JoinType joinType, String joinSplitAlias) {
+		if (relationship.isToMany()) {
+			forcingDistinct = true;
+		}
+
+		getJoinStack().pushJoin(relationship, joinType, joinSplitAlias);
+	}
+
+	/**
+	 * Always returns true.
+	 */
+	@Override
+	public boolean supportsTableAliases() {
+		return true;
+	}
+
+	static final class ColumnTracker {
+
+		private DbAttribute attribute;
+		private String alias;
+
+		ColumnTracker(String alias, DbAttribute attribute) {
+			this.attribute = attribute;
+			this.alias = alias;
+		}
+
+		@Override
+		public boolean equals(Object object) {
+			if (!(object instanceof ColumnTracker)) {
+				return false;
+			}
+
+			ColumnTracker other = (ColumnTracker) object;
+			return new EqualsBuilder().append(alias, other.alias).append(attribute, other.attribute).isEquals();
+		}
+
+		@Override
+		public int hashCode() {
+			return new HashCodeBuilder(31, 5).append(alias).append(attribute).toHashCode();
+		}
+
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/81f4fb50/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
index 440f7b1..416f14c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/JoinStack.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.access.translator.select;
 
-import java.io.IOException;
 import java.util.List;
 
 import org.apache.cayenne.dba.DbAdapter;
@@ -45,178 +44,178 @@ import org.apache.commons.collections.Transformer;
  */
 public class JoinStack {
 
-    protected JoinTreeNode rootNode;
-    protected JoinTreeNode topNode;
-    private QuotingStrategy quotingStrategy;
-
-    private int aliasCounter;
-
-    /**
-     * Helper class to process DbEntity qualifiers
-     */
-    private QualifierTranslator qualifierTranslator;
-
-    protected JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
-        this.rootNode = new JoinTreeNode(this);
-        this.rootNode.setTargetTableAlias(newAlias());
-
-        this.quotingStrategy = dbAdapter.getQuotingStrategy();
-        this.qualifierTranslator = dbAdapter.getQualifierTranslator(assembler);
-
-        resetStack();
-    }
-
-    String getCurrentAlias() {
-        return topNode.getTargetTableAlias();
-    }
-
-    /**
-     * Returns the number of configured joins.
-     */
-    protected int size() {
-        // do not count root as a join
-        return rootNode.size() - 1;
-    }
-
-    void appendRootWithQuoteSqlIdentifiers(Appendable out, DbEntity rootEntity) throws IOException {
-
-        out.append(quotingStrategy.quotedFullyQualifiedName(rootEntity));
-        out.append(' ');
-        out.append(quotingStrategy.quotedIdentifier(rootEntity, rootNode.getTargetTableAlias()));
-    }
-
-    /**
-     * Appends all configured joins to the provided output object.
-     */
-    protected void appendJoins(Appendable out) throws IOException {
-
-        // skip root, recursively append its children
-        for (JoinTreeNode child : rootNode.getChildren()) {
-            appendJoinSubtree(out, child);
-        }
-    }
-
-    protected void appendJoinSubtree(Appendable out, JoinTreeNode node) throws IOException {
-
-        DbRelationship relationship = node.getRelationship();
-
-        DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
-        String srcAlias = node.getSourceTableAlias();
-        String targetAlias = node.getTargetTableAlias();
-
-        switch (node.getJoinType()) {
-        case INNER:
-            out.append(" JOIN");
-            break;
-        case LEFT_OUTER:
-            out.append(" LEFT JOIN");
-            break;
-        default:
-            throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
-        }
-
-        out.append(' ');
-        out.append(quotingStrategy.quotedFullyQualifiedName(targetEntity));
-
-        out.append(' ');
-        out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias));
-        out.append(" ON (");
-
-        List<DbJoin> joins = relationship.getJoins();
-        int len = joins.size();
-        for (int i = 0; i < len; i++) {
-            DbJoin join = joins.get(i);
-            if (i > 0) {
-                out.append(" AND ");
-            }
-
-            out.append(quotingStrategy.quotedIdentifier(relationship.getSourceEntity(), srcAlias, join.getSourceName()));
-            out.append(" = ");
-            out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias, join.getTargetName()));
-        }
-
-        /**
-         * Attaching root Db entity's qualifier
-         */
-        Expression dbQualifier = targetEntity.getQualifier();
-        if (dbQualifier != null) {
-            dbQualifier = dbQualifier.transform(new JoinedDbEntityQualifierTransformer(node));
-
-            if (len > 0) {
-                out.append(" AND ");
-            }
-            qualifierTranslator.setOut(out);
-            qualifierTranslator.doAppendPart(dbQualifier);
-        }
-
-        out.append(')');
-
-        for (JoinTreeNode child : node.getChildren()) {
-            appendJoinSubtree(out, child);
-        }
-    }
-
-    /**
-     * Append join information to the qualifier - the part after "WHERE".
-     */
-    protected void appendQualifier(Appendable out, boolean firstQualifierElement) throws IOException {
-        // nothing as standard join is performed before "WHERE"
-    }
-
-    /**
-     * Pops the stack all the way to the root node.
-     */
-    void resetStack() {
-        topNode = rootNode;
-    }
-
-    /**
-     * Finds or creates a JoinTreeNode for the given arguments and sets it as
-     * the next current join.
-     */
-    void pushJoin(DbRelationship relationship, JoinType joinType, String alias) {
-        topNode = topNode.findOrCreateChild(relationship, joinType, alias);
-    }
-
-    protected String newAlias() {
-        return "t" + aliasCounter++;
-    }
-
-    /**
-     * Class to translate *joined* DB Entity qualifiers annotation to *current*
-     * Obj-entity qualifiers annotation This is done by changing all Obj-paths
-     * to concatenated Db-paths to root entity and rejecting all original
-     * Db-paths
-     */
-    class JoinedDbEntityQualifierTransformer implements Transformer {
-
-        StringBuilder pathToRoot;
-
-        JoinedDbEntityQualifierTransformer(JoinTreeNode node) {
-            pathToRoot = new StringBuilder();
-            while (node != null && node.getRelationship() != null) {
-                String relName = node.getRelationship().getName();
-
-                /**
-                 * We must be in the same join as 'node', otherwise incorrect
-                 * join statement like JOIN t1 ... ON (t0.id=t1.id AND
-                 * t2.qualifier=0) could be generated
-                 */
-                if (node.getJoinType() == JoinType.LEFT_OUTER) {
-                    relName += Entity.OUTER_JOIN_INDICATOR;
-                }
-                relName += ObjEntity.PATH_SEPARATOR;
-
-                pathToRoot.insert(0, relName);
-                node = node.getParent();
-            }
-        }
-
-        public Object transform(Object input) {
-            if (input instanceof ASTObjPath) {
-                return new ASTDbPath(pathToRoot.toString() + ((SimpleNode) input).getOperand(0));
-            }
-            return input;
-        }
-    }
+	protected JoinTreeNode rootNode;
+	protected JoinTreeNode topNode;
+	private QuotingStrategy quotingStrategy;
+
+	private int aliasCounter;
+
+	/**
+	 * Helper class to process DbEntity qualifiers
+	 */
+	private QualifierTranslator qualifierTranslator;
+
+	protected JoinStack(DbAdapter dbAdapter, DataMap dataMap, QueryAssembler assembler) {
+		this.rootNode = new JoinTreeNode(this);
+		this.rootNode.setTargetTableAlias(newAlias());
+
+		this.quotingStrategy = dbAdapter.getQuotingStrategy();
+		this.qualifierTranslator = dbAdapter.getQualifierTranslator(assembler);
+
+		resetStack();
+	}
+
+	String getCurrentAlias() {
+		return topNode.getTargetTableAlias();
+	}
+
+	/**
+	 * Returns the number of configured joins.
+	 */
+	protected int size() {
+		// do not count root as a join
+		return rootNode.size() - 1;
+	}
+
+	void appendRootWithQuoteSqlIdentifiers(StringBuilder out, DbEntity rootEntity) {
+
+		out.append(quotingStrategy.quotedFullyQualifiedName(rootEntity));
+		out.append(' ');
+		out.append(quotingStrategy.quotedIdentifier(rootEntity, rootNode.getTargetTableAlias()));
+	}
+
+	/**
+	 * Appends all configured joins to the provided output object.
+	 */
+	protected void appendJoins(StringBuilder out) {
+
+		// skip root, recursively append its children
+		for (JoinTreeNode child : rootNode.getChildren()) {
+			appendJoinSubtree(out, child);
+		}
+	}
+
+	protected void appendJoinSubtree(StringBuilder out, JoinTreeNode node) {
+
+		DbRelationship relationship = node.getRelationship();
+
+		DbEntity targetEntity = (DbEntity) relationship.getTargetEntity();
+		String srcAlias = node.getSourceTableAlias();
+		String targetAlias = node.getTargetTableAlias();
+
+		switch (node.getJoinType()) {
+		case INNER:
+			out.append(" JOIN");
+			break;
+		case LEFT_OUTER:
+			out.append(" LEFT JOIN");
+			break;
+		default:
+			throw new IllegalArgumentException("Unsupported join type: " + node.getJoinType());
+		}
+
+		out.append(' ');
+		out.append(quotingStrategy.quotedFullyQualifiedName(targetEntity));
+
+		out.append(' ');
+		out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias));
+		out.append(" ON (");
+
+		List<DbJoin> joins = relationship.getJoins();
+		int len = joins.size();
+		for (int i = 0; i < len; i++) {
+			DbJoin join = joins.get(i);
+			if (i > 0) {
+				out.append(" AND ");
+			}
+
+			out.append(quotingStrategy.quotedIdentifier(relationship.getSourceEntity(), srcAlias, join.getSourceName()));
+			out.append(" = ");
+			out.append(quotingStrategy.quotedIdentifier(targetEntity, targetAlias, join.getTargetName()));
+		}
+
+		/**
+		 * Attaching root Db entity's qualifier
+		 */
+		Expression dbQualifier = targetEntity.getQualifier();
+		if (dbQualifier != null) {
+			dbQualifier = dbQualifier.transform(new JoinedDbEntityQualifierTransformer(node));
+
+			if (len > 0) {
+				out.append(" AND ");
+			}
+			qualifierTranslator.setOut(out);
+			qualifierTranslator.doAppendPart(dbQualifier);
+		}
+
+		out.append(')');
+
+		for (JoinTreeNode child : node.getChildren()) {
+			appendJoinSubtree(out, child);
+		}
+	}
+
+	/**
+	 * Append join information to the qualifier - the part after "WHERE".
+	 */
+	protected void appendQualifier(StringBuilder out, boolean firstQualifierElement) {
+		// nothing as standard join is performed before "WHERE"
+	}
+
+	/**
+	 * Pops the stack all the way to the root node.
+	 */
+	void resetStack() {
+		topNode = rootNode;
+	}
+
+	/**
+	 * Finds or creates a JoinTreeNode for the given arguments and sets it as
+	 * the next current join.
+	 */
+	void pushJoin(DbRelationship relationship, JoinType joinType, String alias) {
+		topNode = topNode.findOrCreateChild(relationship, joinType, alias);
+	}
+
+	protected String newAlias() {
+		return "t" + aliasCounter++;
+	}
+
+	/**
+	 * Class to translate *joined* DB Entity qualifiers annotation to *current*
+	 * Obj-entity qualifiers annotation This is done by changing all Obj-paths
+	 * to concatenated Db-paths to root entity and rejecting all original
+	 * Db-paths
+	 */
+	class JoinedDbEntityQualifierTransformer implements Transformer {
+
+		StringBuilder pathToRoot;
+
+		JoinedDbEntityQualifierTransformer(JoinTreeNode node) {
+			pathToRoot = new StringBuilder();
+			while (node != null && node.getRelationship() != null) {
+				String relName = node.getRelationship().getName();
+
+				/**
+				 * We must be in the same join as 'node', otherwise incorrect
+				 * join statement like JOIN t1 ... ON (t0.id=t1.id AND
+				 * t2.qualifier=0) could be generated
+				 */
+				if (node.getJoinType() == JoinType.LEFT_OUTER) {
+					relName += Entity.OUTER_JOIN_INDICATOR;
+				}
+				relName += ObjEntity.PATH_SEPARATOR;
+
+				pathToRoot.insert(0, relName);
+				node = node.getParent();
+			}
+		}
+
+		public Object transform(Object input) {
+			if (input instanceof ASTObjPath) {
+				return new ASTDbPath(pathToRoot.toString() + ((SimpleNode) input).getOperand(0));
+			}
+			return input;
+		}
+	}
 }


Mime
View raw message