All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.relique.jdbc.csv.CsvResultSet Maven / Gradle / Ivy

The newest version!
/*
 *	CsvJdbc - a JDBC driver for CSV files
 *	Copyright (C) 2001	Jonathan Ackerman
 *
 *	This library is free software; you can redistribute it and/or
 *	modify it under the terms of the GNU Lesser General Public
 *	License as published by the Free Software Foundation; either
 *	version 2.1 of the License, or (at your option) any later version.
 *
 *	This library is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *	Lesser General Public License for more details.
 *
 *	You should have received a copy of the GNU Lesser General Public
 *	License along with this library; if not, write to the Free Software
 *	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.relique.jdbc.csv;

import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLTimeoutException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.relique.io.DataReader;

/**
 * This class implements the java.sql.ResultSet JDBC interface for the
 * CsvJdbc driver.
 *
 * @author	   Jonathan Ackerman
 * @author	   Michael Maraya
 * @author	   Tomasz Skutnik
 * @author	   Chetan Gupta
 */
public class CsvResultSet implements ResultSet
{
	/** Metadata for this ResultSet */
	private ResultSetMetaData resultSetMetaData;

	/** Statement that produced this ResultSet */
	private CsvStatement statement;

	private int resultSetType = ResultSet.TYPE_SCROLL_SENSITIVE;
	
	/** Helper class that performs the actual file reads */
	private DataReader reader;

	/** Table referenced by the Statement */
	private String tableName;

	/** Last column name index read */
	private int lastIndexRead = -1;

	private LogicalExpression whereClause;

	private List groupByColumns;
	
	private List distinctColumns;

	private LogicalExpression havingClause;

	private List orderByColumns;

	private List queryEnvironment;

	private List aggregateFunctions;

	private Set> distinctValues;

	private Map parentObjectEnvironment;

	private Map recordEnvironment;

	private List usedColumns;

	private StringConverter converter;

	private ArrayList> bufferedRecordEnvironments = null;

	private int currentRow;

	/** Number of rows that have not matched the where clause and have been skipped */
	private int nonMatchingRows;

	private boolean hitTail = false;

	/** Result of last call to next() */
	private boolean nextResult = true;

	private int maxRows;
	
	private int fetchSize;

	private int fetchDirection;

	private int limit;

	private int offset;

	private int maxDataLines;

	private boolean isClosed = false;

	/**
	 * Wrapper for any exception thrown by OrderByComparator.
	 */
	public class OrderByException extends RuntimeException
	{	
		public OrderByException(String message)
		{
			super(message);
		}
	}

	/**
	 * Compares SQL ORDER BY expressions for two records.
	 */
	public class OrderByComparator implements Comparator>
	{
		@Override
		public int compare(Map recordEnvironment1, Map recordEnvironment2)
		{
			int retval = 0;
			
			try
			{
				int i = 0;
				while (i < orderByColumns.size() && retval == 0)
				{
					Object []o = orderByColumns.get(i);
					Integer direction = (Integer)o[0];
					Expression expr = (Expression)o[1];
					recordEnvironment = recordEnvironment1;
					Map objectEnvironment1 = updateRecordEnvironment(true);
					if (converter != null)
						objectEnvironment1.put(StringConverter.COLUMN_NAME, converter);
					Comparable result1 = (Comparable)expr.eval(objectEnvironment1);
					recordEnvironment = recordEnvironment2;
					Map objectEnvironment2 = updateRecordEnvironment(true);
					if (converter != null)
						objectEnvironment2.put(StringConverter.COLUMN_NAME, converter);
					Comparable result2 = (Comparable)expr.eval(objectEnvironment2);
					if (result1 == null)
					{
						if (result2 == null)
							retval = 0;
						else
							retval = -1;
					}
					else if (result2 == null)
					{
						retval = 1;
					}
					else
					{
						retval = result1.compareTo(result2);
					}
					if (direction.intValue() < 0)
						retval = -retval;
					i++;
				}
			}
			catch (SQLException e)
			{
				throw new OrderByException(e.getMessage());
			}
			return retval;
		}
	}

