package org.postgresql.jdbc2;

// IMPORTANT NOTE: This file implements the JDBC 2 version of the driver.
// If you make any modifications to this file, you must make sure that the
// changes are also made (if relevent) to the related JDBC 1 class in the
// org.postgresql.jdbc1 package.

import java.lang.*;
import java.sql.*;
import java.util.*;
import org.postgresql.*;
import org.postgresql.util.*;

/**
 * A ResultSetMetaData object can be used to find out about the types and
 * properties of the columns in a ResultSet
 *
 * @see java.sql.ResultSetMetaData
 */
public class ResultSetMetaData implements java.sql.ResultSetMetaData
{
	Vector rows;
	Field[] fields;

	/*
	 *	Initialise for a result with a tuple set and
	 *	a field descriptor set
	 *
	 * @param rows the Vector of rows returned by the ResultSet
	 * @param fields the array of field descriptors
	 */
	public ResultSetMetaData(Vector rows, Field[] fields)
	{
		this.rows = rows;
		this.fields = fields;
	}

	/*
	 * Whats the number of columns in the ResultSet?
	 *
	 * @return the number
	 * @exception SQLException if a database access error occurs
	 */
	public int getColumnCount() throws SQLException
	{
		return fields.length;
	}

	/*
	 * Is the column automatically numbered (and thus read-only)
	 * I believe that PostgreSQL does not support this feature.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return true if so
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isAutoIncrement(int column) throws SQLException
	{
		return false;
	}

	/*
	 * Does a column's case matter? ASSUMPTION: Any field that is
	 * not obviously case insensitive is assumed to be case sensitive
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return true if so
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isCaseSensitive(int column) throws SQLException
	{
		int sql_type = getField(column).getSQLType();

		switch (sql_type)
		{
			case Types.SMALLINT:
			case Types.INTEGER:
			case Types.FLOAT:
			case Types.REAL:
			case Types.DOUBLE:
			case Types.DATE:
			case Types.TIME:
			case Types.TIMESTAMP:
				return false;
			default:
				return true;
		}
	}

	/*
	 * Can the column be used in a WHERE clause?  Basically for
	 * this, I split the functions into two types: recognised
	 * types (which are always useable), and OTHER types (which
	 * may or may not be useable).	The OTHER types, for now, I
	 * will assume they are useable.  We should really query the
	 * catalog to see if they are useable.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return true if they can be used in a WHERE clause
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isSearchable(int column) throws SQLException
	{
		int sql_type = getField(column).getSQLType();

		// This switch is pointless, I know - but it is a set-up
		// for further expansion.
		switch (sql_type)
		{
			case Types.OTHER:
				return true;
			default:
				return true;
		}
	}

	/*
	 * Is the column a cash value?	6.1 introduced the cash/money
	 * type, which haven't been incorporated as of 970414, so I
	 * just check the type name for both 'cash' and 'money'
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return true if its a cash column
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isCurrency(int column) throws SQLException
	{
		String type_name = getField(column).getPGType();

		return type_name.equals("cash") || type_name.equals("money");
	}

	/*
	 * Indicates the nullability of values in the designated column.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return one of the columnNullable values
	 * @exception SQLException if a database access error occurs
	 */
	public int isNullable(int column) throws SQLException
	{
		/*
		 * TODO This needs a real implementation, taking into account columns
		 * defined with NOT NULL or PRIMARY KEY, CHECK constraints, views,
		 * functions etc.
		 */
		return columnNullableUnknown;
	}

