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

org.firebirdsql.jdbc.FBDatabaseMetaData Maven / Gradle / Ivy

There is a newer version: 6.0.0-beta-1
Show newest version
/*
 * 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.EncodingFactory;
import org.firebirdsql.gds.impl.GDSFactory;
import org.firebirdsql.gds.impl.GDSHelper;
import org.firebirdsql.gds.impl.GDSType;
import org.firebirdsql.gds.ng.DatatypeCoder;
import org.firebirdsql.gds.ng.DefaultDatatypeCoder;
import org.firebirdsql.gds.ng.fields.RowDescriptor;
import org.firebirdsql.gds.ng.fields.RowDescriptorBuilder;
import org.firebirdsql.gds.ng.fields.RowValue;
import org.firebirdsql.gds.ng.fields.RowValueBuilder;
import org.firebirdsql.jaybird.Version;
import org.firebirdsql.jca.FBManagedConnectionFactory;
import org.firebirdsql.jdbc.escape.FBEscapedFunctionHelper;
import org.firebirdsql.jdbc.metadata.*;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;
import org.firebirdsql.util.FirebirdSupportInfo;

import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.sql.*;
import java.util.*;

import static org.firebirdsql.gds.ISCConstants.*;
import static org.firebirdsql.jdbc.metadata.FbMetadataConstants.*;
import static org.firebirdsql.jdbc.metadata.TypeMetadata.getDataType;
import static org.firebirdsql.jdbc.metadata.TypeMetadata.getDataTypeName;
import static org.firebirdsql.util.FirebirdSupportInfo.supportInfoFor;

/**
 * Comprehensive information about the database as a whole.
 *
 * @author David Jencks
 * @author Mark Rotteveel
 */
@SuppressWarnings("RedundantThrows")
public class FBDatabaseMetaData implements FirebirdDatabaseMetaData {

    private final static Logger log = LoggerFactory.getLogger(FBDatabaseMetaData.class);
    // Extra space to allow for longer patterns (avoids string right truncation errors)
    private static final int OBJECT_NAME_PARAMETER_LENGTH = OBJECT_NAME_LENGTH + 10;
    private static final String OBJECT_NAME_TYPE = "varchar(" + OBJECT_NAME_LENGTH + ")";
    private static final String OBJECT_NAME_PARAMETER = "cast(? as varchar(" + OBJECT_NAME_PARAMETER_LENGTH + ")) ";

    protected static final DatatypeCoder datatypeCoder =
            DefaultDatatypeCoder.forEncodingFactory(EncodingFactory.createInstance(StandardCharsets.UTF_8));

    private static final byte[] TRUE_BYTES = getBytes("T");
    private static final byte[] FALSE_BYTES = getBytes("F");
    private static final byte[] YES_BYTES = getBytes("YES");
    private static final byte[] NO_BYTES = getBytes("NO");
    private static final byte[] EMPTY_STRING_BYTES = getBytes("");
    private static final byte[] CASESENSITIVE = TRUE_BYTES;
    private static final byte[] CASEINSENSITIVE = FALSE_BYTES;
    private static final byte[] UNSIGNED = TRUE_BYTES;
    private static final byte[] SIGNED = FALSE_BYTES;
    private static final byte[] FIXEDSCALE = TRUE_BYTES;
    private static final byte[] VARIABLESCALE = FALSE_BYTES;
    private static final byte[] NOTAUTOINC = FALSE_BYTES;
    private static final byte[] INT_ZERO = createInt(0);
    private static final byte[] SHORT_ZERO = createShort(0);
    private static final byte[] SHORT_ONE = createShort(1);
    private static final byte[] RADIX_BINARY = createInt(2);
    private static final byte[] RADIX_TEN = createInt(10);
    private static final byte[] RADIX_TEN_SHORT = createShort(10);
    private static final byte[] RADIX_BINARY_SHORT = createShort(2);
    private static final byte[] TYPE_PRED_NONE = createShort(DatabaseMetaData.typePredNone);
    private static final byte[] TYPE_PRED_BASIC = createShort(DatabaseMetaData.typePredBasic);
    private static final byte[] TYPE_SEARCHABLE = createShort(DatabaseMetaData.typeSearchable);
    private static final byte[] TYPE_NULLABLE = createShort(DatabaseMetaData.typeNullable);
    private static final byte[] PROCEDURE_NO_RESULT = createShort(DatabaseMetaData.procedureNoResult);
    private static final byte[] PROCEDURE_RETURNS_RESULT = createShort(DatabaseMetaData.procedureReturnsResult);
    private static final byte[] PROCEDURE_NO_NULLS = createShort(DatabaseMetaData.procedureNoNulls);
    private static final byte[] PROCEDURE_NULLABLE = createShort(DatabaseMetaData.procedureNullable);
    private static final byte[] PROCEDURE_COLUMN_IN = createShort(DatabaseMetaData.procedureColumnIn);
    private static final byte[] PROCEDURE_COLUMN_OUT = createShort(DatabaseMetaData.procedureColumnOut);
    private static final byte[] BIGINT_PRECISION = createInt(19);
    private static final byte[] INTEGER_PRECISION = createInt(10);
    private static final byte[] SMALLINT_PRECISION = createInt(5);
    private static final byte[] DATE_PRECISION = createInt(10);
    private static final byte[] TIME_PRECISION = createInt(8);
    private static final byte[] TIMESTAMP_PRECISION = createInt(19);
    private static final byte[] TIME_WITH_TIMEZONE_PRECISION = createInt(19);
    private static final byte[] TIMESTAMP_WITH_TIMEZONE_PRECISION = createInt(30);
    private static final byte[] BOOLEAN_PRECISION = createInt(1);
    private static final byte[] DECFLOAT_16_PRECISION = createInt(16);
    private static final byte[] DECFLOAT_34_PRECISION = createInt(34);
    private static final byte[] COLUMN_NO_NULLS = createInt(DatabaseMetaData.columnNoNulls);
    private static final byte[] COLUMN_NULLABLE = createInt(DatabaseMetaData.columnNullable);
    private static final byte[] IMPORTED_KEY_NO_ACTION = createShort(DatabaseMetaData.importedKeyNoAction);
    private static final byte[] IMPORTED_KEY_CASCADE = createShort(DatabaseMetaData.importedKeyCascade);
    private static final byte[] IMPORTED_KEY_SET_NULL = createShort(DatabaseMetaData.importedKeySetNull);
    private static final byte[] IMPORTED_KEY_SET_DEFAULT = createShort(DatabaseMetaData.importedKeySetDefault);
    private static final byte[] IMPORTED_KEY_NOT_DEFERRABLE = createShort(DatabaseMetaData.importedKeyNotDeferrable);
    private static final byte[] TABLE_INDEX_OTHER = createShort(DatabaseMetaData.tableIndexOther);
    private static final byte[] ASC_BYTES = getBytes("A");
    private static final byte[] DESC_BYTES = getBytes("D");

    private GDSHelper gdsHelper;
    private FBConnection connection;
    private final FirebirdSupportInfo firebirdSupportInfo;

    private static final int STATEMENT_CACHE_SIZE = 12;
    private final Map statements = new LruPreparedStatementCache(STATEMENT_CACHE_SIZE);
    private final FirebirdVersionMetaData versionMetaData;

    protected FBDatabaseMetaData(FBConnection c) throws SQLException {
        this.gdsHelper = c.getGDSHelper();
        this.connection = c;
        firebirdSupportInfo = supportInfoFor(c);
        versionMetaData = FirebirdVersionMetaData.getVersionMetaDataFor(c);
    }

    @Override
    public void close() {
        synchronized (statements) {
            if (statements.isEmpty()) {
                return;
            }
            try {
                for (FBStatement stmt : statements.values()) {
                    try {
                        stmt.close();
                    } catch (Exception e) {
                        log.warn("error closing cached statements in DatabaseMetaData.close; "
                                + "see debug level for stacktrace");
                        log.debug("error closing cached statements in DatabaseMetaData.close", e);
                    }
                }
            } finally {
                statements.clear();
            }
        }
    }

    @Override
    public boolean allProceduresAreCallable() throws SQLException {
        //returns all procedures whether or not you have execute permission
        return false;
    }

    @Override
    public boolean allTablesAreSelectable() throws SQLException {
        //returns all tables matching criteria independent of access permissions.
        return false;
    }

    @Override
    public String getURL() throws SQLException {
        // TODO Think of a less complex way to obtain the url or just return null?
        GDSType gdsType = ((FBManagedConnectionFactory) connection.mc.getManagedConnectionFactory()).getGDSType();
        return GDSFactory.getJdbcUrl(gdsType, connection.mc.getDatabase());
    }

    @Override
    public String getUserName() throws SQLException {
        return gdsHelper.getUserName();
    }

    @Override
    public boolean isReadOnly() throws SQLException {
        return false;//could be true, not yetimplemented
    }

    @Override
    public boolean nullsAreSortedHigh() throws SQLException {
        // in Firebird 1.5.x NULLs are always sorted at the end
        // in Firebird 2.0.x NULLs are sorted low
        return false;
    }

    @Override
    public boolean nullsAreSortedLow() throws SQLException {
        // in Firebird 1.5.x NULLs are always sorted at the end
        // in Firebird 2.0.x NULLs are sorted low
        return gdsHelper.compareToVersion(2, 0) >= 0;
    }

    @Override
    public boolean nullsAreSortedAtStart() throws SQLException {
        // in Firebird 1.5.x NULLs are always sorted at the end
        // in Firebird 2.0.x NULLs are sorted low
        return false;
    }

    @Override
    public boolean nullsAreSortedAtEnd() throws SQLException {
        // in Firebird 1.5.x NULLs are always sorted at the end
        // in Firebird 2.0.x NULLs are sorted low
        return gdsHelper.compareToVersion(2, 0) < 0;
    }

    @Override
    public String getDatabaseProductName() throws SQLException {
        return gdsHelper.getDatabaseProductName();
    }

    @Override
    public String getDatabaseProductVersion() throws SQLException {
        return gdsHelper.getDatabaseProductVersion();
    }

    @Override
    public String getDriverName() throws SQLException {
        return "Jaybird JCA/JDBC driver";
    }

    @Override
    public String getDriverVersion() throws SQLException {
        return Version.JAYBIRD_SIMPLE_VERSION;
    }

    @Override
    public int getDriverMajorVersion() {
        return Version.JAYBIRD_MAJOR_VERSION;
    }

    @Override
    public int getDriverMinorVersion() {
        return Version.JAYBIRD_MINOR_VERSION;
    }

    @Override
    public boolean usesLocalFiles() throws SQLException {
        return false;
    }

    @Override
    public boolean usesLocalFilePerTable() throws SQLException {
        return false;
    }

    @Override
    public boolean supportsMixedCaseIdentifiers() throws SQLException {
        return false;
    }

    // TODO implement statement pooling on the server.. then in the driver
    @Override
    public boolean supportsStatementPooling() throws SQLException {
        return false;
    }

    @Override
    public boolean locatorsUpdateCopy() throws SQLException {
        // Firebird creates a new blob when making changes
        return true;
    }

    @Override
    public boolean storesUpperCaseIdentifiers() throws SQLException {
        return true;
    }

    @Override
    public boolean storesLowerCaseIdentifiers() throws SQLException {
        return false;
    }

    @Override
    public boolean storesMixedCaseIdentifiers() throws SQLException {
        return false;
    }

    @Override
    public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException {
        return true;
    }

    @Override
    public boolean storesUpperCaseQuotedIdentifiers() throws SQLException {
        return false;
    }

    @Override
    public boolean storesLowerCaseQuotedIdentifiers() throws SQLException {
        return false;
    }

    @Override
    public boolean storesMixedCaseQuotedIdentifiers() throws SQLException {
        return false;
    }

    @Override
    public String getIdentifierQuoteString() throws SQLException {
        // TODO Return " " when connected with dialect 1?
        return "\"";
    }

    @Override
    public String getSQLKeywords() throws SQLException {
        return versionMetaData.getSqlKeywords();
    }

    /**
     * {@inheritDoc}
     * 

* NOTE: Some of the functions listed may only work on Firebird 2.1 or higher, or when equivalent UDFs * are installed. *

*/ @Override public String getNumericFunctions() throws SQLException { return collectionToCommaSeparatedList(FBEscapedFunctionHelper.getSupportedNumericFunctions()); } private static String collectionToCommaSeparatedList(Collection collection) { StringBuilder sb = new StringBuilder(); for (String item : collection) { sb.append(item); sb.append(','); } sb.setLength(sb.length() - 1); return sb.toString(); } /** * {@inheritDoc} *

* NOTE: Some of the functions listed may only work on Firebird 2.1 or higher, or when equivalent UDFs * are installed. *

*/ @Override public String getStringFunctions() throws SQLException { return collectionToCommaSeparatedList(FBEscapedFunctionHelper.getSupportedStringFunctions()); } /** * {@inheritDoc} *

* NOTE: Some of the functions listed may only work on Firebird 2.1 or higher, or when equivalent UDFs * are installed. *

*/ @Override public String getSystemFunctions() throws SQLException { return collectionToCommaSeparatedList(FBEscapedFunctionHelper.getSupportedSystemFunctions()); } /** * {@inheritDoc} *

* NOTE: Some of the functions listed may only work on Firebird 2.1 or higher, or when equivalent UDFs * are installed. *

*/ @Override public String getTimeDateFunctions() throws SQLException { return collectionToCommaSeparatedList(FBEscapedFunctionHelper.getSupportedTimeDateFunctions()); } @Override public String getSearchStringEscape() throws SQLException { return "\\"; } @Override public String getExtraNameCharacters() throws SQLException { return "$"; } @Override public boolean supportsAlterTableWithAddColumn() throws SQLException { return true; } @Override public boolean supportsAlterTableWithDropColumn() throws SQLException { return true; } @Override public boolean supportsColumnAliasing() throws SQLException { return true; } @Override public boolean nullPlusNonNullIsNull() throws SQLException { return true; } @Override public boolean supportsConvert() throws SQLException { return true; } /** * {@inheritDoc} *

* See also {@code org.firebirdsql.jdbc.escape.ConvertFunction} for caveats. *

*/ @Override public boolean supportsConvert(int fromType, int toType) throws SQLException { switch (fromType) { case JaybirdTypeCodes.DECFLOAT: if (!firebirdSupportInfo.supportsDecfloat()) { return false; } // Intentional fallthrough case Types.TINYINT: // Doesn't exist in Firebird; handled as if SMALLINT case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.FLOAT: case Types.REAL: case Types.DOUBLE: case Types.NUMERIC: case Types.DECIMAL: // Numerical values all convertible to the same types. switch (toType) { case Types.TINYINT: // Doesn't exist in Firebird; handled as if SMALLINT case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.FLOAT: case Types.REAL: case Types.DOUBLE: case Types.NUMERIC: case Types.DECIMAL: case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting numerical values to binary types will result in ASCII bytes of string conversion, not to the // binary representation of the number (eg 1 will be converted to binary 0x31 (ASCII '1'), not 0x01) case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; case JaybirdTypeCodes.DECFLOAT: return firebirdSupportInfo.supportsDecfloat(); default: return false; } case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: case Types.ROWID: // Internally rowid is not discernible from BINARY // String and binary values all convertible to the same types // Be aware though that casting of binary to non-string/binary will perform the same conversion as // if it is an ASCII string value. Eg the binary string value 0x31 cast to integer will be 1, not 49. switch (toType) { case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; case Types.TINYINT: // Doesn't exist in Firebird; handled as if SMALLINT case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.FLOAT: case Types.REAL: case Types.DOUBLE: case Types.NUMERIC: case Types.DECIMAL: case Types.DATE: case Types.TIME: case Types.TIMESTAMP: return fromType != Types.ROWID; case JaybirdTypeCodes.DECFLOAT: return fromType != Types.ROWID && firebirdSupportInfo.supportsDecfloat(); case Types.BOOLEAN: return fromType != Types.ROWID && firebirdSupportInfo.supportsBoolean(); case Types.ROWID: // As size of rowid is context dependent, we can't cast to it using the convert escape return false; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: return fromType != Types.ROWID && firebirdSupportInfo.supportsTimeZones(); default: return false; } case Types.DATE: switch(toType) { case Types.DATE: case Types.TIMESTAMP: return true; case Types.TIME: case JaybirdTypeCodes.TIME_WITH_TIMEZONE: return false; case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: return firebirdSupportInfo.supportsTimeZones(); case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting date/time values to binary types will result in ASCII bytes of string conversion case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; default: return false; } case Types.TIME: switch(toType) { case Types.TIMESTAMP: case Types.TIME: return true; case Types.DATE: return false; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: return firebirdSupportInfo.supportsTimeZones(); case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting date/time values to binary types will result in ASCII bytes of string conversion case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; default: return false; } case Types.TIMESTAMP: switch(toType) { case Types.TIMESTAMP: case Types.TIME: case Types.DATE: return true; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: return firebirdSupportInfo.supportsTimeZones(); case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting date/time values to binary types will result in ASCII bytes of string conversion case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; default: return false; } case Types.NULL: // If a type can be cast to itself, then null can be cast to it as well return toType != Types.NULL && supportsConvert(toType, toType); case Types.BOOLEAN: if (firebirdSupportInfo.supportsBoolean()) { switch (toType) { case Types.BOOLEAN: case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting boolean values to binary types will result in ASCII bytes of string conversion case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; default: return false; } } return false; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: if (firebirdSupportInfo.supportsTimeZones()) { switch (toType) { case Types.TIME: case Types.TIMESTAMP: return true; case Types.DATE: return false; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: return true; case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting date/time values to binary types will result in ASCII bytes of string conversion case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; default: return false; } } return false; case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: if (firebirdSupportInfo.supportsTimeZones()) { switch (toType) { case Types.TIME: case Types.TIMESTAMP: case Types.DATE: case JaybirdTypeCodes.TIME_WITH_TIMEZONE: case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: return true; case Types.CHAR: case Types.VARCHAR: case Types.LONGVARCHAR: case Types.CLOB: case Types.NCHAR: case Types.LONGNVARCHAR: case Types.NVARCHAR: case Types.NCLOB: return true; // casting date/time values to binary types will result in ASCII bytes of string conversion case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: case Types.BLOB: return true; default: return false; } } return false; case Types.ARRAY: // Arrays are not supported by Jaybird (and casting would be tricky anyway) return false; // Unsupported types case Types.BIT: case Types.OTHER: case Types.JAVA_OBJECT: case Types.DISTINCT: case Types.STRUCT: case Types.REF: case Types.DATALINK: case Types.SQLXML: case JaybirdTypeCodes.REF_CURSOR: default: return false; } } @Override public boolean supportsTableCorrelationNames() throws SQLException { return true; } @Override public boolean supportsDifferentTableCorrelationNames() throws SQLException { return false; } @Override public boolean supportsExpressionsInOrderBy() throws SQLException { return firebirdSupportInfo.isVersionEqualOrAbove(1, 5); } @Override public boolean supportsOrderByUnrelated() throws SQLException { return true; } @Override public boolean supportsGroupBy() throws SQLException { return true; } @Override public boolean supportsGroupByUnrelated() throws SQLException { // TODO Verify return false; } @Override public boolean supportsGroupByBeyondSelect() throws SQLException { // TODO Verify return false; } @Override public boolean supportsLikeEscapeClause() throws SQLException { return true; } @Override public boolean supportsMultipleResultSets() throws SQLException { return false; } @Override public boolean supportsMultipleTransactions() throws SQLException { return true; } @Override public boolean supportsNonNullableColumns() throws SQLException { return true; } @Override public boolean supportsMinimumSQLGrammar() throws SQLException { return true; } @Override public boolean supportsCoreSQLGrammar() throws SQLException { return true; } @Override public boolean supportsExtendedSQLGrammar() throws SQLException { return true; } @Override public boolean supportsANSI92EntryLevelSQL() throws SQLException { return true; } @Override public boolean supportsANSI92IntermediateSQL() throws SQLException { // TODO Verify return false; } @Override public boolean supportsANSI92FullSQL() throws SQLException { // TODO Verify return false; } @Override public boolean supportsIntegrityEnhancementFacility() throws SQLException { return true; // rrokytskyy: yep, they call so foreign keys + cascade deletes } @Override public boolean supportsOuterJoins() throws SQLException { return true; } @Override public boolean supportsFullOuterJoins() throws SQLException { return true; } @Override public boolean supportsLimitedOuterJoins() throws SQLException { return true; } /** * {@inheritDoc} * * @return the vendor term, always {@code null} because schemas are not supported by database server (see JDBC CTS * for details). */ @Override public String getSchemaTerm() throws SQLException { return null; } @Override public String getProcedureTerm() throws SQLException { return "PROCEDURE"; } /** * {@inheritDoc} * * @return the vendor term, always {@code null} because catalogs are not supported by database server (see JDBC CTS * for details). */ @Override public String getCatalogTerm() throws SQLException { return null; } @Override public boolean isCatalogAtStart() throws SQLException { return false; } /** * {@inheritDoc} * * @return the separator string, always {@code null} because catalogs are not supported by database server (see * JDBC CTS for details). */ @Override public String getCatalogSeparator() throws SQLException { return null; } @Override public boolean supportsSchemasInDataManipulation() throws SQLException { return false; } @Override public boolean supportsSchemasInProcedureCalls() throws SQLException { return false; } @Override public boolean supportsSchemasInTableDefinitions() throws SQLException { return false; } @Override public boolean supportsSchemasInIndexDefinitions() throws SQLException { return false; } @Override public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { return false; } @Override public boolean supportsCatalogsInDataManipulation() throws SQLException { return false; } @Override public boolean supportsCatalogsInProcedureCalls() throws SQLException { return false; } @Override public boolean supportsCatalogsInTableDefinitions() throws SQLException { return false; } @Override public boolean supportsCatalogsInIndexDefinitions() throws SQLException { return false; } @Override public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { return false; } @Override public boolean supportsPositionedDelete() throws SQLException { return true; } @Override public boolean supportsPositionedUpdate() throws SQLException { return true; } @Override public boolean supportsSelectForUpdate() throws SQLException { return true; } @Override public boolean supportsStoredProcedures() throws SQLException { return true; } @Override public boolean supportsSubqueriesInComparisons() throws SQLException { return true; } @Override public boolean supportsSubqueriesInExists() throws SQLException { return true; } @Override public boolean supportsSubqueriesInIns() throws SQLException { return true; } @Override public boolean supportsSubqueriesInQuantifieds() throws SQLException { return true; } @Override public boolean supportsCorrelatedSubqueries() throws SQLException { return true; } @Override public boolean supportsUnion() throws SQLException { return true; } @Override public boolean supportsUnionAll() throws SQLException { return true; } @Override public boolean supportsOpenCursorsAcrossCommit() throws SQLException { return false;//only when commit retaining is executed I think } @Override public boolean supportsOpenCursorsAcrossRollback() throws SQLException { return false;//commit retaining only. } @Override public boolean supportsOpenStatementsAcrossCommit() throws SQLException { return true; } @Override public boolean supportsOpenStatementsAcrossRollback() throws SQLException { return true; } //---------------------------------------------------------------------- // The following group of methods exposes various limitations // based on the target database with the current driver. // Unless otherwise specified, a result of zero means there is no // limit, or the limit is not known. @Override public int getMaxBinaryLiteralLength() throws SQLException { return 0; // TODO 32764 Test (assumed on length/2 and max string literal length) } @Override public int getMaxCharLiteralLength() throws SQLException { return 32765; } @Override public int getMaxColumnNameLength() throws SQLException { return getMaxObjectNameLength(); } private int getMaxObjectNameLength() { if (gdsHelper.compareToVersion(4, 0) < 0) { return OBJECT_NAME_LENGTH_BEFORE_V4_0; } else { return OBJECT_NAME_LENGTH_V4_0; } } @Override public int getMaxColumnsInGroupBy() throws SQLException { return 0; //I don't know } @Override public int getMaxColumnsInIndex() throws SQLException { return 0; //I don't know } @Override public int getMaxColumnsInOrderBy() throws SQLException { return 0; //I don't know } @Override public int getMaxColumnsInSelect() throws SQLException { return 0; //I don't know } @Override public int getMaxColumnsInTable() throws SQLException { return 32767; // Depends on datatypes and sizes, at most 64 kbyte excluding blobs (but including blob ids) } @Override public int getMaxConnections() throws SQLException { return 0; //I don't know } @Override public int getMaxCursorNameLength() throws SQLException { return 31; } @Override public int getMaxIndexLength() throws SQLException { if (gdsHelper.compareToVersion(2, 0) < 0) { return 252; // See http://www.firebirdsql.org/en/firebird-technical-specifications/ } else { return 0; // 1/4 of page size, maybe retrieve page size and use that? } } @Override public int getMaxSchemaNameLength() throws SQLException { return 0; //No schemas } @Override public int getMaxProcedureNameLength() throws SQLException { return getMaxObjectNameLength(); } @Override public int getMaxCatalogNameLength() throws SQLException { return 0; //No catalogs } @Override public int getMaxRowSize() throws SQLException { if (gdsHelper.compareToVersion(1, 5) >= 0) return 65531; else return 0; } @Override public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { return false; // Blob sizes are not included in rowsize } @Override public int getMaxStatementLength() throws SQLException { if (gdsHelper.compareToVersion(3, 0) >= 0) { // 10 MB return 10 * 1024 * 1024; } else { // 64 KB return 64 * 1024; } } @Override public int getMaxStatements() throws SQLException { // Limited by max handles, but this includes other objects than statements return 0; } @Override public int getMaxTableNameLength() throws SQLException { return getMaxObjectNameLength(); } @Override public int getMaxTablesInSelect() throws SQLException { // TODO Check if there is a max return 0; } @Override public int getMaxUserNameLength() throws SQLException { return getMaxObjectNameLength(); } //---------------------------------------------------------------------- @Override public int getDefaultTransactionIsolation() throws SQLException { return Connection.TRANSACTION_READ_COMMITTED; } @Override public boolean supportsTransactions() throws SQLException { return true; } @Override public boolean supportsTransactionIsolationLevel(int level) throws SQLException { switch (level) { case Connection.TRANSACTION_NONE: return false; case Connection.TRANSACTION_READ_COMMITTED: return true; case Connection.TRANSACTION_READ_UNCOMMITTED: return false; case Connection.TRANSACTION_REPEATABLE_READ: return true; case Connection.TRANSACTION_SERIALIZABLE: return true; default: return false; } } /** * {@inheritDoc} *

* Although Firebird supports both DML and DDL in transactions, it is not possible to use objects in the same * transaction that defines them. For example, it is not possible to insert into a table in the same transaction * that created it. *

*/ @Override public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { return true; } @Override public boolean supportsDataManipulationTransactionsOnly() throws SQLException { return false; } /** * {@inheritDoc} *

* Read the note on {@link #supportsDataDefinitionAndDataManipulationTransactions()}. *

*/ @Override public boolean dataDefinitionCausesTransactionCommit() throws SQLException { return false; } /** * {@inheritDoc} *

* Read the note on {@link #supportsDataDefinitionAndDataManipulationTransactions()}. *

*/ @Override public boolean dataDefinitionIgnoredInTransactions() throws SQLException { return false; } private static final String GET_PROCEDURES_START = "select " + "cast(RDB$PROCEDURE_NAME as " + OBJECT_NAME_TYPE + ") as PROCEDURE_NAME," + "RDB$DESCRIPTION as REMARKS," + "RDB$PROCEDURE_OUTPUTS as PROCEDURE_TYPE " + "from " + "RDB$PROCEDURES " + "where "; private static final String GET_PROCEDURES_END = "1 = 1 order by 1"; @Override public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(9, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_CAT", "PROCEDURES").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_SCHEM", "ROCEDURES").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_NAME", "PROCEDURES").addField() .at(3).simple(SQL_VARYING, 31, "FUTURE1", "PROCEDURES").addField() .at(4).simple(SQL_VARYING, 31, "FUTURE2", "PROCEDURES").addField() .at(5).simple(SQL_VARYING, 31, "FUTURE3", "PROCEDURES").addField() // Field in Firebird is actually a blob, using Integer.MAX_VALUE for length .at(6).simple(SQL_VARYING, Integer.MAX_VALUE, "REMARKS", "PROCEDURES").addField() .at(7).simple(SQL_SHORT, 0, "PROCEDURE_TYPE", "PROCEDURES").addField() .at(8).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SPECIFIC_NAME", "PROCEDURES").addField() .toRowDescriptor(); Clause procedureClause = new Clause("RDB$PROCEDURE_NAME", procedureNamePattern); String sql = GET_PROCEDURES_START; sql += procedureClause.getCondition(); if (firebirdSupportInfo.supportsPackages()) { sql += "RDB$PACKAGE_NAME is null and "; } sql += GET_PROCEDURES_END; List params = procedureClause.hasCondition() ? Collections.singletonList(procedureClause.getValue()) : Collections.emptyList(); try (ResultSet rs = doQuery(sql, params)) { if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(rs.getString("PROCEDURE_NAME"))) .at(6).set(getBytes(rs.getString("REMARKS"))) .at(7).set(rs.getShort("PROCEDURE_TYPE") == 0 ? PROCEDURE_NO_RESULT : PROCEDURE_RETURNS_RESULT) .at(8).set(valueBuilder.get(2)) .toRowValue(true) ); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } private static final String GET_PROCEDURE_COLUMNS_START = "select " + "cast(PP.RDB$PROCEDURE_NAME as " + OBJECT_NAME_TYPE + ") as PROCEDURE_NAME," + "cast(PP.RDB$PARAMETER_NAME as " + OBJECT_NAME_TYPE + ") as COLUMN_NAME," + "PP.RDB$PARAMETER_TYPE as COLUMN_TYPE," + "F.RDB$FIELD_TYPE as FIELD_TYPE," + "F.RDB$FIELD_SUB_TYPE as FIELD_SUB_TYPE," + "F.RDB$FIELD_PRECISION as FIELD_PRECISION," + "F.RDB$FIELD_SCALE as FIELD_SCALE," + "F.RDB$FIELD_LENGTH as FIELD_LENGTH," + "F.RDB$NULL_FLAG as NULL_FLAG," + "PP.RDB$DESCRIPTION as REMARKS," + "F.RDB$CHARACTER_LENGTH AS CHAR_LEN," + "PP.RDB$PARAMETER_NUMBER + 1 AS PARAMETER_NUMBER," + "F.RDB$CHARACTER_SET_ID " + "from " + "RDB$PROCEDURE_PARAMETERS PP," + "RDB$FIELDS F " + "where "; private static final String GET_PROCEDURE_COLUMNS_END = " PP.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME " + "order by " + "PP.RDB$PROCEDURE_NAME," + "PP.RDB$PARAMETER_TYPE desc," + "PP.RDB$PARAMETER_NUMBER "; @Override public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(20, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_CAT", "COLUMNINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_SCHEM", "COLUMNINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PROCEDURE_NAME", "COLUMNINFO").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "COLUMNINFO").addField() .at(4).simple(SQL_SHORT, 0, "COLUMN_TYPE", "COLUMNINFO").addField() .at(5).simple(SQL_LONG, 0, "DATA_TYPE", "COLUMNINFO").addField() .at(6).simple(SQL_VARYING, 31, "TYPE_NAME", "COLUMNINFO").addField() .at(7).simple(SQL_LONG, 0, "PRECISION", "COLUMNINFO").addField() .at(8).simple(SQL_LONG, 0, "LENGTH", "COLUMNINFO").addField() .at(9).simple(SQL_SHORT, 0, "SCALE", "COLUMNINFO").addField() .at(10).simple(SQL_SHORT, 0, "RADIX", "COLUMNINFO").addField() .at(11).simple(SQL_SHORT, 0, "NULLABLE", "COLUMNINFO").addField() // Field in Firebird is actually a blob, using Integer.MAX_VALUE for length .at(12).simple(SQL_VARYING, Integer.MAX_VALUE, "REMARKS", "COLUMNINFO").addField() .at(13).simple(SQL_VARYING, 31, "COLUMN_DEF", "COLUMNINFO").addField() .at(14).simple(SQL_LONG, 0, "SQL_DATA_TYPE", "COLUMNINFO").addField() .at(15).simple(SQL_LONG, 0, "SQL_DATETIME_SUB", "COLUMNINFO").addField() .at(16).simple(SQL_LONG, 0, "CHAR_OCTET_LENGTH", "COLUMNINFO").addField() .at(17).simple(SQL_LONG, 0, "ORDINAL_POSITION", "COLUMNINFO").addField() .at(18).simple(SQL_VARYING, 3, "IS_NULLABLE", "COLUMNINFO").addField() .at(19).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SPECIFIC_NAME", "COLUMNINFO").addField() .toRowDescriptor(); Clause procedureClause = new Clause("PP.RDB$PROCEDURE_NAME", procedureNamePattern); Clause columnClause = new Clause("PP.RDB$PARAMETER_NAME", columnNamePattern); String sql = GET_PROCEDURE_COLUMNS_START; sql += procedureClause.getCondition(); sql += columnClause.getCondition(); if (firebirdSupportInfo.supportsPackages()) { sql += "PP.RDB$PACKAGE_NAME is null and "; } sql += GET_PROCEDURE_COLUMNS_END; List params = new ArrayList<>(2); if (procedureClause.hasCondition()) { params.add(procedureClause.getValue()); } if (columnClause.hasCondition()) { params.add(columnClause.getValue()); } try (ResultSet rs = doQuery(sql, params)) { // if nothing found, return an empty result set if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); final boolean supportsFloatBinaryPrecision = firebirdSupportInfo.supportsFloatBinaryPrecision(); do { final short columnType = rs.getShort("COLUMN_TYPE"); final short fieldType = rs.getShort("FIELD_TYPE"); final short fieldSubType = rs.getShort("FIELD_SUB_TYPE"); final short fieldScale = rs.getShort("FIELD_SCALE"); final int characterSetId = rs.getInt("RDB$CHARACTER_SET_ID"); // TODO: Find out what the difference is with NULL_FLAG in RDB$PROCEDURE_PARAMETERS (might be ODS dependent) final short nullFlag = rs.getShort("NULL_FLAG"); final int dataType = getDataType(fieldType, fieldSubType, fieldScale, characterSetId); valueBuilder .at(2).set(getBytes(rs.getString("PROCEDURE_NAME"))) .at(3).set(getBytes(rs.getString("COLUMN_NAME"))) // TODO: Unsure if procedureColumnOut is correct, maybe procedureColumnResult, or need ODS dependent use of RDB$PROCEDURE_TYPE to decide on selectable or executable? // TODO: ResultSet columns should not be first according to JDBC 4.1 description .at(4).set(columnType == 0 ? PROCEDURE_COLUMN_IN : PROCEDURE_COLUMN_OUT) .at(5).set(createInt(dataType)) .at(6).set(getBytes(getDataTypeName(fieldType, fieldSubType, fieldScale))) .at(8).set(createInt(rs.getShort("FIELD_LENGTH"))) .at(10).set(RADIX_TEN_SHORT) .at(11).set(nullFlag == 1 ? PROCEDURE_NO_NULLS : PROCEDURE_NULLABLE) .at(12).set(getBytes(rs.getString("REMARKS"))) // TODO: Need to write ODS version dependent method to retrieve some of the info for indexes 13 (From 2.0 defaults for procedure parameters), 14 and 15 // TODO: Find correct value for ORDINAL_POSITION (+ order of columns and intent, see JDBC-229) .at(17).set(createInt(rs.getInt("PARAMETER_NUMBER"))) // TODO: Find out if there is a conceptual difference with NULLABLE (idx 11) .at(18).set(nullFlag == 1 ? NO_BYTES : YES_BYTES) .at(19).set(valueBuilder.get(2)); switch (dataType) { case Types.DECIMAL: case Types.NUMERIC: { short precision = rs.getShort("FIELD_PRECISION"); if (precision == 0 && fieldType == int128_type) { precision = NUMERIC_INT128_PRECISION; } valueBuilder .at(7).set(createInt(precision)) .at(9).set(createShort(-1 * fieldScale)); } break; case Types.CHAR: case Types.VARCHAR: case Types.BINARY: case Types.VARBINARY: short charLen = rs.getShort("CHAR_LEN"); if (!rs.wasNull()) { valueBuilder.at(7).set(createInt(charLen)); } else { valueBuilder.at(8).set(valueBuilder.get(8)); } valueBuilder.at(16).set(valueBuilder.get(8)); break; case Types.FLOAT: if (supportsFloatBinaryPrecision) { valueBuilder .at(7).set(createInt(FLOAT_BINARY_PRECISION)) .at(10).set(RADIX_BINARY_SHORT); } else { valueBuilder.at(7).set(createInt(FLOAT_DECIMAL_PRECISION)); } break; case Types.DOUBLE: if (supportsFloatBinaryPrecision) { valueBuilder .at(7).set(createInt(DOUBLE_BINARY_PRECISION)) .at(10).set(RADIX_BINARY_SHORT); } else { valueBuilder.at(7).set(createInt(DOUBLE_DECIMAL_PRECISION)); } break; case Types.BIGINT: valueBuilder .at(7).set(BIGINT_PRECISION) .at(9).set(SHORT_ZERO); break; case Types.INTEGER: valueBuilder .at(7).set(INTEGER_PRECISION) .at(9).set(SHORT_ZERO); break; case Types.SMALLINT: valueBuilder .at(7).set(SMALLINT_PRECISION) .at(9).set(SHORT_ZERO); break; case Types.DATE: valueBuilder.at(7).set(DATE_PRECISION); break; case Types.TIME: valueBuilder.at(7).set(TIME_PRECISION); break; case Types.TIMESTAMP: valueBuilder.at(7).set(TIMESTAMP_PRECISION); break; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: valueBuilder.at(7).set(TIME_WITH_TIMEZONE_PRECISION); break; case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: valueBuilder.at(7).set(TIMESTAMP_WITH_TIMEZONE_PRECISION); break; case Types.BOOLEAN: valueBuilder .at(7).set(BOOLEAN_PRECISION) .at(10).set(RADIX_BINARY_SHORT); break; case JaybirdTypeCodes.DECFLOAT: switch (fieldType) { case dec16_type: valueBuilder.at(7).set(DECFLOAT_16_PRECISION); break; case dec34_type: valueBuilder.at(7).set(DECFLOAT_34_PRECISION); break; } break; } rows.add(valueBuilder.toRowValue(true)); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } public static final String TABLE = "TABLE"; public static final String SYSTEM_TABLE = "SYSTEM TABLE"; public static final String VIEW = "VIEW"; public static final String GLOBAL_TEMPORARY = "GLOBAL TEMPORARY"; /** * Table types supported for Firebird 2.5 and up (will also work with 2.1 and earlier though) */ private static final String[] ALL_TYPES_2_5 = {TABLE, SYSTEM_TABLE, VIEW, GLOBAL_TEMPORARY}; /** * Table types supported for Firebird 2.1 and lower */ private static final String[] ALL_TYPES_2_1 = {TABLE, SYSTEM_TABLE, VIEW}; @Override public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String types[]) throws SQLException { if (hasGlobalTemporaryTables()) { return getTables_2_5(tableNamePattern, types); } else { return getTables_2_1(tableNamePattern, types); } } private static final String GET_TABLE_ORDER_BY = " order by 4, 3"; //@formatter:off private static final String LEGACY_IS_TABLE = " rdb$relation_type is null and rdb$view_blr is null "; private static final String LEGACY_IS_VIEW = " rdb$relation_type is null and rdb$view_blr is not null "; private static final String TABLE_COLUMNS_2_5 = " select cast(null as " + OBJECT_NAME_TYPE + ") as TABLE_CAT," + "cast(null as " + OBJECT_NAME_TYPE + ") as TABLE_SCHEM," + "cast(RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as TABLE_NAME," + "cast(case" + " when rdb$relation_type = 0 or " + LEGACY_IS_TABLE + " then case when RDB$SYSTEM_FLAG = 1 then '" + SYSTEM_TABLE + "' else '" + TABLE + "' end" + " when rdb$relation_type = 1 or " + LEGACY_IS_VIEW + " then '" + VIEW + "'" + " when rdb$relation_type = 2 then '" + TABLE + "'" // external table; assume as normal table + " when rdb$relation_type = 3 then '" + SYSTEM_TABLE + "'" // virtual (monitoring) table: assume system + " when rdb$relation_type in (4, 5) then '" + GLOBAL_TEMPORARY + "'" + "end as varchar(31)) as TABLE_TYPE," + "RDB$DESCRIPTION as REMARKS," + "cast(null as " + OBJECT_NAME_TYPE + ") as TYPE_CAT," + "cast(null as " + OBJECT_NAME_TYPE + ") as TYPE_SCHEM," + "cast(null as varchar(31)) as TYPE_NAME," + "cast(null as " + OBJECT_NAME_TYPE + ") as SELF_REFERENCING_COL_NAME," + "cast(null as varchar(31)) as REF_GENERATION," + "cast(RDB$OWNER_NAME as " + OBJECT_NAME_TYPE + ") as OWNER_NAME " + "from RDB$RELATIONS "; //@formatter:on /** * Implementation of {@link #getTables(String, String, String, String[])} for Firebird 2.5 and up. */ private ResultSet getTables_2_5(String tableNamePattern, String[] types) throws SQLException { if (types == null) { types = ALL_TYPES_2_5; } Clause nameClause = new Clause("RDB$RELATION_NAME", tableNamePattern); String sql = TABLE_COLUMNS_2_5; List params; if (nameClause.hasCondition()) { sql = sql + " where " + nameClause.getCondition(); params = Collections.singletonList(nameClause.getValue()); } else { params = Collections.emptyList(); } Set typeSet = new HashSet<>(Arrays.asList(types)); if (!typeSet.containsAll(Arrays.asList(ALL_TYPES_2_5))) { // Only construct conditions when we don't query for all StringBuilder typeCondition = new StringBuilder(112); if (typeSet.contains(SYSTEM_TABLE) && typeSet.contains(TABLE)) { typeCondition.append(" (rdb$relation_type in (0, 2, 3) or " + LEGACY_IS_TABLE + ") "); } else if (typeSet.contains(SYSTEM_TABLE)) { typeCondition.append(" (rdb$relation_type in (0, 3) or " + LEGACY_IS_TABLE + ") and rdb$system_flag = 1 "); // We assume that external tables are never system and that virtual tables are always system } else if (typeSet.contains(TABLE)) { typeCondition.append(" (rdb$relation_type in (0, 2) or " + LEGACY_IS_TABLE + ") and rdb$system_flag = 0 "); // We assume that external tables are never system and that virtual tables are always system } if (typeSet.contains(VIEW)) { if (typeCondition.length() > 0) { typeCondition.append(" or "); } typeCondition.append(" (rdb$relation_type = 1 or " + LEGACY_IS_VIEW + ") "); // We assume (but don't check) that views are never system } if (typeSet.contains(GLOBAL_TEMPORARY)) { if (typeCondition.length() > 0) { typeCondition.append(" or "); } typeCondition.append(" rdb$relation_type in (4, 5) "); } if (typeCondition.length() == 0) { // Requested types are unknown, query nothing typeCondition.append(" 1 = 0 "); } sql = sql + (nameClause.hasCondition() ? " (" + typeCondition + ") " : " where " + typeCondition + " "); } else if (nameClause.hasCondition()) { // Clause condition always ends in "and" sql += " 1=1 "; } sql = sql + GET_TABLE_ORDER_BY; return doQuery(sql, params); } //@formatter:off private static final String TABLE_COLUMNS_FORMAT_2_1 = " select cast(null as " + OBJECT_NAME_TYPE + ") as TABLE_CAT," + "cast(null as " + OBJECT_NAME_TYPE + ") as TABLE_SCHEM," + "cast(RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as TABLE_NAME," + "cast('%s' as varchar(31)) as TABLE_TYPE," + "RDB$DESCRIPTION as REMARKS," + "cast(null as " + OBJECT_NAME_TYPE + ") as TYPE_CAT," + "cast(null as " + OBJECT_NAME_TYPE + ") as TYPE_SCHEM," + "cast(null as varchar(31)) as TYPE_NAME," + "cast(null as " + OBJECT_NAME_TYPE + ") as SELF_REFERENCING_COL_NAME," + "cast(null as varchar(31)) as REF_GENERATION," + "cast(RDB$OWNER_NAME as " + OBJECT_NAME_TYPE + ") as OWNER_NAME " + "from RDB$RELATIONS "; private static final String TABLE_COLUMNS_SYSTEM_2_1 = String.format(TABLE_COLUMNS_FORMAT_2_1, SYSTEM_TABLE); private static final String TABLE_COLUMNS_NORMAL = String.format(TABLE_COLUMNS_FORMAT_2_1, TABLE); private static final String TABLE_COLUMNS_VIEW = String.format(TABLE_COLUMNS_FORMAT_2_1, VIEW); private static final String GET_TABLES_ALL_2_1 = TABLE_COLUMNS_SYSTEM_2_1 + " where ? = 'T' and RDB$SYSTEM_FLAG = 1 and rdb$view_blr is null" + " union" + TABLE_COLUMNS_NORMAL + " where ? = 'T' and RDB$SYSTEM_FLAG = 0 and rdb$view_blr is null" + " union" + TABLE_COLUMNS_VIEW + " where ? = 'T' and rdb$view_blr is not null " + GET_TABLE_ORDER_BY; private static final String GET_TABLES_EXACT_2_1 = TABLE_COLUMNS_SYSTEM_2_1 + " where ? = 'T' and RDB$SYSTEM_FLAG = 1 and rdb$view_blr is null" + " and RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER + " union" + TABLE_COLUMNS_NORMAL + " where ? = 'T' and RDB$SYSTEM_FLAG = 0 and rdb$view_blr is null" + " and RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER + " union" + TABLE_COLUMNS_VIEW + " where ? = 'T' and rdb$view_blr is not null" + " and RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER + GET_TABLE_ORDER_BY; private static final String GET_TABLES_LIKE_2_1 = TABLE_COLUMNS_SYSTEM_2_1 + " where ? = 'T' and RDB$SYSTEM_FLAG = 1 and rdb$view_blr is null" + " and trim(trailing from RDB$RELATION_NAME) like " + OBJECT_NAME_PARAMETER + " escape '\\'" + " union" + TABLE_COLUMNS_NORMAL + " where ? = 'T' and RDB$SYSTEM_FLAG = 0 and rdb$view_blr is null" + " and trim(trailing from RDB$RELATION_NAME) like " + OBJECT_NAME_PARAMETER + " escape '\\'" + " union" + TABLE_COLUMNS_VIEW + " where ? = 'T' and rdb$view_blr is not null" + " and trim(trailing from RDB$RELATION_NAME) like " + OBJECT_NAME_PARAMETER + " escape '\\'" + GET_TABLE_ORDER_BY; private static final String GET_TABLES_STARTING_WITH_2_1 = TABLE_COLUMNS_SYSTEM_2_1 + " where ? = 'T' and RDB$SYSTEM_FLAG = 1 and rdb$view_blr is null" + " and RDB$RELATION_NAME starting with " + OBJECT_NAME_PARAMETER + " union" + TABLE_COLUMNS_NORMAL + " where ? = 'T' and RDB$SYSTEM_FLAG = 0 and rdb$view_blr is null" + " and RDB$RELATION_NAME starting with " + OBJECT_NAME_PARAMETER + " union" + TABLE_COLUMNS_VIEW + " where ? = 'T' and rdb$view_blr is not null" + " and RDB$RELATION_NAME starting with " + OBJECT_NAME_PARAMETER + GET_TABLE_ORDER_BY; //@formatter:on /** * Implementation of {@link #getTables(String, String, String, String[])} for Firebird 2.1 and lower. */ private ResultSet getTables_2_1(String tableNamePattern, String[] types) throws SQLException { if (types == null) { types = ALL_TYPES_2_1; } MetadataPattern metadataPattern = MetadataPattern.compile(tableNamePattern); String sql; List params; MetadataPattern.ConditionType conditionType = metadataPattern.getConditionType(); if (conditionType == MetadataPattern.ConditionType.NONE) { sql = GET_TABLES_ALL_2_1; params = Arrays.asList( getWantsSystemTables(types), getWantsTables(types), getWantsViews(types)); } else { switch (conditionType) { case SQL_EQUALS: sql = GET_TABLES_EXACT_2_1; break; case SQL_LIKE: sql = GET_TABLES_LIKE_2_1; break; case SQL_STARTING_WITH: sql = GET_TABLES_STARTING_WITH_2_1; break; default: throw new AssertionError("Unexpected condition type " + conditionType); } String conditionValue = metadataPattern.getConditionValue(); params = Arrays.asList( getWantsSystemTables(types), conditionValue, getWantsTables(types), conditionValue, getWantsViews(types), conditionValue); } return doQuery(sql, params); } @Override public ResultSet getSchemas() throws SQLException { return getSchemas(null, null); } @Override public ResultSet getCatalogs() throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(1, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "TABLECATALOGS").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } @Override public ResultSet getTableTypes() throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(1, datatypeCoder) .at(0).simple(SQL_VARYING, 31, "TABLE_TYPE", "TABLETYPES").addField() .toRowDescriptor(); final String[] types = hasGlobalTemporaryTables() ? ALL_TYPES_2_5 : ALL_TYPES_2_1; final List rows = new ArrayList<>(types.length); for (String type : types) { rows.add(RowValue.of(rowDescriptor, getBytes(type))); } return new FBResultSet(rowDescriptor, rows); } @Override public String[] getTableTypeNames() throws SQLException { String[] allTypes = hasGlobalTemporaryTables() ? ALL_TYPES_2_5 : ALL_TYPES_2_1; return Arrays.copyOf(allTypes, allTypes.length); } private boolean hasGlobalTemporaryTables() throws SQLException { return getOdsMajorVersion() == 11 && getOdsMinorVersion() >= 2 || getOdsMajorVersion() > 11; } //@formatter:off private static final String GET_COLUMNS_COMMON = "SELECT cast(RF.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") AS RELATION_NAME," + "cast(RF.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") AS FIELD_NAME," + "F.RDB$FIELD_TYPE AS FIELD_TYPE," + "F.RDB$FIELD_SUB_TYPE AS FIELD_SUB_TYPE," + "F.RDB$FIELD_PRECISION AS FIELD_PRECISION," + "F.RDB$FIELD_SCALE AS FIELD_SCALE," + "F.RDB$FIELD_LENGTH AS FIELD_LENGTH," + "F.RDB$CHARACTER_LENGTH AS CHAR_LEN," + "RF.RDB$DESCRIPTION AS REMARKS," + "RF.RDB$DEFAULT_SOURCE AS DEFAULT_SOURCE," + "F.RDB$DEFAULT_SOURCE AS DOMAIN_DEFAULT_SOURCE," + "RF.RDB$FIELD_POSITION + 1 AS FIELD_POSITION," + "RF.RDB$NULL_FLAG AS NULL_FLAG," + "F.RDB$NULL_FLAG AS SOURCE_NULL_FLAG," + "F.RDB$COMPUTED_BLR AS COMPUTED_BLR," + "F.RDB$CHARACTER_SET_ID,"; private static final String GET_COLUMNS_3_0_START = GET_COLUMNS_COMMON + "CASE WHEN RF.RDB$IDENTITY_TYPE IS NULL THEN CAST('NO' AS VARCHAR(3)) ELSE CAST('YES' AS VARCHAR(3)) END AS IS_IDENTITY," + "CASE RF.RDB$IDENTITY_TYPE WHEN 0 THEN CAST('ALWAYS' AS VARCHAR(10)) WHEN 1 THEN CAST('BY DEFAULT' AS VARCHAR(10)) ELSE NULL END AS JB_IDENTITY_TYPE " + "FROM RDB$RELATION_FIELDS RF," + "RDB$FIELDS F " + "WHERE "; private static final String GET_COLUMNS_START = GET_COLUMNS_COMMON + "'NO' AS IS_IDENTITY," + "CAST(NULL AS VARCHAR(10)) AS JB_IDENTITY_TYPE " + "FROM RDB$RELATION_FIELDS RF," + "RDB$FIELDS F " + "WHERE "; public static final String GET_COLUMNS_END = " RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME " + "order by RF.RDB$RELATION_NAME, RF.RDB$FIELD_POSITION"; //@formatter:on /** * {@inheritDoc} * *

* Jaybird defines these additional columns: *

    *
  1. JB_IS_IDENTITY String => Indicates whether this column is an identity column (NOTE: Jaybird * specific column; retrieve by name!). * There is a subtle difference with the meaning of {@code IS_AUTOINCREMENT}. This column indicates if the column * is a true identity column. *
      *
    • YES --- if the column is an identity column
    • *
    • NO --- if the column is not an identity column
    • *
    *
  2. *
  3. JB_IDENTITY_TYPE String => Type of identity column (NOTE: Jaybird specific column; retrieve by * name!) *
      *
    • ALWAYS --- for a GENERATED ALWAYS AS IDENTITY column (not yet supported in Firebird 3!)
    • *
    • BY DEFAULT --- for a GENERATED BY DEFAULT AS IDENTITY column
    • *
    • null --- if the column is not an identity type (or the identity type is unknown)
    • *
    *
  4. *
*

*/ @Override public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(26, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "COLUMNINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "COLUMNINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "COLUMNINFO").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "COLUMNINFO").addField() .at(4).simple(SQL_LONG, 0, "DATA_TYPE", "COLUMNINFO").addField() .at(5).simple(SQL_VARYING | 1, 31, "TYPE_NAME", "COLUMNINFO").addField() .at(6).simple(SQL_LONG, 0, "COLUMN_SIZE", "COLUMNINFO").addField() .at(7).simple(SQL_LONG, 0, "BUFFER_LENGTH", "COLUMNINFO").addField() .at(8).simple(SQL_LONG, 0, "DECIMAL_DIGITS", "COLUMNINFO").addField() .at(9).simple(SQL_LONG, 0, "NUM_PREC_RADIX", "COLUMNINFO").addField() .at(10).simple(SQL_LONG, 0, "NULLABLE", "COLUMNINFO").addField() // Field in Firebird is actually a blob, using Integer.MAX_VALUE for length .at(11).simple(SQL_VARYING | 1, Integer.MAX_VALUE, "REMARKS", "COLUMNINFO").addField() .at(12).simple(SQL_VARYING | 1, 31, "COLUMN_DEF", "COLUMNINFO").addField() .at(13).simple(SQL_LONG, 0, "SQL_DATA_TYPE", "COLUMNINFO").addField() .at(14).simple(SQL_LONG, 0, "SQL_DATETIME_SUB", "COLUMNINFO").addField() .at(15).simple(SQL_LONG, 0, "CHAR_OCTET_LENGTH", "COLUMNINFO").addField() .at(16).simple(SQL_LONG, 0, "ORDINAL_POSITION", "COLUMNINFO").addField() .at(17).simple(SQL_VARYING, 3, "IS_NULLABLE", "COLUMNINFO").addField() .at(18).simple(SQL_VARYING, OBJECT_NAME_LENGTH, getScopeCatalogColumnName(), "COLUMNINFO").addField() .at(19).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SCOPE_SCHEMA", "COLUMNINFO").addField() .at(20).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SCOPE_TABLE", "COLUMNINFO").addField() .at(21).simple(SQL_SHORT, 0, "SOURCE_DATA_TYPE", "COLUMNINFO").addField() .at(22).simple(SQL_VARYING, 3, "IS_AUTOINCREMENT", "COLUMNINFO").addField() .at(23).simple(SQL_VARYING, 3, "IS_GENERATEDCOLUMN", "COLUMNINFO").addField() .at(24).simple(SQL_VARYING, 3, "JB_IS_IDENTITY", "COLUMNINFO").addField() .at(25).simple(SQL_VARYING, 10, "JB_IDENTITY_TYPE", "COLUMNINFO").addField() .toRowDescriptor(); Clause tableClause = new Clause("RF.RDB$RELATION_NAME", tableNamePattern); Clause columnClause = new Clause("RF.RDB$FIELD_NAME", columnNamePattern); String sql = hasIdentityColumns() ? GET_COLUMNS_3_0_START : GET_COLUMNS_START; sql += tableClause.getCondition(); sql += columnClause.getCondition(); sql += GET_COLUMNS_END; List params = new ArrayList<>(2); if (tableClause.hasCondition()) { params.add(tableClause.getValue()); } if (columnClause.hasCondition()) { params.add(columnClause.getValue()); } try (ResultSet rs = doQuery(sql, params)) { if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); final boolean supportsFloatBinaryPrecision = firebirdSupportInfo.supportsFloatBinaryPrecision(); do { final short fieldType = rs.getShort("FIELD_TYPE"); final short fieldSubType = rs.getShort("FIELD_SUB_TYPE"); final short fieldScale = rs.getShort("FIELD_SCALE"); final int characterSetId = rs.getInt("RDB$CHARACTER_SET_ID"); final int dataType = getDataType(fieldType, fieldSubType, fieldScale, characterSetId); valueBuilder .at(2).set(getBytes(rs.getString("RELATION_NAME"))) .at(3).set(getBytes(rs.getString("FIELD_NAME"))) .at(4).set(createInt(dataType)) .at(5).set(getBytes(getDataTypeName(fieldType, fieldSubType, fieldScale))) .at(9).set(RADIX_TEN); switch (dataType) { case Types.DECIMAL: case Types.NUMERIC: { short precision = rs.getShort("FIELD_PRECISION"); if (precision == 0 && fieldType == int128_type) { precision = NUMERIC_INT128_PRECISION; } valueBuilder .at(6).set(createInt(precision)) .at(8).set(createInt(fieldScale * (-1))); } break; case Types.CHAR: case Types.VARCHAR: case Types.BINARY: case Types.VARBINARY: valueBuilder.at(15).set(createInt(rs.getShort("FIELD_LENGTH"))); short charLen = rs.getShort("CHAR_LEN"); if (!rs.wasNull()) { valueBuilder.at(6).set(createInt(charLen)); } else { valueBuilder.at(6).set(valueBuilder.get(15)); } break; case Types.FLOAT: if (supportsFloatBinaryPrecision) { valueBuilder .at(6).set(createInt(FLOAT_BINARY_PRECISION)) .at(9).set(RADIX_BINARY); } else { valueBuilder.at(6).set(createInt(FLOAT_DECIMAL_PRECISION)); } break; case Types.DOUBLE: if (supportsFloatBinaryPrecision) { valueBuilder .at(6).set(createInt(DOUBLE_BINARY_PRECISION)) .at(9).set(RADIX_BINARY); } else { valueBuilder.at(6).set(createInt(DOUBLE_DECIMAL_PRECISION)); } break; case Types.BIGINT: valueBuilder .at(6).set(BIGINT_PRECISION) .at(8).set(INT_ZERO); break; case Types.INTEGER: valueBuilder .at(6).set(INTEGER_PRECISION) .at(8).set(INT_ZERO); break; case Types.SMALLINT: valueBuilder .at(6).set(SMALLINT_PRECISION) .at(8).set(INT_ZERO); break; case Types.DATE: valueBuilder.at(6).set(DATE_PRECISION); break; case Types.TIME: valueBuilder.at(6).set(TIME_PRECISION); break; case Types.TIMESTAMP: valueBuilder.at(6).set(TIMESTAMP_PRECISION); break; case JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE: valueBuilder.at(6).set(TIMESTAMP_WITH_TIMEZONE_PRECISION); break; case JaybirdTypeCodes.TIME_WITH_TIMEZONE: valueBuilder.at(6).set(TIME_WITH_TIMEZONE_PRECISION); break; case Types.BOOLEAN: valueBuilder .at(6).set(BOOLEAN_PRECISION) .at(9).set(RADIX_BINARY); break; case JaybirdTypeCodes.DECFLOAT: switch (fieldType) { case dec16_type: valueBuilder.at(6).set(DECFLOAT_16_PRECISION); break; case dec34_type: valueBuilder.at(6).set(DECFLOAT_34_PRECISION); break; } break; } final short nullFlag = rs.getShort("NULL_FLAG"); final short sourceNullFlag = rs.getShort("SOURCE_NULL_FLAG"); valueBuilder.at(10).set(nullFlag == 1 || sourceNullFlag == 1 ? COLUMN_NO_NULLS : COLUMN_NULLABLE) .at(11).set(getBytes(rs.getString("REMARKS"))); String column_def = rs.getString("DEFAULT_SOURCE"); if (column_def == null) { column_def = rs.getString("DOMAIN_DEFAULT_SOURCE"); } if (column_def != null) { // TODO This looks suspicious (what if it contains default) int defaultPos = column_def.toUpperCase().indexOf("DEFAULT"); if (defaultPos >= 0) column_def = column_def.substring(7).trim(); valueBuilder.at(12).set(getBytes(column_def)); } valueBuilder .at(16).set(createInt(rs.getInt("FIELD_POSITION"))) .at(17).set(nullFlag == 1 || sourceNullFlag == 1 ? NO_BYTES : YES_BYTES); final boolean isIdentity = Objects.equals("YES", rs.getString("IS_IDENTITY")); if (isIdentity) { // identity column is an autoincrement for sure valueBuilder.at(22).set(YES_BYTES); } else { switch (dataType) { case Types.INTEGER: case Types.TINYINT: case Types.BIGINT: case Types.SMALLINT: // Could be autoincrement by trigger, but we simply don't know valueBuilder.at(22).set(EMPTY_STRING_BYTES); break; case Types.NUMERIC: case Types.DECIMAL: if (fieldScale == 0 && fieldType != int128_type) { // Could be autoincrement by trigger, but we simply don't know valueBuilder.at(22).set(EMPTY_STRING_BYTES); } else { // Scaled NUMERIC/DECIMAL or INT128-based: definitely not autoincrement valueBuilder.at(22).set(NO_BYTES); } break; default: // All other types are never autoincrement valueBuilder.at(22).set(NO_BYTES); } } // Retrieving COMPUTED_BLR to check if it was NULL or not rs.getString("COMPUTED_BLR"); // consider identity columns to be generated columns boolean isGenerated = !rs.wasNull() || isIdentity; valueBuilder.at(23).set(isGenerated ? YES_BYTES : NO_BYTES); valueBuilder.at(24).set(isIdentity ? YES_BYTES : NO_BYTES); valueBuilder.at(25).set(getBytes(rs.getString("JB_IDENTITY_TYPE"))); rows.add(valueBuilder.toRowValue(true)); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } private boolean hasIdentityColumns() throws SQLException { return getOdsMajorVersion() >= 12; } /** * Gets the name of the correct scope catalog column name based on the JDBC version for use in * {@link #getColumns(String, String, String, String)}. *

