From 02126dcce90bdd8848b07ae7c6037cacca9af330 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 13 Oct 2006 02:36:21 +0000 Subject: [PATCH] Support for query clauses, joins, field overriding and a bunch of other useful stuff from Zell. Adjusted formatting to 100 columns. --- .../jdbc/depot/DepotMarshaller.java | 190 +++++++--------- .../jdbc/depot/DepotRepository.java | 144 ++++++------- .../jdbc/depot/FieldMarshaller.java | 12 +- src/java/com/samskivert/jdbc/depot/Key.java | 59 +++-- .../samskivert/jdbc/depot/KeyGenerator.java | 4 +- .../jdbc/depot/PersistenceContext.java | 5 +- src/java/com/samskivert/jdbc/depot/Query.java | 204 +++++++++++++++++- .../jdbc/depot/TableKeyGenerator.java | 19 +- .../depot/clause/FieldOverrideClause.java | 80 +++++++ .../jdbc/depot/clause/GroupByClause.java | 71 ++++++ .../jdbc/depot/clause/JoinClause.java | 74 +++++++ .../jdbc/depot/clause/LimitClause.java | 68 ++++++ .../jdbc/depot/clause/OrderByClause.java | 75 +++++++ .../jdbc/depot/clause/QueryClause.java | 53 +++++ .../depot/expression/ColumnExpression.java | 70 ++++++ .../depot/expression/LiteralExpression.java | 55 +++++ .../jdbc/depot/expression/SQLExpression.java | 46 ++++ .../depot/expression/ValueExpression.java | 56 +++++ 18 files changed, 1039 insertions(+), 246 deletions(-) create mode 100644 src/java/com/samskivert/jdbc/depot/clause/FieldOverrideClause.java create mode 100644 src/java/com/samskivert/jdbc/depot/clause/GroupByClause.java create mode 100644 src/java/com/samskivert/jdbc/depot/clause/JoinClause.java create mode 100644 src/java/com/samskivert/jdbc/depot/clause/LimitClause.java create mode 100644 src/java/com/samskivert/jdbc/depot/clause/OrderByClause.java create mode 100644 src/java/com/samskivert/jdbc/depot/clause/QueryClause.java create mode 100644 src/java/com/samskivert/jdbc/depot/expression/ColumnExpression.java create mode 100644 src/java/com/samskivert/jdbc/depot/expression/LiteralExpression.java create mode 100644 src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java create mode 100644 src/java/com/samskivert/jdbc/depot/expression/ValueExpression.java diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java index 6729d1f..310d613 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java +++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java @@ -41,6 +41,7 @@ import javax.persistence.TableGenerator; import javax.persistence.Transient; import com.samskivert.jdbc.JDBCUtil; +import com.samskivert.jdbc.depot.expression.ColumnExpression; import com.samskivert.util.ArrayUtil; import com.samskivert.util.ListUtil; import com.samskivert.util.StringUtil; @@ -48,15 +49,14 @@ import com.samskivert.util.StringUtil; import static com.samskivert.jdbc.depot.Log.log; /** - * Handles the marshalling and unmarshalling of persistent instances to JDBC - * primitives ({@link PreparedStatement} and {@link ResultSet}). + * Handles the marshalling and unmarshalling of persistent instances to JDBC primitives ({@link + * PreparedStatement} and {@link ResultSet}). */ public class DepotMarshaller { - /** The name of the private static field that must be defined for all - * persistent object classes which is used to handle schema migration. If - * automatic schema migration is not desired, define this field and set its - * value to -1. */ + /** The name of a private static field that must be defined for all persistent object classes. + * It is used to handle schema migration. If automatic schema migration is not desired, define + * this field and set its value to -1. */ public static final String SCHEMA_VERSION_FIELD = "SCHEMA_VERSION"; /** @@ -70,8 +70,8 @@ public class DepotMarshaller _tableName = _pclass.getName(); _tableName = _tableName.substring(_tableName.lastIndexOf(".")+1); - // if the entity defines a new TableGenerator, map that in our static - // table as those are shared across all entities + // if the entity defines a new TableGenerator, map that in our static table as those are + // shared across all entities TableGenerator generator = pclass.getAnnotation(TableGenerator.class); if (generator != null) { context.tableGenerators.put(generator.name(), generator); @@ -83,8 +83,7 @@ public class DepotMarshaller int mods = field.getModifiers(); // check for a static constant schema version - if ((mods & Modifier.STATIC) != 0 && - field.getName().equals(SCHEMA_VERSION_FIELD)) { + if ((mods & Modifier.STATIC) != 0 && field.getName().equals(SCHEMA_VERSION_FIELD)) { try { _schemaVersion = (Integer)field.get(null); } catch (Exception e) { @@ -94,8 +93,7 @@ public class DepotMarshaller } // the field must be public, non-static and non-transient - if (((mods & Modifier.PUBLIC) == 0) || - ((mods & Modifier.STATIC) != 0) || + if (((mods & Modifier.PUBLIC) == 0) || ((mods & Modifier.STATIC) != 0) || field.getAnnotation(Transient.class) != null) { continue; } @@ -119,8 +117,8 @@ public class DepotMarshaller } } - // if the entity defines a single-columnar primary key, figure out if - // we will be generating values for it + // if the entity defines a single-columnar primary key, figure out if we will be generating + // values for it if (_pkColumns != null) { GeneratedValue gv = null; FieldMarshaller keyField = null; @@ -142,11 +140,11 @@ public class DepotMarshaller // the primary key must be numeric if we are to auto-assign it Class ftype = keyField.getField().getType(); - boolean isNumeric = (ftype.equals(Byte.TYPE) || - ftype.equals(Byte.class) || ftype.equals(Short.TYPE) || - ftype.equals(Short.class) || ftype.equals(Integer.TYPE) || - ftype.equals(Integer.class) || ftype.equals(Long.TYPE) || - ftype.equals(Long.class)); + boolean isNumeric = ( + ftype.equals(Byte.TYPE) || ftype.equals(Byte.class) || + ftype.equals(Short.TYPE) || ftype.equals(Short.class) || + ftype.equals(Integer.TYPE) || ftype.equals(Integer.class) || + ftype.equals(Long.TYPE) || ftype.equals(Long.class)); if (!isNumeric) { throw new IllegalArgumentException( "Cannot use @GeneratedValue on non-numeric column"); @@ -173,13 +171,11 @@ public class DepotMarshaller // generate our full list of fields/columns for use in queries _allFields = fields.toArray(new String[fields.size()]); - _fullColumnList = StringUtil.join(_allFields, ","); // create the SQL used to create and migrate our table _columnDefinitions = new String[_allFields.length]; for (int ii = 0; ii < _allFields.length; ii++) { - _columnDefinitions[ii] = - _fields.get(_allFields[ii]).getColumnDefinition(); + _columnDefinitions[ii] = _fields.get(_allFields[ii]).getColumnDefinition(); } // add the primary key, if we have one if (hasPrimaryKey()) { @@ -188,22 +184,20 @@ public class DepotMarshaller indices[ii] = _pkColumns.get(ii).getColumnName(); } _columnDefinitions = ArrayUtil.append( - _columnDefinitions, - "PRIMARY KEY (" + StringUtil.join(indices, ", ") + ")"); + _columnDefinitions, "PRIMARY KEY (" + StringUtil.join(indices, ", ") + ")"); } _postamble = ""; // TODO: add annotations for the postamble // if we did not find a schema version field, complain if (_schemaVersion < 0) { - log.warning("Unable to read " + _pclass.getName() + "." + - SCHEMA_VERSION_FIELD + ". Schema migration disabled."); + log.warning("Unable to read " + _pclass.getName() + "." + SCHEMA_VERSION_FIELD + + ". Schema migration disabled."); } } /** - * Returns the name of the table in which persistence instances of this - * class are stored. By default this is the classname of the persistent - * object without the package. + * Returns the name of the table in which persistence instances of this class are stored. By + * default this is the classname of the persistent object without the package. */ public String getTableName () { @@ -219,9 +213,8 @@ public class DepotMarshaller } /** - * Returns a key configured with the primary key of the supplied object. - * Throws an exception if the persistent object did not declare a primary - * key. + * Returns a key configured with the primary key of the supplied object. Throws an exception + * if the persistent object did not declare a primary key. */ public Key getPrimaryKey (Object object) { @@ -242,8 +235,8 @@ public class DepotMarshaller } /** - * Creates a primary key record for the type of object handled by this - * marshaller, using the supplied primary key vlaue. + * Creates a primary key record for the type of object handled by this marshaller, using the + * supplied primary key value. */ public Key makePrimaryKey (Comparable... values) { @@ -253,39 +246,35 @@ public class DepotMarshaller } if (values.length != _pkColumns.size()) { throw new IllegalArgumentException( - "Argument count (" + values.length + ") must match " + - "primary key size (" + _pkColumns.size() + ")"); + "Argument count (" + values.length + ") must match primary key size (" + + _pkColumns.size() + ")"); } - String[] indices = new String[_pkColumns.size()]; + ColumnExpression[] columns = new ColumnExpression[_pkColumns.size()]; for (int ii = 0; ii < _pkColumns.size(); ii++) { FieldMarshaller field = _pkColumns.get(ii); - indices[ii] = field.getColumnName(); + columns[ii] = new ColumnExpression(_pclass, field.getColumnName()); } - return new Key(indices, values); + return new Key(columns, values); } /** - * Initializes the table used by this marshaller. If the table does not - * exist, it will be created. If the schema version specified by the - * persistent object is newer than the database schema, it will be - * migrated. + * Initializes the table used by this marshaller. If the table does not exist, it will be + * created. If the schema version specified by the persistent object is newer than the database + * schema, it will be migrated. */ public void init (Connection conn) throws SQLException { // check to see if our schema version table exists, create it if not JDBCUtil.createTableIfMissing(conn, SCHEMA_VERSION_TABLE, - new String[] { "persistentClass VARCHAR(255) NOT NULL", - "version INTEGER NOT NULL" }, - ""); + new String[] { "persistentClass VARCHAR(255) NOT NULL", + "version INTEGER NOT NULL" }, ""); // now create the table for our persistent class if it does not exist if (!JDBCUtil.tableExists(conn, getTableName())) { - log.fine("Creating table " + getTableName() + - " (" + StringUtil.join(_columnDefinitions, ", ") + ") " + - _postamble); - JDBCUtil.createTableIfMissing( - conn, getTableName(), _columnDefinitions, _postamble); + log.fine("Creating table " + getTableName() + " (" + + StringUtil.join(_columnDefinitions, ", ") + ") " + _postamble); + JDBCUtil.createTableIfMissing(conn, getTableName(), _columnDefinitions, _postamble); updateVersion(conn, 1); } @@ -305,11 +294,11 @@ public class DepotMarshaller return; } - log.info("Migrating " + getTableName() + " from " + currentVersion + - " to " + _schemaVersion + "..."); + log.info("Migrating " + getTableName() + " from " + + currentVersion + " to " + _schemaVersion + "..."); - // otherwise try to migrate the schema; doing column additions - // magically and running any registered hand-migrations + // otherwise try to migrate the schema; doing column additions magically and running any + // registered hand-migrations DatabaseMetaData meta = conn.getMetaData(); ResultSet rs = meta.getColumns(null, null, getTableName(), "%"); HashSet columns = new HashSet(); @@ -325,8 +314,7 @@ public class DepotMarshaller // otherwise add the column String coldef = fmarsh.getColumnDefinition(); - String query = "alter table " + getTableName() + - " add column " + coldef; + String query = "alter table " + getTableName() + " add column " + coldef; // try to add it to the appropriate spot int fidx = ListUtil.indexOf(_allFields, fmarsh.getColumnName()); @@ -351,26 +339,8 @@ public class DepotMarshaller } /** - * Creates a query for instances of this persistent object type using the - * supplied key. If null is supplied all instances will be loaded. - */ - public PreparedStatement createQuery (Connection conn, Key key) - throws SQLException - { - String query = "select " + _fullColumnList + " from " + getTableName(); - if (key != null) { - query += " where " + key.toWhereClause(); - } - PreparedStatement pstmt = conn.prepareStatement(query); - if (key != null) { - key.bindArguments(pstmt, 1); - } - return pstmt; - } - - /** - * Creates a persistent object from the supplied result set. The result set - * must have come from a query provided by {@link #createQuery}. + * Creates a persistent object from the supplied result set. The result set must have come from + * a query provided by {@link #createQuery}. */ public T createObject (ResultSet rs) throws SQLException @@ -387,15 +357,14 @@ public class DepotMarshaller throw sqe; } catch (Exception e) { - String errmsg = "Failed to unmarshall persistent object " + - "[pclass=" + _pclass.getName() + "]"; + String errmsg = "Failed to unmarshall persistent object [pclass=" + + _pclass.getName() + "]"; throw (SQLException)new SQLException(errmsg).initCause(e); } } /** - * Creates a statement that will insert the supplied persistent object into - * the database. + * Creates a statement that will insert the supplied persistent object into the database. */ public PreparedStatement createInsert (Connection conn, Object po) throws SQLException @@ -403,8 +372,8 @@ public class DepotMarshaller try { StringBuilder insert = new StringBuilder(); insert.append("insert into ").append(getTableName()); - insert.append(" (").append(_fullColumnList).append(")"); - insert.append(" values("); + insert.append(" (").append(StringUtil.join(_allFields, ",")); + insert.append(")").append(" values("); for (int ii = 0; ii < _allFields.length; ii++) { if (ii > 0) { insert.append(", "); @@ -426,18 +395,18 @@ public class DepotMarshaller throw sqe; } catch (Exception e) { - String errmsg = "Failed to marshall persistent object " + - "[pclass=" + _pclass.getName() + "]"; + String errmsg = "Failed to marshall persistent object [pclass=" + + _pclass.getName() + "]"; throw (SQLException)new SQLException(errmsg).initCause(e); } } /** - * Fills in the primary key just assigned to the supplied persistence - * object by the execution of the results of {@link #createInsert}. + * Fills in the primary key just assigned to the supplied persistence object by the execution + * of the results of {@link #createInsert}. * - * @return the newly assigned primary key or null if the object does not - * use primary keys or this is not the right time to assign the key. + * @return the newly assigned primary key or null if the object does not use primary keys or + * this is not the right time to assign the key. */ public Key assignPrimaryKey (Connection conn, Object po, boolean postFactum) throws SQLException @@ -457,15 +426,13 @@ public class DepotMarshaller _pkColumns.get(0).getField().set(po, nextValue); return makePrimaryKey(nextValue); } catch (Exception e) { - String errmsg = "Failed to assign primary key " + - "[type=" + _pclass + "]"; + String errmsg = "Failed to assign primary key [type=" + _pclass + "]"; throw (SQLException) new SQLException(errmsg).initCause(e); } } /** - * Creates a statement that will update the supplied persistent object - * using the supplied key. + * Creates a statement that will update the supplied persistent object using the supplied key. */ public PreparedStatement createUpdate (Connection conn, Object po, Key key) throws SQLException @@ -490,7 +457,7 @@ public class DepotMarshaller } update.append(field).append(" = ?"); } - update.append(" where ").append(key.toWhereClause()); + update.append(" where "); key.appendClause(null, update); try { PreparedStatement pstmt = conn.prepareStatement(update.toString()); @@ -515,12 +482,11 @@ public class DepotMarshaller } /** - * Creates a statement that will update the specified set of fields for all - * persistent objects that match the supplied key. + * Creates a statement that will update the specified set of fields for all persistent objects + * that match the supplied key. */ public PreparedStatement createPartialUpdate ( - Connection conn, Key key, - String[] modifiedFields, Object[] modifiedValues) + Connection conn, Key key, String[] modifiedFields, Object[] modifiedValues) throws SQLException { StringBuilder update = new StringBuilder(); @@ -532,7 +498,7 @@ public class DepotMarshaller } update.append(field).append(" = ?"); } - update.append(" where ").append(key.toWhereClause()); + update.append(" where "); key.appendClause(null, update); PreparedStatement pstmt = conn.prepareStatement(update.toString()); idx = 0; @@ -552,21 +518,19 @@ public class DepotMarshaller public PreparedStatement createDelete (Connection conn, Key key) throws SQLException { - String query = "delete from " + getTableName() + - " where " + key.toWhereClause(); - PreparedStatement pstmt = conn.prepareStatement(query); + StringBuilder query = new StringBuilder("delete from " + getTableName() + " where "); + key.appendClause(null, query); + PreparedStatement pstmt = conn.prepareStatement(query.toString()); key.bindArguments(pstmt, 1); return pstmt; } /** - * Creates a statement that will update the specified set of fields, using - * the supplied literal SQL values, for all persistent objects that match - * the supplied key. + * Creates a statement that will update the specified set of fields, using the supplied literal + * SQL values, for all persistent objects that match the supplied key. */ public PreparedStatement createLiteralUpdate ( - Connection conn, Key key, - String[] modifiedFields, Object[] modifiedValues) + Connection conn, Key key, String[] modifiedFields, Object[] modifiedValues) throws SQLException { StringBuilder update = new StringBuilder(); @@ -578,7 +542,7 @@ public class DepotMarshaller update.append(modifiedFields[ii]).append(" = "); update.append(modifiedValues[ii]); } - update.append(" where ").append(key.toWhereClause()); + update.append(" where "); key.appendClause(null, update); PreparedStatement pstmt = conn.prepareStatement(update.toString()); key.bindArguments(pstmt, 1); @@ -589,8 +553,7 @@ public class DepotMarshaller throws SQLException { String update = "update " + SCHEMA_VERSION_TABLE + - " set version = " + version + - " where persistentClass = '" + getTableName() + "'"; + " set version = " + version + " where persistentClass = '" + getTableName() + "'"; String insert = "insert into " + SCHEMA_VERSION_TABLE + " values('" + getTableName() + "', " + version + ")"; Statement stmt = conn.createStatement(); @@ -624,8 +587,7 @@ public class DepotMarshaller protected String _tableName; /** A field marshaller for each persistent field in our object. */ - protected HashMap _fields = - new HashMap(); + protected HashMap _fields = new HashMap(); /** The field marshallers for our persistent object's primary key columns * or null if it did not define a primary key. */ @@ -634,10 +596,6 @@ public class DepotMarshaller /** The generator to use for auto-generating primary key values, or null. */ protected KeyGenerator _keyGenerator; - /** The persisent fields of our object, in definition order, separated by - * commas for easy use in a select statement. */ - protected String _fullColumnList; - /** The persisent fields of our object, in definition order. */ protected String[] _allFields; diff --git a/src/java/com/samskivert/jdbc/depot/DepotRepository.java b/src/java/com/samskivert/jdbc/depot/DepotRepository.java index 2748eb7..b29b10e 100644 --- a/src/java/com/samskivert/jdbc/depot/DepotRepository.java +++ b/src/java/com/samskivert/jdbc/depot/DepotRepository.java @@ -3,7 +3,7 @@ // // samskivert library - useful routines for java programs // Copyright (C) 2006 Michael Bayne, Pär Winzell -// +// // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or @@ -27,21 +27,20 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import com.samskivert.io.PersistenceException; import com.samskivert.jdbc.ConnectionProvider; +import com.samskivert.jdbc.depot.clause.QueryClause; /** - * Provides a base for classes that provide access to persistent objects. Also - * defines the mechanism by which all persistent queries and updates are routed - * through the distributed cache. + * Provides a base for classes that provide access to persistent objects. Also defines the + * mechanism by which all persistent queries and updates are routed through the distributed cache. */ public class DepotRepository { /** - * Creates a repository with the supplied connection provider and its own - * private persistence context. + * Creates a repository with the supplied connection provider and its own private persistence + * context. */ protected DepotRepository (ConnectionProvider conprov) { @@ -72,9 +71,9 @@ public class DepotRepository throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(type); - return _ctx.invoke(new Query(key) { + return _ctx.invoke(new Query(_ctx, type, key) { public T invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createQuery(conn, _key); + PreparedStatement stmt = createQuery(conn); try { T result = null; ResultSet rs = stmt.executeQuery(); @@ -105,13 +104,13 @@ public class DepotRepository * Loads all persistent objects that match the specified key. */ protected > Collection findAll ( - Class type, Key key) + Class type, Key key, QueryClause... clauses) throws PersistenceException { final DepotMarshaller marsh = _ctx.getMarshaller(type); - return _ctx.invoke(new Query>(key) { + return _ctx.invoke(new Query>(_ctx, type, key, clauses) { public ArrayList invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createQuery(conn, _key); + PreparedStatement stmt = createQuery(conn); try { ArrayList results = new ArrayList(); ResultSet rs = stmt.executeQuery(); @@ -128,8 +127,8 @@ public class DepotRepository } /** - * Inserts the supplied persistent object into the database, assigning its - * primary key (if it has one) in the process. + * Inserts the supplied persistent object into the database, assigning its primary key (if it + * has one) in the process. * * @return the number of rows modified by this action, this should always * be one. @@ -156,8 +155,8 @@ public class DepotRepository } /** - * Updates all fields of the supplied persistent object, using its primary - * key to identify the row to be updated. + * Updates all fields of the supplied persistent object, using its primary key to identify the + * row to be updated. * * @return the number of rows modified by this action. */ @@ -178,8 +177,8 @@ public class DepotRepository } /** - * Updates just the specified fields of the supplied persistent object, - * using its primary key to identify the row to be updated. + * Updates just the specified fields of the supplied persistent object, using its primary key + * to identify the row to be updated. * * @return the number of rows modified by this action. */ @@ -189,8 +188,7 @@ public class DepotRepository final DepotMarshaller marsh = _ctx.getMarshaller(record.getClass()); return _ctx.invoke(new Modifier(marsh.getPrimaryKey(record)) { public int invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createUpdate( - conn, record, _key, modifiedFields); + PreparedStatement stmt = marsh.createUpdate(conn, record, _key, modifiedFields); try { return stmt.executeUpdate(); } finally { @@ -201,38 +199,33 @@ public class DepotRepository } /** - * Updates the specified columns for all persistent objects matching the - * supplied primary key. + * Updates the specified columns for all persistent objects matching the supplied primary key. * * @param type the type of the persistent object to be modified. * @param primaryKey the primary key to match in the update. - * @param fieldsValues an array containing the names of the fields/columns - * and the values to be assigned, in key, value, key, value, etc. order. + * @param fieldsValues an array containing the names of the fields/columns and the values to be + * assigned, in key, value, key, value, etc. order. * * @return the number of rows modified by this action. */ - protected int updatePartial ( - Class type, Comparable primaryKey, Object ... fieldsValues) + protected int updatePartial (Class type, Comparable primaryKey, Object ... fieldsValues) throws PersistenceException { return updatePartial( - type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey), - fieldsValues); + type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey), fieldsValues); } /** - * Updates the specified columns for all persistent objects matching the - * supplied key. + * Updates the specified columns for all persistent objects matching the supplied key. * * @param type the type of the persistent object to be modified. * @param key the key to match in the update. - * @param fieldsValues an array containing the names of the fields/columns - * and the values to be assigned, in key, value, key, value, etc. order. + * @param fieldsValues an array containing the names of the fields/columns and the values to be + * assigned, in key, value, key, value, etc. order. * * @return the number of rows modified by this action. */ - protected int updatePartial ( - Class type, Key key, Object ... fieldsValues) + protected int updatePartial (Class type, Key key, Object ... fieldsValues) throws PersistenceException { // separate the arguments into keys and values @@ -246,8 +239,7 @@ public class DepotRepository final DepotMarshaller marsh = _ctx.getMarshaller(type); return _ctx.invoke(new Modifier(key) { public int invoke (Connection conn) throws SQLException { - PreparedStatement stmt = marsh.createPartialUpdate( - conn, _key, fields, values); + PreparedStatement stmt = marsh.createPartialUpdate(conn, _key, fields, values); try { return stmt.executeUpdate(); } finally { @@ -258,10 +250,9 @@ public class DepotRepository } /** - * Updates the specified columns for all persistent objects matching the - * supplied primary key. The values in this case must be literal SQL to be - * inserted into the update statement. In general this is used when you - * want to do something like the following: + * Updates the specified columns for all persistent objects matching the supplied primary + * key. The values in this case must be literal SQL to be inserted into the update + * statement. In general this is used when you want to do something like the following: * *
      * update FOO set BAR = BAR + 1;
@@ -270,26 +261,22 @@ public class DepotRepository
      *
      * @param type the type of the persistent object to be modified.
      * @param primaryKey the key to match in the update.
-     * @param fieldsValues an array containing the names of the fields/columns
-     * and the values to be assigned, in key, literal value, key, literal
-     * value, etc. order.
+     * @param fieldsValues an array containing the names of the fields/columns and the values to be
+     * assigned, in key, literal value, key, literal value, etc. order.
      *
      * @return the number of rows modified by this action.
      */
-    protected  int updateLiteral (
-        Class type, Comparable primaryKey, String ... fieldsValues)
+    protected  int updateLiteral (Class type, Comparable primaryKey, String ... fieldsValues)
         throws PersistenceException
     {
         return updateLiteral(
-            type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey),
-            fieldsValues);
+            type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey), fieldsValues);
     }
 
     /**
-     * Updates the specified columns for all persistent objects matching the
-     * supplied primary key. The values in this case must be literal SQL to be
-     * inserted into the update statement. In general this is used when you
-     * want to do something like the following:
+     * Updates the specified columns for all persistent objects matching the supplied primary
+     * key. The values in this case must be literal SQL to be inserted into the update
+     * statement. In general this is used when you want to do something like the following:
      *
      * 
      * update FOO set BAR = BAR + 1;
@@ -298,14 +285,12 @@ public class DepotRepository
      *
      * @param type the type of the persistent object to be modified.
      * @param key the key to match in the update.
-     * @param fieldsValues an array containing the names of the fields/columns
-     * and the values to be assigned, in key, literal value, key, literal
-     * value, etc. order.
+     * @param fieldsValues an array containing the names of the fields/columns and the values to be
+     * assigned, in key, literal value, key, literal value, etc. order.
      *
      * @return the number of rows modified by this action.
      */
