} // getMainSchemaName()

        public override DataSet materializeMainSchemaTable(Table table, Column[] columns, int maxRows)
        {
            DocumentConverter documentConverter = _schemaBuilder.getDocumentConverter(table);

            SelectItem[]   selectItems    = MetaModelHelper.createSelectItems(columns);
            DataSetHeader  header         = new CachingDataSetHeader(selectItems);
            DocumentSource documentSource = getDocumentSourceForTable(table.getName());

            DataSet dataSet = new DocumentSourceDataSet(header, documentSource, documentConverter);

            if (maxRows > 0)
            {
                dataSet = new MaxRowsDataSet(dataSet, maxRows);
            }

            return(dataSet);
        } // materializeMainSchemaTable()
        }     // getCarthesianProduct()

        #endregion getCarthesianProduct()

        /**
         * Executes a simple nested loop join. The innerLoopDs will be copied in an
         * in-memory dataset.
         *
         */
        public static InMemoryDataSet nestedLoopJoin(DataSet innerLoopDs, DataSet outerLoopDs, IEnumerable <FilterItem> filtersIterable)
        {
            List <FilterItem> filters = new List <FilterItem>();

            foreach (FilterItem fi in filtersIterable)
            {
                filters.Add(fi);
            }
            List <Row> innerRows = innerLoopDs.toRows();

            List <SelectItem> allItems = new List <SelectItem>(NArrays.AsList <SelectItem>(outerLoopDs.getSelectItems()));

            allItems.AddRange(NArrays.AsList(innerLoopDs.getSelectItems()));

            HashSet <FilterItem> applicable_filters = applicableFilters(filters, allItems);

            DataSetHeader jointHeader = new CachingDataSetHeader(allItems);

            List <Row> resultRows = new List <Row>();

            foreach (Row outerRow in outerLoopDs)
            {
                foreach (Row innerRow in innerRows)
                {
                    Object[] joinedRowObjects = new Object[outerRow.getValues().Length + innerRow.getValues().Length];

                    Array.Copy(outerRow.getValues(), 0, joinedRowObjects, 0, outerRow.getValues().Length);
                    Array.Copy(innerRow.getValues(), 0, joinedRowObjects, outerRow.getValues().Length,
                               innerRow.getValues().Length);

                    Row joinedRow = new DefaultRow(jointHeader, joinedRowObjects);
                    IEnumerable <FilterItem> selected_items = applicable_filters.Where(fi => isJoinedRowAccepted(fi, joinedRow));

                    if (applicable_filters.IsEmpty() || (selected_items != null && selected_items.Count <FilterItem>() != 0))
                    {
                        resultRows.Add(joinedRow);
                    }
                }
            }

            return(new InMemoryDataSet(jointHeader, resultRows));
        }     // nestedLoopJoin()
        }     // getGrouped()

        /**
         * 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)
        {
            List <SelectItem> functionItems = getAggregateFunctionSelectItems(workSelectItems);

            if (functionItems.IsEmpty())
            {
                return(dataSet);
            }

            AggregateBuilder <Object> t;
            Dictionary <SelectItem, AggregateBuilder <Object> > aggregateBuilders = new Dictionary <SelectItem, AggregateBuilder <Object> >();

            foreach (SelectItem item in functionItems)
            {
                aggregateBuilders.Add(item, item.getAggregateFunction().createAggregateBuilder <object>());
            }

            DataSetHeader header;
            bool          onlyAggregates;

            if (functionItems.Count != workSelectItems.Count)
            {
                onlyAggregates = false;
                header         = new CachingDataSetHeader(workSelectItems);
            }
            else
            {
                onlyAggregates = true;
                header         = new SimpleDataSetHeader(workSelectItems);
            }

            List <Row> resultRows = new List <Row>();

            while (dataSet.next())
            {
                Row inputRow = dataSet.getRow();
                foreach (SelectItem item in functionItems)
                {
                    AggregateBuilder <object> aggregateBuilder = aggregateBuilders[item];
                    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 ArgumentException("Expression function not supported: " + item);
                    }
                }

                // If the result should also contain non-aggregated values, we
                // will keep those in the rows list
                if (!onlyAggregates)
                {
                    Object[] values = new Object[header.size()];
                    for (int i = 0; i < header.size(); i++)
                    {
                        Object value = inputRow.getValue(header.getSelectItem(i));
                        if (value != null)
                        {
                            values[i] = value;
                        }
                    }
                    resultRows.Add(new DefaultRow(header, values));
                }
            }
            dataSet.close();

            // Collect the aggregates
            Dictionary <SelectItem, Object> functionResult = new Dictionary <SelectItem, Object>();

            foreach (SelectItem item in functionItems)
            {
                AggregateBuilder <object> aggregateBuilder = aggregateBuilders[item];
                Object result = aggregateBuilder.getAggregate();
                functionResult.Add(item, result);
            }

            // if there are no result rows (no matching records at all), we still
            // need to return a record with the aggregates
            bool 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[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.Count; i++)
                {
                    Row      row    = resultRows[i];
                    Object[] values = row.getValues();
                    foreach (KeyValuePair <SelectItem, Object> entry in functionResult)
                    {
                        SelectItem item      = entry.Key;
                        int        itemIndex = row.indexOf(item);
                        if (itemIndex != -1)
                        {
                            Object value = entry.Value;
                            values[itemIndex] = value;
                        }
                    }
                    resultRows[i] = new DefaultRow(header, values);
                }
            }
            return(new InMemoryDataSet(header, resultRows));
        }     // getAggregated()
        }                                                           // getGrouped()

        public static DataSet getGrouped(List <SelectItem> selectItems, DataSet dataSet, GroupByItem[] groupByItems)
        {
            DataSet result = dataSet;

            if (groupByItems != null && groupByItems.Length > 0)
            {
                Dictionary <Row, Dictionary <SelectItem, List <Object> > > uniqueRows = new Dictionary <Row, Dictionary <SelectItem, List <Object> > >();

                SelectItem[] groupBySelects = new SelectItem[groupByItems.Length];
                for (int i = 0; i < groupBySelects.Length; i++)
                {
                    groupBySelects[i] = groupByItems[i].getSelectItem();
                }
                DataSetHeader groupByHeader = new CachingDataSetHeader(groupBySelects);

                // Creates a list of SelectItems that have aggregate functions
                List <SelectItem> functionItems = getAggregateFunctionSelectItems(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
                    Dictionary <SelectItem, List <Object> > functionInput;
                    if (!uniqueRows.ContainsKey(uniqueRow))
                    {
                        // If this group already exist, use an existing function
                        // input
                        functionInput = new Dictionary <SelectItem, List <Object> >();
                        foreach (SelectItem item in functionItems)
                        {
                            functionInput.Add(item, new List <Object>());
                        }
                        uniqueRows.Add(uniqueRow, functionInput);
                    }
                    else
                    {
                        // If this is a new group, create a new function input
                        functionInput = uniqueRows[uniqueRow];
                    }

                    // Loop through aggregate functions to check for validity
                    foreach (SelectItem item in functionItems)
                    {
                        List <Object> objects = functionInput[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 ArgumentException("Expression function not supported: " + item);
                        }
                    }
                }

                dataSet.close();
                List <Row>    resultData   = new List <Row>();
                DataSetHeader resultHeader = new CachingDataSetHeader(selectItems);

                int count = uniqueRows.Count;
                // Loop through the groups to generate aggregates
                foreach (KeyValuePair <Row, Dictionary <SelectItem, List <Object> > > key_value in uniqueRows)
                {
                    Row row = key_value.Key;
                    Dictionary <SelectItem, List <Object> > functionInput = key_value.Value;
                    Object[] resultRow = new Object[selectItems.Count];
                    // Loop through select items to generate a row
                    int i = 0;
                    foreach (SelectItem item in 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[item];
                            if (objects != null)
                            {
                                Object functionResult = item.getAggregateFunction().evaluate(objects.ToArray());
                                resultRow[i] = functionResult;
                            }
                            else
                            {
                                if (item.getAggregateFunction() != 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);
        }     // getGrouped()
        } // containsNonSelectScalaFunctions()

        /**
         * 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 ArgumentException("Left DataSet cannot be null");
            }
            if (ds2 == null)
            {
                throw new ArgumentException("Right DataSet cannot be null");
            }
            SelectItem[] si1         = ds1.getSelectItems();
            SelectItem[] si2         = ds2.getSelectItems();
            SelectItem[] selectItems = new SelectItem[si1.Length + si2.Length];
            Array.Copy(si1, 0, selectItems, 0, si1.Length);
            Array.Copy(si2, 0, selectItems, si1.Length, si2.Length);

            List <Row> resultRows = new List <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));
            }

            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 List <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.Count > 0)
                {
                    resultRows.AddRange(carthesianRows);
                }
                else
                {
                    Object[] values = ds1row.getValues();
                    Object[] row    = new Object[selectItems.Length];
                    Array.Copy(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));
        } // getLeftJoin()