* Rationale: in older versions of JDBC this column was misspelled as "SCOPE_CATLOG" instead of * "SCOPE_CATALOG". This was fixed in JDBC 4.1 *

* * @return The scope catalog name. */ private String getScopeCatalogColumnName() { final String scopeCatalog; if (getJDBCMajorVersion() > 4 || getJDBCMajorVersion() == 4 && getJDBCMinorVersion() >= 1) { scopeCatalog = "SCOPE_CATALOG"; } else { scopeCatalog = "SCOPE_CATLOG"; } return scopeCatalog; } private static final String GET_COLUMN_PRIVILEGES_START = "select " + "cast(RF.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as TABLE_NAME," + "cast(RF.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as COLUMN_NAME," + "cast(UP.RDB$GRANTOR as " + OBJECT_NAME_TYPE + ") as GRANTOR," + "cast(UP.RDB$USER as " + OBJECT_NAME_TYPE + ") as GRANTEE," + "cast(UP.RDB$PRIVILEGE as varchar(6)) as PRIVILEGE," + "UP.RDB$GRANT_OPTION as IS_GRANTABLE " + "from " + "RDB$RELATION_FIELDS RF," + "RDB$FIELDS F," + "RDB$USER_PRIVILEGES UP " + "where " + "RF.RDB$RELATION_NAME = UP.RDB$RELATION_NAME and " + "RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME and " + "(UP.RDB$FIELD_NAME is null or " + "UP.RDB$FIELD_NAME = RF.RDB$FIELD_NAME) and " + "UP.RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER + " and (("; private static final String GET_COLUMN_PRIVILEGES_END = " UP.RDB$OBJECT_TYPE = 0) or " + "(RF.RDB$FIELD_NAME is null and UP.RDB$OBJECT_TYPE = 0)) " + "order by 2,5 "; private static final Map PRIVILEGE_MAPPING; static { Map tempMapping = new HashMap<>(); tempMapping.put("A", getBytes("ALL")); tempMapping.put("S", getBytes("SELECT")); tempMapping.put("D", getBytes("DELETE")); tempMapping.put("I", getBytes("INSERT")); tempMapping.put("U", getBytes("UPDATE")); tempMapping.put("R", getBytes("REFERENCE")); // TODO: JDBC apidoc specifies REFRENCES (yes: typo and + S) tempMapping.put("M", getBytes("MEMBEROF")); PRIVILEGE_MAPPING = Collections.unmodifiableMap(tempMapping); } /** * Maps the (one character) Firebird privilege to the equivalent JDBC privilege. * * @param firebirdPrivilege * Firebird privilege * @return JDBC privilege encoded as byte array */ private static byte[] mapPrivilege(String firebirdPrivilege) { return PRIVILEGE_MAPPING.get(firebirdPrivilege); } @Override public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(8, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "COLUMNPRIV").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "COLUMNPRIV").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "COLUMNPRIV").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "COLUMNPRIV").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "GRANTOR", "COLUMNPRIV").addField() .at(5).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "GRANTEE", "COLUMNPRIV").addField() .at(6).simple(SQL_VARYING, 31, "PRIVILEGE", "COLUMNPRIV").addField() .at(7).simple(SQL_VARYING, 31, "IS_GRANTABLE", "COLUMNPRIV").addField() .toRowDescriptor(); Clause columnClause = new Clause("RF.RDB$FIELD_NAME", columnNamePattern); String sql = GET_COLUMN_PRIVILEGES_START; sql += columnClause.getCondition(); sql += GET_COLUMN_PRIVILEGES_END; List params = new ArrayList<>(2); params.add(table); if (columnClause.hasCondition()) { params.add(columnClause.getValue()); } try (ResultSet rs = doQuery(sql, params)) { // return empty result set if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(rs.getString("TABLE_NAME"))) .at(3).set(getBytes(rs.getString("COLUMN_NAME"))) .at(4).set(getBytes(rs.getString("GRANTOR"))) .at(5).set(getBytes(rs.getString("GRANTEE"))) .at(6).set(mapPrivilege(rs.getString("PRIVILEGE"))) .at(7).set(rs.getShort("IS_GRANTABLE") == 0 ? NO_BYTES : YES_BYTES) .toRowValue(true) ); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } private static final String GET_TABLE_PRIVILEGES_START = "select " + "cast(RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as TABLE_NAME," + "cast(RDB$GRANTOR as " + OBJECT_NAME_TYPE + ") as GRANTOR," + "cast(RDB$USER as " + OBJECT_NAME_TYPE + ") as GRANTEE," + "cast(RDB$PRIVILEGE as varchar(6)) as PRIVILEGE," + "RDB$GRANT_OPTION as IS_GRANTABLE " + "from" + " RDB$USER_PRIVILEGES " + "where "; private static final String GET_TABLE_PRIVILEGES_END = " RDB$OBJECT_TYPE = 0 and" + " RDB$FIELD_NAME is null " + "order by 1, 4"; @Override public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { final RowDescriptor rowDescriptor = buildTablePrivilegeRSMetaData(); Clause tableClause = new Clause("RDB$RELATION_NAME", tableNamePattern); String sql = GET_TABLE_PRIVILEGES_START; sql += tableClause.getCondition(); sql += GET_TABLE_PRIVILEGES_END; List params = tableClause.hasCondition() ? Collections.singletonList(tableClause.getValue()) : Collections.emptyList(); try (ResultSet rs = doQuery(sql, params)) { // if nothing found, return an empty result set if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } return processTablePrivileges(rowDescriptor, rs); } } protected final RowDescriptor buildTablePrivilegeRSMetaData() { return new RowDescriptorBuilder(7, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "TABLEPRIV").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "TABLEPRIV").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "TABLEPRIV").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "GRANTOR", "TABLEPRIV").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "GRANTEE", "TABLEPRIV").addField() .at(5).simple(SQL_VARYING, 31, "PRIVILEGE", "TABLEPRIV").addField() .at(6).simple(SQL_VARYING, 31, "IS_GRANTABLE", "TABLEPRIV").addField() .toRowDescriptor(); } protected final FBResultSet processTablePrivileges(final RowDescriptor rowDescriptor, final ResultSet fbTablePrivileges) throws SQLException { final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(fbTablePrivileges.getString("TABLE_NAME"))) .at(3).set(getBytes(fbTablePrivileges.getString("GRANTOR"))) .at(4).set(getBytes(fbTablePrivileges.getString("GRANTEE"))) .at(5).set(mapPrivilege(fbTablePrivileges.getString("PRIVILEGE"))) .at(6).set(fbTablePrivileges.getShort("IS_GRANTABLE") == 0 ? NO_BYTES : YES_BYTES) .toRowValue(true) ); } while (fbTablePrivileges.next()); return new FBResultSet(rowDescriptor, rows); } //@formatter:off private static final String GET_BEST_ROW_IDENT = "SELECT " + "CAST(rf.rdb$field_name AS " + OBJECT_NAME_TYPE + ") AS column_name," + "f.rdb$field_type AS field_type," + "f.rdb$field_sub_type AS field_sub_type," + "f.rdb$field_scale AS field_scale," + "f.rdb$field_precision AS field_precision," + "f.RDB$CHARACTER_SET_ID " + "FROM rdb$relation_constraints rc " + "INNER JOIN rdb$index_segments idx ON idx.rdb$index_name = rc.rdb$index_name " + "INNER JOIN rdb$relation_fields rf ON rf.rdb$field_name = idx.rdb$field_name " + " AND rf.rdb$relation_name = rc.rdb$relation_name " + "INNER JOIN rdb$fields f ON f.rdb$field_name = rf.rdb$field_source " + "WHERE " + "rc.rdb$relation_name = " + OBJECT_NAME_PARAMETER + "AND rc.rdb$constraint_type = 'PRIMARY KEY'"; //@formatter:on /** * {@inheritDoc} *

* Jaybird considers the primary key (scoped as {@code bestRowSession} as the best identifier for all scopes. * Pseudo column {@code RDB$DB_KEY} (scoped as {@code bestRowTransaction} is considered the second-best alternative * for scopes {@code bestRowTemporary} and {@code bestRowTransaction} if {@code table} has no primary key. *

*

* Jaybird currently considers {@code RDB$DB_KEY} to be {@link DatabaseMetaData#bestRowTransaction} even if the * dbkey_scope is set to 1 (session). This may change in the future. See also {@link #getRowIdLifetime()}. *

*/ @Override public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(8, datatypeCoder) .at(0).simple(SQL_SHORT, 0, "SCOPE", "ROWIDENTIFIER").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "ROWIDENTIFIER").addField() .at(2).simple(SQL_LONG, 0, "DATA_TYPE", "ROWIDENTIFIER").addField() .at(3).simple(SQL_VARYING, 31, "TYPE_NAME", "ROWIDENTIFIER").addField() .at(4).simple(SQL_LONG, 0, "COLUMN_SIZE", "ROWIDENTIFIER").addField() .at(5).simple(SQL_LONG, 0, "BUFFER_LENGTH", "ROWIDENTIFIER").addField() .at(6).simple(SQL_SHORT, 0, "DECIMAL_DIGITS", "ROWIDENTIFIER").addField() .at(7).simple(SQL_SHORT, 0, "PSEUDO_COLUMN", "ROWIDENTIFIER").addField() .toRowDescriptor(); final RowValueBuilder rowValueBuilder = new RowValueBuilder(rowDescriptor); final List rows = getPrimaryKeyIdentifier(table, rowValueBuilder); // if no primary key exists, add RDB$DB_KEY as pseudo-column if (rows.size() == 0) { // NOTE: Currently is always ROWID_VALID_TRANSACTION final RowIdLifetime rowIdLifetime = getRowIdLifetime(); if (rowIdLifetime == RowIdLifetime.ROWID_VALID_TRANSACTION && scope == DatabaseMetaData.bestRowSession) { // consider RDB$DB_KEY scope transaction return new FBResultSet(rowDescriptor, Collections.emptyList()); } try (ResultSet pseudoColumns = getPseudoColumns(catalog, schema, escapeWildcards(table), "RDB$DB\\_KEY")) { if (!pseudoColumns.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } rows.add(rowValueBuilder .at(0).set(createShort( rowIdLifetime == RowIdLifetime.ROWID_VALID_TRANSACTION ? DatabaseMetaData.bestRowTransaction : DatabaseMetaData.bestRowSession)) .at(1).set(getBytes("RDB$DB_KEY")) .at(2).set(createInt(Types.ROWID)) .at(3).set(getBytes(getDataTypeName(char_type, 0, CS_BINARY))) .at(4).set(createInt(pseudoColumns.getInt(6))) .at(7).set(createShort(DatabaseMetaData.bestRowPseudo)) .toRowValue(true)); } } return new FBResultSet(rowDescriptor, rows); } /** * Get primary key of the table as best row identifier. * * @param table * name of the table. * @param valueBuilder * Builder for row values * @return list of result set values, when empty, no primary key has been defined for a table or the table does not * exist. The returned list can be modified by caller if needed. * @throws SQLException * if something went wrong. */ private List getPrimaryKeyIdentifier(String table, final RowValueBuilder valueBuilder) throws SQLException { try (ResultSet rs = doQuery(GET_BEST_ROW_IDENT, Collections.singletonList(table))) { final List rows = new ArrayList<>(); while (rs.next()) { short fieldType = rs.getShort("FIELD_TYPE"); short fieldSubType = rs.getShort("FIELD_SUB_TYPE"); short fieldScale = rs.getShort("FIELD_SCALE"); int characterSetId = rs.getInt("RDB$CHARACTER_SET_ID"); rows.add(valueBuilder .at(0).set(createShort(DatabaseMetaData.bestRowSession)) .at(1).set(getBytes(rs.getString("COLUMN_NAME"))) .at(2).set(createShort(getDataType(fieldType, fieldSubType, fieldScale, characterSetId))) .at(3).set(getBytes(getDataTypeName(fieldType, fieldSubType, fieldScale))) .at(4).set(createInt(rs.getInt("FIELD_PRECISION"))) .at(6).set(createShort(fieldScale)) .at(7).set(createShort(bestRowNotPseudo)) .toRowValue(true) ); } return rows; } } /** * {@inheritDoc} *

* Jaybird considers both {@code RDB$DB_KEY} and {@code RDB$RECORD_VERSION} (Firebird 3 and higher) as version * columns. *

*

* Jaybird only returns pseudo-column as version columns, so 'last updated' columns updated by a trigger, * calculated columns, or other forms of change tracking are not reported by this method. *

*/ @Override public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(8, datatypeCoder) .at(0).simple(SQL_SHORT, 0, "SCOPE", "VERSIONCOL").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "VERSIONCOL").addField() .at(2).simple(SQL_LONG, 0, "DATA_TYPE", "VERSIONCOL").addField() .at(3).simple(SQL_VARYING, 31, "TYPE_NAME", "VERSIONCOL").addField() .at(4).simple(SQL_LONG, 0, "COLUMN_SIZE", "VERSIONCOL").addField() .at(5).simple(SQL_LONG, 0, "BUFFER_LENGTH", "VERSIONCOL").addField() .at(6).simple(SQL_SHORT, 0, "DECIMAL_DIGITS", "VERSIONCOL").addField() .at(7).simple(SQL_SHORT, 0, "PSEUDO_COLUMN", "VERSIONCOL").addField() .toRowDescriptor(); if (table == null || "".equals(table)) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } try (ResultSet pseudoColumns = getPseudoColumns(catalog, schema, escapeWildcards(table), "%")) { if (!pseudoColumns.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } List rowValues = new ArrayList<>(2); RowValueBuilder rowValueBuilder = new RowValueBuilder(rowDescriptor); do { String columnName = pseudoColumns.getString(4); boolean isDbKey = "RDB$DB_KEY".equals(columnName); boolean isRecordVersion = !isDbKey && "RDB$RECORD_VERSION".equals(columnName); // Protect against future addition of other pseudo columns if (!(isDbKey || isRecordVersion)) continue; rowValueBuilder .at(1).set(getBytes(columnName)) .at(2).set(createInt(pseudoColumns.getInt(5))) .at(3).set(getBytes(isDbKey ? "CHAR" : "BIGINT")) .at(4).set(createInt(pseudoColumns.getInt(6))) .at(5).set(createInt(isDbKey ? pseudoColumns.getInt(11) : 8)) .at(6).set(isRecordVersion ? SHORT_ZERO : null) .at(7).set(createShort(DatabaseMetaData.versionColumnPseudo)); rowValues.add(rowValueBuilder.toRowValue(true)); } while (pseudoColumns.next()); return new FBResultSet(rowDescriptor, rowValues); } } private static final String GET_PRIMARY_KEYS = "select " + "cast(RC.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as TABLE_NAME," + "cast(ISGMT.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as COLUMN_NAME," + "CAST((ISGMT.RDB$FIELD_POSITION + 1) as SMALLINT) as KEY_SEQ," + "cast(RC.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as PK_NAME " + "from " + "RDB$RELATION_CONSTRAINTS RC " + "INNER JOIN RDB$INDEX_SEGMENTS ISGMT ON RC.RDB$INDEX_NAME = ISGMT.RDB$INDEX_NAME " + "where RC.RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER + "and RC.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY' " + "order by ISGMT.RDB$FIELD_NAME "; @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { RowDescriptor rowDescriptor = new RowDescriptorBuilder(6, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "COLUMNINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "COLUMNINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "COLUMNINFO").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "COLUMNINFO").addField() .at(4).simple(SQL_SHORT, 0, "KEY_SEQ", "COLUMNINFO").addField() .at(5).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PK_NAME", "COLUMNINFO").addField() .toRowDescriptor(); List params = Collections.singletonList(table); try (ResultSet rs = doQuery(GET_PRIMARY_KEYS, params)) { // if nothing found, return empty result set if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(rs.getString("TABLE_NAME"))) .at(3).set(getBytes(rs.getString("COLUMN_NAME"))) .at(4).set(createShort(rs.getShort("KEY_SEQ"))) .at(5).set(getBytes(rs.getString("PK_NAME"))) .toRowValue(true) ); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } private static final String GET_IMPORTED_KEYS = "select " +"cast(PK.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as PKTABLE_NAME" +",cast(ISP.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as PKCOLUMN_NAME" +",cast(FK.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as FKTABLE_NAME" +",cast(ISF.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as FKCOLUMN_NAME" +",CAST((ISP.RDB$FIELD_POSITION + 1) as SMALLINT) as KEY_SEQ" +",cast(RC.RDB$UPDATE_RULE as varchar(11)) as UPDATE_RULE" +",cast(RC.RDB$DELETE_RULE as varchar(11)) as DELETE_RULE" +",cast(PK.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as PK_NAME" +",cast(FK.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as FK_NAME " +"from " +"RDB$RELATION_CONSTRAINTS PK" +",RDB$RELATION_CONSTRAINTS FK" +",RDB$REF_CONSTRAINTS RC" +",RDB$INDEX_SEGMENTS ISP" +",RDB$INDEX_SEGMENTS ISF " +"WHERE FK.RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER +"and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME " +"and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ " +"and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME " +"and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME " +"and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION " +"order by 1, 5 "; private static final Map ACTION_MAPPING; static { Map tempMap = new HashMap<>(); tempMap.put("NO ACTION", IMPORTED_KEY_NO_ACTION); tempMap.put("RESTRICT", IMPORTED_KEY_NO_ACTION); tempMap.put("CASCADE", IMPORTED_KEY_CASCADE); tempMap.put("SET NULL", IMPORTED_KEY_SET_NULL); tempMap.put("SET DEFAULT", IMPORTED_KEY_SET_DEFAULT); ACTION_MAPPING = Collections.unmodifiableMap(tempMap); } /** * Maps the Firebird action name to the equivalent JDBC action. * * @param fbAction * Firebird action * @return JDBC action encoded as byte array */ private static byte[] mapAction(String fbAction) { return ACTION_MAPPING.get(fbAction); } @Override public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(14, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_CAT", "COLUMNINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_SCHEM", "COLUMNINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_NAME", "COLUMNINFO").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKCOLUMN_NAME", "COLUMNINFO").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_CAT", "COLUMNINFO").addField() .at(5).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_SCHEM", "COLUMNINFO").addField() .at(6).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_NAME", "COLUMNINFO").addField() .at(7).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKCOLUMN_NAME", "COLUMNINFO").addField() .at(8).simple(SQL_SHORT, 0, "KEY_SEQ", "COLUMNINFO").addField() .at(9).simple(SQL_SHORT, 0, "UPDATE_RULE", "COLUMNINFO").addField() .at(10).simple(SQL_SHORT, 0, "DELETE_RULE", "COLUMNINFO").addField() .at(11).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FK_NAME", "COLUMNINFO").addField() .at(12).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PK_NAME", "COLUMNINFO").addField() .at(13).simple(SQL_SHORT, 0, "DEFERRABILITY", "COLUMNINFO").addField() .toRowDescriptor(); List params = Collections.singletonList(table); try (ResultSet rs = doQuery(GET_IMPORTED_KEYS, params)) { // if nothing found, return an empty result set if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(rs.getString("PKTABLE_NAME"))) .at(3).set(getBytes(rs.getString("PKCOLUMN_NAME"))) .at(6).set(getBytes(rs.getString("FKTABLE_NAME"))) .at(7).set(getBytes(rs.getString("FKCOLUMN_NAME"))) .at(8).set(createShort(rs.getShort("KEY_SEQ"))) .at(9).set(mapAction(rs.getString("UPDATE_RULE"))) .at(10).set(mapAction(rs.getString("DELETE_RULE"))) .at(11).set(getBytes(rs.getString("FK_NAME"))) .at(12).set(getBytes(rs.getString("PK_NAME"))) .at(13).set(IMPORTED_KEY_NOT_DEFERRABLE) .toRowValue(true) ); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } private static final String GET_EXPORTED_KEYS = "select " +"cast(PK.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as PKTABLE_NAME" +",cast(ISP.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as PKCOLUMN_NAME" +",cast(FK.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as FKTABLE_NAME" +",cast(ISF.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as FKCOLUMN_NAME" +",CAST((ISP.RDB$FIELD_POSITION + 1) as SMALLINT) as KEY_SEQ" +",cast(RC.RDB$UPDATE_RULE as varchar(11)) as UPDATE_RULE" +",cast(RC.RDB$DELETE_RULE as varchar(11)) as DELETE_RULE" +",cast(PK.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as PK_NAME" +",cast(FK.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as FK_NAME " +"from " +"RDB$RELATION_CONSTRAINTS PK" +",RDB$RELATION_CONSTRAINTS FK" +",RDB$REF_CONSTRAINTS RC" +",RDB$INDEX_SEGMENTS ISP" +",RDB$INDEX_SEGMENTS ISF " +"WHERE PK.RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER +"and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME " +"and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ " +"and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME " +"and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME " +"and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION " +"order by 3, 5 "; @Override public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(14, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_CAT", "COLUMNINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_SCHEM", "COLUMNINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_NAME", "COLUMNINFO").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKCOLUMN_NAME", "COLUMNINFO").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_CAT", "COLUMNINFO").addField() .at(5).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_SCHEM", "COLUMNINFO").addField() .at(6).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_NAME", "COLUMNINFO").addField() .at(7).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKCOLUMN_NAME", "COLUMNINFO").addField() .at(8).simple(SQL_SHORT, 0, "KEY_SEQ", "COLUMNINFO").addField() .at(9).simple(SQL_SHORT, 0, "UPDATE_RULE", "COLUMNINFO").addField() .at(10).simple(SQL_SHORT, 0, "DELETE_RULE", "COLUMNINFO").addField() .at(11).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FK_NAME", "COLUMNINFO").addField() .at(12).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PK_NAME", "COLUMNINFO").addField() .at(13).simple(SQL_SHORT, 0, "DEFERRABILITY", "COLUMNINFO").addField() .toRowDescriptor(); List params = Collections.singletonList(table); try (ResultSet rs = doQuery(GET_EXPORTED_KEYS, params)) { // if nothing found, return an empty result set if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(rs.getString("PKTABLE_NAME"))) .at(3).set(getBytes(rs.getString("PKCOLUMN_NAME"))) .at(6).set(getBytes(rs.getString("FKTABLE_NAME"))) .at(7).set(getBytes(rs.getString("FKCOLUMN_NAME"))) .at(8).set(createShort(rs.getShort("KEY_SEQ"))) .at(9).set(mapAction(rs.getString("UPDATE_RULE"))) .at(10).set(mapAction(rs.getString("DELETE_RULE"))) .at(11).set(getBytes(rs.getString("FK_NAME"))) .at(12).set(getBytes(rs.getString("PK_NAME"))) .at(13).set(IMPORTED_KEY_NOT_DEFERRABLE) .toRowValue(true) ); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } private static final String GET_CROSS_KEYS = "select " +"cast(PK.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as PKTABLE_NAME" +",cast(ISP.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as PKCOLUMN_NAME" +",cast(FK.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") as FKTABLE_NAME" +",cast(ISF.RDB$FIELD_NAME as " + OBJECT_NAME_TYPE + ") as FKCOLUMN_NAME" +",CAST((ISP.RDB$FIELD_POSITION + 1) as SMALLINT) as KEY_SEQ" +",cast(RC.RDB$UPDATE_RULE as varchar(11)) as UPDATE_RULE" +",cast(RC.RDB$DELETE_RULE as varchar(11)) as DELETE_RULE" +",cast(PK.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as PK_NAME" +",cast(FK.RDB$CONSTRAINT_NAME as " + OBJECT_NAME_TYPE + ") as FK_NAME" +" from " +"RDB$RELATION_CONSTRAINTS PK" +",RDB$RELATION_CONSTRAINTS FK" +",RDB$REF_CONSTRAINTS RC" +",RDB$INDEX_SEGMENTS ISP" +",RDB$INDEX_SEGMENTS ISF " +"WHERE PK.RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER +"and FK.RDB$RELATION_NAME = " + OBJECT_NAME_PARAMETER +"and FK.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME " +"and PK.RDB$CONSTRAINT_NAME = RC.RDB$CONST_NAME_UQ " +"and ISP.RDB$INDEX_NAME = PK.RDB$INDEX_NAME " +"and ISF.RDB$INDEX_NAME = FK.RDB$INDEX_NAME " +"and ISP.RDB$FIELD_POSITION = ISF.RDB$FIELD_POSITION " +"order by 3, 5 "; @Override public ResultSet getCrossReference( String primaryCatalog, String primarySchema, String primaryTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(14, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_CAT", "COLUMNINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_SCHEM", "COLUMNINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKTABLE_NAME", "COLUMNINFO").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PKCOLUMN_NAME", "COLUMNINFO").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_CAT", "COLUMNINFO").addField() .at(5).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_SCHEM", "COLUMNINFO").addField() .at(6).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKTABLE_NAME", "COLUMNINFO").addField() .at(7).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FKCOLUMN_NAME", "COLUMNINFO").addField() .at(8).simple(SQL_SHORT, 0, "KEY_SEQ", "COLUMNINFO").addField() .at(9).simple(SQL_SHORT, 0, "UPDATE_RULE", "COLUMNINFO").addField() .at(10).simple(SQL_SHORT, 0, "DELETE_RULE", "COLUMNINFO").addField() .at(11).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "FK_NAME", "COLUMNINFO").addField() .at(12).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "PK_NAME", "COLUMNINFO").addField() .at(13).simple(SQL_SHORT, 0, "DEFERRABILITY", "COLUMNINFO").addField() .toRowDescriptor(); final List params = Arrays.asList(primaryTable, foreignTable); try (ResultSet rs = doQuery(GET_CROSS_KEYS, params)) { // return empty result set if nothing found if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { rows.add(valueBuilder .at(2).set(getBytes(rs.getString("PKTABLE_NAME"))) .at(3).set(getBytes(rs.getString("PKCOLUMN_NAME"))) .at(6).set(getBytes(rs.getString("FKTABLE_NAME"))) .at(7).set(getBytes(rs.getString("FKCOLUMN_NAME"))) .at(8).set(createShort(rs.getShort("KEY_SEQ"))) .at(9).set(mapAction(rs.getString("UPDATE_RULE"))) .at(10).set(mapAction(rs.getString("DELETE_RULE"))) .at(11).set(getBytes(rs.getString("FK_NAME"))) .at(12).set(getBytes(rs.getString("PK_NAME"))) .at(13).set(IMPORTED_KEY_NOT_DEFERRABLE) .toRowValue(true) ); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } /** * Function to convert integer values to encoded byte arrays for shorts. * * @param value * integer value to convert * @return encoded byte array representing the value */ private static byte[] createShort(int value) { assert (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) : String.format("Value \"%d\" outside range of short", value); return datatypeCoder.encodeShort(value); } /** * Function to convert integer values to encoded byte arrays for integers. * * @param value * integer value to convert * @return encoded byte array representing the value */ private static byte[] createInt(int value) { return datatypeCoder.encodeInt(value); } @Override public ResultSet getTypeInfo() throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(18, datatypeCoder) .at(0).simple(SQL_VARYING, 31, "TYPE_NAME", "TYPEINFO").addField() .at(1).simple(SQL_LONG, 0, "DATA_TYPE", "TYPEINFO").addField() .at(2).simple(SQL_LONG, 0, "PRECISION", "TYPEINFO").addField() .at(3).simple(SQL_VARYING, 1, "LITERAL_PREFIX", "TYPEINFO").addField() .at(4).simple(SQL_VARYING, 1, "LITERAL_SUFFIX", "TYPEINFO").addField() .at(5).simple(SQL_VARYING, 31, "CREATE_PARAMS", "TYPEINFO").addField() .at(6).simple(SQL_SHORT, 0, "NULLABLE", "TYPEINFO").addField() .at(7).simple(SQL_TEXT, 1, "CASE_SENSITIVE", "TYPEINFO").addField() .at(8).simple(SQL_SHORT, 0, "SEARCHABLE", "TYPEINFO").addField() .at(9).simple(SQL_TEXT, 1, "UNSIGNED_ATTRIBUTE", "TYPEINFO").addField() .at(10).simple(SQL_TEXT, 1, "FIXED_PREC_SCALE", "TYPEINFO").addField() .at(11).simple(SQL_TEXT, 1, "AUTO_INCREMENT", "TYPEINFO").addField() .at(12).simple(SQL_VARYING, 31, "LOCAL_TYPE_NAME", "TYPEINFO").addField() .at(13).simple(SQL_SHORT, 0, "MINIMUM_SCALE", "TYPEINFO").addField() .at(14).simple(SQL_SHORT, 0, "MAXIMUM_SCALE", "TYPEINFO").addField() .at(15).simple(SQL_LONG, 0, "SQL_DATA_TYPE", "TYPEINFO").addField() .at(16).simple(SQL_LONG, 0, "SQL_DATETIME_SUB", "TYPEINFO").addField() .at(17).simple(SQL_LONG, 0, "NUM_PREC_RADIX", "TYPEINFO").addField() .toRowDescriptor(); final byte[] blobTypePred = firebirdSupportInfo.supportsFullSearchableBlobs() ? TYPE_SEARCHABLE : TYPE_PRED_BASIC; //dialect 3 only final List rows = new ArrayList<>(20); // DECFLOAT=-6001 (TODO Change when standardized) if (firebirdSupportInfo.supportsDecfloat()) { rows.add(RowValue.of(rowDescriptor, getBytes("DECFLOAT"), createInt(JaybirdTypeCodes.DECFLOAT), DECFLOAT_34_PRECISION, null, null, getBytes("precision"), TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, VARIABLESCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_DEC34), null, RADIX_TEN)); } //BIGINT=-5 rows.add(RowValue.of(rowDescriptor, getBytes("BIGINT"), createInt(Types.BIGINT), BIGINT_PRECISION, null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_INT64), null, RADIX_TEN)); //LONGVARBINARY=-4 rows.add(RowValue.of(rowDescriptor, getBytes("BLOB SUB_TYPE BINARY"), createInt(Types.LONGVARBINARY), INT_ZERO, getBytes("x'"), getBytes("'"), null, TYPE_NULLABLE, CASESENSITIVE, blobTypePred, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_BLOB), null, RADIX_TEN)); //VARBINARY=-3 rows.add(RowValue.of(rowDescriptor, getBytes("VARCHAR"), createInt(Types.VARBINARY), createInt(32765), getBytes("x'"), getBytes("'"), getBytes("length"), TYPE_NULLABLE, CASESENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_VARYING), null, RADIX_TEN)); //BINARY=-2 rows.add(RowValue.of(rowDescriptor, getBytes("CHAR"), createInt(Types.BINARY), createInt(32767), getBytes("x'"), getBytes("'"), getBytes("length"), TYPE_NULLABLE, CASESENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TEXT), null, RADIX_TEN)); //LONGVARCHAR=-1 rows.add(RowValue.of(rowDescriptor, getBytes("BLOB SUB_TYPE TEXT"), createInt(Types.LONGVARCHAR), INT_ZERO, getBytes("'"), getBytes("'"), null, TYPE_NULLABLE, CASESENSITIVE, blobTypePred, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_BLOB), null, RADIX_TEN)); //CHAR=1 rows.add(RowValue.of(rowDescriptor, getBytes("CHAR"), createInt(Types.CHAR), createInt(32767), getBytes("'"), getBytes("'"), getBytes("length"), TYPE_NULLABLE, CASESENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TEXT), null, RADIX_TEN)); // also for numeric final byte[] maxDecimalPrecision = createInt(firebirdSupportInfo.maxDecimalPrecision()); final byte[] maxDecimalScale = maxDecimalPrecision; //NUMERIC=2 rows.add(RowValue.of(rowDescriptor, getBytes("NUMERIC"), createInt(Types.NUMERIC), maxDecimalPrecision, null, null, getBytes("precision,scale"), TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, maxDecimalScale, createInt(SQL_INT64), null, RADIX_TEN)); // Handle INT128 as a JDBC type NUMERIC if (firebirdSupportInfo.supportsInt128()) { rows.add(RowValue.of(rowDescriptor, getBytes("INT128"), createInt(Types.NUMERIC), createInt(NUMERIC_INT128_PRECISION), null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_INT128), null, RADIX_TEN)); } //DECIMAL=3 rows.add(RowValue.of(rowDescriptor, getBytes("DECIMAL"), createInt(Types.DECIMAL), maxDecimalPrecision, null, null, getBytes("precision,scale"), TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, maxDecimalScale, createInt(SQL_INT64), null, RADIX_TEN)); //INTEGER=4 rows.add(RowValue.of(rowDescriptor, getBytes("INTEGER"), createInt(Types.INTEGER), INTEGER_PRECISION, null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_LONG), null, RADIX_TEN)); //SMALLINT=5 rows.add(RowValue.of(rowDescriptor, getBytes("SMALLINT"), createInt(Types.SMALLINT), SMALLINT_PRECISION, null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_SHORT), null, RADIX_TEN)); boolean supportsFloatBinaryPrecision = firebirdSupportInfo.supportsFloatBinaryPrecision(); //FLOAT=6 // Technically this describes REAL, but historically FLOAT == REAL in Firebird, and Jaybird has only used FLOAT int floatPrecision = supportsFloatBinaryPrecision ? 24 : 7; // We're intentionally not communicating the max FLOAT precision of 53 (which is a synonym of DOUBLE PRECISION) // nor are we reporting "precision" for column CREATE_PARAMS rows.add(RowValue.of(rowDescriptor, getBytes("FLOAT"), createInt(Types.FLOAT), createInt(floatPrecision), null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, VARIABLESCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_FLOAT), null, supportsFloatBinaryPrecision ? RADIX_BINARY : RADIX_TEN)); //DOUBLE=8 int doublePrecision = supportsFloatBinaryPrecision ? 53 : 15; rows.add(RowValue.of(rowDescriptor, getBytes("DOUBLE PRECISION"), createInt(Types.DOUBLE), createInt(doublePrecision), null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, SIGNED, VARIABLESCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_DOUBLE), null, supportsFloatBinaryPrecision ? RADIX_BINARY : RADIX_TEN)); //VARCHAR=12 rows.add(RowValue.of(rowDescriptor, getBytes("VARCHAR"), createInt(Types.VARCHAR), createInt(32765), getBytes("'"), getBytes("'"), getBytes("length"), TYPE_NULLABLE, CASESENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_VARYING), null, RADIX_TEN)); //BOOLEAN=16 if (getDatabaseMajorVersion() >= 3) { rows.add(RowValue.of(rowDescriptor, getBytes("BOOLEAN"), createInt(Types.BOOLEAN), BOOLEAN_PRECISION, null, null, null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_PRED_BASIC, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_BOOLEAN), null, RADIX_BINARY)); } //DATE=91 rows.add(RowValue.of(rowDescriptor, getBytes("DATE"), createInt(Types.DATE), DATE_PRECISION, getBytes("date'"), getBytes("'"), null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TYPE_DATE), null, RADIX_TEN)); //TIME=92 rows.add(RowValue.of(rowDescriptor, getBytes("TIME"), createInt(Types.TIME), TIME_PRECISION, getBytes("time'"), getBytes("'"), null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TYPE_TIME), null, RADIX_TEN)); //TIMESTAMP=93 rows.add(RowValue.of(rowDescriptor, getBytes("TIMESTAMP"), createInt(Types.TIMESTAMP), TIMESTAMP_PRECISION, getBytes("timestamp'"), getBytes("'"), null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TIMESTAMP), null, RADIX_TEN)); //OTHER=1111 rows.add(RowValue.of(rowDescriptor, getBytes("ARRAY"), createInt(Types.OTHER), INT_ZERO, null, null, null, TYPE_NULLABLE, CASESENSITIVE, TYPE_PRED_NONE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_ARRAY), null, RADIX_TEN)); //BLOB=2004 // Should we split this into all negative blob types currently known in the DB? // Blob is potentially searchable with like, etc, acting as if it isn't. rows.add(RowValue.of(rowDescriptor, getBytes("BLOB SUB_TYPE <0 "), createInt(Types.BLOB), INT_ZERO, null, null, null, TYPE_NULLABLE, CASESENSITIVE, TYPE_PRED_NONE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_BLOB), null, RADIX_TEN)); if (firebirdSupportInfo.supportsTimeZones()) { //TIME_WITH_TIMEZONE=2013 rows.add(RowValue.of(rowDescriptor, getBytes("TIME WITH TIME ZONE"), createInt(JaybirdTypeCodes.TIME_WITH_TIMEZONE), TIME_WITH_TIMEZONE_PRECISION, getBytes("time'"), getBytes("'"), null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TIME_TZ), null, RADIX_TEN)); //TIMESTAMP_WITH_TIMEZONE=2014 rows.add(RowValue.of(rowDescriptor, getBytes("TIMESTAMP WITH TIME ZONE"), createInt(JaybirdTypeCodes.TIMESTAMP_WITH_TIMEZONE), TIMESTAMP_WITH_TIMEZONE_PRECISION, getBytes("timestamp'"), getBytes("'"), null, TYPE_NULLABLE, CASEINSENSITIVE, TYPE_SEARCHABLE, UNSIGNED, FIXEDSCALE, NOTAUTOINC, null, SHORT_ZERO, SHORT_ZERO, createInt(SQL_TIMESTAMP_TZ), null, RADIX_TEN)); } return new FBResultSet(rowDescriptor, rows); } private static final String GET_INDEX_INFO = "SELECT " + "cast(ind.RDB$RELATION_NAME as " + OBJECT_NAME_TYPE + ") AS TABLE_NAME" + ",ind.RDB$UNIQUE_FLAG AS UNIQUE_FLAG" + ",cast(ind.RDB$INDEX_NAME as " + OBJECT_NAME_TYPE + ") as INDEX_NAME" + ",ise.rdb$field_position + 1 as ORDINAL_POSITION" + ",cast(ise.rdb$field_name as " + OBJECT_NAME_TYPE + ") as COLUMN_NAME" + ",ind.RDB$EXPRESSION_SOURCE as EXPRESSION_SOURCE" + ",ind.RDB$INDEX_TYPE as ASC_OR_DESC " + "FROM " + "rdb$indices ind " + "LEFT JOIN rdb$index_segments ise ON ind.rdb$index_name = ise.rdb$index_name " + "WHERE " + "ind.rdb$relation_name = " + OBJECT_NAME_PARAMETER + "ORDER BY 2, 3, 4"; @Override public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(13, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "INDEXINFO").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "INDEXINFO").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "INDEXINFO").addField() .at(3).simple(SQL_TEXT, 1, "NON_UNIQUE", "INDEXINFO").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "INDEX_QUALIFIER", "INDEXINFO").addField() .at(5).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "INDEX_NAME", "INDEXINFO").addField() .at(6).simple(SQL_SHORT, 0, "TYPE", "INDEXINFO").addField() .at(7).simple(SQL_SHORT, 0, "ORDINAL_POSITION", "INDEXINFO").addField() // Field with EXPRESSION_SOURCE (used for expression indexes) in Firebird is actually a blob, using Integer.MAX_VALUE for length .at(8).simple(SQL_VARYING, Integer.MAX_VALUE, "COLUMN_NAME", "INDEXINFO").addField() .at(9).simple(SQL_VARYING, 31, "ASC_OR_DESC", "INDEXINFO").addField() .at(10).simple(SQL_LONG, 0, "CARDINALITY", "INDEXINFO").addField() .at(11).simple(SQL_LONG, 0, "PAGES", "INDEXINFO").addField() .at(12).simple(SQL_VARYING, 31, "FILTER_CONDITION", "INDEXINFO").addField() .toRowDescriptor(); List params = Collections.singletonList(table); try (ResultSet rs = doQuery(GET_INDEX_INFO, params)) { if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } final List rows = new ArrayList<>(); final RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { final boolean isNotUnique = rs.getInt("UNIQUE_FLAG") == 0; if (unique && isNotUnique) { // Skip indices that are not unique, as requested continue; } valueBuilder .at(2).set(getBytes(rs.getString("TABLE_NAME"))) .at(3).set(isNotUnique ? TRUE_BYTES : FALSE_BYTES) .at(5).set(getBytes(rs.getString("INDEX_NAME"))) .at(6).set(TABLE_INDEX_OTHER); String columnName = rs.getString("COLUMN_NAME"); if (rs.wasNull()) { valueBuilder.at(7).set(SHORT_ONE); String expressionSource = rs.getString("EXPRESSION_SOURCE"); if (expressionSource != null) { valueBuilder.at(8).set(getBytes(expressionSource)); } } else { valueBuilder .at(7).set(createShort(rs.getShort("ORDINAL_POSITION"))) .at(8).set(getBytes(columnName)); } int ascOrDesc = rs.getInt("ASC_OR_DESC"); if (ascOrDesc == 0) { valueBuilder.at(9).set(ASC_BYTES); } else if (ascOrDesc == 1) { valueBuilder.at(9).set(DESC_BYTES); } // NOTE: We are setting CARDINALITY and PAGES to NULL as we don't have this info; might contravene JDBC spec // TODO index 10: use 1 / RDB$STATISTICS for approximation of CARDINALITY? // TODO index 11: query RDB$PAGES for PAGES information? rows.add(valueBuilder.toRowValue(true)); } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } @Override public boolean supportsResultSetType(int type) throws SQLException { // TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE? switch (type){ case ResultSet.TYPE_FORWARD_ONLY: case ResultSet.TYPE_SCROLL_INSENSITIVE : case ResultSet.TYPE_SCROLL_SENSITIVE : return true; default: return false; } } @Override public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { // TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE? switch(type) { case ResultSet.TYPE_FORWARD_ONLY: case ResultSet.TYPE_SCROLL_INSENSITIVE : case ResultSet.TYPE_SCROLL_SENSITIVE : return concurrency == ResultSet.CONCUR_READ_ONLY || concurrency == ResultSet.CONCUR_UPDATABLE; default: return false; } } @Override public boolean ownUpdatesAreVisible(int type) throws SQLException { // TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE? return ResultSet.TYPE_SCROLL_INSENSITIVE == type || ResultSet.TYPE_SCROLL_SENSITIVE == type; } @Override public boolean ownDeletesAreVisible(int type) throws SQLException { // TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE? return ResultSet.TYPE_SCROLL_INSENSITIVE == type || ResultSet.TYPE_SCROLL_SENSITIVE == type; } @Override public boolean ownInsertsAreVisible(int type) throws SQLException { // TODO Return false for TYPE_SCROLL_SENSITVE as we only support it by downgrading to INSENSITIVE? return ResultSet.TYPE_SCROLL_INSENSITIVE == type || ResultSet.TYPE_SCROLL_SENSITIVE == type; } @Override public boolean othersUpdatesAreVisible(int type) throws SQLException { return false; } @Override public boolean othersDeletesAreVisible(int type) throws SQLException { return false; } @Override public boolean othersInsertsAreVisible(int type) throws SQLException { return false; } @Override public boolean updatesAreDetected(int type) throws SQLException { return false; } @Override public boolean deletesAreDetected(int type) throws SQLException { return false; } @Override public boolean insertsAreDetected(int type) throws SQLException { return false; } @Override public boolean supportsBatchUpdates() throws SQLException { return true; } /** * {@inheritDoc} *

* UDTs are not supported by Firebird. This method will always return an empty ResultSet. *

*/ @Override public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(7, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TYPE_CAT", "UDT").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TYPE_SCHEM", "UDT").addField() .at(2).simple(SQL_VARYING, 31, "TYPE_NAME", "UDT").addField() .at(3).simple(SQL_VARYING, 31, "CLASS_NAME", "UDT").addField() .at(4).simple(SQL_LONG, 0, "DATA_TYPE", "UDT").addField() .at(5).simple(SQL_VARYING, 31, "REMARKS", "UDT").addField() .at(6).simple(SQL_SHORT, 0, "BASE_TYPE", "UDT").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } @Override public Connection getConnection() throws SQLException { return connection; } /** * {@inheritDoc} *

* UDTs are not supported by Firebird. This method will always return an empty ResultSet. *

*/ @Override public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(21, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TYPE_CAT", "ATTRIBUTES").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TYPE_SCHEM", "ATTRIBUTES").addField() .at(2).simple(SQL_VARYING, 31, "TYPE_NAME", "ATTRIBUTES").addField() .at(3).simple(SQL_VARYING, 31, "ATTR_NAME", "ATTRIBUTES").addField() .at(4).simple(SQL_LONG, 0, "DATA_TYPE", "ATTRIBUTES").addField() .at(5).simple(SQL_VARYING, 31, "ATTR_TYPE_NAME", "ATTRIBUTES").addField() .at(6).simple(SQL_LONG, 0, "ATTR_SIZE", "ATTRIBUTES").addField() .at(7).simple(SQL_LONG, 0, "DECIMAL_DIGITS", "ATTRIBUTES").addField() .at(8).simple(SQL_LONG, 0, "NUM_PREC_RADIX", "ATTRIBUTES").addField() .at(9).simple(SQL_LONG, 0, "NULLABLE", "ATTRIBUTES").addField() .at(10).simple(SQL_VARYING, 80, "REMARKS", "ATTRIBUTES").addField() .at(11).simple(SQL_VARYING, 31, "ATTR_DEF", "ATTRIBUTES").addField() .at(12).simple(SQL_LONG, 0, "SQL_DATA_TYPE", "ATTRIBUTES").addField() .at(13).simple(SQL_LONG, 0, "SQL_DATETIME_SUB", "ATTRIBUTES").addField() .at(14).simple(SQL_LONG, 0, "CHAR_OCTET_LENGTH", "ATTRIBUTES").addField() .at(15).simple(SQL_SHORT, 0, "ORDINAL_POSITION", "ATTRIBUTES").addField() .at(16).simple(SQL_VARYING, 31, "IS_NULLABLE", "ATTRIBUTES").addField() .at(17).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SCOPE_CATALOG", "ATTRIBUTES").addField() .at(18).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SCOPE_SCHEMA", "ATTRIBUTES").addField() .at(19).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SCOPE_TABLE", "ATTRIBUTES").addField() .at(20).simple(SQL_SHORT, 0, "SOURCE_DATA_TYPE", "ATTRIBUTES").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } @Override public boolean supportsSavepoints() throws SQLException { return firebirdSupportInfo.supportsSavepoint(); } @Override public boolean supportsNamedParameters() throws SQLException { return false; } @Override public boolean supportsMultipleOpenResults() throws SQLException { return false; } @Override public boolean supportsGetGeneratedKeys() throws SQLException { return firebirdSupportInfo.supportsInsertReturning() && GeneratedKeysSupportFactory.isGeneratedKeysSupportLoaded() && connection.getGeneratedKeysSupport().supportsGetGeneratedKeys(); } /** * {@inheritDoc} *

* Supertypes are not supported by Firebird. This method will always return an empty ResultSet. *

*/ @Override public ResultSet getSuperTypes(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(6, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TYPE_CAT", "SUPERTYPES").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TYPE_SCHEM", "SUPERTYPES").addField() .at(2).simple(SQL_VARYING, 31, "TYPE_NAME", "SUPERTYPES").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SUPERTYPE_CAT", "SUPERTYPES").addField() .at(4).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SUPERTYPE_SCHEM", "SUPERTYPES").addField() .at(5).simple(SQL_VARYING, 31, "SUPERTYPE_NAME", "SUPERTYPES").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } /** * {@inheritDoc} *

* Supertables are not supported by Firebird. This method will always return an empty ResultSet. *

*/ @Override public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(4, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "SUPERTABLES").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "SUPERTABLES").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "SUPERTABLES").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "SUPERTABLE_NAME", "SUPERTABLES").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } @Override public boolean supportsResultSetHoldability(int holdability) throws SQLException { return holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT || holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override public int getResultSetHoldability() throws SQLException { // TODO Retrieve default holdable connection property return ResultSet.CLOSE_CURSORS_AT_COMMIT; } @Override public int getDatabaseMajorVersion() throws SQLException { return gdsHelper.getDatabaseProductMajorVersion(); } @Override public int getDatabaseMinorVersion() throws SQLException { return gdsHelper.getDatabaseProductMinorVersion(); } @Override public int getOdsMajorVersion() throws SQLException { return gdsHelper.getCurrentDatabase().getOdsMajor(); } @Override public int getOdsMinorVersion() throws SQLException { return gdsHelper.getCurrentDatabase().getOdsMinor(); } @Override public int getDatabaseDialect() throws SQLException { return gdsHelper.getCurrentDatabase().getDatabaseDialect(); } @Override public int getConnectionDialect() throws SQLException { return gdsHelper.getCurrentDatabase().getConnectionDialect(); } /** * {@inheritDoc} *

* Firebird primarily uses SQL standard SQL states, but may occasionally use values from X/Open. *

*/ @Override public int getSQLStateType() throws SQLException { return DatabaseMetaData.sqlStateSQL; } @Override public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { return false; } /** * {@inheritDoc} *

* The holdable result sets remain open, others are closed, but this happens before the statement is executed. *

*/ @Override public boolean autoCommitFailureClosesAllResultSets() throws SQLException { // the holdable result sets remain open, others are closed, but this // happens before the statement is executed return false; } @Override public ResultSet getClientInfoProperties() throws SQLException { // TODO Return context info? final RowDescriptor rowDescriptor = new RowDescriptorBuilder(4, datatypeCoder) .at(0).simple(SQL_VARYING, 31, "NAME", "CLIENTINFO").addField() .at(1).simple(SQL_LONG, 4, "MAX_LEN", "CLIENTINFO").addField() .at(2).simple(SQL_VARYING, 31, "DEFAULT", "CLIENTINFO").addField() .at(3).simple(SQL_VARYING, 31, "DESCRIPTION", "CLIENTINFO").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } /** * {@inheritDoc} * *

* This method does not return columns of functions defined in packages. *

*/ @Override public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { return GetFunctionColumns.create(getDbMetadataMediator()) .getFunctionColumns(catalog, schemaPattern, functionNamePattern, columnNamePattern); } /** * {@inheritDoc} * *

* Jaybird defines a number of additional columns. As these are not defined in JDBC, their position may change with * revisions of JDBC. We recommend to retrieve these columns by name. The following additional columns are * available: *

    *
  1. JB_FUNCTION_SOURCE String => The source of the function (for Firebird 3+ PSQL functions only)).
  2. *
  3. JB_FUNCTION_KIND String => The kind of function, one of "UDF", "PSQL" (Firebird 3+) or * "UDR" (Firebird 3+)
  4. *
  5. JB_MODULE_NAME String => Value of {@code RDB$MODULE_NAME} (is {@code null} for PSQL)
  6. *
  7. JB_ENTRYPOINT String => Value of {@code RDB$ENTRYPOINT} (is {@code null} for PSQL)
  8. *
  9. JB_ENGINE_NAME String => Value of {@code RDB$ENGINE_NAME} (is {@code null} for UDF and PSQL)
  10. *
*

*

* This method does not return functions defined in packages. *

*/ @Override public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { return GetFunctions.create(getDbMetadataMediator()).getFunctions(catalog, schemaPattern, functionNamePattern); } @Override public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(2, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "TABLESCHEMAS").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CATALOG", "TABLESCHEMAS").addField() .toRowDescriptor(); return new FBResultSet(rowDescriptor, Collections.emptyList()); } @Override public boolean isWrapperFor(Class iface) throws SQLException { return iface != null && iface.isAssignableFrom(FBDatabaseMetaData.class); } @Override public T unwrap(Class iface) throws SQLException { if (!isWrapperFor(iface)) throw new SQLException("Unable to unwrap to class " + iface.getName()); return iface.cast(this); } /** * Determine if there are no SQL wildcard characters ('%' or '_') in the given pattern. * * @param pattern * The pattern to be checked for wildcards * @return true if there are no wildcards in the pattern, * false otherwise * @deprecated Will be removed in Jaybird 5 */ @Deprecated public static boolean hasNoWildcards(String pattern) { if (pattern == null) return true; for (int pos = 0; pos < pattern.length(); pos++) { char ch = pattern.charAt(pos); if (ch == '_' || ch == '%') { return false; } else if (ch == '\\' && pos < pattern.length() - 1) { char nextCh = pattern.charAt(pos + 1); if (nextCh == '\\' || nextCh == '%' || nextCh == '_') { // We were an escape, skip next character pos += 1; } } } return true; } /** * Strips all backslash-escapes from a string. * * @param pattern * The string to be stripped * @return pattern with all backslash-escapes removed * @deprecated Will be removed in Jaybird 5 */ @Deprecated public static String stripEscape(String pattern) { if (pattern == null) return null; StringBuilder stripped = new StringBuilder(pattern.length()); for (int pos = 0; pos < pattern.length(); pos++) { char ch = pattern.charAt(pos); if (ch != '\\') { stripped.append(ch); } else if (pos < pattern.length() - 1 && pattern.charAt(pos + 1) == '\\') { // We are an escape for a backslash, append backslash and skip next position stripped.append('\\'); pos += 1; } } return stripped.toString(); } /** * Escapes the like wildcards and escape ({@code \_%} in the provided search string with a {@code \}. *

* Primary purpose is to escape object names with wildcards for use in metadata patterns for literal matches, but * it can also be used to escape for SQL {@code LIKE}. *

* * @param objectName * Object name to escape. * @return Object name with wildcards escaped. */ public static String escapeWildcards(String objectName) { return MetadataPattern.escapeWildcards(objectName); } protected String getWantsSystemTables(String[] types) { for (String type : types) { if (SYSTEM_TABLE.equals(type)) { return "T"; } } return "F"; } protected String getWantsTables(String[] types) { for (String type : types) { if (TABLE.equals(type)) { return "T"; } } return "F"; } protected String getWantsViews(String[] types) { for (String type : types) { if (VIEW.equals(type)) { return "T"; } } return "F"; } //@formatter:off // Suitable for Firebird 2.5 and earlier private static final String GET_PSEUDO_COLUMNS_FRAGMENT_FB_25 = "select " + " RDB$RELATION_NAME, " + " RDB$DBKEY_LENGTH, " + " 'F' AS HAS_RECORD_VERSION, " + " '' AS RECORD_VERSION_NULLABLE " // unknown nullability (and doesn't matter, no RDB$RECORD_VERSION) + "from rdb$relations "; // Suitable for Firebird 3 and higher private static final String GET_PSEUDO_COLUMNS_FRAGMENT_FB_30 = "select " + " RDB$RELATION_NAME, " + " RDB$DBKEY_LENGTH, " + " RDB$DBKEY_LENGTH = 8 as HAS_RECORD_VERSION, " + " case " + " when RDB$RELATION_TYPE in (0, 1, 4, 5) then 'NO' " // table, view, GTT preserve + delete: never null + " when RDB$RELATION_TYPE in (2, 3) then 'YES' " // external + virtual: always null + " else ''" // unknown or unsupported (by Jaybird) type: unknown nullability + " end as RECORD_VERSION_NULLABLE " + "from rdb$relations "; private static final String GET_PSEUDO_COLUMNS_END = " order by RDB$RELATION_NAME"; //@formatter:on @Override public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { final RowDescriptor rowDescriptor = new RowDescriptorBuilder(12, datatypeCoder) .at(0).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_CAT", "PSEUDOCOLUMNS").addField() .at(1).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_SCHEM", "PSEUDOCOLUMNS").addField() .at(2).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "TABLE_NAME", "PSEUDOCOLUMNS").addField() .at(3).simple(SQL_VARYING, OBJECT_NAME_LENGTH, "COLUMN_NAME", "PSEUDOCOLUMNS").addField() .at(4).simple(SQL_LONG, 0, "DATA_TYPE", "PSEUDOCOLUMNS").addField() .at(5).simple(SQL_LONG, 0, "COLUMN_SIZE", "PSEUDOCOLUMNS").addField() .at(6).simple(SQL_LONG, 0, "DECIMAL_DIGITS", "PSEUDOCOLUMNS").addField() .at(7).simple(SQL_LONG, 0, "NUM_PREC_RADIX", "PSEUDOCOLUMNS").addField() .at(8).simple(SQL_VARYING, 50, "COLUMN_USAGE", "PSEUDOCOLUMNS").addField() // Field in Firebird is actually a blob, using Integer.MAX_VALUE for length .at(9).simple(SQL_VARYING | 1, Integer.MAX_VALUE, "REMARKS", "PSEUDOCOLUMNS").addField() .at(10).simple(SQL_LONG, 0, "CHAR_OCTET_LENGTH", "PSEUDOCOLUMNS").addField() .at(11).simple(SQL_VARYING, 3, "IS_NULLABLE", "PSEUDOCOLUMNS").addField() .toRowDescriptor(); if ("".equals(tableNamePattern) || "".equals(columnNamePattern)) { // Matching table and/or column not possible return new FBResultSet(rowDescriptor, Collections.emptyList()); } final boolean supportsRecordVersion = firebirdSupportInfo.supportsRecordVersionPseudoColumn(); final MetadataPatternMatcher matcher = MetadataPattern.compile(columnNamePattern).toMetadataPatternMatcher(); final boolean retrieveDbKey = matcher.matches("RDB$DB_KEY"); final boolean retrieveRecordVersion = supportsRecordVersion && matcher.matches("RDB$RECORD_VERSION"); if (!(retrieveDbKey || retrieveRecordVersion)) { // No matching columns return new FBResultSet(rowDescriptor, Collections.emptyList()); } Clause tableNameClause = new Clause("RDB$RELATION_NAME", tableNamePattern); String sql = (supportsRecordVersion ? GET_PSEUDO_COLUMNS_FRAGMENT_FB_30 : GET_PSEUDO_COLUMNS_FRAGMENT_FB_25); if (tableNameClause.hasCondition()) { sql += " where " + tableNameClause.getCondition(false); } sql += GET_PSEUDO_COLUMNS_END; List params = tableNameClause.hasCondition() ? Collections.singletonList(tableNameClause.getValue()) : Collections.emptyList(); try (ResultSet rs = doQuery(sql, params)) { if (!rs.next()) { return new FBResultSet(rowDescriptor, Collections.emptyList()); } byte[] dbKeyBytes = retrieveDbKey ? getBytes("RDB$DB_KEY") : null; byte[] dbKeyRemark = retrieveDbKey ? getBytes("The RDB$DB_KEY column in a select list will be renamed by Firebird to DB_KEY in the " + "result set (both as column name and label). Result set getters in Jaybird will map this, but " + "in introspection of ResultSetMetaData, DB_KEY will be reported. Identification as a " + "Types.ROWID will only work in a select list (ResultSetMetaData), not for parameters " + "(ParameterMetaData), but Jaybird will allow setting a RowId value.") : null; byte[] recordVersionBytes = retrieveRecordVersion ? getBytes("RDB$RECORD_VERSION") : null; byte[] noUsageRestrictions = getBytes(PseudoColumnUsage.NO_USAGE_RESTRICTIONS.name()); List rows = new ArrayList<>(); RowValueBuilder valueBuilder = new RowValueBuilder(rowDescriptor); do { byte[] tableNameBytes = getBytes(rs.getString("RDB$RELATION_NAME")); if (retrieveDbKey) { int dbKeyLength = rs.getInt("RDB$DBKEY_LENGTH"); byte[] dbKeyLengthBytes = createInt(dbKeyLength); valueBuilder .at(2).set(tableNameBytes) .at(3).set(dbKeyBytes) .at(4).set(createInt(Types.ROWID)) .at(5).set(dbKeyLengthBytes) .at(7).set(RADIX_TEN) .at(8).set(noUsageRestrictions) .at(9).set(dbKeyRemark) .at(10).set(dbKeyLengthBytes) .at(11).set(NO_BYTES); rows.add(valueBuilder.toRowValue(true)); } if (retrieveRecordVersion && rs.getBoolean("HAS_RECORD_VERSION")) { valueBuilder .at(2).set(tableNameBytes) .at(3).set(recordVersionBytes) .at(4).set(createInt(Types.BIGINT)) .at(5).set(BIGINT_PRECISION) .at(6).set(INT_ZERO) .at(7).set(RADIX_TEN) .at(8).set(noUsageRestrictions) .at(11).set(getBytes(rs.getString("RECORD_VERSION_NULLABLE"))); rows.add(valueBuilder.toRowValue(true)); } } while (rs.next()); return new FBResultSet(rowDescriptor, rows); } } @Override public boolean generatedKeyAlwaysReturned() throws SQLException { // TODO Double check if this is correct return false; } @Override public String getProcedureSourceCode(String procedureName) throws SQLException { String sResult = null; String sql = "Select RDB$PROCEDURE_SOURCE From RDB$PROCEDURES Where " + "RDB$PROCEDURE_NAME = ?"; List params = new ArrayList<>(); params.add(procedureName); try (ResultSet rs = doQuery(sql, params)) { if (rs.next()) sResult = rs.getString(1); } return sResult; } @Override public String getTriggerSourceCode(String triggerName) throws SQLException { String sResult = null; String sql = "Select RDB$TRIGGER_SOURCE From RDB$TRIGGERS Where RDB$TRIGGER_NAME = ?"; List params = new ArrayList<>(); params.add(triggerName); try (ResultSet rs = doQuery(sql, params)) { if (rs.next()) sResult = rs.getString(1); } return sResult; } @Override public String getViewSourceCode(String viewName) throws SQLException { String sResult = null; String sql = "Select RDB$VIEW_SOURCE From RDB$RELATIONS Where RDB$RELATION_NAME = ?"; List params = new ArrayList<>(); params.add(viewName); try (ResultSet rs = doQuery(sql, params)) { if (rs.next()) sResult = rs.getString(1); } return sResult; } protected static byte[] getBytes(String value) { return value != null ? value.getBytes(StandardCharsets.UTF_8) : null; } private FBPreparedStatement getStatement(String sql, boolean standalone) throws SQLException { synchronized (statements) { if (!standalone) { // Check cache FBPreparedStatement cachedStatement = statements.get(sql); if (cachedStatement != null) { if (cachedStatement.isClosed()) { statements.remove(sql); } else { return cachedStatement; } } } InternalTransactionCoordinator.MetaDataTransactionCoordinator metaDataTransactionCoordinator = new InternalTransactionCoordinator.MetaDataTransactionCoordinator(connection.txCoordinator); FBPreparedStatement newStatement = new FBPreparedStatement(gdsHelper, sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT, metaDataTransactionCoordinator, metaDataTransactionCoordinator, true, standalone, false); if (!standalone) { statements.put(sql, newStatement); } return newStatement; } } /** * Execute an sql query with a given set of parameters. * * @param sql * The sql statement to be used for the query * @param params * The parameters to be used in the query * @throws SQLException * if a database access error occurs */ protected ResultSet doQuery(String sql, List params) throws SQLException { return doQuery(sql, params, false); } /** * Execute an sql query with a given set of parameters. * * @param sql * The sql statement to be used for the query * @param params * The parameters to be used in the query * @param standalone * The query to be executed is a standalone query (should not be cached and be closed asap) * @throws SQLException * if a database access error occurs */ protected ResultSet doQuery(String sql, List params, boolean standalone) throws SQLException { FBPreparedStatement s = getStatement(sql, standalone); for (int i = 0; i < params.size(); i++) { s.setStringForced(i + 1, params.get(i)); } return s.executeMetaDataQuery(); } /** * {@inheritDoc} *

* Minimum lifetime supported by Firebird is transaction-scope, and this can be changed to session-scope with * {@code isc_dpb_dbkey_scope} set to {@code 1} (eg connection property {@code dbkey_scope=1}). This implementation, * however, will always report {@link RowIdLifetime#ROWID_VALID_TRANSACTION}. *

*/ @Override public RowIdLifetime getRowIdLifetime() throws SQLException { return RowIdLifetime.ROWID_VALID_TRANSACTION; } static final int JDBC_MAJOR_VERSION = 4; static final int JDBC_MINOR_VERSION; static { int tempVersion; try { String javaImplementation = getSystemPropertyPrivileged("java.specification.version"); if (javaImplementation == null) { // Assume minimum: JDBC 4.1 tempVersion = 1; } else { int javaVersionMajor; try { javaVersionMajor = (int) Double.parseDouble(javaImplementation); } catch (NumberFormatException e) { javaVersionMajor = 1; } if (javaVersionMajor >= 9) { // JDK 9 or higher: JDBC 4.3 tempVersion = 3; } else if ("1.8".compareTo(javaImplementation) <= 0) { // JDK 1.8 or higher: JDBC 4.2 tempVersion = 2; } else { // JDK 1.7 (or lower): JDBC 4.1 tempVersion = 1; } } } catch (RuntimeException ex) { // default to 1 (JDBC 4.1) when privileged call fails tempVersion = 1; } JDBC_MINOR_VERSION = tempVersion; } @Override public int getJDBCMajorVersion() { return JDBC_MAJOR_VERSION; } @Override public int getJDBCMinorVersion() { return JDBC_MINOR_VERSION; } private static String getSystemPropertyPrivileged(final String propertyName) { return AccessController.doPrivileged(new PrivilegedAction() { public String run() { return System.getProperty(propertyName); } }); } private static class LruPreparedStatementCache extends LinkedHashMap { private final int maxCapacity; private LruPreparedStatementCache(int maxCapacity) { super(16, 0.75f, true); this.maxCapacity = maxCapacity; } @Override protected boolean removeEldestEntry(Map.Entry eldest) { if (size() <= maxCapacity) { return false; } try { FBPreparedStatement statement = eldest.getValue(); statement.close(); } catch (Exception e) { log.debug("Closing eldest cached metadata statement yielded an exception; ignored", e); } return true; } } protected DbMetadataMediator getDbMetadataMediator() { return new DbMetadataMediatorImpl(); } private class DbMetadataMediatorImpl extends DbMetadataMediator { @Override protected FirebirdSupportInfo getFirebirdSupportInfo() { return firebirdSupportInfo; } @Override protected ResultSet performMetaDataQuery(MetadataQuery metadataQuery) throws SQLException { return doQuery(metadataQuery.getQueryText(), metadataQuery.getParameters(), metadataQuery.isStandalone()); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy