org.firebirdsql.jdbc.FBResultSetMetaData Maven / Gradle / Ivy
Show all versions of jaybird-jdk17 Show documentation
/*
* Firebird Open Source JavaEE Connector - JDBC Driver
*
* Distributable under LGPL license.
* You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
*
* 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
* LGPL License for more details.
*
* This file was created by members of the firebird development team.
* All individual contributions remain the Copyright (C) of those
* individuals. Contributors to this file are either listed here or
* can be obtained from a source control history command.
*
* All rights reserved.
*/
package org.firebirdsql.jdbc;
import org.firebirdsql.encodings.EncodingDefinition;
import org.firebirdsql.encodings.IEncodingFactory;
import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.impl.GDSHelper;
import org.firebirdsql.gds.ng.fields.FieldDescriptor;
import org.firebirdsql.gds.ng.fields.RowDescriptor;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.*;
/**
* An object that can be used to get information about the types and properties of the columns in
* a ResultSet
object.
*
* @author David Jencks
* @author Mark Rotteveel
*/
public class FBResultSetMetaData extends AbstractFieldMetaData implements FirebirdResultSetMetaData {
private static final int ID_UNICODE_FSS = 3;
private final ColumnStrategy columnStrategy;
/**
* Creates a new FBResultSetMetaData
instance.
*
* @param rowDescriptor
* a row descriptor
* @param connection
* a FBConnection
value
* @throws SQLException
* if an error occurs
*
* TODO Need another constructor for metadata from constructed
* result set, where we supply the ext field info.
*/
protected FBResultSetMetaData(RowDescriptor rowDescriptor, FBConnection connection) throws SQLException {
super(rowDescriptor, connection);
// Decide how to handle column names and column labels
if (isColumnLabelForName(connection)) {
columnStrategy = ColumnStrategy.COLUMN_LABEL_FOR_NAME;
} else {
columnStrategy = ColumnStrategy.DEFAULT;
}
}
private static boolean isColumnLabelForName(FBConnection connection) throws SQLException {
if (connection == null) {
return false;
}
GDSHelper gdsHelper = connection.getGDSHelper();
return gdsHelper != null && gdsHelper.getConnectionProperties().isColumnLabelForName();
}
/**
* Returns the number of columns in this ResultSet
object.
*
* @return the number of columns
* @throws SQLException
* if a database access error occurs
*/
@Override
public int getColumnCount() throws SQLException {
return getFieldCount();
}
/**
* Indicates whether the designated column is automatically numbered.
*
* The current implementation always returns false
.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isAutoIncrement(int column) throws SQLException {
return false;
}
/**
* Indicates whether a column's case matters.
*
* The current implementation always returns true
.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isCaseSensitive(int column) throws SQLException {
return true;
}
/**
* Indicates whether the designated column can be used in a where clause.
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isSearchable(int column) throws SQLException {
final int sqlType = getFieldDescriptor(column).getType() & ~1;
return !((sqlType == ISCConstants.SQL_ARRAY)
|| (sqlType == ISCConstants.SQL_BLOB));
}
/**
* Indicates whether the designated column is a cash value.
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isCurrency(int column) throws SQLException {
return false;
}
/**
* Indicates the nullability of values in the designated column.
*
* @param column
* the first column is 1, the second is 2, ...
* @return the nullability status of the given column; one of columnNoNulls
,
* columnNullable
or columnNullableUnknown
* @throws SQLException
* if a database access error occurs
*/
@Override
public int isNullable(int column) throws SQLException {
return (getFieldDescriptor(column).getType() & 1) == 1
? ResultSetMetaData.columnNullable
: ResultSetMetaData.columnNoNulls;
}
/**
* Indicates whether values in the designated column are signed numbers.
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isSigned(int column) throws SQLException {
return isSignedInternal(column);
}
/**
* Indicates the designated column's normal maximum width in characters.
*
* @param column
* the first column is 1, the second is 2, ...
* @return the normal maximum number of characters allowed as the width
* of the designated column
* @throws SQLException
* if a database access error occurs
*/
@Override
public int getColumnDisplaySize(int column) throws SQLException {
final int colType = getColumnType(column);
final int precision = getPrecision(column);
switch (colType) {
case Types.DECIMAL:
case Types.NUMERIC:
return precision + 2; // sign + decimal separator
case Types.FLOAT:
case Types.DOUBLE:
return precision + 2; // sign + decimal separator
case Types.INTEGER:
case Types.BIGINT:
case Types.SMALLINT:
return precision + 1; // sign
default:
return precision;
}
}
/**
* Gets the designated column's suggested title for use in printouts and
* displays. The suggested title is usually specified by the SQL AS
* clause. If a SQL AS
is not specified, the value returned from
* getColumnLabel
will be the same as the value returned by the
* getColumnName
method.
*
* @param column
* the first column is 1, the second is 2, ...
* @return the suggested column title
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getColumnLabel(int column) throws SQLException {
return columnStrategy.getColumnLabel(getFieldDescriptor(column));
}
/**
* Get the designated column's name.
*
* @param column
* the first column is 1, the second is 2, ...
* @return column name
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getColumnName(int column) throws SQLException {
return columnStrategy.getColumnName(getFieldDescriptor(column));
}
/**
* Get the designated column's table's schema.
*
* Firebird has no schemas. This method always returns the empty string.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return schema name or "" if not applicable
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getSchemaName(int column) throws SQLException {
return "";
}
/**
* Get the designated column's specified column size.
*
* For numeric data, this is the maximum precision. For character data, this is the length in characters.
* For datetime datatypes, this is the length in characters of the String representation (assuming the
* maximum allowed precision of the fractional seconds component). For binary data, this is the length in bytes.
* For the ROWID datatype, this is the length in bytes. 0 is returned for data types where the
* column size is not applicable.
*
*
* NOTE For NUMERIC
and DECIMAL
we attempt to retrieve the exact precision from the
* metadata, if this is not possible (eg the column is dynamically defined in the query), the reported precision is
* the maximum precision allowed by the underlying storage data type.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return precision
* @throws SQLException
* if a database access error occurs
*/
@Override
public int getPrecision(int column) throws SQLException {
return getPrecisionInternal(column);
}
/**
* Gets the designated column's number of digits to right of the decimal point.
* 0 is returned for data types where the scale is not applicable.
*
* @param column
* the first column is 1, the second is 2, ...
* @return scale
* @throws SQLException
* if a database access error occurs
*/
@Override
public int getScale(int column) throws SQLException {
return getScaleInternal(column);
}
/**
* Gets the designated column's table name.
*
* @param column
* the first column is 1, the second is 2, ...
* @return table name or "" if not applicable
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getTableName(int column) throws SQLException {
String result = getFieldDescriptor(column).getOriginalTableName();
if (result == null) result = "";
return result;
}
@Override
public String getTableAlias(int column) throws SQLException {
String result = getFieldDescriptor(column).getTableAlias();
if (result == null) result = getTableName(column);
return result;
}
/**
* Gets the designated column's table's catalog name.
*
* Firebird has no catalogs. This method always returns the empty string.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return the name of the catalog for the table in which the given column
* appears or "" if not applicable
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getCatalogName(int column) throws SQLException {
return "";
}
/**
* Retrieves the designated column's SQL type.
*
* @param column
* the first column is 1, the second is 2, ...
* @return SQL type from java.sql.Types
* @throws SQLException
* if a database access error occurs
* @see Types
*/
@Override
public int getColumnType(int column) throws SQLException {
return getFieldType(column);
}
/**
* Retrieves the designated column's database-specific type name.
*
* @param column
* the first column is 1, the second is 2, ...
* @return type name used by the database. If the column type is
* a user-defined type, then a fully-qualified type name is returned.
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getColumnTypeName(int column) throws SQLException {
return getFieldTypeName(column);
}
/**
* Indicates whether the designated column is definitely not writable.
*
* The current implementation always returns false
.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isReadOnly(int column) throws SQLException {
// TODO Need to consider privileges!!
return false;
}
/**
* Indicates whether it is possible for a write on the designated column to succeed.
*
* The current implementation always returns true
.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isWritable(int column) throws SQLException {
// TODO Needs privileges?
return true;
}
/**
* Indicates whether a write on the designated column will definitely succeed.
*
* The current implementation always returns true
.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return true
if so; false
otherwise
* @throws SQLException
* if a database access error occurs
*/
@Override
public boolean isDefinitelyWritable(int column) throws SQLException {
// TODO Need to consider privileges!!!
return true;
}
/**
* Returns the fully-qualified name of the Java class whose instances
* are manufactured if the method ResultSet.getObject
* is called to retrieve a value from the column.
*
* ResultSet.getObject
may return a subclass of the
* class returned by this method.
*
*
* @param column
* the first column is 1, the second is 2, ...
* @return the fully-qualified name of the class in the Java programming
* language that would be used by the method
* ResultSet.getObject
to retrieve the value in the specified
* column. This is the class name used for custom mapping.
* @throws SQLException
* if a database access error occurs
*/
@Override
public String getColumnClassName(int column) throws SQLException {
return getFieldClassName(column);
}
//@formatter:off
private static final String GET_FIELD_INFO =
"SELECT "
+ " RF.RDB$RELATION_NAME as RELATION_NAME"
+ ", RF.RDB$FIELD_NAME as FIELD_NAME"
+ ", F.RDB$FIELD_LENGTH as FIELD_LENGTH"
+ ", F.RDB$FIELD_PRECISION as FIELD_PRECISION"
+ ", F.RDB$FIELD_SCALE as FIELD_SCALE"
+ ", F.RDB$FIELD_SUB_TYPE as FIELD_SUB_TYPE"
+ ", F.RDB$CHARACTER_SET_ID as CHARACTER_SET_ID"
+ ", F.RDB$SYSTEM_FLAG as SYSTEM_FLAG"
+ ", F.RDB$CHARACTER_LENGTH as CHAR_LEN"
+ " FROM"
+ " RDB$RELATION_FIELDS RF "
+ ", RDB$FIELDS F "
+ " WHERE "
+ " RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME"
+ " AND"
+ " RF.RDB$FIELD_NAME = ?"
+ " AND"
+ " RF.RDB$RELATION_NAME = ?";
//@formatter:on
@Override
protected Map getExtendedFieldInfo(FBConnection connection) throws SQLException {
if (connection == null) return Collections.emptyMap();
// Apparently there is a limit in the UNION
// It is necessary to split in several queries
// Although the problem reported with 93 UNION use only 70
int pending = getFieldCount();
Map result = new HashMap<>();
final FBDatabaseMetaData metaData = (FBDatabaseMetaData) connection.getMetaData();
while (pending > 0) {
StringBuilder sb = new StringBuilder();
int maxLength = Math.min(pending, 70);
List params = new ArrayList<>(2 * maxLength);
for (int i = 1; i <= maxLength; i++) {
String relationName = getFieldDescriptor(i).getOriginalTableName();
String fieldName = getFieldDescriptor(i).getOriginalName();
if (relationName == null || relationName.equals("")
|| fieldName == null || fieldName.equals("")) continue;
if (sb.length() > 0) {
sb.append('\n').append("UNION ALL").append('\n');
}
sb.append(GET_FIELD_INFO);
params.add(fieldName);
params.add(relationName);
}
pending -= maxLength;
if (sb.length() == 0) continue;
try (ResultSet rs = metaData.doQuery(sb.toString(), params, true)) {
while (rs.next()) {
ExtendedFieldInfo fieldInfo = new ExtendedFieldInfo();
fieldInfo.relationName = rs.getString("RELATION_NAME");
fieldInfo.fieldName = rs.getString("FIELD_NAME");
fieldInfo.fieldLength = rs.getInt("FIELD_LENGTH");
fieldInfo.fieldPrecision = rs.getInt("FIELD_PRECISION");
fieldInfo.fieldScale = rs.getInt("FIELD_SCALE");
fieldInfo.fieldSubtype = rs.getInt("FIELD_SUB_TYPE");
fieldInfo.characterSetId = rs.getInt("CHARACTER_SET_ID");
boolean systemField = rs.getBoolean("SYSTEM_FLAG");
fieldInfo.characterLength = rs.getInt("CHAR_LEN");
if (rs.wasNull()) {
if (systemField && fieldInfo.characterSetId == ID_UNICODE_FSS) {
fieldInfo.characterLength = fieldInfo.fieldLength;
} else {
IEncodingFactory encodingFactory = getRowDescriptor().getEncodingFactory();
final EncodingDefinition encodingDefinition =
encodingFactory.getEncodingDefinitionByCharacterSetId(fieldInfo.characterSetId);
final int charsetSize = encodingDefinition != null
? encodingDefinition.getMaxBytesPerChar()
: 1;
fieldInfo.characterLength = fieldInfo.fieldLength / charsetSize;
}
}
result.put(new FieldKey(fieldInfo.relationName, fieldInfo.fieldName), fieldInfo);
}
}
params.clear();
}
return result;
}
/**
* Strategy for retrieving column labels and column names
*/
private enum ColumnStrategy {
/**
* Default, JDBC-compliant, strategy for column naming.
*
* columnLabel is the AS clause (xsqlvar.aliasname) if specified,
* otherwise xsqlvar.sqlname.
*
*
* columnName is xsqlvar.sqlname if specified, otherwise xsqlvar.aliasname (TODO: change this?)
*
*/
DEFAULT {
@Override
String getColumnName(FieldDescriptor fieldDescriptor) {
return fieldDescriptor.getOriginalName() != null
? fieldDescriptor.getOriginalName()
: getColumnLabel(fieldDescriptor);
}
},
/**
* Alternative strategy for column naming (related to columnLabelForName connection property)
*
* This strategy is not JDBC-compliant, but is provided as a workaround for use with com.sun.rowset.CachedRowSetImpl and
* for people expecting the old behavior.
*
* columnLabel is the AS clause (xsqlvar.aliasname) if specified,
* otherwise xsqlvar.sqlname.
*
*
* columnName is identical to columnLabel.
*
*/
COLUMN_LABEL_FOR_NAME {
@Override
String getColumnName(FieldDescriptor fieldDescriptor) {
return getColumnLabel(fieldDescriptor);
}
};
/**
* Retrieve the columnName for the specified column.
*
* @param fieldDescriptor
* Column descriptor
* @return value for the columnName
*/
abstract String getColumnName(FieldDescriptor fieldDescriptor);
/**
* Retrieve the columnLabel for the specified column.
*
* @param fieldDescriptor
* Column descriptor
* @return value for the columnLabel
*/
String getColumnLabel(FieldDescriptor fieldDescriptor) {
if (fieldDescriptor.getFieldName() != null) {
return fieldDescriptor.getFieldName();
} else if (fieldDescriptor.getOriginalName() != null) {
return fieldDescriptor.getOriginalName();
} else {
return "";
}
}
}
}