org.firebirdsql.jdbc.GeneratedKeysQueryBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaybird Show documentation
Show all versions of jaybird Show documentation
JDBC Driver for the Firebird RDBMS
The newest version!
/*
* Firebird Open Source 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.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
import org.firebirdsql.jdbc.metadata.MetadataPattern;
import org.firebirdsql.jaybird.parser.StatementDetector;
import org.firebirdsql.jaybird.parser.LocalStatementType;
import org.firebirdsql.jaybird.parser.FirebirdReservedWords;
import org.firebirdsql.jaybird.parser.SqlParser;
import org.firebirdsql.jaybird.parser.StatementIdentification;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* Builds (updates) queries to add generated keys support.
*
* @author Mark Rotteveel
* @since 4.0
*/
final class GeneratedKeysQueryBuilder {
// TODO Add caching for column info
private static final System.Logger logger = System.getLogger(GeneratedKeysQueryBuilder.class.getName());
private static final Map statementTypeToQueryType;
static {
Map temp = new EnumMap<>(LocalStatementType.class);
temp.put(LocalStatementType.INSERT, GeneratedKeysSupport.QueryType.INSERT);
temp.put(LocalStatementType.UPDATE, GeneratedKeysSupport.QueryType.UPDATE);
temp.put(LocalStatementType.DELETE, GeneratedKeysSupport.QueryType.DELETE);
temp.put(LocalStatementType.UPDATE_OR_INSERT, GeneratedKeysSupport.QueryType.UPDATE_OR_INSERT);
temp.put(LocalStatementType.MERGE, GeneratedKeysSupport.QueryType.MERGE);
statementTypeToQueryType = Collections.unmodifiableMap(temp);
}
private static final int IDX_COLUMN_NAME = 4;
private static final int IDX_ORDINAL_POSITION = 17;
private final String originalSql;
private final StatementIdentification statementIdentification;
private final Set supportedQueryTypes;
/**
* Creates a generated keys query builder.
*
* @param originalSql
* Original statement text
* @param statementIdentification
* Parsed statement identification
* @param supportedQueryTypes
* Supported query types
*/
private GeneratedKeysQueryBuilder(String originalSql, StatementIdentification statementIdentification,
Set supportedQueryTypes) {
this.originalSql = originalSql;
this.statementIdentification = statementIdentification;
this.supportedQueryTypes = supportedQueryTypes;
}
/**
* Creates a generated keys query builder that will always return the original SQL.
*
* @param originalSql
* Original statement text
*/
private GeneratedKeysQueryBuilder(String originalSql) {
this(originalSql, null, Collections.emptySet());
}
/**
* Create a generated keys query builder.
*
* @param statementText
* Statement text
* @param supportedQueryTypes
* Query types to support for generated keys
* @return A generated keys query builder
*/
static GeneratedKeysQueryBuilder create(String statementText,
Set supportedQueryTypes) {
try {
StatementDetector detector = new StatementDetector();
// NOTE: We currently don't care about the version of reserved words, so we use the latest.
// This may change once we apply multiple visitors (e.g. JDBC escape processing) with a single parser.
SqlParser.withReservedWords(FirebirdReservedWords.latest())
.withVisitor(detector)
.of(statementText)
.parse();
return new GeneratedKeysQueryBuilder(statementText, detector.toStatementIdentification(), supportedQueryTypes);
} catch (RuntimeException e) {
if (logger.isLoggable(System.Logger.Level.DEBUG)) {
logger.log(System.Logger.Level.DEBUG, "Exception parsing query: " + statementText, e);
}
return new GeneratedKeysQueryBuilder(statementText);
}
}
/**
* @return {@code true} when the query type is supported for returning generated keys
*/
boolean isSupportedType() {
if (statementIdentification == null) {
return false;
}
LocalStatementType statementType = statementIdentification.getStatementType();
GeneratedKeysSupport.QueryType queryType =
statementTypeToQueryType.getOrDefault(statementType, GeneratedKeysSupport.QueryType.UNSUPPORTED);
return supportedQueryTypes.contains(queryType);
}
/**
* Produces Query instance for the {@link java.sql.Statement#NO_GENERATED_KEYS} option.
*
* Historically Jaybird allows generated keys retrieval if a {@code RETURNING} clause is explicitly present, even
* when executed with NO_GENERATED_KEYS. This avoids issues with executeUpdate producing result sets. This is done
* irrespective of the configured {@code supportedQueryTypes}.
*
*
* @return Query object that only has {@link org.firebirdsql.jdbc.GeneratedKeysSupport.Query#generatesKeys()} with
* value {@code true} if the original statement already had a {@code RETURNING} clause.
*/
GeneratedKeysSupport.Query forNoGeneratedKeysOption() {
if (hasReturning()) {
return new GeneratedKeysSupport.Query(true, originalSql);
}
return new GeneratedKeysSupport.Query(false, originalSql);
}
/**
* Returns a generated keys query object for all columns (if supported).
*
* @param databaseMetaData
* Database meta data
* @return Query object
* @throws SQLException
* if a database access error occurs
*/
GeneratedKeysSupport.Query forReturnGeneratedKeysOption(FirebirdDatabaseMetaData databaseMetaData)
throws SQLException {
if (hasReturning()) {
// See also comment on forNoGeneratedKeysOption
return new GeneratedKeysSupport.Query(true, originalSql);
}
if (isSupportedType()) {
// TODO Use an strategy when creating this builder or even push this up to the GeneratedKeysSupportFactory?
if (supportsReturningAll(databaseMetaData)) {
return useReturningAll();
}
return useReturningAllColumnsByName(databaseMetaData);
}
return new GeneratedKeysSupport.Query(false, originalSql);
}
/**
* Determines support for {@code RETURNING *}.
*
* @param databaseMetaData
* Database meta data
* @return {@code true} if this version of Firebird supports {@code RETURNING *}.
* @throws SQLException
* for database access errors
*/
private boolean supportsReturningAll(FirebirdDatabaseMetaData databaseMetaData) throws SQLException {
return databaseMetaData.getDatabaseMajorVersion() >= 4;
}
/**
* Generates the query using {@code RETURNING *}
*/
private GeneratedKeysSupport.Query useReturningAll() {
// NOTE: Using DIALECT_1 strategy as it doesn't quote object names ("*" is interpreted as an object name, and we
// don't want to quote it)
return addColumnsByNameImpl(Collections.singletonList("*"), QuoteStrategy.DIALECT_1);
}
/**
* Generates the query by retrieving all column names and appending them to a {@code RETURNING} clause.
*/
private GeneratedKeysSupport.Query useReturningAllColumnsByName(FirebirdDatabaseMetaData databaseMetaData)
throws SQLException {
List columnNames = getAllColumnNames(statementIdentification.getTableName(), databaseMetaData);
QuoteStrategy quoteStrategy = QuoteStrategy.forDialect(databaseMetaData.getConnectionDialect());
return addColumnsByNameImpl(columnNames, quoteStrategy);
}
private boolean hasReturning() {
return statementIdentification != null && statementIdentification.returningClauseDetected();
}
/**
* Returns a generated keys query object with columns identified by the indexes passed
*
* @param columnIndexes
* 1-based indexes of the columns to return
* @param databaseMetaData
* Database meta data
* @return Query object
* @throws SQLException
* if a database access error occurs or the query cannot be built
*/
GeneratedKeysSupport.Query forColumnsByIndex(int[] columnIndexes, FirebirdDatabaseMetaData databaseMetaData)
throws SQLException {
if (hasReturning()) {
// See also comment on forNoGeneratedKeysOption
return new GeneratedKeysSupport.Query(true, originalSql);
} else if (columnIndexes == null || columnIndexes.length == 0) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_generatedKeysArrayEmptyOrNull)
.messageParameter("columnIndexes")
.toSQLException();
} else if (isSupportedType()) {
List columnNames = getColumnNames(statementIdentification.getTableName(), columnIndexes, databaseMetaData);
QuoteStrategy quoteStrategy = QuoteStrategy.forDialect(databaseMetaData.getConnectionDialect());
return addColumnsByNameImpl(columnNames, quoteStrategy);
} else {
// Unsupported type, ignore column indexes
return new GeneratedKeysSupport.Query(false, originalSql);
}
}
/**
* Returns a generated keys query object for the specified columns.
*
* @param columnNames
* Array with column names to add (NOTE: current implementation expects already quoted where necessary)
* @return Query object
* @throws SQLException
* if a database access error occurs
*/
GeneratedKeysSupport.Query forColumnsByName(String[] columnNames) throws SQLException {
if (hasReturning()) {
// See also comment on forNoGeneratedKeysOption
return new GeneratedKeysSupport.Query(true, originalSql);
} else if (columnNames == null || columnNames.length == 0) {
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_generatedKeysArrayEmptyOrNull)
.messageParameter("columnNames")
.toSQLException();
} else if (isSupportedType()) {
// NOTE: Using DIALECT_1 strategy as it doesn't quote object names (currently assumes already quoted)
return addColumnsByNameImpl(Arrays.asList(columnNames), QuoteStrategy.DIALECT_1);
} else {
// Unsupported type, ignore column names
return new GeneratedKeysSupport.Query(false, originalSql);
}
}
@SuppressWarnings("java:S135")
private GeneratedKeysSupport.Query addColumnsByNameImpl(List columnNames, QuoteStrategy quoteStrategy) {
assert columnNames != null && !columnNames.isEmpty() : "Column names are required";
StringBuilder returningQuery = new StringBuilder(originalSql);
// Strip whitespace and ';' from end
for (int idx = returningQuery.length() - 1; idx >= 0; idx--) {
char currentChar = returningQuery.charAt(idx);
if (currentChar == ';') {
returningQuery.setLength(idx);
break;
} else if (!Character.isWhitespace(currentChar)) {
returningQuery.setLength(idx + 1);
break;
}
}
returningQuery
.append('\n')
.append("RETURNING ");
for (String columnName : columnNames) {
quoteStrategy
.appendQuoted(columnName, returningQuery)
.append(',');
}
// Delete last ','
returningQuery.setLength(returningQuery.length() - 1);
return new GeneratedKeysSupport.Query(true, returningQuery.toString());
}
private List getAllColumnNames(String tableName, FirebirdDatabaseMetaData databaseMetaData)
throws SQLException {
try (ResultSet rs = databaseMetaData.getColumns(null, null, normalizeObjectName(tableName), null)) {
if (rs.next()) {
List columns = new ArrayList<>();
do {
columns.add(rs.getString(IDX_COLUMN_NAME));
} while (rs.next());
return columns;
}
throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_generatedKeysNoColumnsFound)
.messageParameter(tableName)
.toSQLException();
}
}
private List getColumnNames(String tableName, int[] columnIndexes,
FirebirdDatabaseMetaData databaseMetaData) throws SQLException {
Map columnByIndex = mapColumnNamesByIndex(tableName, columnIndexes, databaseMetaData);
List columns = new ArrayList<>(columnIndexes.length);
for (int indexToAdd : columnIndexes) {
String columnName = columnByIndex.get(indexToAdd);
if (columnName == null) {
throw FbExceptionBuilder
.forNonTransientException(JaybirdErrorCodes.jb_generatedKeysInvalidColumnPosition)
.messageParameter(indexToAdd)
.messageParameter(tableName)
.toSQLException();
}
columns.add(columnName);
}
return columns;
}
private Map mapColumnNamesByIndex(String tableName, int[] columnIndexes,
FirebirdDatabaseMetaData databaseMetaData) throws SQLException {
try (ResultSet rs = databaseMetaData.getColumns(null, null, normalizeObjectName(tableName), null)) {
if (!rs.next()) {
throw FbExceptionBuilder.forNonTransientException(JaybirdErrorCodes.jb_generatedKeysNoColumnsFound)
.messageParameter(tableName)
.toSQLException();
}
Map columnByIndex = new HashMap<>();
int[] sortedIndexes = columnIndexes.clone();
Arrays.sort(sortedIndexes);
do {
int columnPosition = rs.getInt(IDX_ORDINAL_POSITION);
if (Arrays.binarySearch(sortedIndexes, columnPosition) >= 0) {
columnByIndex.put(columnPosition, rs.getString(IDX_COLUMN_NAME));
}
} while (rs.next());
return columnByIndex;
}
}
/**
* Normalizes an object name from the parser.
*
* Like-wildcard characters are escaped, and unquoted identifiers are uppercased, and quoted identifiers are
* returned with the quotes stripped and double double quotes replaced by a single double quote.
*
*
* @param objectName
* Object name
* @return Normalized object name
*/
private String normalizeObjectName(String objectName) {
if (objectName == null) return null;
objectName = objectName.trim();
objectName = MetadataPattern.escapeWildcards(objectName);
if (objectName.length() > 2
&& objectName.charAt(0) == '"'
&& objectName.charAt(objectName.length() - 1) == '"') {
return objectName.substring(1, objectName.length() - 1).replace("\"\"", "\"");
}
return objectName.toUpperCase(Locale.ROOT);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy