/*
 * Copyright (c) 2004, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.jdbc;

import org.postgresql.PGResultSetMetaData;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.Field;
import org.postgresql.core.ServerVersion;
import org.postgresql.util.GT;
import org.postgresql.util.Gettable;
import org.postgresql.util.GettableHashMap;
import org.postgresql.util.JdbcBlackHole;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

public class PgResultSetMetaData implements ResultSetMetaData, PGResultSetMetaData {
  protected final BaseConnection connection;
  protected final Field[] fields;

  private boolean fieldInfoFetched;

  
Initialise for a result with a tuple set and a field descriptor set
Params:
  • connection – the connection to retrieve metadata
  • fields – the array of field descriptors
/** * Initialise for a result with a tuple set and a field descriptor set * * @param connection the connection to retrieve metadata * @param fields the array of field descriptors */
public PgResultSetMetaData(BaseConnection connection, Field[] fields) { this.connection = connection; this.fields = fields; this.fieldInfoFetched = false; } public int getColumnCount() throws SQLException { return fields.length; }
{@inheritDoc}

It is believed that PostgreSQL does not support this feature.

Params:
  • column – the first column is 1, the second is 2...
Throws:
Returns:true if so
/** * {@inheritDoc} * * <p>It is believed 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 { fetchFieldMetaData(); Field field = getField(column); FieldMetadata metadata = field.getMetadata(); return metadata != null && metadata.autoIncrement; }
{@inheritDoc}

Does a column's case matter? ASSUMPTION: Any field that is not obviously case insensitive is assumed to be case sensitive

Params:
  • column – the first column is 1, the second is 2...
Throws:
Returns:true if so
/** * {@inheritDoc} * * <p>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 { Field field = getField(column); return connection.getTypeInfo().isCaseSensitive(field.getOID()); }
{@inheritDoc}

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.

Params:
  • column – the first column is 1, the second is 2...
Throws:
Returns:true if they can be used in a WHERE clause
/** * {@inheritDoc} * * <p>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 */
@Override public boolean isSearchable(int column) throws SQLException { return true; }
{@inheritDoc}

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'

Params:
  • column – the first column is 1, the second is 2...
Throws:
Returns:true if its a cash column
/** * {@inheritDoc} * * <p>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 */
@Override public boolean isCurrency(int column) throws SQLException { String typeName = getPGType(column); return typeName.equals("cash") || typeName.equals("money"); } @Override public int isNullable(int column) throws SQLException { fetchFieldMetaData(); Field field = getField(column); return field.getMetadata().nullable; }
{@inheritDoc}

Is the column a signed number? In PostgreSQL, all numbers are signed, so this is trivial. However, strings are not signed (duh!)

Params:
  • column – the first column is 1, the second is 2...