	/**
	 * Constructor for the CsvResultSet object 
	 *
	 * @param statement Statement that produced this ResultSet
	 * @param reader Helper class that performs the actual file reads
	 * @param tableName Table referenced by the Statement
	 * @param queryEnvironment each query expression in the Statement.
	 * @param isDistinct true if Statement contains DISTINCT keyword.
	 * @param resultSetType indicates whether ResultSet be scrollable.
	 * @param whereClause expression for the SQL where clause.
	 * @param groupByColumns expressions for SQL GROUP BY clause.
	 * @param havingClause expression for SQL HAVING clause.s
	 * @param orderByColumns expressions for SQL ORDER BY clause.
	 * @param sqlLimit maximum number of rows set with SQL LIMIT clause.
	 * @param sqlOffset number of rows to skip with SQL OFFSET clause.
	 * @param columnTypes A comma-separated string specifying the type of the i-th column of the database table (not of the result).
	 * @param skipLeadingLines number of lines to skip at start of file.
	 * @param parentObjectEnvironment environment for current row in parent of sub-queries.
	 * @throws ClassNotFoundException in case the typed columns fail.
	 * @throws SQLException if executing the SQL statement fails.
	 */
	protected CsvResultSet(CsvStatement statement,
			DataReader reader,
			String tableName,
			List queryEnvironment,
			boolean isDistinct,
			int resultSetType, 
			LogicalExpression whereClause,
			List groupByColumns,
			LogicalExpression havingClause,
			List orderByColumns,
			int sqlLimit,
			int sqlOffset,
			String columnTypes,
			int skipLeadingLines,
			int maxDataLines,
			Map parentObjectEnvironment) throws ClassNotFoundException, SQLException
	{
		this.statement = statement;
		maxRows = statement.getMaxRows();
		fetchSize = statement.getFetchSize();
		fetchDirection = statement.getFetchDirection();
		this.limit = sqlLimit;
		this.offset = sqlOffset;
		this.resultSetType = resultSetType;
		this.reader = reader;
		this.tableName = tableName;
		this.queryEnvironment = new ArrayList<>(queryEnvironment);
		this.aggregateFunctions = new ArrayList<>();
		this.whereClause = whereClause;
		if (groupByColumns != null)
			this.groupByColumns = new ArrayList<>(groupByColumns);
		else
			this.groupByColumns = null;
		this.havingClause = havingClause;
		if (orderByColumns != null)
			this.orderByColumns = new ArrayList<>(orderByColumns);
		else
			this.orderByColumns = null;
		if (isDistinct)
			this.distinctValues = new HashSet<>();
		this.maxDataLines = maxDataLines;
		this.parentObjectEnvironment = parentObjectEnvironment;

		String timeFormat = ((CsvConnection)statement.getConnection()).getTimeFormat();
		String dateFormat = ((CsvConnection)statement.getConnection()).getDateFormat();
		String timestampFormat = ((CsvConnection)statement.getConnection()).getTimestampFormat();
		String timeZone = ((CsvConnection)statement.getConnection()).getTimeZoneName();
		Locale locale = ((CsvConnection)statement.getConnection()).getLocale();
		boolean useDateTimeFormatter = ((CsvConnection)statement.getConnection()).getUseDateTimeFormatter();
		this.converter = new StringConverter(dateFormat, timeFormat, timestampFormat, timeZone, locale, useDateTimeFormatter);
		if (reader instanceof CsvReader)
		{
			((CsvReader) reader).setConverter(converter);
			if(!"".equals(columnTypes))
				((CsvReader) reader).setColumnTypes(columnTypes);
		}

		String[] columnNames = reader.getColumnNames();

		String tableAlias = reader.getTableAlias();
		HashSet allReaderColumns = new HashSet<>();
		for (int i = 0; i < columnNames.length; i++)
		{
			String columnName = columnNames[i].toUpperCase();
			allReaderColumns.add(columnName);
			if (tableName != null)
				allReaderColumns.add(tableName.toUpperCase() + "." + columnName);
			if (tableAlias != null)
				allReaderColumns.add(tableAlias + "." + columnName);
		}

		if (whereClause!= null)
			this.usedColumns = new LinkedList<>(whereClause.usedColumns(allReaderColumns));
		else
			this.usedColumns = new LinkedList<>();

		if (!(this.resultSetType == TYPE_FORWARD_ONLY || this.resultSetType == TYPE_SCROLL_INSENSITIVE ||
			this.resultSetType == TYPE_SCROLL_SENSITIVE))
		{
			throw new SQLException(CsvResources.getString("invalidResultSetType") + ": " + this.resultSetType);
		}

		/*
		 * Replace any "select *" with the list of column names in that table.
		 */
		for (int i = 0; i < this.queryEnvironment.size(); i++)
		{
			Object[] o = this.queryEnvironment.get(i);
			if (o[1] instanceof AsteriskExpression)
			{
				AsteriskExpression asteriskExpression = (AsteriskExpression)o[1];
				
				/*
				 * Check that any table alias is valid.
				 */
				String asterisk = asteriskExpression.toString();
				if (!(asterisk.equals("*") || (tableAlias != null && asterisk.equalsIgnoreCase(tableAlias + ".*"))))
					throw new SQLException(CsvResources.getString("invalidColumnName") + ": " + asterisk);
				this.queryEnvironment.remove(i);
				for (int j = 0; j < columnNames.length; j++)
				{
					this.queryEnvironment.add(i + j, new Object[]{columnNames[j], new ColumnName(columnNames[j])});
				}
			}
		}

		/*
		 * Replace any "group by 2" with the 2nd column in the query list.
		 */
		if (this.groupByColumns != null)
		{
			for (int i = 0; i < this.groupByColumns.size(); i++)
			{
				Expression expression = this.groupByColumns.get(i);
				if (expression instanceof NumericConstant)
				{
					NumericConstant n = (NumericConstant)expression;
					if (!(n.value instanceof Integer))
						throw new SQLException(CsvResources.getString("invalidGroupBy") + ": " + n);
					int index = n.value.intValue();

					/*
					 * Column numbering in SQL starts at 1, not 0.
					 */
					index--;

					if (index < 0 || index >= this.queryEnvironment.size())
					{
						throw new SQLException(CsvResources.getString("invalidGroupBy") + ": " + (index + 1));
					}
					Object[] q = this.queryEnvironment.get(index);
					this.groupByColumns.set(i, (Expression)q[1]);
				}
			}
		}

		if (this.groupByColumns != null)
		{
			for (Expression expr : this.groupByColumns)
			{
				this.usedColumns.addAll(expr.usedColumns(allReaderColumns));
			}
			if (havingClause!= null)
				this.usedColumns.addAll(havingClause.usedColumns(allReaderColumns));
		}

		/*
		 * Replace any "order by 2" with the 2nd column in the query list.
		 */
		if (this.orderByColumns != null)
		{
			for (Object []o : this.orderByColumns)
			{
				Expression expression = (Expression)o[1];
				if (expression instanceof NumericConstant)
				{
					NumericConstant n = (NumericConstant)expression;
					if (!(n.value instanceof Integer))
						throw new SQLException(CsvResources.getString("invalidOrderBy") + ": " + n);
					int index = n.value.intValue();
					
					/*
					 * Column numbering in SQL starts at 1, not 0.
					 */
					index--;

					if (index < 0 || index >= this.queryEnvironment.size())
					{
						throw new SQLException(CsvResources.getString("invalidOrderBy") + ": " + (index + 1));
					}
					Object[] q = this.queryEnvironment.get(index);
					o[1] = q[1];
				}
			}
		}

		if (this.orderByColumns != null)
		{
			for (Object []o : this.orderByColumns)
			{
				Expression expr = (Expression)o[1];
				this.usedColumns.addAll(expr.usedColumns(allReaderColumns));
			}
		}

		/*
		 * Find any SQL aggregate functions so they can be evaluated separately.
		 */
		for (int i = 0; i < this.queryEnvironment.size(); i++)
		{
			Object[] o = this.queryEnvironment.get(i);
			Expression expr = (Expression)o[1];

			/*
			 * Exclude aggregate functions in subquery as they do not affect this query.
			 */
			if (!(expr instanceof SubQueryExpression))
			{
				List exprAggregateFunctions = expr.aggregateFunctions();
				this.aggregateFunctions.addAll(exprAggregateFunctions);
				for (AggregateFunction aggregateFunction : exprAggregateFunctions)
				{
					this.usedColumns.addAll(aggregateFunction.aggregateColumns(allReaderColumns));
				}
			}
		}

		if (aggregateFunctions.size() > 0 && this.groupByColumns == null)
		{
			/*
			 * Check there is no mix of query columns and aggregate functions.
			 */
			List allUsedColumns = new LinkedList<>();
			for (int i = 0; i < this.queryEnvironment.size(); i++)
			{
				Object[] o = this.queryEnvironment.get(i);
				if (o[1] != null)
				{
					allUsedColumns.addAll(((Expression)o[1]).usedColumns(allReaderColumns));
				}
			}
			if (allUsedColumns.size() > 0 && aggregateFunctions.size() > 0)
				throw new SQLException(CsvResources.getString("columnsWithAggregateFunctions"));
		}
		if (whereClause != null && whereClause.aggregateFunctions().size() > 0)
			throw new SQLException(CsvResources.getString("noAggregateFunctions"));

		if (!((CsvConnection)statement.getConnection()).isIndexedFiles())
		{
			//TODO no check when indexedFiles=true because unit test TestCsvDriver.testFromNonExistingIndexedTable then fails.
			/*
			 * Check that each selected expression is valid, using only column names contained in the table.
			 */
			for (int i = 0; i < this.queryEnvironment.size(); i++)
			{
				Object[] o = this.queryEnvironment.get(i);
				if (o[1] != null)
				{
					Expression expr = (Expression)o[1];
					if (expr instanceof SubQueryExpression)
					{
						/*
						 * Any invalid column names will be caught in the sub-query.
						 */
					}
					else
					{
						List exprUsedColumns = expr.usedColumns(allReaderColumns);
						for (Object usedColumn : exprUsedColumns)
						{
							if (!allReaderColumns.contains(usedColumn))
								throw new SQLException(CsvResources.getString("invalidColumnName") + ": " + usedColumn);
						}
					}
				}
				//TODO selected column aliases are allowed in WHERE clause (although this is invalid SQL) and unit tested in TestCsvDriver.testFieldAsAlias so add all aliases to list too.
				allReaderColumns.add(o[0].toString());
			}
		}
		
		/*
		 * Check that all columns used in the WHERE, GROUP BY, HAVING
		 * and ORDER BY clauses do exist in the table.
		 */
		if (!((CsvConnection)statement.getConnection()).isIndexedFiles())
		{
			for (String usedColumn : this.usedColumns)
			{
				if (!allReaderColumns.contains(usedColumn) && !parentObjectEnvironment.containsKey(usedColumn))
				{
					throw new SQLException(CsvResources.getString("invalidColumnName") + ": " + usedColumn);
				}
			}

			checkGroupBy(allReaderColumns);

			if (this.orderByColumns != null)
			{
				for (Object []o : this.orderByColumns)
				{
					Expression expr = (Expression)o[1];
					List exprUsedColumns = new LinkedList<>(expr.usedColumns(allReaderColumns));
					if (expr instanceof SubQueryExpression)
					{
						/*
						 * Differentiate which columns are from this table and which
						 * columns are from the subquery table.
						 * 
						 * Go through all the columns and only add the ones that are defined
						 * in this table.
						 */
						List copy = new LinkedList<>();
						for (String usedColumn : exprUsedColumns)
						{
							if (allReaderColumns.contains(usedColumn.toUpperCase()))
								copy.add(usedColumn);
						}
						exprUsedColumns = copy;
					}

					for (String usedColumn : exprUsedColumns)
					{
						if (!allReaderColumns.contains(usedColumn) && !parentObjectEnvironment.containsKey(usedColumn))
						{
							throw new SQLException(CsvResources.getString("invalidColumnName") + ": " + usedColumn);
						}
					}

					if (exprUsedColumns.isEmpty())
					{
						/*
						 * Must order by something that contains at least one column, not 'foo' or 1+1.
						 */
						throw new SQLException(CsvResources.getString("invalidOrderBy") + ": " + expr);
					}
				}
			}
		}

		if (this.distinctValues != null)
		{
			for (int i = 0; i < this.queryEnvironment.size(); i++)
			{
				Object[] o = this.queryEnvironment.get(i);
				Expression expr = (Expression)o[1];
				this.usedColumns.addAll(expr.usedColumns(allReaderColumns));
			}
		}

		if (this.groupByColumns != null ||
		this.orderByColumns != null || this.aggregateFunctions.size() > 0 ||
			isScrollable())
		{
			bufferedRecordEnvironments = new ArrayList<>();
			currentRow = 0;
		}

		if (this.groupByColumns != null)
		{
			/*
			 * Read all rows and group them together based on GROUP BY expressions.
			 */
			int savedMaxRows = maxRows;
			int savedLimit = limit;
			maxRows = 0;
			limit = -1;
			ArrayList> groupOrder = new ArrayList<>();
			Map, ArrayList>> groups = new MinimumMemoryMap<>();
			try
			{
				while (next())
				{
					Map objectEnvironment = updateRecordEnvironment(true);
					if (converter != null)
						objectEnvironment.put(StringConverter.COLUMN_NAME, converter);
					ArrayList groupByKeys = new ArrayList<>(this.groupByColumns.size());
					for (Expression expr : this.groupByColumns)
					{
						groupByKeys.add(expr.eval(objectEnvironment));
					}
					ArrayList> groupByValues = groups.get(groupByKeys);
					if (groupByValues == null)
					{
						groupByValues = new ArrayList<>();
						groups.put(groupByKeys, groupByValues);
						groupOrder.add(groupByKeys);
					}
					groupByValues.add(recordEnvironment);
				}
				bufferedRecordEnvironments.clear();
				for (ArrayList groupByKey : groupOrder)
				{
					ArrayList> values = groups.get(groupByKey);

					/*
					 * Create a row in the ResultSet for each group with a
					 * reference to all the rows in that group so we can
					 * later calculate any aggregate functions for each group.
					 */
					Map firstRow = new MinimumMemoryMap<>(values.get(0));
					firstRow.put(AggregateFunction.GROUPING_COLUMN_NAME, values);

					if (this.havingClause == null || Boolean.TRUE.equals(this.havingClause.isTrue(firstRow)))
						bufferedRecordEnvironments.add(firstRow);
				}

				if (this.orderByColumns != null)
				{
					sortRows(sqlOffset);
				}
			}
			finally
			{
				maxRows = savedMaxRows;
				limit = savedLimit;
			}

			/*
			 * Rewind back to before the row so we can read it.
			 */
			currentRow = 0;
			nextResult = true;
			recordEnvironment = null;
			updateRecordEnvironment(false);
			hitTail = true;

		}
		else if (this.aggregateFunctions.size() > 0)
		{
			/*
			 * Read all rows, evaluating the aggregate functions for each row to
			 * produce a single row result.
			 */
			int savedMaxRows = maxRows;
			int savedLimit = limit;
			maxRows = 0;
			limit = -1;
			try
			{
				while (next())
				{
					for (Object o : this.aggregateFunctions)
					{
						AggregateFunction func = (AggregateFunction)o;
						func.processRow(recordEnvironment);
					}
				}

				/*
				 * Create a single row ResultSet from the aggregate functions.
				 */
				bufferedRecordEnvironments.clear();
				if ((savedLimit < 0 || savedLimit > 0) && sqlOffset == 0)
					bufferedRecordEnvironments.add(new HashMap<>());
			}
			finally
			{
				maxRows = savedMaxRows;
				limit = savedLimit;
			}

			/*
			 * Rewind back to before the row so we can read it.
			 */
			currentRow = 0;
			nextResult = true;
			recordEnvironment = null;
			updateRecordEnvironment(false);
			hitTail = true;

		}
		else if (this.orderByColumns != null)
		{
			/*
			 * Read all rows into memory and sort them based on SQL ORDER BY expressions.
			 */
			int savedMaxRows = maxRows;
			int savedLimit = limit;
			maxRows = 0;
			limit = -1;
			try
			{
				while (next())
					;
			}
			finally
			{
				maxRows = savedMaxRows;
				limit = savedLimit;
			}
			sortRows(sqlOffset);

			/*
			 * Rewind back to before first row so we can now read them in sorted order.
			 */
			currentRow = 0;
			nextResult = true;
			recordEnvironment = null;
			updateRecordEnvironment(false);
		}
		else if (sqlOffset > 0)
		{
			int savedMaxRows = maxRows;
			int savedLimit = limit;
			maxRows = 0;
			limit = -1;

			/*
			 * Skip the first n rows.
			 */
			try
			{
				while (sqlOffset > 0)
				{
					if (!next())
						break;
					sqlOffset--;
				}
			}
			finally
			{
				maxRows = savedMaxRows;
				limit = savedLimit;
				currentRow = 0;
				nextResult = true;
				if (bufferedRecordEnvironments != null)
					bufferedRecordEnvironments.clear();
			}
		}
	}

	private boolean isScrollable()
	{
		return (this.resultSetType == ResultSet.TYPE_SCROLL_INSENSITIVE ||
			this.resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE);
	}

	/**
	 * Check that all selected and ORDER BY columns also appear in any GROUP BY clause. 
	 * @throws SQLException
	 */
	private void checkGroupBy(Set allReaderColumns) throws SQLException
	{
		if (this.groupByColumns != null)
		{
			for (Expression expr : this.groupByColumns)
			{
				List exprUsedColumns = expr.usedColumns(allReaderColumns);
				if (exprUsedColumns.isEmpty())
				{
					/*
					 * Must group by something that contains at least one column, not 'foo' or 1+1.
					 */
					throw new SQLException(CsvResources.getString("invalidGroupBy") + ": " + expr);
				}
			}
			ArrayList groupingColumns = new ArrayList<>();
			for (Expression expr : this.groupByColumns)
			{
				groupingColumns.addAll(expr.usedColumns(allReaderColumns));
			}
			ArrayList queryEnvironmentColumns = new ArrayList<>();
			for (int i = 0; i < this.queryEnvironment.size(); i++)
			{
				Object[] o = this.queryEnvironment.get(i);
				queryEnvironmentColumns.add(o[0].toString());
				if (o[1] != null)
				{
					Expression expr = (Expression)o[1];
					for (Object o2 : expr.usedColumns(allReaderColumns))
					{
						queryEnvironmentColumns.add(o2.toString());
					}
				}
			}
			for (int i = 0; i < this.queryEnvironment.size(); i++)
			{
				Object[] o = this.queryEnvironment.get(i);
				if (!groupingColumns.contains(o[0]))
				{
					if (o[1] != null)
					{
						Expression expr = (Expression)o[1];
						for (Object o2 : expr.usedColumns(allReaderColumns))
						{
							String columnName = o2.toString();
							if (!groupingColumns.contains(columnName))
							{
								if (tableName == null || (!groupingColumns.contains(tableName.toUpperCase() + "." + columnName)))
								{
									String tableAlias = this.reader.getTableAlias();
									if (tableAlias == null || (!groupingColumns.contains(tableAlias + "." + columnName)))
									{
										/*
										 * GROUP BY must include all queried columns.
										 */
										throw new SQLException(CsvResources.getString("columnNotInGroupBy") + ": " + columnName);
									}
								}
							}
						}
					}
				}
			}
			if (this.havingClause != null)
			{
				for (String columnName : this.havingClause.usedColumns(allReaderColumns))
				{
					if (!queryEnvironmentColumns.contains(columnName))
					{
						throw new SQLException(CsvResources.getString("invalidHaving") + ": " + columnName);
					}
				}
			}
			if (this.orderByColumns != null)
			{
				for (Object []o : this.orderByColumns)
				{
					Expression expr = (Expression)o[1];
					for (Object o2 : expr.usedColumns(allReaderColumns))
					{
						if (!queryEnvironmentColumns.contains(o2.toString()))
							throw new SQLException(CsvResources.getString("orderByNotInGroupBy") + ": " + o2);
					}
				}
			}

			/*
			 * A query containing GROUP BY without any aggregate functions can be simplified
			 * to a SELECT DISTINCT, avoiding the need to load all records into memory.
			 */
			boolean hasAggregateFunctions = false;
			for (int i = 0; i < this.queryEnvironment.size(); i++)
			{
				Object[] o = this.queryEnvironment.get(i);
				Expression expr = (Expression)o[1];
				if (expr.aggregateFunctions().size() > 0)
					hasAggregateFunctions = true;
			}
			if (this.havingClause != null && this.havingClause.aggregateFunctions().size() > 0)
				hasAggregateFunctions = true;
			if (!hasAggregateFunctions)
			{
				this.distinctValues = new HashSet<>();
				this.distinctColumns = new ArrayList<>(this.groupByColumns);
				this.groupByColumns = null;
			}
		}
	}

	private void sortRows(int sqlOffset) throws SQLException
	{
		ArrayList> allRows = new ArrayList<>(bufferedRecordEnvironments);
		bufferedRecordEnvironments.clear();
		try
		{
			allRows.sort(new OrderByComparator());
		}
		catch (OrderByException e)
		{
			throw new SQLException(e.getMessage());
		}
		int rowLimit = allRows.size();
		if (maxRows != 0 && maxRows < rowLimit)
			rowLimit = maxRows;
		if (limit >= 0 && sqlOffset + limit < rowLimit)
			rowLimit = sqlOffset + limit;

		for (int i = sqlOffset; i < rowLimit; i++)
			bufferedRecordEnvironments.add(allRows.get(i));
	}

	private void checkOpen() throws SQLException
	{
		if (isClosed)
			throw new SQLException(CsvResources.getString("closedResultSet"));
	}

	private void checkTimeout() throws SQLTimeoutException
	{
		if (statement.getTimeoutMillis() != Long.MAX_VALUE)
		{
			if (System.currentTimeMillis() >= statement.getTimeoutMillis())
				throw new SQLTimeoutException();
		}
	}

	@Override
	public boolean next() throws SQLException
	{
		boolean thereWasAnAnswer;

		checkOpen();
		checkTimeout();

		/*
		 * Has query been cancelled by another thread?
		 */
		if (statement.isCancelled())
			throw new SQLException(CsvResources.getString("statementCancelled"));

		if ((this.groupByColumns != null ||
		this.aggregateFunctions.size() > 0 ||
			this.orderByColumns != null || isScrollable()) &&
			currentRow < bufferedRecordEnvironments.size())
		{
			currentRow++;
			recordEnvironment = bufferedRecordEnvironments.get(currentRow - 1);
			updateRecordEnvironment(true);
			thereWasAnAnswer = true;
		}
		else
		{
			if(maxRows != 0 && currentRow >= maxRows)
			{
				// Do not fetch any more rows, we have reached the row limit set by caller.
				thereWasAnAnswer = false;
			}
			else if (maxDataLines != 0 && currentRow >= maxDataLines)
			{
				// We have already read the maximum number of rows from CSV file. Stop now.
				thereWasAnAnswer = false;
			}
			else if(limit >= 0 && currentRow >= limit)
			{
				thereWasAnAnswer = false;
			}
			else if(hitTail)
			{
				thereWasAnAnswer = false;
			}
			else
			{
				thereWasAnAnswer = reader.next();
			}

			if(thereWasAnAnswer)
			{
				recordEnvironment = reader.getEnvironment();
				recordEnvironment.put(CsvStatement.STATEMENT_COLUMN_NAME, statement);

				/*
				 * Always include line number in CSV file, so it can be evaluated later.
				 */
				addLineNumberEnvironment(recordEnvironment);
			}
			else
			{
				recordEnvironment = null;
			}

			// We have a where clause or DISTINCT keyword, honor it
			if (whereClause != null || distinctValues != null)
			{
				Map objectEnvironment = updateRecordEnvironment(thereWasAnAnswer);
				while (thereWasAnAnswer)
				{
					if (whereClause == null || Boolean.TRUE.equals(whereClause.isTrue(objectEnvironment)))
					{
						/*
						 * Check HAVING clause if no aggregate functions in query and
						 * it is being processed just like SELECT DISTINCT.
						 * In this case HAVING is exactly the same as a WHERE clause.
						 */
						if (this.distinctColumns == null || this.havingClause == null || Boolean.TRUE.equals(this.havingClause.isTrue(objectEnvironment)))
						{
							if (distinctValues == null || addDistinctEnvironment(objectEnvironment))
							{
								break;
							}
						}
					}
					this.nonMatchingRows++;
					thereWasAnAnswer = reader.next();
					if(thereWasAnAnswer)
					{
						recordEnvironment = reader.getEnvironment();
						recordEnvironment.put(CsvStatement.STATEMENT_COLUMN_NAME, statement);

						/*
						 * Always include line number in CSV file, so it can be evaluated later.
						 */
						addLineNumberEnvironment(recordEnvironment);
					}
					else
					{
						recordEnvironment = null;
					}
					objectEnvironment = updateRecordEnvironment(thereWasAnAnswer);
				}
			}
			if (this.orderByColumns != null || isScrollable())
			{
				if(thereWasAnAnswer)
				{
					Map env = reader.getEnvironment();
					env.put(CsvStatement.STATEMENT_COLUMN_NAME, statement);

					/*
					 * Always include line number in CSV file, so it can be evaluated later.
					 */
					addLineNumberEnvironment(env);

					bufferedRecordEnvironments.add(env);
					currentRow++;
				}
				else
				{
					hitTail = true;
					currentRow = bufferedRecordEnvironments.size() + 1;
				}
			}
			else
			{
				if (thereWasAnAnswer)
					currentRow++;
				else
					hitTail = true;
			}
		}

		nextResult = thereWasAnAnswer;
		return thereWasAnAnswer;
	}

	private Map updateRecordEnvironment(boolean thereWasAnAnswer) throws SQLException
	{
		Map objectEnvironment = new MinimumMemoryMap<>();
		if(!thereWasAnAnswer)
		{
			recordEnvironment = null;
			return objectEnvironment;
		}

		/*
		 * Set any parent environment first so it is overridden by current environment.
		 */
		objectEnvironment.put(CsvStatement.STATEMENT_COLUMN_NAME, this.statement);
		objectEnvironment.putAll(this.parentObjectEnvironment);

		for (int i = 0; i < queryEnvironment.size(); i++)
		{
			Object[] o = queryEnvironment.get(i);
			String key = (String) o[0];
			Object value = ((Expression) o[1]).eval(recordEnvironment);
			objectEnvironment.put(key.toUpperCase(), value);
		}
		for (int i=0; i '2012-06-31'.
		 */
		key = StringConverter.COLUMN_NAME;
		Object stringConverter = recordEnvironment.get(key);
		if (stringConverter != null)
			objectEnvironment.put(key, stringConverter);

		/*
		 * Always include the java.sql.Statement object that user-defined
		 * SQL functions can refer back to.
		 */
		key = CsvStatement.STATEMENT_COLUMN_NAME;
		Object statement = recordEnvironment.get(key);
		if (statement != null)
			objectEnvironment.put(key, statement);

		return objectEnvironment;
	}

	private void addLineNumberEnvironment(Map recordEnvironment) throws SQLException
	{
		/*
		 * No line numbers if rows are grouped.
		 */
		if (this.groupByColumns != null)
			return;

		String key = SQLLineNumberFunction.LINE_NUMBER_COLUMN_NAME;
		int lineNumber = this.currentRow + this.nonMatchingRows + 1;
		if (this.offset > 0)
			lineNumber += this.offset;
		recordEnvironment.put(key, Integer.valueOf(lineNumber));
	}

	private boolean addDistinctEnvironment(Map objectEnvironment) throws SQLException
	{
		boolean isDistinct;

		/*
		 * Create list of query values for this row, either for a simple
		 * GROUP BY statement, or for a SELECT DISTINCT.
		 */
		ArrayList environment;
		if (this.distinctColumns != null)
		{
			environment = new ArrayList<>(distinctColumns.size());
			for (int i = 0; i < distinctColumns.size(); i++)
			{
				Object value = distinctColumns.get(i).eval(objectEnvironment);
				environment.add(value);
			}
		}
		else
		{
			environment = new ArrayList<>(queryEnvironment.size());
			for (int i = 0; i < queryEnvironment.size(); i++)
			{
				Object[] o = queryEnvironment.get(i);
				Object value = ((Expression) o[1]).eval(objectEnvironment);
				environment.add(value);
			}
		}

		/*
		 * Has this list of values been read before for this query?
		 */
		isDistinct = distinctValues.add(environment);
		return isDistinct;
	}

	@Override
	public void close() throws SQLException
	{
		isClosed = true;
		reader.close();

		/*
		 * Ensure that long row lists for this query can now be GC'ed
		 * even if the application still has a reference to this ResultSet.
		 */
		distinctValues = null;
		parentObjectEnvironment = null;
		bufferedRecordEnvironments = null;
		recordEnvironment = null;
	}

	@Override
	public boolean wasNull() throws SQLException
	{
		if(lastIndexRead >= 0)
		{
			return getString(lastIndexRead) == null;
		}
		else
		{
			throw new SQLException(CsvResources.getString("noGetMethod"));
		}
	}

	//======================================================================
	// Methods for accessing results by column index
	//======================================================================

	@Override
	public String getString(int columnIndex) throws SQLException
	{
		Object o = getObject(columnIndex);
		if (o != null)
			return o.toString();
		return null;
	}

	@Override
	public boolean getBoolean(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Boolean b = converter.parseBoolean(s);
			if (b != null)
				return b.booleanValue();
		}
		return false;
	}

	@Override
	public byte getByte(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Byte b = converter.parseByte(s);
			if (b != null)
				return b.byteValue();
		}
		return 0;
	}

	@Override
	public short getShort(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Short n = converter.parseShort(s);
			if (n != null)
				return n.shortValue();
		}
		return 0;
	}

	@Override
	public int getInt(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Integer i = converter.parseInt(s);
			if (i != null)
				return i.intValue();
		}
		return 0;
	}

	@Override
	public long getLong(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Long l = converter.parseLong(s);
			if (l != null)
				return l.longValue();
		}
		return 0;
	}

	@Override
	public float getFloat(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Float f = converter.parseFloat(s);
			if (f != null)
				return f.floatValue();
		}
		return 0;
	}

	@Override
	public double getDouble(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
		{
			Double d = converter.parseDouble(s);
			if (d != null)
				return d.doubleValue();
		}
		return 0;
	}

	@Override
	@Deprecated
	public BigDecimal getBigDecimal(int columnIndex, int scale)
			throws SQLException
	{
		// let getBigDecimal(int) handle this for now
		return getBigDecimal(columnIndex);
	}

	@Override
	public byte[] getBytes(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
			return converter.parseBytes(s);
		return null;
	}

	@Override
	public Date getDate(int columnIndex) throws SQLException
	{
		return (Date) getObject(columnIndex);
	}

	@Override
	public Time getTime(int columnIndex) throws SQLException
	{
		return (Time) getObject(columnIndex);
	}

	@Override
	public Timestamp getTimestamp(int columnIndex) throws SQLException
	{
		Object o = getObject(columnIndex);
		if (o instanceof Date)
			o = new Timestamp(((Date)o).getTime());
		return (Timestamp) o;
	}

	@Override
	public InputStream getAsciiStream(int columnIndex) throws SQLException
	{
		String s = getString(columnIndex);
		if (s != null)
			return converter.parseAsciiStream(s);
		return null;
	}

	@Override
	@Deprecated
	public InputStream getUnicodeStream(int columnIndex) throws SQLException
	{
		// delegate to getAsciiStream(int)
		return getAsciiStream(columnIndex);
	}

	@Override
	public InputStream getBinaryStream(int columnIndex) throws SQLException
	{
		// delegate to getAsciiStream(int)
		return getAsciiStream(columnIndex);
	}

	//======================================================================
	// Methods for accessing results by column name
	//======================================================================

	@Override
	public String getString(String columnName) throws SQLException
	{
		return getString(findColumn(columnName));
	}

	@Override
	public boolean getBoolean(String columnName) throws SQLException
	{
		return getBoolean(findColumn(columnName));
	}

	@Override
	public byte getByte(String columnName) throws SQLException
	{
		return getByte(findColumn(columnName));
	}

	@Override
	public short getShort(String columnName) throws SQLException
	{
		return getShort(findColumn(columnName));
	}

	@Override
	public int getInt(String columnName) throws SQLException
	{
		return getInt(findColumn(columnName));
	}

	@Override
	public long getLong(String columnName) throws SQLException
	{
		return getLong(findColumn(columnName));
	}

	@Override
	public float getFloat(String columnName) throws SQLException
	{
		return getFloat(findColumn(columnName));
	}

	@Override
	public double getDouble(String columnName) throws SQLException
	{
		return getDouble(findColumn(columnName));
	}

	@Override
	@Deprecated
	public BigDecimal getBigDecimal(String columnName, int scale)
			throws SQLException
	{
		return getBigDecimal(findColumn(columnName));
	}

	@Override
	public byte[] getBytes(String columnName) throws SQLException
	{
		return getBytes(findColumn(columnName));
	}

	@Override
	public Date getDate(String columnName) throws SQLException
	{
		return getDate(findColumn(columnName));
	}

	@Override
	public Time getTime(String columnName) throws SQLException
	{
		return getTime(findColumn(columnName));
	}

	@Override
	public Timestamp getTimestamp(String columnName) throws SQLException
	{
		return getTimestamp(findColumn(columnName));
	}

	@Override
	public InputStream getAsciiStream(String columnName) throws SQLException
	{
		return getAsciiStream(findColumn(columnName));
	}

	@Override
	@Deprecated
	public InputStream getUnicodeStream(String columnName) throws SQLException
	{
		return getUnicodeStream(findColumn(columnName));
	}

	@Override
	public InputStream getBinaryStream(String columnName) throws SQLException
	{
		return getBinaryStream(findColumn(columnName));
	}

	//=====================================================================
	// Advanced features:
	//=====================================================================

	@Override
	public SQLWarning getWarnings() throws SQLException
	{
		return null;
	}

	@Override
	public void clearWarnings() throws SQLException
	{
	}

	@Override
	public String getCursorName() throws SQLException
	{
		checkOpen();

		throw new SQLFeatureNotSupportedException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getCursorName()");
	}

	@Override
	public ResultSetMetaData getMetaData() throws SQLException
	{
		if (resultSetMetaData == null)
		{
			String[] readerTypeNames = reader.getColumnTypes(); 
			String[] readerColumnNames = reader.getColumnNames();
			int[] readerColumnSizes = reader.getColumnSizes();
			String tableAlias = reader.getTableAlias();
			int columnCount = queryEnvironment.size();
			String []columnNames = new String[columnCount];
			String []columnLabels = new String[columnCount];
			int []columnSizes = new int[columnCount];
			String []typeNames = new String[columnCount];

			/*
			 * Create a record containing dummy values.
			 */
			HashSet allReaderColumns = new HashSet<>();
			HashMap env = new HashMap<>();
			for(int i=0; i 0);
		}
	}

	@Override
	public boolean isFirst() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			return currentRow == 1;
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.isFirst()");
		}
	}

	@Override
	public boolean isLast() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			if (!hitTail && currentRow != 0)
			{
				next();
				previous();
			}
			return (currentRow == bufferedRecordEnvironments.size());
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.isLast()");
		}
	}

	@Override
	public void beforeFirst() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			first();
			previous();
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.beforeFirst()");
		}
	}

	@Override
	public void afterLast() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			while(next());
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.afterLast()");
		}
	}

	@Override
	public boolean first() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			currentRow = 0;
			boolean thereWasAnAnswer = next();
			updateRecordEnvironment(thereWasAnAnswer);
			return thereWasAnAnswer;
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.first()");
		}
	}

	@Override
	public boolean last() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			afterLast();
			previous();
			return (this.bufferedRecordEnvironments.size() != 0);
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.last()");
		}
	}

	@Override
	public int getRow() throws SQLException
	{
		checkOpen();

		if (!isScrollable() && !nextResult)
			return 0;
		else
			return currentRow;
	}

	@Override
	public boolean absolute(int row) throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			boolean found;
			if(row < 0)
			{
				last();
				row = currentRow + row + 1;
			}
			else
			{
				// this is a no-op if we have already buffered enough lines.
				while((bufferedRecordEnvironments.size() < row) && next());
			}
			if (row <= 0)
			{
				found = false;
				currentRow = 0;
			}
			else if(row > bufferedRecordEnvironments.size())
			{
				found = false;
				currentRow = bufferedRecordEnvironments.size() + 1;
			}
			else
			{
				found = true;
				currentRow = row;
				recordEnvironment = bufferedRecordEnvironments.get(currentRow - 1);
			}
			updateRecordEnvironment(found);
			return found;
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.absolute()");
		}
	}

	@Override
	public boolean relative(int rows) throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			if(currentRow + rows >= 0)
				return absolute(currentRow + rows);
			currentRow = 0;
			updateRecordEnvironment(false);
			return false;
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.relative()");
		}
	}

	@Override
	public boolean previous() throws SQLException
	{
		checkOpen();

		if (isScrollable())
		{
			if(currentRow > 1)
			{
				currentRow--;
				recordEnvironment = bufferedRecordEnvironments.get(currentRow - 1);
				updateRecordEnvironment(true);
				return true;
			}
			else
			{
				currentRow = 0;
				recordEnvironment = null;
				updateRecordEnvironment(false);
				return false;
			}
		}
		else
		{
			throw new SQLException(CsvResources.getString("wrongResultSetType") + ": ResultSet.previous()");
		}
	}

	//---------------------------------------------------------------------
	// Properties
	//---------------------------------------------------------------------

	@Override
	public void setFetchDirection(int direction) throws SQLException
	{
		checkOpen();

		if (direction == ResultSet.FETCH_FORWARD ||
			direction == ResultSet.FETCH_REVERSE ||
			direction == ResultSet.FETCH_UNKNOWN)
		{
			this.fetchDirection = direction;
		}
		else
		{
			throw new SQLException(CsvResources.getString("unsupportedDirection") + ": " + direction);
		}
	}

	@Override
	public int getFetchDirection() throws SQLException
	{
		checkOpen();

		return fetchDirection;
	}

	@Override
	public void setFetchSize(int rows) throws SQLException
	{
		fetchSize = rows;
	}

	@Override
	public int getFetchSize() throws SQLException
	{
		return fetchSize;
	}

	@Override
	public int getType() throws SQLException
	{
		return resultSetType;
	}

	@Override
	public int getConcurrency() throws SQLException
	{
		return CONCUR_READ_ONLY;
	}

	//---------------------------------------------------------------------
	// Updates
	//---------------------------------------------------------------------

	@Override
	public boolean rowUpdated() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.rowUpdated()");
	}

	@Override
	public boolean rowInserted() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.rowInserted()");
	}

	@Override
	public boolean rowDeleted() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.rowDeleted()");
	}

	@Override
	public void updateNull(int columnIndex) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateNull(int)");
	}

	@Override
	public void updateBoolean(int columnIndex, boolean x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBoolean(int, boolean)");
	}

	@Override
	public void updateByte(int columnIndex, byte x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateByte(int, byte)");
	}

	@Override
	public void updateShort(int columnIndex, short x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateShort(int, short)");
	}

	@Override
	public void updateInt(int columnIndex, int x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateInt(int, int)");
	}

	@Override
	public void updateLong(int columnIndex, long x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateLong(int, long)");
	}

	@Override
	public void updateFloat(int columnIndex, float x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateFloat(int, float)");
	}

	@Override
	public void updateDouble(int columnIndex, double x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateDouble(int, double)");
	}

	@Override
	public void updateBigDecimal(int columnIndex, BigDecimal x)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBigDecimal(int, BigDecimal)");
	}

	@Override
	public void updateString(int columnIndex, String x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateString(int, String)");
	}

	@Override
	public void updateBytes(int columnIndex, byte[] x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBytes(int, byte[])");
	}

	@Override
	public void updateDate(int columnIndex, Date x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateDate(int, Date)");
	}

	@Override
	public void updateTime(int columnIndex, Time x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateTime(int, Time)");
	}

	@Override
	public void updateTimestamp(int columnIndex, Timestamp x)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateTimestamp(int, Timestamp)");
	}

	@Override
	public void updateAsciiStream(int columnIndex, InputStream x, int length)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateAsciiStream(int, InputStream, int)");
	}

	@Override
	public void updateBinaryStream(int columnIndex, InputStream x, int length)
		   throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBinaryStreamint, InputStream, int)");
	}

	@Override
	public void updateCharacterStream(int columnIndex, Reader x, int length)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateCharacterStream(int, Reader, int)");
	}

	@Override
	public void updateObject(int columnIndex, Object x, int scale)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.udpateObject(int, Object)");
	}

	@Override
	public void updateObject(int columnIndex, Object x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateObject(int, Object, int)");
	}

	@Override
	public void updateNull(String columnName) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateNull(String)");
	}

	@Override
	public void updateBoolean(String columnName, boolean x)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBoolean(String, boolean)");
	}

	@Override
	public void updateByte(String columnName, byte x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateByte(String, byte)");
	}

	@Override
	public void updateShort(String columnName, short x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateShort(String, short)");
	}

	@Override
	public void updateInt(String columnName, int x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateInt(String, int)");
	}

	@Override
	public void updateLong(String columnName, long x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateLong(String, long)");
	}

	@Override
	public void updateFloat(String columnName, float x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateFloat(String, float)");
	}

	@Override
	public void updateDouble(String columnName, double x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateDouble(String, double)");
	}

	@Override
	public void updateBigDecimal(String columnName, BigDecimal x)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBigDecimal(String, BigDecimal)");
	}

	@Override
	public void updateString(String columnName, String x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateString(String, String)");
	}

	@Override
	public void updateBytes(String columnName, byte[] x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBytes(String, byte[])");
	}

	@Override
	public void updateDate(String columnName, Date x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateDate(String, Date)");
	}

	@Override
	public void updateTime(String columnName, Time x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateTime(String, Time)");
	}

	@Override
	public void updateTimestamp(String columnName, Timestamp x)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateTimestamp(String, Timestamp)");
	}

	@Override
	public void updateAsciiStream(String columnName, InputStream x, int length)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateAsciiStream(String, InputStream, int)");
	}

	@Override
	public void updateBinaryStream(String columnName, InputStream x, int length)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateBinaryStream(String, InputStream, int)");
	}

	@Override
	public void updateCharacterStream(String columnName, Reader reader,
			int length) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateCharacterStream(String, Reader, int)");
	}

	@Override
	public void updateObject(String columnName, Object x, int scale)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateObject(String, Object, int)");
	}

	@Override
	public void updateObject(String columnName, Object x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateObject(String, Object)");
	}

	@Override
	public void insertRow() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.insertRow()");
	}

	@Override
	public void updateRow() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.updateRow()");
	}

	@Override
	public void deleteRow() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.deleteRow()");
	}

	@Override
	public void refreshRow() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.refreshRow()");
	}

	@Override
	public void cancelRowUpdates() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.cancelRowUpdates()");
	}

	@Override
	public void moveToInsertRow() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.moveToInsertRow()");
	}

	@Override
	public void moveToCurrentRow() throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.moveToeCurrentRow()");
	}

	@Override
	public Statement getStatement() throws SQLException
	{
		return statement;
	}

	@Override
	public Object getObject(int i, Map> map) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getObject(int, Map)");
	}

	@Override
	public Ref getRef(int i) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getRef(int)");
	}

	@Override
	public Blob getBlob(int i) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getBlob(int)");
	}

	@Override
	public Clob getClob(int i) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getClob(int)");
	}

	@Override
	public Array getArray(int i) throws SQLException
	{
		Object result = getObject(i);
		if (result == null || result instanceof Array) {
			return (Array) result;
		}
		throw new SQLException(CsvResources.getString("invalidColumnType") +
			": column #" + i + " is not an array");
	}

	@Override
	public Object getObject(String colName, Map> map) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getObject(String, Map)");
	}

	@Override
	public Ref getRef(String colName) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getRef(String)");
	}

	@Override
	public Blob getBlob(String colName) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getBlob(String)");
	}

	@Override
	public Clob getClob(String colName) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getClob(String)");
	}

	@Override
	public Array getArray(String colName) throws SQLException
	{
		return getArray(findColumn(colName));
	}

	@Override
	public Date getDate(int columnIndex, Calendar cal) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getDate(int, Calendar)");
	}

	@Override
	public Date getDate(String columnName, Calendar cal) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getDate(String, Calendar)");
	}

	@Override
	public Time getTime(int columnIndex, Calendar cal) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getTime(int, Calendar)");
	}

	@Override
	public Time getTime(String columnName, Calendar cal) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getTime(String, Calendar)");
	}

	@Override
	public Timestamp getTimestamp(int columnIndex, Calendar cal)
			throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getTimestamp(int, Calendar)");
	}

	@Override
	public Timestamp getTimestamp(String columnName, Calendar cal)
		   throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getTimestamp(String, Calendar)");
	}

	//---------------------------------------------------------------------
	// CSV JDBC private helper methods
	//---------------------------------------------------------------------

	/**
	 * Perform pre-accessor method processing
	 * @param columnIndex the first column is 1, the second is 2, ...
	 * @exception SQLException if a database access error occurs
	 */
	private void preAccessor(int columnIndex) throws SQLException
	{
		// set last read column index for wasNull()
		lastIndexRead = columnIndex;

		checkOpen();

		if (columnIndex < 1 || columnIndex > this.queryEnvironment.size())
		{
			throw new SQLException(CsvResources.getString("invalidColumnIndex") + ": "+columnIndex);
		}
		if (this.currentRow == 0)
		{
			// Cursor is still before first row.
			throw new SQLException(CsvResources.getString("noCurrentRow"));
		}
	}

	@Override
	public URL getURL(int columnIndex) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.getURL(int)");
	}

	@Override
	public URL getURL(String columnName) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.getURL(String)");
	}

	@Override
	public void updateRef(int columnIndex, Ref x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateRef(int,java.sql.Ref)");
	}

	@Override
	public void updateRef(String columnName, Ref x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateRef(String,java.sql.Ref)");
	}

	@Override
	public void updateBlob(int columnIndex, Blob x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateBlob(int,java.sql.Blob)");
	}

	@Override
	public void updateBlob(String columnName, Blob x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateBlob(String,java.sql.Blob)");
	}

	@Override
	public void updateClob(int columnIndex, Clob x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateClob(int,java.sql.Clob)");
	}

	@Override
	public void updateClob(String columnName, Clob x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateClob(String,java.sql.Clob)");
	}

	@Override
	public void updateArray(int columnIndex, Array x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateArray(int,java.sql.Array)");
	}

	@Override
	public void updateArray(String columnName, Array x) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") + ": ResultSet.updateArray(String,java.sql.Array)");
	}
	
	@Override
	public int getHoldability() throws SQLException
	{
		// TODO Auto-generated method stub
		return 0;
	}
	
	@Override
	public Reader getNCharacterStream(int columnIndex) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public Reader getNCharacterStream(String columnLabel) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public String getNString(int columnIndex) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public String getNString(String columnLabel) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public boolean isClosed() throws SQLException
	{
		return isClosed;
	}
	
	@Override
	public void updateAsciiStream(int columnIndex, InputStream x)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateAsciiStream(String columnLabel, InputStream x)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateAsciiStream(int columnIndex, InputStream x, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateAsciiStream(String columnLabel, InputStream x, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBinaryStream(int columnIndex, InputStream x)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBinaryStream(String columnLabel, InputStream x)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBinaryStream(int columnIndex, InputStream x, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBinaryStream(String columnLabel, InputStream x,
			long length) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBlob(int columnIndex, InputStream inputStream)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBlob(String columnLabel, InputStream inputStream)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBlob(int columnIndex, InputStream inputStream, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateBlob(String columnLabel, InputStream inputStream,
			long length) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateCharacterStream(int columnIndex, Reader x)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateCharacterStream(String columnLabel, Reader reader)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateCharacterStream(int columnIndex, Reader x, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateCharacterStream(String columnLabel, Reader reader,
			long length) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateClob(int columnIndex, Reader reader) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateClob(String columnLabel, Reader reader)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateClob(int columnIndex, Reader reader, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateClob(String columnLabel, Reader reader, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNCharacterStream(int columnIndex, Reader x)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNCharacterStream(String columnLabel, Reader reader)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNCharacterStream(int columnIndex, Reader x, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNCharacterStream(String columnLabel, Reader reader,
			long length) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNClob(int columnIndex, Reader reader) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNClob(String columnLabel, Reader reader)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNClob(int columnIndex, Reader reader, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNClob(String columnLabel, Reader reader, long length)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNString(int columnIndex, String string)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNString(String columnLabel, String string)
			throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public boolean isWrapperFor(Class arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return false;
	}
	
	@Override
	public  T unwrap(Class arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int findColumn(String columnLabel) throws SQLException
	{
		checkOpen();

		if (columnLabel.equals(""))
			throw new SQLException(CsvResources.getString("invalidColumnName") + ": " + columnLabel);
		for (int i = 0; i < this.queryEnvironment.size(); i++)
		{
			Object[] queryEnvEntry = this.queryEnvironment.get(i);
			if(((String)queryEnvEntry[0]).equalsIgnoreCase(columnLabel))
				return i+1;
		}
		throw new SQLException(CsvResources.getString("invalidColumnName") + ": " + columnLabel);
	}

	@Override
	public NClob getNClob(int arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public NClob getNClob(String arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public RowId getRowId(int arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public RowId getRowId(String arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public SQLXML getSQLXML(int arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public SQLXML getSQLXML(String arg0) throws SQLException
	{
		// TODO Auto-generated method stub
		return null;
	}
	
	@Override
	public void updateNClob(int arg0, NClob arg1) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateNClob(String arg0, NClob arg1) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateRowId(int arg0, RowId arg1) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateRowId(String arg0, RowId arg1) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateSQLXML(int arg0, SQLXML arg1) throws SQLException
	{
		// TODO Auto-generated method stub
	}
	
	@Override
	public void updateSQLXML(String arg0, SQLXML arg1) throws SQLException
	{
		// TODO Auto-generated method stub
	}

	@Override
	public  T getObject(String columnLabel, Class type) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getObject(String, Class)");
	}

	@Override
	public  T getObject(int columnIndex, Class type) throws SQLException
	{
		throw new UnsupportedOperationException(CsvResources.getString("methodNotSupported") +
			": ResultSet.getObject(int, Class)");
	}
}