	/*
	 * Is the column a signed number? In PostgreSQL, all numbers
	 * are signed, so this is trivial.	However, strings are not
	 * signed (duh!)
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return true if so
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isSigned(int column) throws SQLException
	{
		int sql_type = getField(column).getSQLType();

		switch (sql_type)
		{
			case Types.SMALLINT:
			case Types.INTEGER:
			case Types.FLOAT:
			case Types.REAL:
			case Types.DOUBLE:
				return true;
			case Types.DATE:
			case Types.TIME:
			case Types.TIMESTAMP:
				return false;	// I don't know about these?
			default:
				return false;
		}
	}

	/*
	 * What is the column's normal maximum width in characters?
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return the maximum width
	 * @exception SQLException if a database access error occurs
	 */
	public int getColumnDisplaySize(int column) throws SQLException
	{
		Field f = getField(column);
		String type_name = f.getPGType();
		int sql_type = f.getSQLType();
		int typmod = f.getMod();

		// I looked at other JDBC implementations and couldn't find a consistent
		// interpretation of the "display size" for numeric values, so this is our's
		// FIXME: currently, only types with a SQL92 or SQL3 pendant are implemented - jens@jens.de

		// fixed length data types
		if (type_name.equals( "int2" ))
			return 6;  // -32768 to +32768 (5 digits and a sign)
		if (type_name.equals( "int4" )
				|| type_name.equals( "oid" ))
			return 11; // -2147483648 to +2147483647
		if (type_name.equals( "int8" ))
			return 20; // -9223372036854775808 to +9223372036854775807
		if (type_name.equals( "money" ))
			return 12; // MONEY = DECIMAL(9,2)
		if (type_name.equals( "float4" ))
			return 11; // i checked it out ans wasn't able to produce more than 11 digits
		if (type_name.equals( "float8" ))
			return 20; // dito, 20
		if (type_name.equals( "char" ))
			return 1;
		if (type_name.equals( "bool" ))
			return 1;
		if (type_name.equals( "date" ))
			return 14; // "01/01/4713 BC" - "31/12/32767 AD"
		if (type_name.equals( "time" ))
			return 8;  // 00:00:00-23:59:59
		if (type_name.equals( "timestamp" ))
			return 22; // hhmmm ... the output looks like this: 1999-08-03 22:22:08+02

		// variable length fields
		typmod -= 4;
		if (type_name.equals( "bpchar" )
				|| type_name.equals( "varchar" ))
			return typmod; // VARHDRSZ=sizeof(int32)=4
		if (type_name.equals( "numeric" ))
			return ( (typmod >> 16) & 0xffff )
				   + 1 + ( typmod & 0xffff ); // DECIMAL(p,s) = (p digits).(s digits)

		// if we don't know better
		return f.getLength();
	}

	/*
	 * What is the suggested column title for use in printouts and
	 * displays?  We suggest the ColumnName!
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return the column label
	 * @exception SQLException if a database access error occurs
	 */
	public String getColumnLabel(int column) throws SQLException
	{
		return getColumnName(column);
	}

	/*
	 * What's a column's name?
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return the column name
	 * @exception SQLException if a database access error occurs
	 */
	public String getColumnName(int column) throws SQLException
	{
		Field f = getField(column);
		if (f != null)
			return f.getName();
		return "field" + column;
	}

	/*
	 * What is a column's table's schema?  This relies on us knowing
	 * the table name....which I don't know how to do as yet.  The 
	 * JDBC specification allows us to return "" if this is not
	 * applicable.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return the Schema
	 * @exception SQLException if a database access error occurs
	 */
	public String getSchemaName(int column) throws SQLException
	{
		return "";
	}

	/*
	 * What is a column's number of decimal digits.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return the precision
	 * @exception SQLException if a database access error occurs
	 */
	public int getPrecision(int column) throws SQLException
	{
		int sql_type = getField(column).getSQLType();

		switch (sql_type)
		{
			case Types.SMALLINT:
				return 5;
			case Types.INTEGER:
				return 10;
			case Types.REAL:
				return 8;
			case Types.FLOAT:
				return 16;
			case Types.DOUBLE:
				return 16;
			case Types.VARCHAR:
				return 0;
			case Types.NUMERIC:
				Field f = getField(column);
				if (f != null)
					return ((0xFFFF0000)&f.getMod()) >> 16;
				else
					return 0;
			default:
				return 0;
		}
	}

	/*
	 * What is a column's number of digits to the right of the
	 * decimal point?
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return the scale
	 * @exception SQLException if a database access error occurs
	 */
	public int getScale(int column) throws SQLException
	{
		int sql_type = getField(column).getSQLType();

		switch (sql_type)
		{
			case Types.SMALLINT:
				return 0;
			case Types.INTEGER:
				return 0;
			case Types.REAL:
				return 8;
			case Types.FLOAT:
				return 16;
			case Types.DOUBLE:
				return 16;
			case Types.VARCHAR:
				return 0;
			case Types.NUMERIC:
				Field f = getField(column);
				if (f != null)
					return (((0x0000FFFF)&f.getMod()) - 4);
				else
					return 0;
			default:
				return 0;
		}
	}

	/*
	 * Whats a column's table's name?  How do I find this out?	Both
	 * getSchemaName() and getCatalogName() rely on knowing the table
	 * Name, so we need this before we can work on them.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return column name, or "" if not applicable
	 * @exception SQLException if a database access error occurs
	 */
	public String getTableName(int column) throws SQLException
	{
		return "";
	}

