/* Copyright (c) 2002, 2012, Oracle and/or its affiliates. All rights reserved. The MySQL Connector/J is licensed under the terms of the GPLv2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors. There are special exceptions to the terms and conditions of the GPLv2 as it is applied to this software, see the FLOSS License Exception <http://www.mysql.com/about/legal/licensing/foss-exception.html>. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package com.mysql.jdbc; import java.sql.SQLException; import java.sql.Types; /** * A ResultSetMetaData object can be used to find out about the types and * properties of the columns in a ResultSet * * @author Mark Matthews * @version $Id: ResultSetMetaData.java,v 1.1.2.1 2005/05/13 18:58:38 mmatthews * Exp $ * * @see java.sql.ResultSetMetaData */ public class ResultSetMetaData implements java.sql.ResultSetMetaData { private static int clampedGetLength(Field f) { long fieldLength = f.getLength(); if (fieldLength > Integer.MAX_VALUE) { fieldLength = Integer.MAX_VALUE; } return (int) fieldLength; } /** * Checks if the SQL Type is a Decimal/Number Type * * @param type * SQL Type * * @return ... */ private static final boolean isDecimalType(int type) { switch (type) { case Types.BIT: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.FLOAT: case Types.REAL: case Types.DOUBLE: case Types.NUMERIC: case Types.DECIMAL: return true; } return false; } Field[] fields; boolean useOldAliasBehavior = false; private ExceptionInterceptor exceptionInterceptor; /** * Initialise for a result with a tuple set and a field descriptor set * * @param fields * the array of field descriptors */ public ResultSetMetaData(Field[] fields, boolean useOldAliasBehavior, ExceptionInterceptor exceptionInterceptor) { this.fields = fields; this.useOldAliasBehavior = useOldAliasBehavior; this.exceptionInterceptor = exceptionInterceptor; } /** * What's a column's table's catalog name? * * @param column * the first column is 1, the second is 2... * * @return catalog name, or "" if not applicable * * @throws SQLException * if a database access error occurs */ public String getCatalogName(int column) throws SQLException { Field f = getField(column); String database = f.getDatabaseName(); return (database == null) ? "" : database; //$NON-NLS-1$ } /** * What's the Java character encoding name for the given column? * * @param column * the first column is 1, the second is 2, etc. * * @return the Java character encoding name for the given column, or null if * no Java character encoding maps to the MySQL character set for * the given column. * * @throws SQLException * if an invalid column index is given. */ public String getColumnCharacterEncoding(int column) throws SQLException { String mysqlName = getColumnCharacterSet(column); String javaName = null; if (mysqlName != null) { try { javaName = CharsetMapping.MYSQL_TO_JAVA_CHARSET_MAP.get(mysqlName); } catch (RuntimeException ex) { SQLException sqlEx = SQLError.createSQLException(ex.toString(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null); sqlEx.initCause(ex); throw sqlEx; } } return javaName; } /** * What's the MySQL character set name for the given column? * * @param column * the first column is 1, the second is 2, etc. * * @return the MySQL character set name for the given column * * @throws SQLException * if an invalid column index is given. */ public String getColumnCharacterSet(int column) throws SQLException { return getField(column).getCharacterSet(); } // --------------------------JDBC 2.0----------------------------------- /** * JDBC 2.0 * * <p> * Return the fully qualified name of the Java class whose instances are * manufactured if ResultSet.getObject() is called to retrieve a value from * the column. ResultSet.getObject() may return a subClass of the class * returned by this method. * </p> * * @param column * the column number to retrieve information for * * @return the fully qualified name of the Java class whose instances are * manufactured if ResultSet.getObject() is called to retrieve a * value from the column. * * @throws SQLException * if an error occurs */ public String getColumnClassName(int column) throws SQLException { Field f = getField(column); return getClassNameForJavaType(f.getSQLType(), f.isUnsigned(), f.getMysqlType(), f.isBinary() || f.isBlob(), f.isOpaqueBinary()); } /** * Whats the number of columns in the ResultSet? * * @return the number * * @throws SQLException * if a database access error occurs */ public int getColumnCount() throws SQLException { return this.fields.length; } /** * 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 * * @throws SQLException * if a database access error occurs */ public int getColumnDisplaySize(int column) throws SQLException { Field f = getField(column); int lengthInBytes = clampedGetLength(f); return lengthInBytes / f.getMaxBytesPerCharacter(); } /** * What is the suggested column title for use in printouts and displays? * * @param column * the first column is 1, the second is 2, etc. * * @return the column label * * @throws SQLException * if a database access error occurs */ public String getColumnLabel(int column) throws SQLException { if (this.useOldAliasBehavior) { return getColumnName(column); } return getField(column).getColumnLabel(); } /** * What's a column's name? * * @param column * the first column is 1, the second is 2, etc. * * @return the column name * * @throws SQLException * if a databvase access error occurs */ public String getColumnName(int column) throws SQLException { if (this.useOldAliasBehavior) { return getField(column).getName(); } String name = getField(column).getNameNoAliases(); if (name != null && name.length() == 0) { return getField(column).getName(); } return name; } /** * 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 * * @throws SQLException * if a database access error occurs * * @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 * * @throws SQLException * if a database access error occurs */ public String getColumnTypeName(int column) throws java.sql.SQLException { Field field = getField(column); int mysqlType = field.getMysqlType(); int jdbcType = field.getSQLType(); switch (mysqlType) { case MysqlDefs.FIELD_TYPE_BIT: return "BIT"; case MysqlDefs.FIELD_TYPE_DECIMAL: case MysqlDefs.FIELD_TYPE_NEW_DECIMAL: return field.isUnsigned() ? "DECIMAL UNSIGNED" : "DECIMAL"; case MysqlDefs.FIELD_TYPE_TINY: return field.isUnsigned() ? "TINYINT UNSIGNED" : "TINYINT"; case MysqlDefs.FIELD_TYPE_SHORT: return field.isUnsigned() ? "SMALLINT UNSIGNED" : "SMALLINT"; case MysqlDefs.FIELD_TYPE_LONG: return field.isUnsigned() ? "INT UNSIGNED" : "INT"; case MysqlDefs.FIELD_TYPE_FLOAT: return field.isUnsigned() ? "FLOAT UNSIGNED" : "FLOAT"; case MysqlDefs.FIELD_TYPE_DOUBLE: return field.isUnsigned() ? "DOUBLE UNSIGNED" : "DOUBLE"; case MysqlDefs.FIELD_TYPE_NULL: return "NULL"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_TIMESTAMP: return "TIMESTAMP"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_LONGLONG: return field.isUnsigned() ? "BIGINT UNSIGNED" : "BIGINT"; case MysqlDefs.FIELD_TYPE_INT24: return field.isUnsigned() ? "MEDIUMINT UNSIGNED" : "MEDIUMINT"; case MysqlDefs.FIELD_TYPE_DATE: return "DATE"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_TIME: return "TIME"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_DATETIME: return "DATETIME"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_TINY_BLOB: return "TINYBLOB"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_MEDIUM_BLOB: return "MEDIUMBLOB"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_LONG_BLOB: return "LONGBLOB"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_BLOB: if (getField(column).isBinary()) { return "BLOB";//$NON-NLS-1$ } return "TEXT";//$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_VARCHAR: return "VARCHAR"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_VAR_STRING: if (jdbcType == Types.VARBINARY) { return "VARBINARY"; } return "VARCHAR"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_STRING: if (jdbcType == Types.BINARY) { return "BINARY"; } return "CHAR"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_ENUM: return "ENUM"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_YEAR: return "YEAR"; // $NON_NLS-1$ case MysqlDefs.FIELD_TYPE_SET: return "SET"; //$NON-NLS-1$ case MysqlDefs.FIELD_TYPE_GEOMETRY: return "GEOMETRY"; //$NON-NLS-1$ default: return "UNKNOWN"; //$NON-NLS-1$ } } /** * Returns the field instance for the given column index * * @param columnIndex * the column number to retrieve a field instance for * * @return the field instance for the given column index * * @throws SQLException * if an error occurs */ protected Field getField(int columnIndex) throws SQLException { if ((columnIndex < 1) || (columnIndex > this.fields.length)) { throw SQLError.createSQLException(Messages.getString("ResultSetMetaData.46"), //$NON-NLS-1$ SQLError.SQL_STATE_INVALID_COLUMN_NUMBER, this.exceptionInterceptor); } return this.fields[columnIndex - 1]; } /** * What is a column's number of decimal digits. * * @param column * the first column is 1, the second is 2... * * @return the precision * * @throws SQLException * if a database access error occurs */ public int getPrecision(int column) throws SQLException { Field f = getField(column); // if (f.getMysqlType() == MysqlDefs.FIELD_TYPE_NEW_DECIMAL) { // return f.getLength(); // } if (isDecimalType(f.getSQLType())) { if (f.getDecimals() > 0) { return clampedGetLength(f) - 1 + f.getPrecisionAdjustFactor(); } return clampedGetLength(f) + f.getPrecisionAdjustFactor(); } switch (f.getMysqlType()) { case MysqlDefs.FIELD_TYPE_TINY_BLOB: case MysqlDefs.FIELD_TYPE_BLOB: case MysqlDefs.FIELD_TYPE_MEDIUM_BLOB: case MysqlDefs.FIELD_TYPE_LONG_BLOB: return clampedGetLength(f); // this may change in the future // for now, the server only // returns FIELD_TYPE_BLOB for _all_ // BLOB types, but varying lengths // indicating the _maximum_ size // for each BLOB type. default: return clampedGetLength(f) / f.getMaxBytesPerCharacter(); } } /** * 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 * * @throws SQLException * if a database access error occurs */ public int getScale(int column) throws SQLException { Field f = getField(column); if (isDecimalType(f.getSQLType())) { return f.getDecimals(); } return 0; } /** * What is a column's table's schema? This relies on us knowing the table * name. 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 * * @throws SQLException * if a database access error occurs */ public String getSchemaName(int column) throws SQLException { return ""; //$NON-NLS-1$ } /** * Whats a column's table's name? * * @param column * the first column is 1, the second is 2... * * @return column name, or "" if not applicable * * @throws SQLException * if a database access error occurs */ public String getTableName(int column) throws SQLException { if (this.useOldAliasBehavior) { return getField(column).getTableName(); } return getField(column).getTableNameNoAliases(); } /** * Is the column automatically numbered (and thus read-only) * * @param column * the first column is 1, the second is 2... * * @return true if so * * @throws SQLException * if a database access error occurs */ public boolean isAutoIncrement(int column) throws SQLException { Field f = getField(column); return f.isAutoIncrement(); } /** * Does a column's case matter? * * @param column * the first column is 1, the second is 2... * * @return true if so * * @throws java.sql.SQLException * if a database access error occurs */ public boolean isCaseSensitive(int column) throws java.sql.SQLException { Field field = getField(column); int sqlType = field.getSQLType(); switch (sqlType) { case Types.BIT: case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.FLOAT: case Types.REAL: case Types.DOUBLE: case Types.DATE: case Types.TIME: case Types.TIMESTAMP: return false; case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: if (field.isBinary()) { return true; } String collationName = field.getCollation(); return ((collationName != null) && !collationName.endsWith("_ci")); default: return true; } } /** * Is the column a cash value? * * @param column * the first column is 1, the second is 2... * * @return true if its a cash column * * @throws SQLException * if a database access error occurs */ public boolean isCurrency(int column) throws SQLException { return false; } /** * Will a write on this column definately succeed? * * @param column * the first column is 1, the second is 2, etc.. * * @return true if so * * @throws SQLException * if a database access error occurs */ public boolean isDefinitelyWritable(int column) throws SQLException { return isWritable(column); } /** * Can you put a NULL in this column? * * @param column * the first column is 1, the second is 2... * * @return one of the columnNullable values * * @throws SQLException * if a database access error occurs */ public int isNullable(int column) throws SQLException { if (!getField(column).isNotNull()) { return java.sql.ResultSetMetaData.columnNullable; } return java.sql.ResultSetMetaData.columnNoNulls; } /** * Is the column definitely not writable? * * @param column * the first column is 1, the second is 2, etc. * * @return true if so * * @throws SQLException * if a database access error occurs */ public boolean isReadOnly(int column) throws SQLException { return getField(column).isReadOnly(); } /** * 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 * * @throws SQLException * if a database access error occurs */ public boolean isSearchable(int column) throws SQLException { return true; } /** * Is the column a signed number? * * @param column * the first column is 1, the second is 2... * * @return true if so * * @throws SQLException * if a database access error occurs */ public boolean isSigned(int column) throws SQLException { Field f = getField(column); int sqlType = f.getSQLType(); switch (sqlType) { case Types.TINYINT: case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.FLOAT: case Types.REAL: case Types.DOUBLE: case Types.NUMERIC: case Types.DECIMAL: return !f.isUnsigned(); case Types.DATE: case Types.TIME: case Types.TIMESTAMP: return false; default: return false; } } /** * Is it possible for a write on the column to succeed? * * @param column * the first column is 1, the second is 2, etc. * * @return true if so * * @throws SQLException * if a database access error occurs */ public boolean isWritable(int column) throws SQLException { return !isReadOnly(column); } /** * Returns a string representation of this object * * @return ... */ public String toString() { StringBuffer toStringBuf = new StringBuffer(); toStringBuf.append(super.toString()); toStringBuf.append(" - Field level information: "); //$NON-NLS-1$ for (int i = 0; i < this.fields.length; i++) { toStringBuf.append("\n\t"); //$NON-NLS-1$ toStringBuf.append(this.fields[i].toString()); } return toStringBuf.toString(); } static String getClassNameForJavaType(int javaType, boolean isUnsigned, int mysqlTypeIfKnown, boolean isBinaryOrBlob, boolean isOpaqueBinary) { switch (javaType) { case Types.BIT: case Types.BOOLEAN: return "java.lang.Boolean"; //$NON-NLS-1$ case Types.TINYINT: if (isUnsigned) { return "java.lang.Integer"; //$NON-NLS-1$ } return "java.lang.Integer"; //$NON-NLS-1$ case Types.SMALLINT: if (isUnsigned) { return "java.lang.Integer"; //$NON-NLS-1$ } return "java.lang.Integer"; //$NON-NLS-1$ case Types.INTEGER: if (!isUnsigned || mysqlTypeIfKnown == MysqlDefs.FIELD_TYPE_INT24) { return "java.lang.Integer"; //$NON-NLS-1$ } return "java.lang.Long"; //$NON-NLS-1$ case Types.BIGINT: if (!isUnsigned) { return "java.lang.Long"; //$NON-NLS-1$ } return "java.math.BigInteger"; //$NON-NLS-1$ case Types.DECIMAL: case Types.NUMERIC: return "java.math.BigDecimal"; //$NON-NLS-1$ case Types.REAL: return "java.lang.Float"; //$NON-NLS-1$ case Types.FLOAT: case Types.DOUBLE: return "java.lang.Double"; //$NON-NLS-1$ case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: if (!isOpaqueBinary) { return "java.lang.String"; //$NON-NLS-1$ } return "[B"; case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: if (mysqlTypeIfKnown == MysqlDefs.FIELD_TYPE_GEOMETRY) { return "[B"; } else if (isBinaryOrBlob) { return "[B"; } else { return "java.lang.String"; } case Types.DATE: return "java.sql.Date"; //$NON-NLS-1$ case Types.TIME: return "java.sql.Time"; //$NON-NLS-1$ case Types.TIMESTAMP: return "java.sql.Timestamp"; //$NON-NLS-1$ default: return "java.lang.Object"; //$NON-NLS-1$ } } /** * Returns true if this either implements the interface argument or is directly or indirectly a wrapper * for an object that does. Returns false otherwise. If this implements the interface then return true, * else if this is a wrapper then return the result of recursively calling <code>isWrapperFor</code> on the wrapped * object. If this does not implement the interface and is not a wrapper, return false. * This method should be implemented as a low-cost operation compared to <code>unwrap</code> so that * callers can use this method to avoid expensive <code>unwrap</code> calls that may fail. If this method * returns true then calling <code>unwrap</code> with the same argument should succeed. * * @param interfaces a Class defining an interface. * @return true if this implements the interface or directly or indirectly wraps an object that does. * @throws java.sql.SQLException if an error occurs while determining whether this is a wrapper * for an object with the given interface. * @since 1.6 */ public boolean isWrapperFor(Class<?> iface) throws SQLException { // This works for classes that aren't actually wrapping // anything return iface.isInstance(this); } /** * Returns an object that implements the given interface to allow access to non-standard methods, * or standard methods not exposed by the proxy. * The result may be either the object found to implement the interface or a proxy for that object. * If the receiver implements the interface then that is the object. If the receiver is a wrapper * and the wrapped object implements the interface then that is the object. Otherwise the object is * the result of calling <code>unwrap</code> recursively on the wrapped object. If the receiver is not a * wrapper and does not implement the interface, then an <code>SQLException</code> is thrown. * * @param iface A Class defining an interface that the result must implement. * @return an object that implements the interface. May be a proxy for the actual implementing object. * @throws java.sql.SQLException If no object found that implements the interface * @since 1.6 */ public Object unwrap(Class<?> iface) throws java.sql.SQLException { try { // This works for classes that aren't actually wrapping // anything return Util.cast(iface, this); } catch (ClassCastException cce) { throw SQLError.createSQLException("Unable to unwrap to " + iface.toString(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor); } } }