Throws:
Returns:true if so
/** * {@inheritDoc} * * <p>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 { Field field = getField(column); return connection.getTypeInfo().isSigned(field.getOID()); } public int getColumnDisplaySize(int column) throws SQLException { Field field = getField(column); return connection.getTypeInfo().getDisplaySize(field.getOID(), field.getMod()); } public String getColumnLabel(int column) throws SQLException { Field field = getField(column); return field.getColumnLabel(); } public String getColumnName(int column) throws SQLException { return getColumnLabel(column); } public String getBaseColumnName(int column) throws SQLException { Field field = getField(column); if (field.getTableOid() == 0) { return ""; } fetchFieldMetaData(); return field.getMetadata().columnName; } public String getSchemaName(int column) throws SQLException { return ""; } private boolean populateFieldsWithMetadata(Gettable<FieldMetadata.Key, FieldMetadata> metadata) { boolean allOk = true; for (Field field : fields) { if (field.getMetadata() != null) { // No need to update metadata continue; } final FieldMetadata fieldMetadata = metadata.get(new FieldMetadata.Key(field.getTableOid(), field.getPositionInTable())); if (fieldMetadata == null) { allOk = false; } else { field.setMetadata(fieldMetadata); } } fieldInfoFetched |= allOk; return allOk; } private void fetchFieldMetaData() throws SQLException { if (fieldInfoFetched) { return; } if (populateFieldsWithMetadata(connection.getFieldMetadataCache())) { return; } StringBuilder sql = new StringBuilder( "SELECT c.oid, a.attnum, a.attname, c.relname, n.nspname, " + "a.attnotnull OR (t.typtype = 'd' AND t.typnotnull), "); if ( connection.haveMinimumServerVersion(ServerVersion.v10)) { sql.append("a.attidentity != '' OR pg_catalog.pg_get_expr(d.adbin, d.adrelid) LIKE '%nextval(%' "); } else { sql.append("pg_catalog.pg_get_expr(d.adbin, d.adrelid) LIKE '%nextval(%' "); } sql.append( "FROM pg_catalog.pg_class c " + "JOIN pg_catalog.pg_namespace n ON (c.relnamespace = n.oid) " + "JOIN pg_catalog.pg_attribute a ON (c.oid = a.attrelid) " + "JOIN pg_catalog.pg_type t ON (a.atttypid = t.oid) " + "LEFT JOIN pg_catalog.pg_attrdef d ON (d.adrelid = a.attrelid AND d.adnum = a.attnum) " + "JOIN ("); // 7.4 servers don't support row IN operations (a,b) IN ((c,d),(e,f)) // so we've got to fake that with a JOIN here. // boolean hasSourceInfo = false; for (Field field : fields) { if (field.getMetadata() != null) { continue; } if (hasSourceInfo) { sql.append(" UNION ALL "); } sql.append("SELECT "); sql.append(field.getTableOid()); if (!hasSourceInfo) { sql.append(" AS oid "); } sql.append(", "); sql.append(field.getPositionInTable()); if (!hasSourceInfo) { sql.append(" AS attnum"); } if (!hasSourceInfo) { hasSourceInfo = true; } } sql.append(") vals ON (c.oid = vals.oid AND a.attnum = vals.attnum) "); if (!hasSourceInfo) { fieldInfoFetched = true; return; } Statement stmt = connection.createStatement(); ResultSet rs = null; GettableHashMap<FieldMetadata.Key, FieldMetadata> md = new GettableHashMap<FieldMetadata.Key, FieldMetadata>(); try { rs = stmt.executeQuery(sql.toString()); while (rs.next()) { int table = (int) rs.getLong(1); int column = (int) rs.getLong(2); String columnName = rs.getString(3); String tableName = rs.getString(4); String schemaName = rs.getString(5); int nullable = rs.getBoolean(6) ? ResultSetMetaData.columnNoNulls : ResultSetMetaData.columnNullable; boolean autoIncrement = rs.getBoolean(7); FieldMetadata fieldMetadata = new FieldMetadata(columnName, tableName, schemaName, nullable, autoIncrement); FieldMetadata.Key key = new FieldMetadata.Key(table, column); md.put(key, fieldMetadata); } } finally { JdbcBlackHole.close(rs); JdbcBlackHole.close(stmt); } populateFieldsWithMetadata(md); connection.getFieldMetadataCache().putAll(md); } public String getBaseSchemaName(int column) throws SQLException { fetchFieldMetaData(); Field field = getField(column); return field.getMetadata().schemaName; } public int getPrecision(int column) throws SQLException { Field field = getField(column); return connection.getTypeInfo().getPrecision(field.getOID(), field.getMod()); } public int getScale(int column) throws SQLException { Field field = getField(column); return connection.getTypeInfo().getScale(field.getOID(), field.getMod()); } public String getTableName(int column) throws SQLException { return getBaseTableName(column); } public String getBaseTableName(int column) throws SQLException { fetchFieldMetaData(); Field field = getField(column); return field.getMetadata().tableName; }
{@inheritDoc}

As with getSchemaName(), we can say that if getTableName() returns n/a, then we can too - otherwise, we need to work on it.

Params:
  • column – the first column is 1, the second is 2...
Throws:
Returns:catalog name, or "" if not applicable
/** * {@inheritDoc} * * <p>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 ""; } public int getColumnType(int column) throws SQLException { return getSQLType(column); } public int getFormat(int column) throws SQLException { return getField(column).getFormat(); } public String getColumnTypeName(int column) throws SQLException { String type = getPGType(column); if (isAutoIncrement(column)) { if ("int4".equals(type)) { return "serial"; } else if ("int8".equals(type)) { return "bigserial"; } } return type; }
{@inheritDoc}

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.

Params:
  • column – the first column is 1, the second is 2, etc.*
Throws:
Returns:true if so*
/** * {@inheritDoc} * * <p>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; }
{@inheritDoc}

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.

Params:
  • column – the first column is 1, the second is 2, etc.
Throws:
Returns:true if so
/** * {@inheritDoc} * * <p>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); }
{@inheritDoc}

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.

Params:
  • column – the first column is 1, the second is 2, etc..
Throws:
Returns:true if so
/** * {@inheritDoc} * * <p>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.
Params:
  • columnIndex – the first column is 1, the second is 2...
Throws:
Returns:the Field description
/** * 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 */
protected Field getField(int columnIndex) throws SQLException { if (columnIndex < 1 || columnIndex > fields.length) { throw new PSQLException( GT.tr("The column index is out of range: {0}, number of columns: {1}.", columnIndex, fields.length), PSQLState.INVALID_PARAMETER_VALUE); } return fields[columnIndex - 1]; } protected String getPGType(int columnIndex) throws SQLException { return connection.getTypeInfo().getPGType(getField(columnIndex).getOID()); } protected int getSQLType(int columnIndex) throws SQLException { return connection.getTypeInfo().getSQLType(getField(columnIndex).getOID()); } // ** JDBC 2 Extensions ** // This can hook into our PG_Object mechanism public String getColumnClassName(int column) throws SQLException { Field field = getField(column); String result = connection.getTypeInfo().getJavaClass(field.getOID()); if (result != null) { return result; } int sqlType = getSQLType(column); switch (sqlType) { case Types.ARRAY: return ("java.sql.Array"); default: String type = getPGType(column); if ("unknown".equals(type)) { return ("java.lang.String"); } return ("java.lang.Object"); } } public boolean isWrapperFor(Class<?> iface) throws SQLException { return iface.isAssignableFrom(getClass()); } public <T> T unwrap(Class<T> iface) throws SQLException { if (iface.isAssignableFrom(getClass())) { return iface.cast(this); } throw new SQLException("Cannot unwrap to " + iface.getName()); } }