	/*
	 * What's a column's table's catalog name?  As with getSchemaName(),
	 * we can say that if getTableName() returns n/a, then we can too -
	 * otherwise, we need to work on it.
	 *
	 * @param column the first column is 1, the second is 2...
	 * @return catalog name, or "" if not applicable
	 * @exception SQLException if a database access error occurs
	 */
	public String getCatalogName(int column) throws SQLException
	{
		return "";
	}

	/*
	 * What is a column's SQL Type? (java.sql.Type int)
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return the java.sql.Type value
	 * @exception SQLException if a database access error occurs
	 * @see org.postgresql.Field#getSQLType
	 * @see java.sql.Types
	 */
	public int getColumnType(int column) throws SQLException
	{
		return getField(column).getSQLType();
	}

	/*
	 * Whats is the column's data source specific type name?
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return the type name
	 * @exception SQLException if a database access error occurs
	 */
	public String getColumnTypeName(int column) throws SQLException
	{
		return getField(column).getPGType();
	}

	/**
	 * Is the column definitely not writable?  In reality, we would
	 * have to check the GRANT/REVOKE stuff for this to be effective,
	 * and I haven't really looked into that yet, so this will get
	 * re-visited.
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return true if so
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isReadOnly(int column) throws SQLException
	{
		return false;
	}

	/**
	 * Is it possible for a write on the column to succeed?  Again, we
	 * would in reality have to check the GRANT/REVOKE stuff, which
	 * I haven't worked with as yet.  However, if it isn't ReadOnly, then
	 * it is obviously writable.
	 *
	 * @param column the first column is 1, the second is 2, etc.
	 * @return true if so
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isWritable(int column) throws SQLException
	{
		return !isReadOnly(column);
	}

	/**
	 * Will a write on this column definately succeed?	Hmmm...this
	 * is a bad one, since the two preceding functions have not been
	 * really defined.	I cannot tell is the short answer.	I thus
	 * return isWritable() just to give us an idea.
	 *
	 * @param column the first column is 1, the second is 2, etc..
	 * @return true if so
	 * @exception SQLException if a database access error occurs
	 */
	public boolean isDefinitelyWritable(int column) throws SQLException
	{
		return false;
	}

	// ********************************************************
	//	END OF PUBLIC INTERFACE
	// ********************************************************

	/**
	 * For several routines in this package, we need to convert
	 * a columnIndex into a Field[] descriptor.  Rather than do
	 * the same code several times, here it is.
	 *
	 * @param columnIndex the first column is 1, the second is 2...
	 * @return the Field description
	 * @exception SQLException if a database access error occurs
	 */
	private Field getField(int columnIndex) throws SQLException
	{
		if (columnIndex < 1 || columnIndex > fields.length)
			throw new PSQLException("postgresql.res.colrange");
		return fields[columnIndex - 1];
	}

	// ** JDBC 2 Extensions **

	// This can hook into our PG_Object mechanism
	public String getColumnClassName(int column) throws SQLException
	{
 /*
     The following data type mapping came from ../Field.java.

     "int2",
     "int4","oid",
     "int8",
     "cash","money",
     "numeric",
     "float4",
     "float8",
     "bpchar","char","char2","char4","char8","char16",
     "varchar","text","name","filename",
     "bool",
     "date",
     "time",
     "abstime","timestamp"

     Types.SMALLINT,
     Types.INTEGER,Types.INTEGER,
     Types.BIGINT,
     Types.DOUBLE,Types.DOUBLE,
     Types.NUMERIC,
     Types.REAL,
     Types.DOUBLE,
     Types.CHAR,Types.CHAR,Types.CHAR,Types.CHAR,Types.CHAR,Types.CHAR,
     Types.VARCHAR,Types.VARCHAR,Types.VARCHAR,Types.VARCHAR,
     Types.BIT,
     Types.DATE,
     Types.TIME,
     Types.TIMESTAMP,Types.TIMESTAMP
 */

	int sql_type = getField(column).getSQLType();

     	switch (sql_type)
        {
       		case Types.BIT:
         		return("java.lang.Boolean");
       		case Types.SMALLINT:
         		return("java.lang.Integer");
       		case Types.INTEGER:
         		return("java.lang.Integer");
       		case Types.BIGINT:
         		return("java.lang.Long");
      		case Types.NUMERIC:
         		return("java.math.BigDecimal");
       		case Types.REAL:
         		return("java.lang.Float");
       		case Types.DOUBLE:
         		return("java.lang.Double");
       		case Types.CHAR:
       		case Types.VARCHAR:
         		return("java.lang.String");
       		case Types.DATE:
       		case Types.TIME:
       		case Types.TIMESTAMP:
         		return("java.sql.Timestamp");
       		default:
	 		throw org.postgresql.Driver.notImplemented();
       }
   }       
}

