You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metamodel.apache.org by ka...@apache.org on 2013/07/22 10:11:03 UTC

[50/64] [partial] Hard rename of all 'org/eobjects' folders to 'org/apache'.

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/AbstractDataContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/AbstractDataContext.java b/core/src/main/java/org/apache/metamodel/AbstractDataContext.java
new file mode 100644
index 0000000..3300e17
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/AbstractDataContext.java
@@ -0,0 +1,463 @@
+/**
+ * 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.eobjects.metamodel;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.CompiledQuery;
+import org.eobjects.metamodel.query.DefaultCompiledQuery;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.query.builder.InitFromBuilder;
+import org.eobjects.metamodel.query.builder.InitFromBuilderImpl;
+import org.eobjects.metamodel.query.parser.QueryParser;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+
+/**
+ * Abstract implementation of the DataContext interface. Provides convenient
+ * implementations of all trivial and datastore-independent methods.
+ * 
+ * @author Kasper Sørensen
+ */
+public abstract class AbstractDataContext implements DataContext {
+
+    private static final String NULL_SCHEMA_NAME_TOKEN = "<metamodel.schema.name.null>";
+    private final ConcurrentMap<String, Schema> _schemaCache = new ConcurrentHashMap<String, Schema>();
+    private final Comparator<? super String> _schemaNameComparator = SchemaNameComparator.getInstance();
+    private String[] _schemaNameCache;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final DataContext refreshSchemas() {
+        _schemaCache.clear();
+        _schemaNameCache = null;
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Schema[] getSchemas() throws MetaModelException {
+        String[] schemaNames = getSchemaNames();
+        Schema[] schemas = new Schema[schemaNames.length];
+        for (int i = 0; i < schemaNames.length; i++) {
+            final String name = schemaNames[i];
+            final Schema schema = _schemaCache.get(getSchemaCacheKey(name));
+            if (schema == null) {
+                final Schema newSchema = getSchemaByName(name);
+                if (newSchema == null) {
+                    throw new MetaModelException("Declared schema does not exist: " + name);
+                }
+                final Schema existingSchema = _schemaCache.putIfAbsent(getSchemaCacheKey(name), newSchema);
+                if (existingSchema == null) {
+                    schemas[i] = newSchema;
+                } else {
+                    schemas[i] = existingSchema;
+                }
+            } else {
+                schemas[i] = schema;
+            }
+        }
+        return schemas;
+    }
+
+    private String getSchemaCacheKey(String name) {
+        if (name == null) {
+            return NULL_SCHEMA_NAME_TOKEN;
+        }
+        return name;
+    }
+
+    /**
+     * m {@inheritDoc}
+     */
+    @Override
+    public final String[] getSchemaNames() throws MetaModelException {
+        if (_schemaNameCache == null) {
+            _schemaNameCache = getSchemaNamesInternal();
+        }
+        String[] schemaNames = Arrays.copyOf(_schemaNameCache, _schemaNameCache.length);
+        Arrays.sort(schemaNames, _schemaNameComparator);
+        return schemaNames;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Schema getDefaultSchema() throws MetaModelException {
+        Schema result = null;
+        String defaultSchemaName = getDefaultSchemaName();
+        if (defaultSchemaName != null) {
+            result = getSchemaByName(defaultSchemaName);
+        }
+        if (result == null) {
+            Schema[] schemas = getSchemas();
+            if (schemas.length == 1) {
+                result = schemas[0];
+            } else {
+                int highestTableCount = -1;
+                for (int i = 0; i < schemas.length; i++) {
+                    final Schema schema = schemas[i];
+                    String name = schema.getName();
+                    if (schema != null) {
+                        name = name.toLowerCase();
+                        boolean isInformationSchema = name.startsWith("information") && name.endsWith("schema");
+                        if (!isInformationSchema && schema.getTableCount() > highestTableCount) {
+                            highestTableCount = schema.getTableCount();
+                            result = schema;
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final InitFromBuilder query() {
+        return new InitFromBuilderImpl(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Query parseQuery(final String queryString) throws MetaModelException {
+        final QueryParser parser = new QueryParser(this, queryString);
+        final Query query = parser.parse();
+        return query;
+    }
+
+    @Override
+    public CompiledQuery compileQuery(final Query query) throws MetaModelException {
+        return new DefaultCompiledQuery(query);
+    }
+
+    @Override
+    public DataSet executeQuery(CompiledQuery compiledQuery, Object... values) {
+        assert compiledQuery instanceof DefaultCompiledQuery;
+
+        final DefaultCompiledQuery defaultCompiledQuery = (DefaultCompiledQuery) compiledQuery;
+        final Query query = defaultCompiledQuery.cloneWithParameterValues(values);
+
+        return executeQuery(query);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final DataSet executeQuery(final String queryString) throws MetaModelException {
+        final Query query = parseQuery(queryString);
+        final DataSet dataSet = executeQuery(query);
+        return dataSet;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Schema getSchemaByName(String name) throws MetaModelException {
+        Schema schema = _schemaCache.get(getSchemaCacheKey(name));
+        if (schema == null) {
+            if (name == null) {
+                schema = getSchemaByNameInternal(null);
+            } else {
+                String[] schemaNames = getSchemaNames();
+                for (String schemaName : schemaNames) {
+                    if (name.equalsIgnoreCase(schemaName)) {
+                        schema = getSchemaByNameInternal(name);
+                        break;
+                    }
+                }
+                if (schema == null) {
+                    for (String schemaName : schemaNames) {
+                        if (name.equalsIgnoreCase(schemaName)) {
+                            // try again with "schemaName" as param instead of
+                            // "name".
+                            schema = getSchemaByNameInternal(schemaName);
+                            break;
+                        }
+                    }
+                }
+            }
+            if (schema != null) {
+                Schema existingSchema = _schemaCache.putIfAbsent(getSchemaCacheKey(schema.getName()), schema);
+                if (existingSchema != null) {
+                    // race conditions may cause two schemas to be created.
+                    // We'll favor the existing schema if possible, since schema
+                    // may contain lazy-loading logic and so on.
+                    return existingSchema;
+                }
+            }
+        }
+        return schema;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Column getColumnByQualifiedLabel(final String columnName) {
+        if (columnName == null) {
+            return null;
+        }
+        Schema schema = null;
+        final String[] schemaNames = getSchemaNames();
+        for (final String schemaName : schemaNames) {
+            if (schemaName == null) {
+                // search without schema name (some databases have only a single
+                // schema with no name)
+                schema = getSchemaByName(null);
+                if (schema != null) {
+                    Column column = getColumn(schema, columnName);
+                    if (column != null) {
+                        return column;
+                    }
+                }
+            } else {
+                // Search case-sensitive
+                Column col = searchColumn(schemaName, columnName, columnName);
+                if (col != null) {
+                    return col;
+                }
+            }
+        }
+
+        final String columnNameInLowerCase = columnName.toLowerCase();
+        for (final String schemaName : schemaNames) {
+            if (schemaName != null) {
+                // search case-insensitive
+                String schameNameInLowerCase = schemaName.toLowerCase();
+                Column col = searchColumn(schameNameInLowerCase, columnName, columnNameInLowerCase);
+                if (col != null) {
+                    return col;
+                }
+            }
+        }
+
+        schema = getDefaultSchema();
+        if (schema != null) {
+            Column column = getColumn(schema, columnName);
+            if (column != null) {
+                return column;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Searches for a particular column within a schema
+     * 
+     * @param schemaNameSearch
+     *            the schema name to use for search
+     * @param columnNameOriginal
+     *            the original column name
+     * @param columnNameSearch
+     *            the column name as it should be searched for (either the same
+     *            as original, or lower case in case of case-insensitive search)
+     * @return
+     */
+    private Column searchColumn(String schemaNameSearch, String columnNameOriginal, String columnNameSearch) {
+        if (columnNameSearch.startsWith(schemaNameSearch)) {
+            Schema schema = getSchemaByName(schemaNameSearch);
+            if (schema != null) {
+                String tableAndColumnPath = columnNameOriginal.substring(schemaNameSearch.length());
+
+                if (tableAndColumnPath.charAt(0) == '.') {
+                    tableAndColumnPath = tableAndColumnPath.substring(1);
+
+                    Column column = getColumn(schema, tableAndColumnPath);
+                    if (column != null) {
+                        return column;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private final Column getColumn(final Schema schema, final String tableAndColumnPath) {
+        Table table = null;
+        String columnPath = tableAndColumnPath;
+        final String[] tableNames = schema.getTableNames();
+        for (final String tableName : tableNames) {
+            if (tableName != null) {
+                // search case-sensitive
+                if (isStartingToken(tableName, tableAndColumnPath)) {
+                    table = schema.getTableByName(tableName);
+                    columnPath = tableAndColumnPath.substring(tableName.length());
+
+                    if (columnPath.charAt(0) == '.') {
+                        columnPath = columnPath.substring(1);
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (table == null) {
+            final String tableAndColumnPathInLowerCase = tableAndColumnPath.toLowerCase();
+            for (final String tableName : tableNames) {
+                if (tableName != null) {
+                    String tableNameInLowerCase = tableName.toLowerCase();
+                    // search case-insensitive
+                    if (isStartingToken(tableNameInLowerCase, tableAndColumnPathInLowerCase)) {
+                        table = schema.getTableByName(tableName);
+                        columnPath = tableAndColumnPath.substring(tableName.length());
+
+                        if (columnPath.charAt(0) == '.') {
+                            columnPath = columnPath.substring(1);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (table == null && tableNames.length == 1) {
+            table = schema.getTables()[0];
+        }
+
+        if (table != null) {
+            Column column = table.getColumnByName(columnPath);
+            if (column != null) {
+                return column;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Table getTableByQualifiedLabel(final String tableName) {
+        if (tableName == null) {
+            return null;
+        }
+        Schema schema = null;
+        String[] schemaNames = getSchemaNames();
+        for (String schemaName : schemaNames) {
+            if (schemaName == null) {
+                // there's an unnamed schema present.
+                schema = getSchemaByName(null);
+                if (schema != null) {
+                    Table table = schema.getTableByName(tableName);
+                    return table;
+                }
+            } else {
+                // case-sensitive search
+                if (isStartingToken(schemaName, tableName)) {
+                    schema = getSchemaByName(schemaName);
+                }
+            }
+        }
+
+        if (schema == null) {
+            final String tableNameInLowerCase = tableName.toLowerCase();
+            for (final String schemaName : schemaNames) {
+                if (schemaName != null) {
+                    // case-insensitive search
+                    final String schemaNameInLowerCase = schemaName.toLowerCase();
+                    if (isStartingToken(schemaNameInLowerCase, tableNameInLowerCase)) {
+                        schema = getSchemaByName(schemaName);
+                    }
+                }
+            }
+        }
+
+        if (schema == null) {
+            schema = getDefaultSchema();
+        }
+
+        String tablePart = tableName.toLowerCase();
+        String schemaName = schema.getName();
+        if (schemaName != null && isStartingToken(schemaName.toLowerCase(), tablePart)) {
+            tablePart = tablePart.substring(schemaName.length());
+            if (tablePart.startsWith(".")) {
+                tablePart = tablePart.substring(1);
+            }
+        }
+
+        return schema.getTableByName(tablePart);
+    }
+
+    private boolean isStartingToken(String partName, String fullName) {
+        if (fullName.startsWith(partName)) {
+            final int length = partName.length();
+            if (length == 0) {
+                return false;
+            }
+            if (fullName.length() > length) {
+                final char nextChar = fullName.charAt(length);
+                if (isQualifiedPathDelim(nextChar)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    protected boolean isQualifiedPathDelim(char c) {
+        return c == '.' || c == '"';
+    }
+
+    /**
+     * Gets schema names from the non-abstract implementation. These schema
+     * names will be cached except if the {@link #refreshSchemas()} method is
+     * called.
+     * 
+     * @return an array of schema names.
+     */
+    protected abstract String[] getSchemaNamesInternal();
+
+    /**
+     * Gets the name of the default schema.
+     * 
+     * @return the default schema name.
+     */
+    protected abstract String getDefaultSchemaName();
+
+    /**
+     * Gets a specific schema from the non-abstract implementation. This schema
+     * object will be cached except if the {@link #refreshSchemas()} method is
+     * called.
+     * 
+     * @param name
+     *            the name of the schema to get
+     * @return a schema object representing the named schema, or null if no such
+     *         schema exists.
+     */
+    protected abstract Schema getSchemaByNameInternal(String name);
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java b/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java
new file mode 100644
index 0000000..602a412
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/AbstractUpdateCallback.java
@@ -0,0 +1,164 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eobjects.metamodel;
+
+import java.util.Arrays;
+
+import org.eobjects.metamodel.create.TableCreationBuilder;
+import org.eobjects.metamodel.delete.RowDeletionBuilder;
+import org.eobjects.metamodel.drop.TableDropBuilder;
+import org.eobjects.metamodel.insert.RowInsertionBuilder;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.update.RowUpdationBuilder;
+
+/**
+ * Abstract implementation of the {@link UpdateCallback} interface. Implements
+ * only the data store agnostic methods.
+ * 
+ * @author Kasper Sørensen
+ */
+public abstract class AbstractUpdateCallback implements UpdateCallback {
+
+    private final DataContext _dataContext;
+
+    public AbstractUpdateCallback(DataContext dataContext) {
+        _dataContext = dataContext;
+    }
+
+    @Override
+    public TableCreationBuilder createTable(String schemaName, String tableName) throws IllegalArgumentException,
+            IllegalStateException {
+        Schema schema = getSchema(schemaName);
+        return createTable(schema, tableName);
+    }
+
+    @Override
+    public TableDropBuilder dropTable(String schemaName, String tableName) throws IllegalArgumentException,
+            IllegalStateException, UnsupportedOperationException {
+        Table table = getTable(schemaName, tableName);
+        return dropTable(table);
+    }
+
+    @Override
+    public TableDropBuilder dropTable(Schema schema, String tableName) throws IllegalArgumentException,
+            IllegalStateException, UnsupportedOperationException {
+        Table table = schema.getTableByName(tableName);
+        if (table == null) {
+            throw new IllegalArgumentException("Nu such table '" + tableName + "' found in schema: " + schema
+                    + ". Available tables are: " + Arrays.toString(schema.getTableNames()));
+        }
+        return dropTable(table);
+    }
+
+    @Override
+    public final RowInsertionBuilder insertInto(String tableName) throws IllegalArgumentException,
+            IllegalStateException {
+        return insertInto(getTable(tableName));
+    }
+
+    @Override
+    public RowInsertionBuilder insertInto(String schemaName, String tableName) throws IllegalArgumentException,
+            IllegalStateException, UnsupportedOperationException {
+        return insertInto(getTable(schemaName, tableName));
+    }
+
+    private Table getTable(String schemaName, String tableName) {
+        final Schema schema = getSchema(schemaName);
+        final Table table = schema.getTableByName(tableName);
+        if (table == null) {
+            throw new IllegalArgumentException("Nu such table '" + tableName + "' found in schema: " + schema
+                    + ". Available tables are: " + Arrays.toString(schema.getTableNames()));
+        }
+        return table;
+    }
+
+    private Schema getSchema(String schemaName) {
+        final Schema schema = _dataContext.getSchemaByName(schemaName);
+        if (schema == null) {
+            throw new IllegalArgumentException("No such schema: " + schemaName);
+        }
+        return schema;
+    }
+
+    @Override
+    public final RowDeletionBuilder deleteFrom(String tableName) {
+        return deleteFrom(getTable(tableName));
+    }
+
+    @Override
+    public RowDeletionBuilder deleteFrom(String schemaName, String tableName) throws IllegalArgumentException,
+            IllegalStateException, UnsupportedOperationException {
+        final Table table = getTable(schemaName, tableName);
+        return deleteFrom(table);
+    }
+
+    @Override
+    public final TableDropBuilder dropTable(String tableName) {
+        return dropTable(getTable(tableName));
+    }
+
+    @Override
+    public final RowUpdationBuilder update(String tableName) {
+        return update(getTable(tableName));
+    }
+
+    private Table getTable(String tableName) {
+        Table table = getDataContext().getTableByQualifiedLabel(tableName);
+        if (table == null) {
+            throw new IllegalArgumentException("No such table: " + tableName);
+        }
+        return table;
+    }
+
+    @Override
+    public DataContext getDataContext() {
+        return _dataContext;
+    }
+
+    @Override
+    public boolean isCreateTableSupported() {
+        // since 2.0 all updateable datacontext have create table support
+        return true;
+    }
+
+    @Override
+    public boolean isInsertSupported() {
+        // since 2.0 all updateable datacontext have create table support
+        return true;
+    }
+
+    @Override
+    public boolean isUpdateSupported() {
+        return isInsertSupported() && isDeleteSupported();
+    }
+
+    @Override
+    public RowUpdationBuilder update(String schemaName, String tableName) throws IllegalArgumentException,
+            IllegalStateException, UnsupportedOperationException {
+        final Table table = getTable(schemaName, tableName);
+        return update(table);
+    }
+
+    @Override
+    public RowUpdationBuilder update(Table table) throws IllegalArgumentException, IllegalStateException,
+            UnsupportedOperationException {
+        return new DeleteAndInsertBuilder(this, table);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java b/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java
new file mode 100644
index 0000000..d7b567c
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/BatchUpdateScript.java
@@ -0,0 +1,29 @@
+/**
+ * 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.eobjects.metamodel;
+
+/**
+ * Indicator sub-interface of {@link UpdateScript}. Implementing your updates
+ * using this interface indicates to the underlying
+ * {@link UpdateableDataContext} that the update script represents a large batch
+ * update and that appropriate optimizations may be taken into use if available.
+ */
+public interface BatchUpdateScript extends UpdateScript {
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/CompositeDataContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/CompositeDataContext.java b/core/src/main/java/org/apache/metamodel/CompositeDataContext.java
new file mode 100644
index 0000000..82d6c01
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/CompositeDataContext.java
@@ -0,0 +1,204 @@
+/**
+ * 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.eobjects.metamodel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+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.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.FromItem;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.schema.CompositeSchema;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.util.Func;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DataContext for composite datacontexts. Composite DataContexts wrap several
+ * other datacontexts and makes cross-datastore querying possible.
+ * 
+ * @author Kasper Sørensen
+ */
+public class CompositeDataContext extends AbstractDataContext {
+
+    private final static Logger logger = LoggerFactory.getLogger(CompositeDataContext.class);
+    private Map<String, CompositeSchema> _compositeSchemas = new HashMap<String, CompositeSchema>();
+    private DataContext[] _delegates;
+
+    public CompositeDataContext(DataContext... delegates) {
+        if (delegates == null) {
+            throw new IllegalArgumentException("delegates cannot be null");
+        }
+        _delegates = delegates;
+    }
+
+    public CompositeDataContext(Collection<DataContext> delegates) {
+        if (delegates == null) {
+            throw new IllegalArgumentException("delegates cannot be null");
+        }
+        _delegates = delegates.toArray(new DataContext[delegates.size()]);
+    }
+
+    @Override
+    public DataSet executeQuery(Query query) throws MetaModelException {
+        // a set of all datacontexts involved
+        Set<DataContext> dataContexts = new HashSet<DataContext>();
+
+        // find all datacontexts involved, by investigating FROM items
+        List<FromItem> items = query.getFromClause().getItems();
+        for (FromItem item : items) {
+            List<FromItem> tableFromItems = MetaModelHelper.getTableFromItems(item);
+            for (FromItem fromItem : tableFromItems) {
+                Table table = fromItem.getTable();
+
+                DataContext dc = getDataContext(table);
+                if (dc == null) {
+                    throw new MetaModelException("Could not resolve child-datacontext for table: " + table);
+                }
+                dataContexts.add(dc);
+            }
+        }
+
+        if (dataContexts.isEmpty()) {
+            throw new MetaModelException("No suiting delegate DataContext to execute query: " + query);
+        } else if (dataContexts.size() == 1) {
+            Iterator<DataContext> it = dataContexts.iterator();
+            assert it.hasNext();
+            DataContext dc = it.next();
+            return dc.executeQuery(query);
+        } else {
+            // we create a datacontext which can materialize tables from
+            // separate datacontexts.
+            final Func<Table, DataContext> dataContextRetrievalFunction = new Func<Table, DataContext>() {
+                @Override
+                public DataContext eval(Table table) {
+                    return getDataContext(table);
+                }
+            };
+            return new CompositeQueryDelegate(dataContextRetrievalFunction).executeQuery(query);
+        }
+    }
+
+    private DataContext getDataContext(Table table) {
+        DataContext result = null;
+        if (table != null) {
+            Schema schema = table.getSchema();
+
+            if (schema != null) {
+                for (DataContext dc : _delegates) {
+                    Schema dcSchema = dc.getSchemaByName(schema.getName());
+                    if (dcSchema != null) {
+
+                        // first round = try with schema identity match
+                        if (dcSchema == schema) {
+                            logger.debug("DataContext for '{}' resolved (using identity) to: '{}'", table, dcSchema);
+                            result = dc;
+                            break;
+                        }
+                    }
+                }
+
+                if (result == null) {
+                    for (DataContext dc : _delegates) {
+                        Schema dcSchema = dc.getSchemaByName(schema.getName());
+                        if (dcSchema != null) {
+                            // second round = try with schema equals method
+                            if (dcSchema.equals(schema)) {
+                                logger.debug("DataContext for '{}' resolved (using equals) to: '{}'", table, dcSchema);
+                                result = dc;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (result == null) {
+            logger.warn("Couldn't resolve DataContext for {}", table);
+        }
+        return result;
+    }
+
+    @Override
+    public String getDefaultSchemaName() throws MetaModelException {
+        for (DataContext dc : _delegates) {
+            Schema schema = dc.getDefaultSchema();
+            if (schema != null) {
+                return schema.getName();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Schema getSchemaByNameInternal(String name) throws MetaModelException {
+        CompositeSchema compositeSchema = _compositeSchemas.get(name);
+        if (compositeSchema != null) {
+            return compositeSchema;
+        }
+        List<Schema> matchingSchemas = new ArrayList<Schema>();
+        for (DataContext dc : _delegates) {
+            Schema schema = dc.getSchemaByName(name);
+            if (schema != null) {
+                matchingSchemas.add(schema);
+            }
+        }
+        if (matchingSchemas.size() == 1) {
+            return matchingSchemas.iterator().next();
+        }
+        if (matchingSchemas.size() > 1) {
+            if (logger.isInfoEnabled()) {
+                logger.info("Name-clash detected for Schema '{}'. Creating CompositeSchema.");
+            }
+            compositeSchema = new CompositeSchema(name, matchingSchemas);
+            _compositeSchemas.put(name, compositeSchema);
+            return compositeSchema;
+        }
+        return null;
+    }
+
+    @Override
+    public String[] getSchemaNamesInternal() throws MetaModelException {
+        Set<String> set = new HashSet<String>();
+        for (DataContext dc : _delegates) {
+            String[] schemaNames = dc.getSchemaNames();
+            for (String name : schemaNames) {
+                if (!MetaModelHelper.isInformationSchema(name)) {
+                    // we skip information schemas, since they're anyways going
+                    // to be incomplete and misleading.
+                    set.add(name);
+                }
+            }
+        }
+        String[] result = set.toArray(new String[set.size()]);
+        Arrays.sort(result);
+        return result;
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java b/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java
new file mode 100644
index 0000000..70b19f1
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/CompositeQueryDelegate.java
@@ -0,0 +1,49 @@
+/**
+ * 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.eobjects.metamodel;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.util.Func;
+
+final class CompositeQueryDelegate extends QueryPostprocessDelegate {
+
+	private final Func<Table, DataContext> _dataContextRetrievalFunction;
+
+	public CompositeQueryDelegate(
+			Func<Table, DataContext> dataContextRetrievalFunction) {
+		_dataContextRetrievalFunction = dataContextRetrievalFunction;
+	}
+
+	@Override
+	protected DataSet materializeMainSchemaTable(Table table, Column[] columns,
+			int maxRows) {
+		// find the appropriate datacontext to execute a simple
+		// table materialization query
+		DataContext dc = _dataContextRetrievalFunction.eval(table);
+		Query q = new Query().select(columns).from(table);
+		if (maxRows >= 0) {
+			q.setMaxRows(maxRows);
+		}
+		return dc.executeQuery(q);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/DataContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/DataContext.java b/core/src/main/java/org/apache/metamodel/DataContext.java
new file mode 100644
index 0000000..b536d7d
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/DataContext.java
@@ -0,0 +1,199 @@
+/**
+ * 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.eobjects.metamodel;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.query.CompiledQuery;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.query.QueryParameter;
+import org.eobjects.metamodel.query.builder.InitFromBuilder;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.Table;
+
+/**
+ * A DataContext represents the central entry point for interactions with
+ * datastores. The DataContext contains of the structure of data (in the form of
+ * schemas) and interactions (in the form of queries) with data.
+ * 
+ * @author Kasper Sørensen
+ */
+public interface DataContext {
+
+    /**
+     * Enforces a refresh of the schemas. If not refreshed, cached schema
+     * objects may be used.
+     * 
+     * @return this DataContext
+     */
+    public DataContext refreshSchemas();
+
+    /**
+     * Gets all schemas within this DataContext.
+     * 
+     * @return the schemas in this data context. Schemas are cached for reuse in
+     *         many situations so if you want to update the schemas, use the
+     *         refreshSchemas() method.
+     * @throws MetaModelException
+     *             if an error occurs retrieving the schema model
+     */
+    public Schema[] getSchemas() throws MetaModelException;
+
+    /**
+     * Gets the names of all schemas within this DataContext.
+     * 
+     * @return an array of valid schema names
+     * @throws MetaModelException
+     *             if an error occurs retrieving the schema model
+     */
+    public String[] getSchemaNames() throws MetaModelException;
+
+    /**
+     * Gets the default schema of this DataContext.
+     * 
+     * @return the schema that you are most probable to be interested in. The
+     *         default schema is determined by finding the schema with most
+     *         available of tables. In a lot of situations there will only be a
+     *         single available schema and in that case this will of course be
+     *         the schema returned.
+     * @throws MetaModelException
+     *             if an error occurs retrieving the schema model
+     */
+    public Schema getDefaultSchema() throws MetaModelException;
+
+    /**
+     * Gets a schema by a specified name.
+     * 
+     * @param name
+     *            the name of the desired schema
+     * @return the Schema with the specified name or null if no such schema
+     *         exists
+     * @throws MetaModelException
+     *             if an error occurs retrieving the schema model
+     */
+    public Schema getSchemaByName(String name) throws MetaModelException;
+
+    /**
+     * Starts building a query using the query builder API. This way of building
+     * queries is the preferred approach since it provides a more type-safe
+     * approach to building API's as well as allows the DataContext
+     * implementation to be aware of the query building process.
+     * 
+     * @return a query builder component at the initial position in building a
+     *         query.
+     */
+    public InitFromBuilder query();
+
+    /**
+     * Parses a string-based SQL query and produces a corresponding
+     * {@link Query} object.
+     * 
+     * @param queryString
+     *            the SQL query to parse
+     * @return a {@link Query} object corresponding to the SQL query.
+     * @throws MetaModelException
+     *             in case the parsing was unsuccesful.
+     */
+    public Query parseQuery(String queryString) throws MetaModelException;
+
+    /**
+     * Executes a query against the DataContext.
+     * 
+     * @param query
+     *            the query object to execute
+     * @return the {@link DataSet} produced from executing the query
+     * @throws MetaModelException
+     *             if the specified query does not make sense or cannot be
+     *             executed because of restraints on the type of datastore.
+     */
+    public DataSet executeQuery(Query query) throws MetaModelException;
+
+    /**
+     * Compiles a query, preparing it for reuse. Often times compiled queries
+     * have a performance improvement when executed, but at the cost of a
+     * preparation time penalty. Therefore it is adviced to use compiled queries
+     * when the same query is to be fired multiple times.
+     * 
+     * Compiled queries can contain {@link QueryParameter}s as operands in the
+     * WHERE clause, making it possible to reuse the same query with different
+     * parameter values.
+     * 
+     * @see CompiledQuery
+     * @see QueryParameter
+     * 
+     * @param query
+     *            the query object to execute, possibly holding one or more
+     *            {@link QueryParameter}s.
+     * @return the {@link CompiledQuery} after preparing the query
+     * 
+     * @throws MetaModelException
+     *             if preparing the query is unsuccesful
+     */
+    public CompiledQuery compileQuery(Query query) throws MetaModelException;
+
+    /**
+     * Executes a compiled query with given values as parameters.
+     * 
+     * @param compiledQuery
+     *            the compiledQuery object to execute
+     * @param values
+     *            the values for parameters in the {@link CompiledQuery}.
+     * @return the {@link DataSet} produced from executing the query.
+     */
+    public DataSet executeQuery(CompiledQuery compiledQuery, Object... values);
+
+    /**
+     * Parses and executes a string-based SQL query.
+     * 
+     * This method is essentially equivalent to calling first
+     * {@link #parseQuery(String)} and then {@link #executeQuery(Query)} with
+     * the parsed query.
+     * 
+     * @param query
+     *            the SQL query to parse
+     * @return the {@link DataSet} produced from executing the query
+     * @throws MetaModelException
+     *             if either parsing or executing the query produces an
+     *             exception
+     */
+    public DataSet executeQuery(String queryString) throws MetaModelException;
+
+    /**
+     * Finds a column in the DataContext based on a fully qualified column
+     * label. The qualified label consists of the the schema, table and column
+     * name, delimited by a dot (.).
+     * 
+     * @param columnName
+     * @return a column that matches the qualified label, or null if no such
+     *         column exists
+     */
+    public Column getColumnByQualifiedLabel(final String columnName);
+
+    /**
+     * Finds a table in the DataContext based on a fully qualified table label.
+     * The qualified label consists of the the schema and table name, delimited
+     * by a dot (.).
+     * 
+     * @param tableName
+     * @return a table that matches the qualified label, or null if no such
+     *         table exists
+     */
+    public Table getTableByQualifiedLabel(final String tableName);
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java b/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java
new file mode 100644
index 0000000..36e4b3e
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/DeleteAndInsertBuilder.java
@@ -0,0 +1,111 @@
+/**
+ * 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.eobjects.metamodel;
+
+import java.util.List;
+import java.util.ListIterator;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.data.DefaultRow;
+import org.eobjects.metamodel.data.Row;
+import org.eobjects.metamodel.data.SimpleDataSetHeader;
+import org.eobjects.metamodel.query.FilterItem;
+import org.eobjects.metamodel.query.SelectItem;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.update.AbstractRowUpdationBuilder;
+import org.eobjects.metamodel.update.RowUpdationBuilder;
+
+/**
+ * Simple implementation of the {@link RowUpdationBuilder} interface, which
+ * simply uses a combined delete+insert strategy for performing updates. Note
+ * that this implementation is not desirable performance-wise in many cases, but
+ * does provide a functional equivalent to a "real" update.
+ */
+public class DeleteAndInsertBuilder extends AbstractRowUpdationBuilder {
+
+    private final AbstractUpdateCallback _updateCallback;
+
+    public DeleteAndInsertBuilder(AbstractUpdateCallback updateCallback, Table table) {
+        super(table);
+        assert updateCallback.isInsertSupported();
+        assert updateCallback.isDeleteSupported();
+        _updateCallback = updateCallback;
+    }
+
+    @Override
+    public void execute() throws MetaModelException {
+        // retain rows in memory
+        List<Row> rows = getRowsToUpdate();
+
+        // delete rows
+        _updateCallback.deleteFrom(getTable()).where(getWhereItems()).execute();
+
+        // modify rows
+        rows = updateRows(rows);
+
+        // insert rows
+        for (Row row : rows) {
+            _updateCallback.insertInto(getTable()).like(row).execute();
+        }
+    }
+
+    private List<Row> updateRows(List<Row> rows) {
+        for (ListIterator<Row> it = rows.listIterator(); it.hasNext();) {
+            final Row original = (Row) it.next();
+            final Row updated = update(original);
+            it.set(updated);
+        }
+        return rows;
+    }
+
+    /**
+     * Produces an updated row out of the original
+     * 
+     * @param original
+     * @return
+     */
+    private Row update(final Row original) {
+        SelectItem[] items = original.getSelectItems();
+        Object[] values = new Object[items.length];
+        for (int i = 0; i < items.length; i++) {
+            final Object value;
+            Column column = items[i].getColumn();
+            if (isSet(column)) {
+                // use update statement's value
+                value = getValues()[i];
+            } else {
+                // use original value
+                value = original.getValue(i);
+            }
+            values[i] = value;
+        }
+        return new DefaultRow(new SimpleDataSetHeader(items), values);
+    }
+
+    protected List<Row> getRowsToUpdate() {
+        final DataContext dc = _updateCallback.getDataContext();
+        final Table table = getTable();
+        final List<FilterItem> whereItems = getWhereItems();
+        final DataSet dataSet = dc.query().from(table).select(table.getColumns()).where(whereItems).execute();
+        final List<Row> rows = dataSet.toRows();
+        return rows;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java b/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java
new file mode 100644
index 0000000..b3baff9
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/InconsistentRowFormatException.java
@@ -0,0 +1,82 @@
+/**
+ * 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.eobjects.metamodel;
+
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.data.Row;
+
+/**
+ * Abstract exception type that represents exceptions that occur when reading a
+ * data format which contain formatting errors or inconsistencies in on or more
+ * rows.
+ * 
+ * Typically {@link InconsistentRowFormatException}s are thrown when calling
+ * {@link DataSet#next()}.
+ * 
+ * All {@link InconsistentRowFormatException}s are optional, meaning that you
+ * can turn them on and off. When turned off the result of
+ * {@link #getProposedRow()} will be used transparently instead of throwing the
+ * exception.
+ * 
+ * @author Kasper Sørensen
+ */
+public abstract class InconsistentRowFormatException extends MetaModelException {
+
+	private static final long serialVersionUID = 1L;
+
+	private final Row _proposedRow;
+	private final int _rowNumber;
+
+	public InconsistentRowFormatException(Row proposedRow, int rowNumber) {
+		super();
+		_proposedRow = proposedRow;
+		_rowNumber = rowNumber;
+	}
+
+	public InconsistentRowFormatException(Row proposedRow, int rowNumber,
+			Exception cause) {
+		super(cause);
+		_proposedRow = proposedRow;
+		_rowNumber = rowNumber;
+	}
+
+	/**
+	 * Gets the row as MetaModel would gracefully interpret it.
+	 * 
+	 * @return a row object which represents the {@link Row} as MetaModel would
+	 *         gracefully interpret it.
+	 */
+	public Row getProposedRow() {
+		return _proposedRow;
+	}
+
+	/**
+	 * Gets the row number (1-based).
+	 * 
+	 * @return the index of the row.
+	 */
+	public int getRowNumber() {
+		return _rowNumber;
+	}
+
+	@Override
+	public String getMessage() {
+		return "Inconsistent row format of row no. " + getRowNumber() + ".";
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/MetaModelException.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/MetaModelException.java b/core/src/main/java/org/apache/metamodel/MetaModelException.java
new file mode 100644
index 0000000..787b8c5
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/MetaModelException.java
@@ -0,0 +1,47 @@
+/**
+ * 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.eobjects.metamodel;
+
+/**
+ * Unchecked exception used to signal errors occuring in MetaModel.
+ * 
+ * All MetaModelExceptions represent errors discovered withing the MetaModel
+ * framework. Typically these will occur if you have put together a query that
+ * is not meaningful or if there is a structural problem in a schema.
+ */
+public class MetaModelException extends RuntimeException {
+
+	private static final long serialVersionUID = 5455738384633428319L;
+
+	public MetaModelException(String message, Exception cause) {
+		super(message, cause);
+	}
+
+	public MetaModelException(String message) {
+		super(message);
+	}
+
+	public MetaModelException(Exception cause) {
+		super(cause);
+	}
+
+	public MetaModelException() {
+		super();
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metamodel/blob/e2e2b37a/core/src/main/java/org/apache/metamodel/MetaModelHelper.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/metamodel/MetaModelHelper.java b/core/src/main/java/org/apache/metamodel/MetaModelHelper.java
new file mode 100644
index 0000000..a105973
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/MetaModelHelper.java
@@ -0,0 +1,775 @@
+/**
+ * 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.eobjects.metamodel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eobjects.metamodel.data.CachingDataSetHeader;
+import org.eobjects.metamodel.data.DataSet;
+import org.eobjects.metamodel.data.DataSetHeader;
+import org.eobjects.metamodel.data.DefaultRow;
+import org.eobjects.metamodel.data.EmptyDataSet;
+import org.eobjects.metamodel.data.FilteredDataSet;
+import org.eobjects.metamodel.data.IRowFilter;
+import org.eobjects.metamodel.data.InMemoryDataSet;
+import org.eobjects.metamodel.data.Row;
+import org.eobjects.metamodel.data.SimpleDataSetHeader;
+import org.eobjects.metamodel.data.SubSelectionDataSet;
+import org.eobjects.metamodel.query.FilterItem;
+import org.eobjects.metamodel.query.FromItem;
+import org.eobjects.metamodel.query.GroupByItem;
+import org.eobjects.metamodel.query.OrderByItem;
+import org.eobjects.metamodel.query.Query;
+import org.eobjects.metamodel.query.SelectItem;
+import org.eobjects.metamodel.query.parser.QueryParser;
+import org.eobjects.metamodel.schema.Column;
+import org.eobjects.metamodel.schema.ColumnType;
+import org.eobjects.metamodel.schema.Schema;
+import org.eobjects.metamodel.schema.SuperColumnType;
+import org.eobjects.metamodel.schema.Table;
+import org.eobjects.metamodel.util.AggregateBuilder;
+import org.eobjects.metamodel.util.CollectionUtils;
+import org.eobjects.metamodel.util.EqualsBuilder;
+import org.eobjects.metamodel.util.Func;
+import org.eobjects.metamodel.util.ObjectComparator;
+import org.eobjects.metamodel.util.Predicate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class contains various helper functionality to common tasks in
+ * MetaModel, eg.:
+ * 
+ * <ul>
+ * <li>Easy-access for traversing common schema items</li>
+ * <li>Manipulate data in memory. These methods are primarily used to enable
+ * queries for non-queryable data sources like CSV files and spreadsheets.</li>
+ * <li>Query rewriting, traversing and manipulation.</li>
+ * </ul>
+ * 
+ * The class is mainly intended for internal use within the framework
+ * operations, but is kept stable, so it can also be used by framework users.
+ */
+public final class MetaModelHelper {
+
+    private final static Logger logger = LoggerFactory.getLogger(MetaModelHelper.class);
+
+    private MetaModelHelper() {
+        // Prevent instantiation
+    }
+
+    /**
+     * Creates an array of tables where all occurences of tables in the provided
+     * list of tables and columns are included
+     */
+    public static Table[] getTables(Collection<Table> tableList, Iterable<Column> columnList) {
+        HashSet<Table> set = new HashSet<Table>();
+        set.addAll(tableList);
+        for (Column column : columnList) {
+            set.add(column.getTable());
+        }
+        return set.toArray(new Table[set.size()]);
+    }
+
+    /**
+     * Determines if a schema is an information schema
+     * 
+     * @param schema
+     * @return
+     */
+    public static boolean isInformationSchema(Schema schema) {
+        String name = schema.getName();
+        return isInformationSchema(name);
+    }
+
+    /**
+     * Determines if a schema name is the name of an information schema
+     * 
+     * @param name
+     * @return
+     */
+    public static boolean isInformationSchema(String name) {
+        if (name == null) {
+            return false;
+        }
+        return QueryPostprocessDataContext.INFORMATION_SCHEMA_NAME.equals(name.toLowerCase());
+    }
+
+    /**
+     * Converts a list of columns to a corresponding array of tables
+     * 
+     * @param columns
+     *            the columns that the tables will be extracted from
+     * @return an array containing the tables of the provided columns.
+     */
+    public static Table[] getTables(Iterable<Column> columns) {
+        ArrayList<Table> result = new ArrayList<Table>();
+        for (Column column : columns) {
+            Table table = column.getTable();
+            if (!result.contains(table)) {
+                result.add(table);
+            }
+        }
+        return result.toArray(new Table[result.size()]);
+    }
+
+    /**
+     * Creates a subset array of columns, where only columns that are contained
+     * within the specified table are included.
+     * 
+     * @param table
+     * @param columns
+     * @return an array containing the columns that exist in the table
+     */
+    public static Column[] getTableColumns(Table table, Iterable<Column> columns) {
+        if (table == null) {
+            return new Column[0];
+        }
+        final List<Column> result = new ArrayList<Column>();
+        for (Column column : columns) {
+            final boolean sameTable = table.equals(column.getTable());
+            if (sameTable) {
+                result.add(column);
+            }
+        }
+        return result.toArray(new Column[result.size()]);
+    }
+
+    /**
+     * Creates a subset array of columns, where only columns that are contained
+     * within the specified table are included.
+     * 
+     * @param table
+     * @param columns
+     * @return an array containing the columns that exist in the table
+     */
+    public static Column[] getTableColumns(Table table, Column[] columns) {
+        return getTableColumns(table, Arrays.asList(columns));
+    }
+
+    public static DataSet getCarthesianProduct(DataSet... fromDataSets) {
+        return getCarthesianProduct(fromDataSets, new FilterItem[0]);
+    }
+
+    public static DataSet getCarthesianProduct(DataSet[] fromDataSets, Iterable<FilterItem> whereItems) {
+        // First check if carthesian product is even nescesary
+        if (fromDataSets.length == 1) {
+            return getFiltered(fromDataSets[0], whereItems);
+        }
+
+        List<SelectItem> selectItems = new ArrayList<SelectItem>();
+        for (DataSet dataSet : fromDataSets) {
+            for (int i = 0; i < dataSet.getSelectItems().length; i++) {
+                SelectItem item = dataSet.getSelectItems()[i];
+                selectItems.add(item);
+            }
+        }
+
+        int selectItemOffset = 0;
+        List<Object[]> data = new ArrayList<Object[]>();
+        for (int fromDataSetIndex = 0; fromDataSetIndex < fromDataSets.length; fromDataSetIndex++) {
+            DataSet fromDataSet = fromDataSets[fromDataSetIndex];
+            SelectItem[] fromSelectItems = fromDataSet.getSelectItems();
+            if (fromDataSetIndex == 0) {
+                while (fromDataSet.next()) {
+                    Object[] values = fromDataSet.getRow().getValues();
+                    Object[] row = new Object[selectItems.size()];
+                    System.arraycopy(values, 0, row, selectItemOffset, values.length);
+                    data.add(row);
+                }
+                fromDataSet.close();
+            } else {
+                List<Object[]> fromDataRows = new ArrayList<Object[]>();
+                while (fromDataSet.next()) {
+                    fromDataRows.add(fromDataSet.getRow().getValues());
+                }
+                fromDataSet.close();
+                for (int i = 0; i < data.size(); i = i + fromDataRows.size()) {
+                    Object[] originalRow = data.get(i);
+                    data.remove(i);
+                    for (int j = 0; j < fromDataRows.size(); j++) {
+                        Object[] newRow = fromDataRows.get(j);
+                        System.arraycopy(newRow, 0, originalRow, selectItemOffset, newRow.length);
+                        data.add(i + j, originalRow.clone());
+                    }
+                }
+            }
+            selectItemOffset += fromSelectItems.length;
+        }
+
+        if (data.isEmpty()) {
+            return new EmptyDataSet(selectItems);
+        }
+
+        final DataSetHeader header = new CachingDataSetHeader(selectItems);
+        final List<Row> rows = new ArrayList<Row>(data.size());
+        for (Object[] objects : data) {
+            rows.add(new DefaultRow(header, objects, null));
+        }
+
+        DataSet result = new InMemoryDataSet(header, rows);
+        if (whereItems != null) {
+            result = getFiltered(result, whereItems);
+        }
+        return result;
+    }
+
+    public static DataSet getCarthesianProduct(DataSet[] fromDataSets, FilterItem... filterItems) {
+        return getCarthesianProduct(fromDataSets, Arrays.asList(filterItems));
+    }
+
+    public static DataSet getFiltered(DataSet dataSet, Iterable<FilterItem> filterItems) {
+        List<IRowFilter> filters = CollectionUtils.map(filterItems, new Func<FilterItem, IRowFilter>() {
+            @Override
+            public IRowFilter eval(FilterItem filterItem) {
+                return filterItem;
+            }
+        });
+        if (filters.isEmpty()) {
+            return dataSet;
+        }
+
+        return new FilteredDataSet(dataSet, filters.toArray(new IRowFilter[filters.size()]));
+    }
+
+    public static DataSet getFiltered(DataSet dataSet, FilterItem... filterItems) {
+        return getFiltered(dataSet, Arrays.asList(filterItems));
+    }
+
+    public static DataSet getSelection(final List<SelectItem> selectItems, final DataSet dataSet) {
+        final SelectItem[] dataSetSelectItems = dataSet.getSelectItems();
+
+        // check if the selection is already the same
+        if (selectItems.size() == dataSetSelectItems.length) {
+            boolean same = true;
+            int i = 0;
+            for (SelectItem selectItem : selectItems) {
+                if (!EqualsBuilder.equals(selectItem, dataSetSelectItems[i])) {
+                    same = false;
+                    break;
+                }
+                i++;
+            }
+
+            if (same) {
+                // return the dataSet unmodified
+                return dataSet;
+            }
+        }
+
+        SelectItem[] selectItemsArray = selectItems.toArray(new SelectItem[selectItems.size()]);
+        return new SubSelectionDataSet(selectItemsArray, dataSet);
+    }
+
+    public static DataSet getSelection(SelectItem[] selectItems, DataSet dataSet) {
+        return getSelection(Arrays.asList(selectItems), dataSet);
+    }
+
+    public static DataSet getGrouped(List<SelectItem> selectItems, DataSet dataSet, Collection<GroupByItem> groupByItems) {
+        return getGrouped(selectItems, dataSet, groupByItems.toArray(new GroupByItem[groupByItems.size()]));
+    }
+
+    public static DataSet getGrouped(List<SelectItem> selectItems, DataSet dataSet, GroupByItem[] groupByItems) {
+        DataSet result = dataSet;
+        if (groupByItems != null && groupByItems.length > 0) {
+            Map<Row, Map<SelectItem, List<Object>>> uniqueRows = new HashMap<Row, Map<SelectItem, List<Object>>>();
+
+            final SelectItem[] groupBySelects = new SelectItem[groupByItems.length];
+            for (int i = 0; i < groupBySelects.length; i++) {
+                groupBySelects[i] = groupByItems[i].getSelectItem();
+            }
+            final DataSetHeader groupByHeader = new CachingDataSetHeader(groupBySelects);
+
+            // Creates a list of SelectItems that have functions
+            List<SelectItem> functionItems = getFunctionSelectItems(selectItems);
+
+            // Loop through the dataset and identify groups
+            while (dataSet.next()) {
+                Row row = dataSet.getRow();
+
+                // Subselect a row prototype with only the unique values that
+                // define the group
+                Row uniqueRow = row.getSubSelection(groupByHeader);
+
+                // function input is the values used for calculating aggregate
+                // functions in the group
+                Map<SelectItem, List<Object>> functionInput;
+                if (!uniqueRows.containsKey(uniqueRow)) {
+                    // If this group already exist, use an existing function
+                    // input
+                    functionInput = new HashMap<SelectItem, List<Object>>();
+                    for (SelectItem item : functionItems) {
+                        functionInput.put(item, new ArrayList<Object>());
+                    }
+                    uniqueRows.put(uniqueRow, functionInput);
+                } else {
+                    // If this is a new group, create a new function input
+                    functionInput = uniqueRows.get(uniqueRow);
+                }
+
+                // Loop through aggregate functions to check for validity
+                for (SelectItem item : functionItems) {
+                    List<Object> objects = functionInput.get(item);
+                    Column column = item.getColumn();
+                    if (column != null) {
+                        Object value = row.getValue(new SelectItem(column));
+                        objects.add(value);
+                    } else if (SelectItem.isCountAllItem(item)) {
+                        // Just use the empty string, since COUNT(*) don't
+                        // evaluate values (but null values should be prevented)
+                        objects.add("");
+                    } else {
+                        throw new IllegalArgumentException("Expression function not supported: " + item);
+                    }
+                }
+            }
+
+            dataSet.close();
+            final List<Row> resultData = new ArrayList<Row>();
+            final DataSetHeader resultHeader = new CachingDataSetHeader(selectItems);
+
+            // Loop through the groups to generate aggregates
+            for (Entry<Row, Map<SelectItem, List<Object>>> entry : uniqueRows.entrySet()) {
+                Row row = entry.getKey();
+                Map<SelectItem, List<Object>> functionInput = entry.getValue();
+                Object[] resultRow = new Object[selectItems.size()];
+                // Loop through select items to generate a row
+                int i = 0;
+                for (SelectItem item : selectItems) {
+                    int uniqueRowIndex = row.indexOf(item);
+                    if (uniqueRowIndex != -1) {
+                        // If there's already a value for the select item in the
+                        // row, keep it (it's one of the grouped by columns)
+                        resultRow[i] = row.getValue(uniqueRowIndex);
+                    } else {
+                        // Use the function input to calculate the aggregate
+                        // value
+                        List<Object> objects = functionInput.get(item);
+                        if (objects != null) {
+                            Object functionResult = item.getFunction().evaluate(objects.toArray());
+                            resultRow[i] = functionResult;
+                        } else {
+                            if (item.getFunction() != null) {
+                                logger.error("No function input found for SelectItem: {}", item);
+                            }
+                        }
+                    }
+                    i++;
+                }
+                resultData.add(new DefaultRow(resultHeader, resultRow, null));
+            }
+
+            if (resultData.isEmpty()) {
+                result = new EmptyDataSet(selectItems);
+            } else {
+                result = new InMemoryDataSet(resultHeader, resultData);
+            }
+        }
+        result = getSelection(selectItems, result);
+        return result;
+    }
+
+    /**
+     * Applies aggregate values to a dataset. This method is to be invoked AFTER
+     * any filters have been applied.
+     * 
+     * @param workSelectItems
+     *            all select items included in the processing of the query
+     *            (including those originating from other clauses than the
+     *            SELECT clause).
+     * @param dataSet
+     * @return
+     */
+    public static DataSet getAggregated(List<SelectItem> workSelectItems, DataSet dataSet) {
+        final List<SelectItem> functionItems = getFunctionSelectItems(workSelectItems);
+        if (functionItems.isEmpty()) {
+            return dataSet;
+        }
+
+        final Map<SelectItem, AggregateBuilder<?>> aggregateBuilders = new HashMap<SelectItem, AggregateBuilder<?>>();
+        for (SelectItem item : functionItems) {
+            aggregateBuilders.put(item, item.getFunction().build());
+        }
+
+        final DataSetHeader header;
+        final boolean onlyAggregates;
+        if (functionItems.size() != workSelectItems.size()) {
+            onlyAggregates = false;
+            header = new CachingDataSetHeader(workSelectItems);
+        } else {
+            onlyAggregates = true;
+            header = new SimpleDataSetHeader(workSelectItems);
+        }
+
+        final List<Row> resultRows = new ArrayList<Row>();
+        while (dataSet.next()) {
+            final Row inputRow = dataSet.getRow();
+            for (SelectItem item : functionItems) {
+                final AggregateBuilder<?> aggregateBuilder = aggregateBuilders.get(item);
+                final Column column = item.getColumn();
+                if (column != null) {
+                    Object value = inputRow.getValue(new SelectItem(column));
+                    aggregateBuilder.add(value);
+                } else if (SelectItem.isCountAllItem(item)) {
+                    // Just use the empty string, since COUNT(*) don't
+                    // evaluate values (but null values should be prevented)
+                    aggregateBuilder.add("");
+                } else {
+                    throw new IllegalArgumentException("Expression function not supported: " + item);
+                }
+            }
+
+            // If the result should also contain non-aggregated values, we
+            // will keep those in the rows list
+            if (!onlyAggregates) {
+                final Object[] values = new Object[header.size()];
+                for (int i = 0; i < header.size(); i++) {
+                    final Object value = inputRow.getValue(header.getSelectItem(i));
+                    if (value != null) {
+                        values[i] = value;
+                    }
+                }
+                resultRows.add(new DefaultRow(header, values));
+            }
+        }
+        dataSet.close();
+
+        // Collect the aggregates
+        Map<SelectItem, Object> functionResult = new HashMap<SelectItem, Object>();
+        for (SelectItem item : functionItems) {
+            AggregateBuilder<?> aggregateBuilder = aggregateBuilders.get(item);
+            Object result = aggregateBuilder.getAggregate();
+            functionResult.put(item, result);
+        }
+
+        // if there are no result rows (no matching records at all), we still
+        // need to return a record with the aggregates
+        final boolean noResultRows = resultRows.isEmpty();
+
+        if (onlyAggregates || noResultRows) {
+            // We will only create a single row with all the aggregates
+            Object[] values = new Object[header.size()];
+            for (int i = 0; i < header.size(); i++) {
+                values[i] = functionResult.get(header.getSelectItem(i));
+            }
+            Row row = new DefaultRow(header, values);
+            resultRows.add(row);
+        } else {
+            // We will create the aggregates as well as regular values
+            for (int i = 0; i < resultRows.size(); i++) {
+                Row row = resultRows.get(i);
+                Object[] values = row.getValues();
+                for (Entry<SelectItem, Object> entry : functionResult.entrySet()) {
+                    SelectItem item = entry.getKey();
+                    int itemIndex = row.indexOf(item);
+                    if (itemIndex != -1) {
+                        Object value = entry.getValue();
+                        values[itemIndex] = value;
+                    }
+                }
+                resultRows.set(i, new DefaultRow(header, values));
+            }
+        }
+
+        return new InMemoryDataSet(header, resultRows);
+    }
+
+    public static List<SelectItem> getFunctionSelectItems(Iterable<SelectItem> selectItems) {
+        return CollectionUtils.filter(selectItems, new Predicate<SelectItem>() {
+            @Override
+            public Boolean eval(SelectItem arg) {
+                return arg.getFunction() != null;
+            }
+        });
+    }
+
+    public static DataSet getOrdered(DataSet dataSet, List<OrderByItem> orderByItems) {
+        return getOrdered(dataSet, orderByItems.toArray(new OrderByItem[orderByItems.size()]));
+    }
+
+    public static DataSet getOrdered(DataSet dataSet, final OrderByItem... orderByItems) {
+        if (orderByItems != null && orderByItems.length != 0) {
+            final int[] sortIndexes = new int[orderByItems.length];
+            for (int i = 0; i < orderByItems.length; i++) {
+                OrderByItem item = orderByItems[i];
+                int indexOf = dataSet.indexOf(item.getSelectItem());
+                sortIndexes[i] = indexOf;
+            }
+
+            final List<Row> data = readDataSetFull(dataSet);
+            if (data.isEmpty()) {
+                return new EmptyDataSet(dataSet.getSelectItems());
+            }
+
+            final Comparator<Object> valueComparator = ObjectComparator.getComparator();
+
+            // create a comparator for doing the actual sorting/ordering
+            final Comparator<Row> comparator = new Comparator<Row>() {
+                public int compare(Row o1, Row o2) {
+                    for (int i = 0; i < sortIndexes.length; i++) {
+                        int sortIndex = sortIndexes[i];
+                        Object sortObj1 = o1.getValue(sortIndex);
+                        Object sortObj2 = o2.getValue(sortIndex);
+                        int compare = valueComparator.compare(sortObj1, sortObj2);
+                        if (compare != 0) {
+                            OrderByItem orderByItem = orderByItems[i];
+                            boolean ascending = orderByItem.isAscending();
+                            if (ascending) {
+                                return compare;
+                            } else {
+                                return compare * -1;
+                            }
+                        }
+                    }
+                    return 0;
+                }
+            };
+
+            Collections.sort(data, comparator);
+
+            dataSet = new InMemoryDataSet(data);
+        }
+        return dataSet;
+    }
+
+    public static List<Row> readDataSetFull(DataSet dataSet) {
+        final List<Row> result;
+        if (dataSet instanceof InMemoryDataSet) {
+            // if dataset is an in memory dataset we have a shortcut to avoid
+            // creating a new list
+            result = ((InMemoryDataSet) dataSet).getRows();
+        } else {
+            result = new ArrayList<Row>();
+            while (dataSet.next()) {
+                result.add(dataSet.getRow());
+            }
+        }
+        dataSet.close();
+        return result;
+    }
+
+    /**
+     * Examines a query and extracts an array of FromItem's that refer
+     * (directly) to tables (hence Joined FromItems and SubQuery FromItems are
+     * traversed but not included).
+     * 
+     * @param q
+     *            the query to examine
+     * @return an array of FromItem's that refer directly to tables
+     */
+    public static FromItem[] getTableFromItems(Query q) {
+        List<FromItem> result = new ArrayList<FromItem>();
+        List<FromItem> items = q.getFromClause().getItems();
+        for (FromItem item : items) {
+            result.addAll(getTableFromItems(item));
+        }
+        return result.toArray(new FromItem[result.size()]);
+    }
+
+    public static List<FromItem> getTableFromItems(FromItem item) {
+        List<FromItem> result = new ArrayList<FromItem>();
+        if (item.getTable() != null) {
+            result.add(item);
+        } else if (item.getSubQuery() != null) {
+            FromItem[] sqItems = getTableFromItems(item.getSubQuery());
+            for (int i = 0; i < sqItems.length; i++) {
+                result.add(sqItems[i]);
+            }
+        } else if (item.getJoin() != null) {
+            FromItem leftSide = item.getLeftSide();
+            result.addAll(getTableFromItems(leftSide));
+            FromItem rightSide = item.getRightSide();
+            result.addAll(getTableFromItems(rightSide));
+        } else {
+            throw new IllegalStateException("FromItem was neither of Table type, SubQuery type or Join type: " + item);
+        }
+        return result;
+    }
+
+    /**
+     * Executes a single row query, like "SELECT COUNT(*), MAX(SOME_COLUMN) FROM
+     * MY_TABLE" or similar.
+     * 
+     * @param dataContext
+     *            the DataContext object to use for executing the query
+     * @param query
+     *            the query to execute
+     * @return a row object representing the single row returned from the query
+     * @throws MetaModelException
+     *             if less or more than one Row is returned from the query
+     */
+    public static Row executeSingleRowQuery(DataContext dataContext, Query query) throws MetaModelException {
+        DataSet dataSet = dataContext.executeQuery(query);
+        boolean next = dataSet.next();
+        if (!next) {
+            throw new MetaModelException("No rows returned from query: " + query);
+        }
+        Row row = dataSet.getRow();
+        next = dataSet.next();
+        if (next) {
+            throw new MetaModelException("More than one row returned from query: " + query);
+        }
+        dataSet.close();
+        return row;
+    }
+
+    /**
+     * Performs a left join (aka left outer join) operation on two datasets.
+     * 
+     * @param ds1
+     *            the left dataset
+     * @param ds2
+     *            the right dataset
+     * @param onConditions
+     *            the conditions to join by
+     * @return the left joined result dataset
+     */
+    public static DataSet getLeftJoin(DataSet ds1, DataSet ds2, FilterItem[] onConditions) {
+        if (ds1 == null) {
+            throw new IllegalArgumentException("Left DataSet cannot be null");
+        }
+        if (ds2 == null) {
+            throw new IllegalArgumentException("Right DataSet cannot be null");
+        }
+        SelectItem[] si1 = ds1.getSelectItems();
+        SelectItem[] si2 = ds2.getSelectItems();
+        SelectItem[] selectItems = new SelectItem[si1.length + si2.length];
+        System.arraycopy(si1, 0, selectItems, 0, si1.length);
+        System.arraycopy(si2, 0, selectItems, si1.length, si2.length);
+
+        List<Row> resultRows = new ArrayList<Row>();
+        List<Row> ds2data = readDataSetFull(ds2);
+        if (ds2data.isEmpty()) {
+            // no need to join, simply return a new view (with null values) on
+            // the previous dataset.
+            return getSelection(selectItems, ds1);
+        }
+
+        final DataSetHeader header = new CachingDataSetHeader(selectItems);
+
+        while (ds1.next()) {
+
+            // Construct a single-row dataset for making a carthesian product
+            // against ds2
+            Row ds1row = ds1.getRow();
+            List<Row> ds1rows = new ArrayList<Row>();
+            ds1rows.add(ds1row);
+
+            DataSet carthesianProduct = getCarthesianProduct(new DataSet[] {
+                    new InMemoryDataSet(new CachingDataSetHeader(si1), ds1rows),
+                    new InMemoryDataSet(new CachingDataSetHeader(si2), ds2data) }, onConditions);
+            List<Row> carthesianRows = readDataSetFull(carthesianProduct);
+            if (carthesianRows.size() > 0) {
+                resultRows.addAll(carthesianRows);
+            } else {
+                Object[] values = ds1row.getValues();
+                Object[] row = new Object[selectItems.length];
+                System.arraycopy(values, 0, row, 0, values.length);
+                resultRows.add(new DefaultRow(header, row));
+            }
+        }
+        ds1.close();
+
+        if (resultRows.isEmpty()) {
+            return new EmptyDataSet(selectItems);
+        }
+
+        return new InMemoryDataSet(header, resultRows);
+    }
+
+    /**
+     * Performs a right join (aka right outer join) operation on two datasets.
+     * 
+     * @param ds1
+     *            the left dataset
+     * @param ds2
+     *            the right dataset
+     * @param onConditions
+     *            the conditions to join by
+     * @return the right joined result dataset
+     */
+    public static DataSet getRightJoin(DataSet ds1, DataSet ds2, FilterItem[] onConditions) {
+        SelectItem[] ds1selects = ds1.getSelectItems();
+        SelectItem[] ds2selects = ds2.getSelectItems();
+        SelectItem[] leftOrderedSelects = new SelectItem[ds1selects.length + ds2selects.length];
+        System.arraycopy(ds1selects, 0, leftOrderedSelects, 0, ds1selects.length);
+        System.arraycopy(ds2selects, 0, leftOrderedSelects, ds1selects.length, ds2selects.length);
+
+        // We will reuse the left join algorithm (but switch the datasets
+        // around)
+        DataSet dataSet = getLeftJoin(ds2, ds1, onConditions);
+
+        dataSet = getSelection(leftOrderedSelects, dataSet);
+        return dataSet;
+    }
+
+    public static SelectItem[] createSelectItems(Column... columns) {
+        SelectItem[] items = new SelectItem[columns.length];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = new SelectItem(columns[i]);
+        }
+        return items;
+    }
+
+    public static DataSet getDistinct(DataSet dataSet) {
+        SelectItem[] selectItems = dataSet.getSelectItems();
+        GroupByItem[] groupByItems = new GroupByItem[selectItems.length];
+        for (int i = 0; i < groupByItems.length; i++) {
+            groupByItems[i] = new GroupByItem(selectItems[i]);
+        }
+        return getGrouped(Arrays.asList(selectItems), dataSet, groupByItems);
+    }
+
+    public static Table[] getTables(Column[] columns) {
+        return getTables(Arrays.asList(columns));
+    }
+
+    public static Column[] getColumnsByType(Column[] columns, final ColumnType columnType) {
+        return CollectionUtils.filter(columns, new Predicate<Column>() {
+            @Override
+            public Boolean eval(Column column) {
+                return column.getType() == columnType;
+            }
+        }).toArray(new Column[0]);
+    }
+
+    public static Column[] getColumnsBySuperType(Column[] columns, final SuperColumnType superColumnType) {
+        return CollectionUtils.filter(columns, new Predicate<Column>() {
+            @Override
+            public Boolean eval(Column column) {
+                return column.getType().getSuperType() == superColumnType;
+            }
+        }).toArray(new Column[0]);
+    }
+
+    public static Query parseQuery(DataContext dc, String queryString) {
+        final QueryParser parser = new QueryParser(dc, queryString);
+        return parser.parse();
+    }
+
+}
\ No newline at end of file