-    protected  int updateLiteral (
-        Class type, Key key, String ... fieldsValues)
+    protected  int updateLiteral (Class type, Key key, String ... fieldsValues)
         throws PersistenceException
     {
         // separate the arguments into keys and values
@@ -319,8 +304,7 @@ public class DepotRepository
         final DepotMarshaller marsh = _ctx.getMarshaller(type);
         return _ctx.invoke(new Modifier(key) {
             public int invoke (Connection conn) throws SQLException {
-                PreparedStatement stmt = marsh.createLiteralUpdate(
-                    conn, _key, fields, values);
+                PreparedStatement stmt = marsh.createLiteralUpdate(conn, _key, fields, values);
                 try {
                     return stmt.executeUpdate();
                 } finally {
@@ -331,13 +315,11 @@ public class DepotRepository
     }
 
     /**
-     * Stores the supplied persisent object in the database. If it has no
-     * primary key assigned (it is null or zero), it will be inserted
-     * directly. Otherwise an update will first be attempted and if that
-     * matches zero rows, the object will be inserted.
+     * Stores the supplied persisent object in the database. If it has no primary key assigned (it
+     * is null or zero), it will be inserted directly. Otherwise an update will first be attempted
+     * and if that matches zero rows, the object will be inserted.
      *
-     * @return the number of rows modified by this action, this should always
-     * be one.
+     * @return the number of rows modified by this action, this should always be one.
      */
     protected int store (final Object record)
         throws PersistenceException
@@ -348,8 +330,8 @@ public class DepotRepository
             public int invoke (Connection conn) throws SQLException {
                 PreparedStatement stmt = null;
                 try {
-                    // if our primary key is null or is the integer 0, assume
-                    // the record has never before been persisted and insert
+                    // if our primary key is null or is the integer 0, assume the record has never
+                    // before been persisted and insert
                     if (_key != null && !Integer.valueOf(0).equals(_key)) {
                         stmt = marsh.createUpdate(conn, record, _key);
                         int mods = stmt.executeUpdate();
@@ -359,8 +341,8 @@ public class DepotRepository
                         stmt.close();
                     }
 
-                    // if the update modified zero rows or the primary key was
-                    // obviously unset, do an insertion
+                    // if the update modified zero rows or the primary key was obviously unset, do
+                    // an insertion
                     updateKey(marsh.assignPrimaryKey(conn, record, false));
                     stmt = marsh.createInsert(conn, record);
                     int mods = stmt.executeUpdate();
@@ -375,36 +357,33 @@ public class DepotRepository
     }
 
     /**
-     * Deletes all persistent objects from the database with a primary key
-     * matching the primary key of the supplied object.
+     * Deletes all persistent objects from the database with a primary key matching the primary key
+     * of the supplied object.
      *
      * @return the number of rows deleted by this action.
      */
     protected  int delete (T record)
         throws PersistenceException
     {
-        @SuppressWarnings("unchecked") Class type =
-            (Class)record.getClass();
+        @SuppressWarnings("unchecked") Class type = (Class)record.getClass();
         DepotMarshaller marsh = _ctx.getMarshaller(type);
         return deleteAll(type, marsh.getPrimaryKey(record));
     }
 
     /**
-     * Deletes all persistent objects from the database with a primary key
-     * matching the supplied primary key.
+     * Deletes all persistent objects from the database with a primary key matching the supplied
+     * primary key.
      *
      * @return the number of rows deleted by this action.
      */
     protected  int delete (Class type, Comparable primaryKey)
         throws PersistenceException
     {
-        return deleteAll(
-            type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey));
+        return deleteAll(type, _ctx.getMarshaller(type).makePrimaryKey(primaryKey));
     }
 
     /**
-     * Deletes all persistent objects from the database that match the supplied
-     * key.
+     * Deletes all persistent objects from the database that match the supplied key.
      *
      * @return the number of rows deleted by this action.
      */
@@ -424,11 +403,12 @@ public class DepotRepository
         });
     }
 
-    protected static abstract class CollectionQuery
-        extends Query
+    protected static abstract class CollectionQuery extends Query
     {
-        public CollectionQuery (Key key) {
-            super(key);
+        public CollectionQuery (PersistenceContext ctx, Class type, Key key)
+            throws PersistenceException
+        {
+            super(ctx, type, key);
         }
 
         public abstract T invoke (Connection conn) throws SQLException;
diff --git a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java
index 0810056..9a14265 100644
--- a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java
+++ b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java
@@ -39,16 +39,15 @@ import javax.persistence.Id;
 import com.samskivert.util.StringUtil;
 
 /**
- * Handles the marshalling and unmarshalling of a particular field of a
- * persistent object.
+ * Handles the marshalling and unmarshalling of a particular field of a persistent object.
  *
  * @see DepotMarshaller
  */
 public abstract class FieldMarshaller
 {
     /**
-     * Creates and returns a field marshaller for the specified field. Throws
-     * an exception if the field in question cannot be marshalled.
+     * Creates and returns a field marshaller for the specified field. Throws an exception if the
+     * field in question cannot be marshalled.
      */
     public static FieldMarshaller createMarshaller (Field field)
     {
@@ -158,9 +157,8 @@ public abstract class FieldMarshaller
         _field = field;
         _columnName = field.getName();
 
-        // read our column metadata from the annotation (if it exists);
-        // annoyingly we can't create a Column instance to read the defaults so
-        // we have to duplicate them here
+        // read our column metadata from the annotation (if it exists); annoyingly we can't create
+        // a Column instance to read the defaults so we have to duplicate them here
         int length = 255;
         boolean nullable = true;
         boolean unique = false;
diff --git a/src/java/com/samskivert/jdbc/depot/Key.java b/src/java/com/samskivert/jdbc/depot/Key.java
index 7ec75dd..17769cb 100644
--- a/src/java/com/samskivert/jdbc/depot/Key.java
+++ b/src/java/com/samskivert/jdbc/depot/Key.java
@@ -22,50 +22,69 @@ package com.samskivert.jdbc.depot;
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+
+import com.samskivert.jdbc.depot.clause.QueryClause;
+import com.samskivert.jdbc.depot.expression.ColumnExpression;
+
 
 /**
  * Encapsulates a key used to match persistent objects in a query.
+ *
+ * TODO: Is it useful for this to implement QueryClause? Can it be meaningfully put on a peer level
+ * with the other clauses?
  */
 public class Key
+    implements QueryClause
 {
-    public String[] indices;
+    public ColumnExpression[] columns;
     public Comparable[] values;
 
     public Key (String index, Comparable value)
     {
-        this(new String[] { index }, new Comparable[] { value });
+        this(new ColumnExpression(null, index), value);
     }
 
-    public Key (String index1, Comparable value1,
-                String index2, Comparable value2)
+    public Key (ColumnExpression column, Comparable value)
     {
-        this(new String[] { index1, index2 },
+        this(new ColumnExpression[] { column }, new Comparable[] { value });
+    }
+
+    public Key (String index1, Comparable value1, String index2, Comparable value2)
+    {
+        this(new ColumnExpression[] { new ColumnExpression(null, index1),
+                                      new ColumnExpression(null, index2) },
              new Comparable[] { value1, value2 });
     }
 
-    public Key (String[] indices, Comparable[] values)
+    public Key (ColumnExpression[] columns, Comparable[] values)
     {
-        this.indices = indices;
+        this.columns = columns;
         this.values = values;
     }
-
-    public String toWhereClause ()
-    {
-        StringBuilder where = new StringBuilder();
-        for (int ii = 0; ii < indices.length; ii++) {
-            if (ii > 0) {
-                where.append(" and ");
-            }
-            where.append(indices[ii]).append(" = ?");
-        }
-        return where.toString();
+    
+    public List getClassSet() {
+        return Arrays.asList(new Class[] { });
     }
 
-    public void bindArguments (PreparedStatement stmt, int startIdx)
+    public void appendClause (Query query, StringBuilder builder)
+    {
+        for (int ii = 0; ii < columns.length; ii++) {
+            if (ii > 0) {
+                builder.append(" and ");
+            }
+            columns[ii].appendExpression(query, builder);
+            builder.append(" = ?");
+        }
+    }
+
+    public int bindArguments (PreparedStatement stmt, int startIdx)
         throws SQLException
     {
-        for (int ii = 0; ii < indices.length; ii++) {
+        for (int ii = 0; ii < values.length; ii++) {
             stmt.setObject(startIdx++, values[ii]);
         }
+        return startIdx;
     }
 }
diff --git a/src/java/com/samskivert/jdbc/depot/KeyGenerator.java b/src/java/com/samskivert/jdbc/depot/KeyGenerator.java
index 9ce40a2..08599ce 100644
--- a/src/java/com/samskivert/jdbc/depot/KeyGenerator.java
+++ b/src/java/com/samskivert/jdbc/depot/KeyGenerator.java
@@ -28,8 +28,8 @@ import java.sql.SQLException;
  */
 public interface KeyGenerator
 {
-    /** If true, this key generator will be run after the insert statement, if
-     * false, it will be run before. */
+    /** If true, this key generator will be run after the insert statement, if false, it will be
+     * run before. */
     public boolean isPostFactum ();
 
     /** Prepares the generator for operation. */
diff --git a/src/java/com/samskivert/jdbc/depot/PersistenceContext.java b/src/java/com/samskivert/jdbc/depot/PersistenceContext.java
index 9e5943a..3bba995 100644
--- a/src/java/com/samskivert/jdbc/depot/PersistenceContext.java
+++ b/src/java/com/samskivert/jdbc/depot/PersistenceContext.java
@@ -21,6 +21,7 @@
 package com.samskivert.jdbc.depot;
 
 import java.util.HashMap;
+
 import javax.persistence.TableGenerator;
 
 import java.sql.Connection;
@@ -36,8 +37,7 @@ import com.samskivert.jdbc.DuplicateKeyException;
 public class PersistenceContext
 {
     /** Map {@link TableGenerator} instances by name. */
-    public HashMap tableGenerators =
-        new HashMap();
+    public HashMap tableGenerators = new HashMap();
 
     /**
      * Creates a persistence context that will use the supplied provider to
@@ -80,6 +80,7 @@ public class PersistenceContext
     /**
      * Invokes a non-modifying query and returns its result.
      */
+    @SuppressWarnings("unchecked")
     public  T invoke (Query query)
         throws PersistenceException
     {
diff --git a/src/java/com/samskivert/jdbc/depot/Query.java b/src/java/com/samskivert/jdbc/depot/Query.java
index 12f5621..b3ad4bc 100644
--- a/src/java/com/samskivert/jdbc/depot/Query.java
+++ b/src/java/com/samskivert/jdbc/depot/Query.java
@@ -3,7 +3,7 @@
 //
 // samskivert library - useful routines for java programs
 // Copyright (C) 2006 Michael Bayne
-// 
+//
 // This library is free software; you can redistribute it and/or modify it
 // under the terms of the GNU Lesser General Public License as published
 // by the Free Software Foundation; either version 2.1 of the License, or
@@ -21,24 +21,212 @@
 package com.samskivert.jdbc.depot;
 
 import java.sql.Connection;
+import java.sql.PreparedStatement;
 import java.sql.SQLException;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.samskivert.io.PersistenceException;
+import com.samskivert.jdbc.depot.clause.FieldOverrideClause;
+import com.samskivert.jdbc.depot.clause.GroupByClause;
+import com.samskivert.jdbc.depot.clause.JoinClause;
+import com.samskivert.jdbc.depot.clause.LimitClause;
+import com.samskivert.jdbc.depot.clause.OrderByClause;
+import com.samskivert.jdbc.depot.clause.QueryClause;
+
 /**
  * Encapsulates a non-modifying query of persistent objects.
  */
 public abstract class Query
 {
-    public Key getKey ()
-    {
-        return _key;
-    }
-
+    /**
+     * Performs the actual JDBC operations associated with this query.
+     */
     public abstract T invoke (Connection conn) throws SQLException;
 
-    protected Query (Key key)
+    /**
+     * Maps a class referenced by this query to its associated table.
+     *
+     * This method is called by individual clauses.
+     */
+    public String getTableName (Class cl)
     {
-        _key = key;
+        return _classMap.get(cl).getTableName();
     }
 
+    /**
+     * Maps a class referenced by this query to the abbreviation used for its associated table. The
+     * abbreviation is transientand only has meaning within the scope of this particular query.
+     *
+     * This method is called by individual clauses.
+     */
+    public String getTableAbbreviation (Class cl)
+    {
+        if (cl.equals(_mainType)) {
+            return "T";
+        }
+        int ix = _classList.indexOf(cl);
+        if (ix < 0) {
+            throw new IllegalArgumentException("Unknown persistence class: " + cl);
+        }
+        return "T" + (ix+1);
+    }
+
+    /**
+     * Translates the data in this Query object into a SQL query suited to instantiate a persistent
+     * object given the supplied key and clauses.  These clauses are assumed to contain no
+     * conflicts.
+     *
+     * If no key is supplied all instances will be loaded.
+     */
+    public  PreparedStatement createQuery (Connection conn)
+        throws SQLException
+    {
+        if (_mainType == null) { // internal error
+            throw new RuntimeException("createQuery() called with _mainClass == null");
+        }
+
+        DepotMarshaller mainMarshaller = _classMap.get(_mainType);
+        String[] fields = mainMarshaller._allFields;
+        StringBuilder query = new StringBuilder("select ");
+        for (int ii = 0; ii < fields.length; ii ++) {
+            if (ii > 0) {
+                query.append(", ");
+            }
+            FieldOverrideClause clause = _disMap.get(fields[ii]);
+            if (clause != null) {
+                clause.appendClause(this, query);
+            } else {
+                query.append("T.").append(fields[ii]);
+            }
+        }
+        query.append("   from " + mainMarshaller.getTableName() + " as T ");
+
+        for (JoinClause clause : _joinClauses) {
+            query.append(" inner join " );
+            clause.appendClause(this, query);
+        }
+        if (_key != null) {
+            query.append(" where ");
+            _key.appendClause(this, query);
+        }
+        if (_groupBy != null) {
+            query.append(" group by ");
+            _groupBy.appendClause(this, query);
+        }
+        if (_orderBy != null) {
+            query.append(" order by ");
+            _orderBy.appendClause(this, query);
+        }
+        if (_limit != null) {
+            query.append(" limit ");
+            _limit.appendClause(this, query);
+        }
+
+        PreparedStatement pstmt = conn.prepareStatement(query.toString());
+        int argIdx = 1;
+        if (_key != null) {
+            argIdx = _key.bindArguments(pstmt, argIdx);
+        }
+        if (_groupBy != null) {
+            argIdx = _groupBy.bindArguments(pstmt, argIdx);
+        }
+        if (_orderBy != null) {
+            argIdx = _orderBy.bindArguments(pstmt, argIdx);
+        }
+        if (_limit != null) {
+            argIdx = _limit.bindArguments(pstmt, argIdx);
+        }
+
+        return pstmt;
+    }
+
+    /**
+     * Creates a new Query object to generate one or more instances of the specified persistent
+     * class, as dictated by the key and query clauses.  A persistence context is supplied for
+     * instantiation of marshallers, which may trigger table creations and schema migrations.
+     */
+    protected Query (PersistenceContext ctx, Class type, Key key, QueryClause... clauses)
+        throws PersistenceException
+    {
+        _key = key;
+        _mainType = type;
+        Set classSet = new HashSet();
+        if (type != null) {
+            classSet.add(type);
+        }
+        if (_key != null) {
+            classSet.addAll(_key.getClassSet());
+        }
+
+        for (QueryClause clause : clauses) {
+            if (clause instanceof JoinClause) {
+                _joinClauses.add((JoinClause) clause);
+                classSet.addAll(((JoinClause) clause).getClassSet());
+
+            } else if (clause instanceof FieldOverrideClause) {
+                _disMap.put(((FieldOverrideClause) clause).getField(),
+                            ((FieldOverrideClause) clause));
+
+            } else if (clause instanceof OrderByClause) {
+                if (_orderBy != null) {
+                    throw new IllegalArgumentException(
+                        "Query can't contain multiple OrderBy clauses.");
+                }
+                _orderBy = (OrderByClause) clause;
+
+            } else if (clause instanceof GroupByClause) {
+                if (_groupBy != null) {
+                    throw new IllegalArgumentException(
+                        "Query can't contain multiple GroupBy clauses.");
+                }
+                _groupBy = (GroupByClause) clause;
+
+            } else if (clause instanceof LimitClause) {
+                if (_limit != null) {
+                    throw new IllegalArgumentException(
+                        "Query can't contain multiple Limit clauses.");
+                }
+                _limit = (LimitClause) clause;
+            }
+        }
+        _classMap = new HashMap();
+
+        for (Class c : classSet) {
+            _classMap.put(c, ctx.getMarshaller(c));
+        }
+        _classList = new ArrayList(classSet);
+    }
+
+    /** The key to this query, or null. Translated into a WHERE clause. */
     protected Key _key;
+
+    /** The persistent class to instantiate for the results. */
+    protected Class _mainType;
+
+    /** A list of referenced classes, used to generate table abbreviations. */
+    protected List _classList;
+
+    /** Classes mapped to marshallers, used for table names and field lists. */
+    protected Map _classMap;
+
+    /** Persistent class fields mapped to field override clauses. */
+    protected Map _disMap = new HashMap();
+
+    /** A list of join clauses, each potentially referencing a new class. */
+    protected List _joinClauses = new ArrayList();
+
+    /** The order by clause, if any. */
+    protected OrderByClause _orderBy;
+
+    /** The group by clause, if any. */
+    protected GroupByClause _groupBy;
+
+    /** The limit clause, if any. */
+    protected LimitClause _limit;
 }
diff --git a/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java b/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java
index 0a3e59a..f08ae38 100644
--- a/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java
+++ b/src/java/com/samskivert/jdbc/depot/TableKeyGenerator.java
@@ -97,17 +97,15 @@ public class TableKeyGenerator implements KeyGenerator
 
             ResultSet rs = stmt.executeQuery();
             if (!rs.next()) {
-                throw new SQLException(
-                    "Failed to find next primary key value [table=" + _table +
-                    ", column=" + _valueColumnName +
-                    ", where=" + _pkColumnName + "=" + _pkColumnValue + "]");
+                throw new SQLException("Failed to find next primary key value [table=" + _table +
+                                       ", column=" + _valueColumnName +
+                                       ", where=" + _pkColumnName + "=" + _pkColumnValue + "]");
             }
             int val = rs.getInt(1);
             JDBCUtil.close(stmt);
 
-            stmt = conn.prepareStatement(
-                "UPDATE " + _table + " SET " + _valueColumnName + " = ? " +
-                "WHERE " + _pkColumnName + " = ?");
+            stmt = conn.prepareStatement("UPDATE " + _table + " SET " + _valueColumnName + " = ? " +
+                                         "WHERE " + _pkColumnName + " = ?");
             stmt.setInt(1, val + _allocationSize);
             stmt.setString(2, _pkColumnValue);
             stmt.executeUpdate();
@@ -118,8 +116,11 @@ public class TableKeyGenerator implements KeyGenerator
         }
     }
 
-    /** Convenience function to return a value or a default fallback. */
-    protected static String defStr (String value, String def) {
+    /**
+     * Convenience function to return a value or a default fallback.
+     */
+    protected static String defStr (String value, String def)
+    {
         if (value == null || value.trim().length() == 0) {
             return def;
         }
diff --git a/src/java/com/samskivert/jdbc/depot/clause/FieldOverrideClause.java b/src/java/com/samskivert/jdbc/depot/clause/FieldOverrideClause.java
new file mode 100644
index 0000000..2d0e345
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/clause/FieldOverrideClause.java
@@ -0,0 +1,80 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.clause;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collection;
+
+import com.samskivert.io.PersistenceException;
+import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.expression.SQLExpression;
+
+/**
+ * Redirects one field of the persistent object we're creating from its default associated column
+ * to a general {@link SQLExpression}.
+ * 
+ * Thus the select portion of a query can include a reference to a different column in a different
+ * table through a {@link ColumnExpression}, or a literal expression such as COUNT(*) through a
+ * {@link LiteralExpression}.
+ */ 
+public class FieldOverrideClause
+    implements QueryClause
+{
+    public FieldOverrideClause (String field, SQLExpression override)
+        throws PersistenceException
+    {
+        super();
+        _field = field;
+        _override = override;
+    }
+
+    /** The field we're overriding. The Query object uses this for indexing. */
+    public String getField ()
+    {
+        return _field;
+    }
+
+    // from QueryClause
+    public Collection getClassSet ()
+    {
+        return null;
+    }
+
+    // from QueryClause
+    public void appendClause (Query query, StringBuilder builder)
+    {
+        _override.appendExpression(query, builder);
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        return argIdx;
+    }
+    
+    /** The name of the field on the persistent object to override. */
+    protected String _field;
+
+    /** The overriding expression. */
+    protected SQLExpression _override;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/clause/GroupByClause.java b/src/java/com/samskivert/jdbc/depot/clause/GroupByClause.java
new file mode 100644
index 0000000..8b7c2ef
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/clause/GroupByClause.java
@@ -0,0 +1,71 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.clause;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collection;
+
+import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.expression.SQLExpression;
+
+/**
+ *  Represents a GROUP BY clause.
+ */
+public class GroupByClause
+    implements QueryClause
+{
+    public GroupByClause (SQLExpression... values)
+    {
+        super();
+        _values = values;
+    }
+
+    // from QueryClause
+    public Collection getClassSet ()
+    {
+        return null;
+    }
+
+    // from QueryClause
+    public void appendClause (Query query, StringBuilder builder)
+    {
+        for (int ii = 0; ii < _values.length; ii++) {
+            if (ii > 0) {
+                builder.append(", ");
+            }
+            _values[ii].appendExpression(query, builder);
+        }
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        for (int ii = 0; ii < _values.length; ii++) {
+            argIdx = _values[ii].bindArguments(pstmt, argIdx);
+        }
+        return argIdx;
+    }
+
+    /** The expressions that are generated for the clause. */
+    protected SQLExpression[] _values;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/clause/JoinClause.java b/src/java/com/samskivert/jdbc/depot/clause/JoinClause.java
new file mode 100644
index 0000000..abd4f63
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/clause/JoinClause.java
@@ -0,0 +1,74 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.clause;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import com.samskivert.io.PersistenceException;
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ *  Represents a JOIN -- currently just an INNER one.
+ */
+public class JoinClause
+    implements QueryClause
+{
+    public JoinClause (String pCol, Class joinClass, String jCol)
+        throws PersistenceException
+    {
+        super();
+        _primaryColumn = pCol;
+        _joinClass = joinClass;
+        _joinColumn = jCol;
+    }
+
+    // from QueryClause
+    public Collection getClassSet () {
+        return Arrays.asList(new Class[] { _joinClass });
+    }
+
+    // from QueryClause
+    public void appendClause (Query query, StringBuilder builder)
+    {
+        String jAbbrev = query.getTableAbbreviation(_joinClass);
+        builder.append(query.getTableName(_joinClass) + " as " + jAbbrev + " on T." +
+                       _primaryColumn + " = " + jAbbrev + "." + _joinColumn);
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        return argIdx;
+    }
+
+    /** The column on which to join. */
+    protected String _primaryColumn;
+
+    /** The class of the table we're to join against. */
+    protected Class _joinClass;
+
+    /** The column we're joining against. */
+    protected String _joinColumn;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/clause/LimitClause.java b/src/java/com/samskivert/jdbc/depot/clause/LimitClause.java
new file mode 100644
index 0000000..7fdd55c
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/clause/LimitClause.java
@@ -0,0 +1,68 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.clause;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collection;
+
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ *  Represents a LIMIT/OFFSET clause, for pagination.
+ */
+public class LimitClause
+    implements QueryClause
+{
+    public LimitClause (int offset, int count)
+    {
+        super();
+        _offset = offset;
+        _count = count;
+    }
+
+    // from QueryClause
+    public Collection getClassSet ()
+    {
+        return null;
+    }
+
+    // from QueryClause
+    public void appendClause (Query query, StringBuilder builder)
+    {
+        builder.append("? offset ?");
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        pstmt.setObject(argIdx++, _count);
+        pstmt.setObject(argIdx++, _offset);
+        return argIdx;
+    }
+    
+    /** The first row of the result set to return. */
+    protected int _offset;
+
+    /** The number of rows, at most, to return. */
+    protected int _count;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/clause/OrderByClause.java b/src/java/com/samskivert/jdbc/depot/clause/OrderByClause.java
new file mode 100644
index 0000000..e472a7b
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/clause/OrderByClause.java
@@ -0,0 +1,75 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.clause;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collection;
+
+import com.samskivert.jdbc.depot.Query;
+import com.samskivert.jdbc.depot.expression.SQLExpression;
+
+/**
+ *  Represents an ORDER BY clause.
+ */
+public class OrderByClause
+    implements QueryClause
+{
+    public OrderByClause (SQLExpression... values)
+    {
+        super();
+        _values = values;
+    }
+    
+    // from QueryClause
+    public Collection getClassSet ()
+    {
+        return null;
+    }
+
+    // from QueryClause
+    public void appendClause (Query query, StringBuilder builder)
+    {
+        for (int ii = 0; ii < _values.length; ii++) {
+            if (ii > 0) {
+                builder.append(", ");
+            }
+            _values[ii].appendExpression(query, builder);
+        }
+        builder.append(_ascending ? " ASC" : " DESC");
+    }
+
+    // from QueryClause
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        for (int ii = 0; ii < _values.length; ii++) {
+            argIdx = _values[ii].bindArguments(pstmt, argIdx);
+        }
+        return argIdx;
+    }
+    
+    /** The expressions that are generated for the clause. */
+    protected SQLExpression[] _values;
+
+    /** Whether the ordering is to be ascending rather than descending. */
+    protected boolean _ascending;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java b/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java
new file mode 100644
index 0000000..178df73
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/clause/QueryClause.java
@@ -0,0 +1,53 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.clause;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Collection;
+
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ * Represents a piece or modifier of an SQL query.
+ */
+public interface QueryClause
+{
+    /**
+     * Return a set of all persistent classes referenced by this clause.  Null may be returned if
+     * no classes are rererenced.
+     */
+    public Collection getClassSet ();
+    
+    /**
+     * Construct the SQL form of this query clause The implementor is invited to call methods on
+     * the Query object to e.g. resolve the current table abbreviations associated with classes.
+     */
+    public void appendClause (Query query, StringBuilder builder);
+
+    /**
+     * Bind any objects that were referenced in the generated SQL.  For each ? that appears in the
+     * SQL, precisely one parameter must be claimed and bound in this method, and argIdx
+     * incremented and returned.
+     */
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/expression/ColumnExpression.java b/src/java/com/samskivert/jdbc/depot/expression/ColumnExpression.java
new file mode 100644
index 0000000..d9b727b
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/expression/ColumnExpression.java
@@ -0,0 +1,70 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.expression;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ * An expression identifying a column of a class, e.g. GameRecord.itemId. If no class is given, no
+ * disambiguation occurs in the generated SQL.
+ */
+public class ColumnExpression
+    implements SQLExpression
+{
+    public ColumnExpression (String column)
+    {
+        this(null, column);
+    }
+
+    public ColumnExpression (Class c, String column)
+    {
+        super();
+        _class = c;
+        _column = column;
+    }
+
+    // from SQLExpression
+    public void appendExpression (Query query, StringBuilder builder)
+    {
+        if (_class == null || query == null) {
+            builder.append(_column);
+        } else {
+            String tRef = query.getTableAbbreviation(_class);
+            builder.append(tRef).append(".").append(_column);
+        }
+    }
+
+    // from SQLExpression
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        return argIdx;
+    }
+
+    /** The table that hosts the column we reference, or null. */
+    protected Class _class;
+
+    /** The name of the column we reference. */
+    protected String _column;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/expression/LiteralExpression.java b/src/java/com/samskivert/jdbc/depot/expression/LiteralExpression.java
new file mode 100644
index 0000000..3a49a14
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/expression/LiteralExpression.java
@@ -0,0 +1,55 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.expression;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ * An expression for things we don't support natively, e.g. COUNT(*).
+ */
+public class LiteralExpression
+    implements SQLExpression
+{
+    public LiteralExpression (String text)
+    {
+        super();
+        _text = text;
+    }
+
+    // from SQLExpression
+    public void appendExpression (Query query, StringBuilder builder)
+    {
+        builder.append(_text);
+    }
+
+    // from SQLExpression
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        return argIdx;
+    }
+
+    /** The literal text of this expression, e.g. COUNT(*) */
+    protected String _text;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java b/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java
new file mode 100644
index 0000000..69d32ca
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/expression/SQLExpression.java
@@ -0,0 +1,46 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.expression;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ * Represents an SQL expression, e.g. column name, function, or constant.
+ */
+public interface SQLExpression
+{
+    /**
+     * Construct the SQL form of this expression. The implementor is invited to call methods on the
+     * Query object to e.g. resolve the current table abbreviations associated with classes.
+     */
+    public void appendExpression (Query query, StringBuilder builder);
+
+    /**
+     * Bind any objects that were referenced in the generated SQL.  For each ? that appears in the
+     * SQL, precisely one parameter must be claimed and bound in this method, and argIdx
+     * incremented and returned.
+     */
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException;
+}
diff --git a/src/java/com/samskivert/jdbc/depot/expression/ValueExpression.java b/src/java/com/samskivert/jdbc/depot/expression/ValueExpression.java
new file mode 100644
index 0000000..020e760
--- /dev/null
+++ b/src/java/com/samskivert/jdbc/depot/expression/ValueExpression.java
@@ -0,0 +1,56 @@
+//
+// $Id$
+//
+// samskivert library - useful routines for java programs
+// Copyright (C) 2006 Michael Bayne, Pär Winzell
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.samskivert.jdbc.depot.expression;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import com.samskivert.jdbc.depot.Query;
+
+/**
+ * A Java value that is bound as a parameter to the query, e.g. 1 or 'abc'. 
+ */
+public class ValueExpression
+    implements SQLExpression
+{
+    public ValueExpression (Comparable _value)
+    {
+        super();
+        this._value = _value;
+    }
+
+    // from SQLExpression
+    public void appendExpression (Query query, StringBuilder builder)
+    {
+        builder.append("?");
+    }
+
+    // from SQLExpression
+    public int bindArguments (PreparedStatement pstmt, int argIdx)
+        throws SQLException
+    {
+        pstmt.setObject(argIdx ++, _value);
+        return argIdx;
+    }
+
+    /** The value to be bound to the SQL parameters. */
+    protected Comparable _value;
+}