blob: 75a2c993a7fdbc7cdd37bf2be5d55f1b10246e09 [file] [log] [blame]
/*
* Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 2021 IBM Corporation. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
// Markus Karg - bug fix for log operator
// 09/09/2011-2.3.1 Guy Pelletier
// - 356197: Add new VPD type to MultitenantType
// 09/14/2011-2.3.1 Guy Pelletier
// - 357533: Allow DDL queries to execute even when Multitenant entities are part of the PU
// 02/04/2013-2.5 Guy Pelletier
// - 389090: JPA 2.1 DDL Generation Support
// 02/19/2015 - Rick Curtis
// - 458877 : Add national character support
// 02/23/2015-2.6 Dalia Abo Sheasha
// - 460607: Change DatabasePlatform StoredProcedureTerminationToken to be configurable
// 02/14/2018-2.7 Will Dazey
// - 529602: Added support for CLOBs in DELETE statements for Oracle
package org.eclipse.persistence.platform.database;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.regex.Pattern;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionOperator;
import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.DatasourceCall;
import org.eclipse.persistence.internal.databaseaccess.FieldTypeDefinition;
import org.eclipse.persistence.internal.expressions.ExpressionSQLPrinter;
import org.eclipse.persistence.internal.expressions.FunctionExpression;
import org.eclipse.persistence.internal.expressions.RelationExpression;
import org.eclipse.persistence.internal.expressions.SQLSelectStatement;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl;
import org.eclipse.persistence.queries.DataModifyQuery;
import org.eclipse.persistence.queries.DataReadQuery;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.queries.ReadQuery;
import org.eclipse.persistence.queries.SQLCall;
import org.eclipse.persistence.queries.StoredProcedureCall;
import org.eclipse.persistence.queries.ValueReadQuery;
import org.eclipse.persistence.tools.schemaframework.TableDefinition;
/**
* <p><b>Purpose</b>: Provides Oracle specific behavior.
* <p><b>Responsibilities</b>:<ul>
* <li> Native SQL for byte[], Date, Time, {@literal &} Timestamp.
* <li> Native sequencing named sequences.
* <li> Native SQL/ROWNUM support for MaxRows and FirstResult filtering.
* </ul>
*
* @since TOPLink/Java 1.0
*/
public class OraclePlatform extends org.eclipse.persistence.platform.database.DatabasePlatform {
protected static DataModifyQuery vpdSetIdentifierQuery;
protected static DataModifyQuery vpdClearIdentifierQuery;
/**
* Whether a FOR UPDATE clause should be printed at the end of the query
*/
protected boolean shouldPrintForUpdateClause;
/**
* Advanced attribute indicating whether identity is supported,
* see comment to setSupportsIdentity method.
*/
protected boolean supportsIdentity;
public OraclePlatform(){
super();
this.cursorCode = -10;
this.pingSQL = "SELECT 1 FROM DUAL";
this.storedProcedureTerminationToken = "";
this.shouldPrintForUpdateClause = true;
}
@Override
public void initializeConnectionData(Connection connection) throws SQLException {
DatabaseMetaData dmd = connection.getMetaData();
// Tested with 11.1.0.6
this.driverSupportsNationalCharacterVarying = Helper.compareVersions(dmd.getDriverVersion(), "11.1") >= 0;
}
/*
* Used for stored procedure definitions.
*/
@Override
public boolean allowsSizeInProcedureArguments() {
return false;
}
/**
* INTERNAL:
* If using native SQL then print a byte[] literally as a hex string otherwise use ODBC format
* as provided in DatabasePlatform.
*/
@Override
protected void appendByteArray(byte[] bytes, Writer writer) throws IOException {
if (usesNativeSQL()) {
writer.write('\'');
Helper.writeHexString(bytes, writer);
writer.write('\'');
} else {
super.appendByteArray(bytes, writer);
}
}
/**
* INTERNAL:
* Appends an Oracle specific date if usesNativeSQL is true otherwise use the ODBC format.
* Native FORMAT: to_date('1997-11-06','yyyy-mm-dd')
*/
@Override
protected void appendDate(java.sql.Date date, Writer writer) throws IOException {
if (usesNativeSQL()) {
writer.write("to_date('");
writer.write(Helper.printDate(date));
writer.write("','yyyy-mm-dd')");
} else {
super.appendDate(date, writer);
}
}
/**
* INTERNAL:
* Appends an Oracle specific time if usesNativeSQL is true otherwise use the ODBC format.
* Native FORMAT: to_date(#####, 'sssss').
*/
@Override
protected void appendTime(java.sql.Time time, Writer writer) throws IOException {
if (usesNativeSQL()) {
writer.write("to_date('");
writer.write(Helper.printTime(time));
writer.write("','hh24:mi:ss')");
} else {
super.appendTime(time, writer);
}
}
/**
* INTERNAL:
* Appends an Oracle specific Timestamp, if usesNativeSQL is true otherwise use the ODBC format.
* Native Format: to_date ('1997-11-06 10:35:45.0' , 'yyyy-mm-dd hh:mm:ss.n')
*/
@Override
protected void appendTimestamp(java.sql.Timestamp timestamp, Writer writer) throws IOException {
if (usesNativeSQL()) {
writer.write("to_date('");
writer.write(Helper.printTimestampWithoutNanos(timestamp));
writer.write("','yyyy-mm-dd hh24:mi:ss')");
} else {
super.appendTimestamp(timestamp, writer);
}
}
/**
* INTERNAL:
* Appends an Oracle specific Timestamp, if usesNativeSQL is true otherwise use the ODBC format.
* Native Format: to_date ('1997-11-06 10:35:45.0' , 'yyyy-mm-dd hh:mm:ss.n')
*/
@Override
protected void appendCalendar(Calendar calendar, Writer writer) throws IOException {
if (usesNativeSQL()) {
writer.write("to_date('");
writer.write(Helper.printCalendarWithoutNanos(calendar));
writer.write("','yyyy-mm-dd hh24:mi:ss')");
} else {
super.appendCalendar(calendar, writer);
}
}
/**
* INTERNAL:
* Build operator.
*/
public ExpressionOperator atan2Operator() {
return ExpressionOperator.simpleTwoArgumentFunction(ExpressionOperator.Atan2, "ATAN2");
}
/**
* INTERNAL:
*/
@Override
protected Hashtable<Class<?>, FieldTypeDefinition> buildFieldTypes() {
Hashtable<Class<?>, FieldTypeDefinition> fieldTypeMapping = new Hashtable<>();
fieldTypeMapping.put(Boolean.class, new FieldTypeDefinition("NUMBER(1) default 0", false));
fieldTypeMapping.put(Integer.class, new FieldTypeDefinition("NUMBER", 10));
fieldTypeMapping.put(Long.class, new FieldTypeDefinition("NUMBER", 19));
fieldTypeMapping.put(Float.class, new FieldTypeDefinition("NUMBER", 19, 4));
fieldTypeMapping.put(Double.class, new FieldTypeDefinition("NUMBER", 19, 4));
fieldTypeMapping.put(Short.class, new FieldTypeDefinition("NUMBER", 5));
fieldTypeMapping.put(Byte.class, new FieldTypeDefinition("NUMBER", 3));
fieldTypeMapping.put(java.math.BigInteger.class, new FieldTypeDefinition("NUMBER", 38));
fieldTypeMapping.put(java.math.BigDecimal.class, new FieldTypeDefinition("NUMBER", 38).setLimits(38, -38, 38));
fieldTypeMapping.put(Number.class, new FieldTypeDefinition("NUMBER", 38).setLimits(38, -38, 38));
if(getUseNationalCharacterVaryingTypeForString()){
fieldTypeMapping.put(String.class, new FieldTypeDefinition("NVARCHAR2", DEFAULT_VARCHAR_SIZE));
}else {
fieldTypeMapping.put(String.class, new FieldTypeDefinition("VARCHAR2", DEFAULT_VARCHAR_SIZE));
}
fieldTypeMapping.put(Character.class, new FieldTypeDefinition("CHAR", 1));
fieldTypeMapping.put(Byte[].class, new FieldTypeDefinition("BLOB", false));
fieldTypeMapping.put(Character[].class, new FieldTypeDefinition("CLOB", false));
fieldTypeMapping.put(byte[].class, new FieldTypeDefinition("BLOB", false));
fieldTypeMapping.put(char[].class, new FieldTypeDefinition("CLOB", false));
fieldTypeMapping.put(java.sql.Blob.class, new FieldTypeDefinition("BLOB", false));
fieldTypeMapping.put(java.sql.Clob.class, new FieldTypeDefinition("CLOB", false));
fieldTypeMapping.put(java.sql.Date.class, new FieldTypeDefinition("DATE", false));
fieldTypeMapping.put(java.sql.Time.class, new FieldTypeDefinition("TIMESTAMP", false));
fieldTypeMapping.put(java.sql.Timestamp.class, new FieldTypeDefinition("TIMESTAMP", false));
//bug 5871089 the default generator requires definitions based on all java types
fieldTypeMapping.put(java.util.Calendar.class, new FieldTypeDefinition("TIMESTAMP"));
fieldTypeMapping.put(java.util.Date.class, new FieldTypeDefinition("TIMESTAMP"));
return fieldTypeMapping;
}
/**
* Build the hint string used for first rows.
*
* Allows it to be overridden
*/
protected String buildFirstRowsHint(int max){
return HINT_START + HINT_END;
}
/**
* INTERNAL:
* Returns null unless the platform supports call with returning
*/
@Override
public DatabaseCall buildCallWithReturning(SQLCall sqlCall, Vector<DatabaseField> returnFields) {
SQLCall call = new SQLCall();
call.setParameters(sqlCall.getParameters());
call.setParameterTypes(sqlCall.getParameterTypes());
Writer writer = new CharArrayWriter(200);
try {
writer.write("BEGIN ");
writer.write(sqlCall.getSQLString());
writer.write(" RETURNING ");
for (int i = 0; i < returnFields.size(); i++) {
DatabaseField field = returnFields.elementAt(i);
writer.write(field.getNameDelimited(this));
if ((i + 1) < returnFields.size()) {
writer.write(", ");
}
}
writer.write(" INTO ");
for (int i = 0; i < returnFields.size(); i++) {
DatabaseField field = returnFields.elementAt(i);
call.appendOut(writer, field);
if ((i + 1) < returnFields.size()) {
writer.write(", ");
}
}
writer.write("; END;");
call.setQueryString(writer.toString());
} catch (IOException exception) {
throw ValidationException.fileError(exception);
}
return call;
}
/**
* INTERNAL:
* Indicates whether the platform can build call with returning.
* In case this method returns true, buildCallWithReturning method
* may be called.
*/
@Override
public boolean canBuildCallWithReturning() {
return true;
}
/**
* INTERNAL:
* Clears both implicit and explicit caches of OracleConnection on Oracle9Platform, noop here.
*/
public void clearOracleConnectionCache(Connection conn) {
}
/**
* INTERNAL:
* Used for stored function calls.
*/
@Override
public String getAssignmentString() {
return ":= ";
}
/**
* INTERNAL:
* DECLARE stanza header for Anonymous PL/SQL block
*/
public String getDeclareBeginString() {
return "DECLARE ";
}
/**
* Used for batch writing and sp defs.
*/
@Override
public String getBatchBeginString() {
return "BEGIN ";
}
/**
* Used for batch writing and sp defs.
*/
@Override
public String getBatchEndString() {
return "END;";
}
/**
* Used for batch writing for row count return.
*/
@Override
public String getBatchRowCountDeclareString() {
return "DECLARE EL_COUNTER NUMBER := 0; ";
}
/**
* Oracle does not return the row count from PLSQL anon blocks,
* so an output parameter is required for this.
*/
@Override
public boolean isRowCountOutputParameterRequired() {
return true;
}
/**
* Used for batch writing for row count return.
*/
@Override
public String getBatchRowCountReturnString() {
return "? := EL_COUNTER; ";
}
/**
* Return the drop schema definition. Subclasses should override as needed.
*/
@Override
public String getDropDatabaseSchemaString(String schema) {
return "DROP SCHEMA " + schema + " RESTRICT";
}
/**
* Used for batch writing for row count return.
*/
@Override
public String getBatchRowCountAssignString() {
return "EL_COUNTER := EL_COUNTER + SQL%ROWCOUNT; ";
}
/**
* INTERNAL:
* returns the maximum number of characters that can be used in a field
* name on this platform.
*/
@Override
public int getMaxFieldNameSize() {
return 30;
}
/**
* Return the catalog information through using the native SQL catalog selects.
* This is required because many JDBC driver do not support meta-data.
* Willcards can be passed as arguments.
*/
public Vector getNativeTableInfo(String table, String creator, AbstractSession session) {
String query = "SELECT * FROM ALL_TABLES WHERE OWNER NOT IN ('SYS', 'SYSTEM')";
if (table != null) {
if (table.indexOf('%') != -1) {
query = query + " AND TABLE_NAME LIKE " + table;
} else {
query = query + " AND TABLE_NAME = " + table;
}
}
if (creator != null) {
if (creator.indexOf('%') != -1) {
query = query + " AND OWNER LIKE " + creator;
} else {
query = query + " AND OWNER = " + creator;
}
}
return session.executeSelectingCall(new SQLCall(query));
}
/**
* Used for sp calls.
*/
@Override
public String getProcedureCallHeader() {
return useJDBCStoredProcedureSyntax() ? "{CALL " : "BEGIN ";
}
/**
* Used for sp calls.
*/
@Override
public String getProcedureCallTail() {
return useJDBCStoredProcedureSyntax() ? "}" : "; END;";
}
/**
* Allows DROP TABLE to cascade dropping of any dependent constraints if the database supports this option.
*/
@Override
public String getDropCascadeString() {
return " CASCADE CONSTRAINTS";
}
@Override
public String getSelectForUpdateString() {
return " FOR UPDATE";
}
@Override
public String getSelectForUpdateWaitString(Integer waitTimeout) {
return " FOR UPDATE WAIT " + waitTimeout;
}
@Override
public String getStoredProcedureParameterPrefix() {
return "P_";
}
/**
* PUBLIC:
* The query to select the current system change number
* from Oracle.
* In order to execute this query a database administrator may need
* to grant execute permission on pl/sql package DBMS_FLASHBACK.
*/
public ValueReadQuery getSystemChangeNumberQuery() {
ValueReadQuery sCNQuery = new ValueReadQuery();
sCNQuery.setSQLString("SELECT DBMS_FLASHBACK.GET_SYSTEM_CHANGE_NUMBER FROM DUAL");
return sCNQuery;
}
/**
* PUBLIC:
* This method returns the query to select the timestamp
* from the server for Oracle.
*/
@Override
public ValueReadQuery getTimestampQuery() {
if (timestampQuery == null) {
timestampQuery = new ValueReadQuery();
timestampQuery.setSQLString("SELECT SYSDATE FROM DUAL");
timestampQuery.setAllowNativeSQLQuery(true);
}
return timestampQuery;
}
/**
* INTERNAL:
* Return an Oracle defined VPD clear identifier query.
*/
@Override
public DatabaseQuery getVPDClearIdentifierQuery(String vpdIdentifier) {
if (vpdClearIdentifierQuery == null) {
vpdClearIdentifierQuery = new DataModifyQuery("CALL DBMS_SESSION.CLEAR_IDENTIFIER()");
}
return vpdClearIdentifierQuery;
}
/**
* INTERNAL:
* Return an Oracle defined VPD identifier function. Used for DDL generation.
*/
@Override
public String getVPDCreationFunctionString(String tableName, String tenantFieldName) {
String functionName = tableName + "_ident_func";
return "CREATE OR REPLACE FUNCTION " + functionName + " (p_schema in VARCHAR2 default NULL, p_object in VARCHAR2 default NULL) RETURN VARCHAR2 AS BEGIN return '" + tenantFieldName + " = sys_context(''userenv'', ''client_identifier'')'; END;";
}
/**
* INTERNAL:
* Return an Oracle defined VPD identifier policy. Used for DDL generation.
*/
@Override
public String getVPDCreationPolicyString(String tableName, AbstractSession session) {
String functionName = tableName + "_ident_func";
String schemaName = session.getDatasourceLogin().getUserName();
String policyName = tableName + "_todo_list_policy";
return "\nCALL DBMS_RLS.ADD_POLICY ('" + schemaName + "', '" + tableName + "', '" + policyName + "', '" + schemaName + "', '" + functionName +"', 'select, update, delete')\n";
}
/**
* INTERNAL:
* Return an Oracle defined VPD identifier policy deletion. Used for DDL generation.
*/
@Override
public String getVPDDeletionString(String tableName, AbstractSession session) {
String schemaName = session.getDatasourceLogin().getUserName();
String policyName = tableName + "_todo_list_policy";
return "\nCALL DBMS_RLS.DROP_POLICY ('" + schemaName + "', '" + tableName + "', '" + policyName + "')";
}
/**
* INTERNAL:
* Return an Oracle defined VPD set identifier query.
*/
@Override
public DatabaseQuery getVPDSetIdentifierQuery(String vpdIdentifier) {
if (vpdSetIdentifierQuery == null) {
vpdSetIdentifierQuery = new DataModifyQuery("CALL DBMS_SESSION.SET_IDENTIFIER(#" + vpdIdentifier + ")");
}
return vpdSetIdentifierQuery;
}
/**
* INTERNAL:
* Get a timestamp value from a result set.
* Overrides the default behavior to specifically return a timestamp. Added
* to overcome an issue with the oracle 9.0.1.4 JDBC driver.
*/
@Override
public Object getObjectFromResultSet(ResultSet resultSet, int columnNumber, int type, AbstractSession session) throws java.sql.SQLException {
//Bug#3381652 10G Drivers return sql.Date instead of timestamp on DATE field
if ((type == Types.TIMESTAMP) || (type == Types.DATE)) {
return resultSet.getTimestamp(columnNumber);
} else {
return super.getObjectFromResultSet(resultSet, columnNumber, type, session);
}
}
/**
* Initialize any platform-specific operators
*/
@Override
protected void initializePlatformOperators() {
super.initializePlatformOperators();
addOperator(operatorOuterJoin());
addOperator(logOperator());
addOperator(ExpressionOperator.simpleTwoArgumentFunction(ExpressionOperator.Concat, "CONCAT"));
addOperator(todayOperator());
addOperator(currentDateOperator());
addOperator(currentTimeOperator());
addOperator(ExpressionOperator.truncateDate());
addOperator(ExpressionOperator.newTime());
addOperator(ExpressionOperator.ifNull());
addOperator(atan2Operator());
addOperator(ExpressionOperator.oracleDateName());
addOperator(operatorLocate());
addOperator(operatorLocate2());
addOperator(regexpOperator());
addOperator(exceptOperator());
}
/**
* INTERNAL:
* Create the EXCEPT operator, MINUS in Oracle.
*/
public static ExpressionOperator exceptOperator() {
ExpressionOperator exOperator = new ExpressionOperator();
exOperator.setType(ExpressionOperator.FunctionOperator);
exOperator.setSelector(ExpressionOperator.Except);
exOperator.printsAs("MINUS ");
exOperator.bePostfix();
exOperator.setNodeClass(ClassConstants.FunctionExpression_Class);
return exOperator;
}
/**
* INTERNAL:
* Create the REGEXP_LIKE operator.
*/
public static ExpressionOperator regexpOperator() {
ExpressionOperator result = new ExpressionOperator();
result.setSelector(ExpressionOperator.Regexp);
result.setType(ExpressionOperator.FunctionOperator);
List<String> v = new ArrayList<>(3);
v.add("REGEXP_LIKE(");
v.add(", ");
v.add(")");
result.printsAs(v);
result.bePrefix();
result.setNodeClass(ClassConstants.FunctionExpression_Class);
v = new ArrayList<>(2);
v.add(".regexp(");
v.add(")");
result.printsJavaAs(v);
return result;
}
/**
* INTERNAL:
* Used by derived platforms (Oracle8Platform and higher)
* to indicate whether app. server should unwrap connection
* to use lob locator.
*/
public boolean isNativeConnectionRequiredForLobLocator() {
return false;
}
@Override
public boolean isOracle() {
return true;
}
/**
* Create the log operator for this platform
*/
protected ExpressionOperator logOperator() {
ExpressionOperator result = new ExpressionOperator();
result.setSelector(ExpressionOperator.Log);
List<String> v = new ArrayList<>(2);
v.add("LOG(10,");
v.add(")");
result.printsAs(v);
result.bePrefix();
result.setNodeClass(FunctionExpression.class);
return result;
}
/**
* Builds a table of maximum numeric values keyed on java class. This is used for type testing but
* might also be useful to end users attempting to sanitize values.
* <p><b>NOTE</b>: BigInteger {@literal &} BigDecimal maximums are dependent upon their precision {@literal &} Scale
*/
@Override
public Hashtable<Class<? extends Number>, ? super Number> maximumNumericValues() {
Hashtable<Class<? extends Number>, ? super Number> values = new Hashtable<>();
values.put(Integer.class, Integer.MAX_VALUE);
values.put(Long.class, Long.MAX_VALUE);
values.put(Double.class, 9.9999E125);
values.put(Short.class, Short.MAX_VALUE);
values.put(Byte.class, Byte.MAX_VALUE);
values.put(Float.class, Float.MAX_VALUE);
values.put(java.math.BigInteger.class, new java.math.BigInteger("0"));
values.put(java.math.BigDecimal.class, new java.math.BigDecimal(new java.math.BigInteger("0"), 38));
return values;
}
/**
* Builds a table of minimum numeric values keyed on java class. This is used for type testing but
* might also be useful to end users attempting to sanitize values.
* <p><b>NOTE</b>: BigInteger {@literal &} BigDecimal minimums are dependent upon their precision {@literal &} Scale
*/
@Override
public Hashtable<Class<? extends Number>, ? super Number> minimumNumericValues() {
Hashtable<Class<? extends Number>, ? super Number> values = new Hashtable<>();
values.put(Integer.class, Integer.MIN_VALUE);
values.put(Long.class, Long.MIN_VALUE);
values.put(Double.class, -1E-129);
values.put(Short.class, Short.MIN_VALUE);
values.put(Byte.class, Byte.MIN_VALUE);
values.put(Float.class, Float.MIN_VALUE);
values.put(java.math.BigInteger.class, new java.math.BigInteger("0"));
values.put(java.math.BigDecimal.class, new java.math.BigDecimal(new java.math.BigInteger("0"), 38));
return values;
}
/**
* INTERNAL:
* Produce a DataReadQuery which updates(!) the sequence number in the db
* and returns it. Currently implemented on Oracle only.
* @param qualifiedSeqName known by Oracle to be a defined sequence
*/
@Override
public ValueReadQuery buildSelectQueryForSequenceObject(String qualifiedSeqName, Integer size) {
return new ValueReadQuery("SELECT " + qualifiedSeqName + ".NEXTVAL FROM DUAL");
}
/**
* INTERNAL:
* Though Oracle doesn't support identity it could be imitated,
* see comment to setSupportsIdentity method.
* @param qualifiedSeqName known by Oracle to be a defined sequence
*/
@Override
public ValueReadQuery buildSelectQueryForIdentity(String qualifiedSeqName, Integer size) {
return new ValueReadQuery("SELECT " + qualifiedSeqName + ".CURRVAL FROM DUAL");
}
/**
* Create the outer join operator for this platform
*/
protected ExpressionOperator operatorOuterJoin() {
ExpressionOperator result = new ExpressionOperator();
result.setSelector(ExpressionOperator.EqualOuterJoin);
List<String> v = new ArrayList<>(2);
v.add(" (+) = ");
result.printsAs(v);
result.bePostfix();
result.setNodeClass(RelationExpression.class);
return result;
}
/**
* INTERNAL:
* Override the default locate operator
*/
protected ExpressionOperator operatorLocate() {
ExpressionOperator result = new ExpressionOperator();
result.setSelector(ExpressionOperator.Locate);
List<String> v = new ArrayList<>(2);
v.add("INSTR(");
v.add(", ");
v.add(")");
result.printsAs(v);
result.bePrefix();
result.setNodeClass(RelationExpression.class);
return result;
}
/**
* INTERNAL:
* Override the default locate operator
*/
protected ExpressionOperator operatorLocate2() {
ExpressionOperator result = new ExpressionOperator();
result.setSelector(ExpressionOperator.Locate2);
List<String> v = new ArrayList<>(2);
v.add("INSTR(");
v.add(", ");
v.add(", ");
v.add(")");
result.printsAs(v);
result.bePrefix();
result.setNodeClass(RelationExpression.class);
return result;
}
/**
* INTERNAL:
* Append the receiver's field 'NULL' constraint clause to a writer.
*/
@Override
public void printFieldNullClause(Writer writer) throws ValidationException {
try {
writer.write(" NULL");
} catch (IOException ioException) {
throw ValidationException.fileError(ioException);
}
}
/**
* Return the current date and time from the server.
*/
public String serverTimestampString() {
return "SYSDATE";
}
/**
* INTERNAL:
* Should the variable name of a stored procedure call be printed as part of the procedure call
* e.g. EXECUTE PROCEDURE MyStoredProc(myvariable = ?)
*/
@Override
public boolean shouldPrintStoredProcedureArgumentNameInCall() {
return false;
}
@Override
public String getProcedureArgument(String name, Object parameter, Integer parameterType,
StoredProcedureCall call, AbstractSession session) {
if(name != null && DatasourceCall.IN.equals(parameterType) && !call.usesBinding(session)) {
return name + "=>" + "?";
}
return "?";
}
/**
* JDBC defines and outer join syntax, many drivers do not support this. So we normally avoid it.
*/
@Override
public boolean shouldUseJDBCOuterJoinSyntax() {
return false;
}
/**
* Some db allow VARCHAR db field to be used in arithmetic operations automatically converting them to numeric:
* UPDATE OL_PHONE SET PHONE_ORDER_VARCHAR = (PHONE_ORDER_VARCHAR + 1) WHERE ...
* SELECT ... WHERE ... t0.MANAGED_ORDER_VARCHAR BETWEEN 1 AND 4 ...
*/
@Override
public boolean supportsAutoConversionToNumericForArithmeticOperations() {
return true;
}
/**
* INTERNAL:
* Indicates whether the platform supports sequence objects.
* This method is to be used *ONLY* by sequencing classes
*/
@Override
public boolean supportsSequenceObjects() {
return true;
}
/**
* INTERNAL:
* Indicates whether the platform supports identity.
* This method is to be used *ONLY* by sequencing classes
*/
@Override
public boolean supportsIdentity() {
return supportsIdentity;
}
/**
* ADVANCED:
* Oracle db doesn't support identity.
* However it's possible to get identity-like behavior
* using sequence in an insert trigger - that's the only
* case when supportsIdentity should be set to true:
* in this case all the sequences that have shouldAcquireValueAfterInsert
* set to true will keep this setting (it would've been reversed in case
* identity is not supported).
* Note that with supportsIdentity==true attempt to create tables that have
* identity fields will fail - Oracle doesn't support identity.
* Therefore if there's table creation reqiured it should be done
* with supportsIdentity==false, then set the flag to true and reset sequencing
* (or logout and login the session).
*/
public void setSupportsIdentity(boolean supportsIdentity) {
this.supportsIdentity = supportsIdentity;
}
/**
* INTERNAL:
* Return if database stored functions are supported.
*/
@Override
public boolean supportsStoredFunctions() {
return true;
}
/**
* Oracle db supports VPD.
*/
@Override
public boolean supportsVPD() {
return true;
}
@Override
public boolean supportsWaitForUpdate() {
return true;
}
/**
* Returns true if the database supports SQL syntax not to wait on a SELECT..FOR UPADTE
* (i.e. In Oracle adding NOWAIT to the end will accomplish this)
*/
public boolean supportsSelectForUpdateNoWait() {
return true;
}
/**
* Create the sysdate operator for this platform
*/
protected ExpressionOperator todayOperator() {
return ExpressionOperator.simpleFunctionNoParentheses(ExpressionOperator.Today, "SYSDATE");
}
protected ExpressionOperator currentDateOperator() {
return ExpressionOperator.simpleFunctionNoParentheses(ExpressionOperator.CurrentDate, "TO_DATE(CURRENT_DATE)");
}
protected ExpressionOperator currentTimeOperator() {
return ExpressionOperator.simpleFunctionNoParentheses(ExpressionOperator.CurrentTime, "SYSDATE");
}
/**
* INTERNAL:
* Indicates whether this Oracle platform can unwrap Oracle connection.
*/
public boolean canUnwrapOracleConnection() {
return false;
}
/**
* INTERNAL:
* If can unwrap returns unwrapped Oracle connection, otherwise original connection.
*/
public Connection unwrapOracleConnection(Connection connection) {
return connection;
}
/**
* Return true if JDBC syntax should be used for stored procedure calls.
*/
public boolean useJDBCStoredProcedureSyntax() {
if (useJDBCStoredProcedureSyntax == null) {
useJDBCStoredProcedureSyntax = this.driverName != null
&& Pattern.compile("Oracle", Pattern.CASE_INSENSITIVE).matcher(this.driverName).find();
}
return useJDBCStoredProcedureSyntax;
}
//Oracle Rownum support
protected String SELECT = "SELECT * FROM (SELECT ";
protected String HINT_START = "/*+ FIRST_ROWS";
protected String HINT_END = " */ ";
protected String FROM = "a.*, ROWNUM rnum FROM (";
protected String END_FROM = ") a ";
protected String MAX_ROW = "WHERE ROWNUM <= ";
protected String MIN_ROW = ") WHERE rnum > ";
// Bug #453208
protected String LOCK_START_PREFIX = " AND (";
protected String LOCK_START_PREFIX_WHERE = " WHERE (";
protected String LOCK_START_SUFFIX = ") IN (";
protected String LOCK_END = " FOR UPDATE";
protected String SELECT_ID_PREFIX = "SELECT ";
protected String SELECT_ID_SUFFIX = " FROM (SELECT ";
protected String FROM_ID = ", ROWNUM rnum FROM (";
protected String END_FROM_ID = ") ";
protected String ORDER_BY_ID = " ORDER BY ";
protected String BRACKET_END = " ) ";
/**
* INTERNAL:
* Print the SQL representation of the statement on a stream, storing the fields
* in the DatabaseCall. This implementation works MaxRows and FirstResult into the SQL using
* Oracle's ROWNUM to filter values if shouldUseRownumFiltering is true.
*/
@Override
public void printSQLSelectStatement(DatabaseCall call, ExpressionSQLPrinter printer, SQLSelectStatement statement) {
int max = 0;
int firstRow = 0;
ReadQuery query = statement.getQuery();
if (query != null) {
max = query.getMaxRows();
firstRow = query.getFirstResult();
}
if (!(this.shouldUseRownumFiltering()) || (!(max > 0) && !(firstRow > 0))) {
super.printSQLSelectStatement(call, printer, statement);
return;
} else {
statement.setUseUniqueFieldAliases(true);
// Bug #453208 - Pessimistic locking with query row limits does not work on Oracle DB.
if (query.isObjectBuildingQuery() && (((ObjectBuildingQuery) query).getLockMode() == ObjectBuildingQuery.LOCK
|| ((ObjectBuildingQuery) query).getLockMode() == ObjectBuildingQuery.LOCK_NOWAIT)) {
if (query.isReadAllQuery() || query.isReadObjectQuery()) {
// Workaround can exist for this specific case
Vector fields = new Vector();
statement.enableFieldAliasesCaching();
String queryString = printOmittingOrderByForUpdateUnion(statement, printer, fields);
duplicateCallParameters(call);
call.setFields(fields);
/* Prints a query similar to the below:
*
* SELECT t1.EMP_ID AS a1, ...
* FROM CMP3_EMPLOYEE t1
* WHERE ...
* AND (t1.EMP_ID) IN (
* SELECT a1 FROM (
* SELECT a1, ROWNUM rnum FROM (
* SELECT t1.EMP_ID AS a1, ...
* FROM CMP3_EMPLOYEE t1
* WHERE ...)
* WHERE ROWNUM <= ?)
* WHERE rnum > ?)
* FOR UPDATE; */
printer.printString(queryString);
printLockStartWithPrimaryKeyFields(statement, printer);
String primaryKeyFields = getPrimaryKeyAliases(statement);
printer.printString(SELECT_ID_PREFIX);
printer.printString(primaryKeyFields);
printer.printString(SELECT_ID_SUFFIX);
printer.printString(buildFirstRowsHint(max));
printer.printString(primaryKeyFields);
printer.printString(FROM_ID);
printer.printString(queryString);
printer.printString(ORDER_BY_ID);
printer.printString(primaryKeyFields);
printer.printString(END_FROM_ID);
printer.printString(MAX_ROW);
printer.printParameter(DatabaseCall.MAXROW_FIELD);
printer.printString(MIN_ROW);
printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
printer.printString(BRACKET_END);
try {
statement.printSQLOrderByClause(printer);
statement.printSQLUnionClause(printer);
} catch (IOException exception) {
throw ValidationException.fileError(exception);
}
printer.printString(LOCK_END);
} else {
throw new UnsupportedOperationException(ExceptionLocalization.buildMessage("ora_pessimistic_locking_with_rownum"));
}
} else {
if (max > 0) {
printer.printString(SELECT);
printer.printString(buildFirstRowsHint(max));
printer.printString(FROM);
call.setFields(statement.printSQL(printer));
printer.printString(END_FROM);
printer.printString(MAX_ROW);
printer.printParameter(DatabaseCall.MAXROW_FIELD);
printer.printString(MIN_ROW);
printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
} else {// firstRow>0
printer.printString(SELECT);
printer.printString(FROM);
call.setFields(statement.printSQL(printer));
printer.printString(END_FROM);
printer.printString(MIN_ROW);
printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
}
}
}
call.setIgnoreFirstRowSetting(true);
call.setIgnoreMaxResultsSetting(true);
}
@SuppressWarnings("unchecked")
// Bug #453208 - Duplicate call parameters since the query is performed twice
private void duplicateCallParameters(DatabaseCall call) {
List newParameterList = new ArrayList(call.getParameters());
newParameterList.addAll(call.getParameters());
call.setParameters(newParameterList);
List<Integer> newParameterTypesList = new ArrayList<>(call.getParameterTypes());
newParameterTypesList.addAll(call.getParameterTypes());
call.setParameterTypes(newParameterTypesList);
}
@SuppressWarnings("unchecked")
private String printOmittingOrderByForUpdateUnion(SQLSelectStatement statement, ExpressionSQLPrinter printer, Vector fields) {
boolean originalShouldPrintForUpdate = this.shouldPrintForUpdateClause;
Writer originalWriter = printer.getWriter();
Vector selectFields = null;
this.shouldPrintForUpdateClause = false;
printer.setWriter(new StringWriter());
try {
selectFields = statement.printSQLSelect(printer);
statement.printSQLWhereKeyWord(printer);
statement.printSQLWhereClause(printer);
statement.printSQLHierarchicalQueryClause(printer);
statement.printSQLGroupByClause(printer);
statement.printSQLHavingClause(printer);
} catch (IOException exception) {
throw ValidationException.fileError(exception);
}
fields.addAll(selectFields);
String query = printer.getWriter().toString();
this.shouldPrintForUpdateClause = originalShouldPrintForUpdate;
printer.setWriter(originalWriter);
return query;
}
private void printLockStartWithPrimaryKeyFields(SQLSelectStatement statement, ExpressionSQLPrinter printer) {
if (statement.getWhereClause() == null) {
printer.printString(LOCK_START_PREFIX_WHERE);
} else {
printer.printString(LOCK_START_PREFIX);
}
Iterator<DatabaseField> iterator = statement.getQuery().getDescriptor().getPrimaryKeyFields().iterator();
while (iterator.hasNext()) {
DatabaseField field = iterator.next();
DatabaseTable alias = statement.getExpressionBuilder().aliasForTable(field.getTable());
printer.printField(field, alias);
if(iterator.hasNext()) {
printer.printString(",");
}
}
printer.printString(LOCK_START_SUFFIX);
}
private String getPrimaryKeyAliases(SQLSelectStatement statement) {
StringBuilder builder = new StringBuilder();
Iterator<DatabaseField> iterator = statement.getQuery().getDescriptor().getPrimaryKeyFields().iterator();
while (iterator.hasNext()) {
builder.append(statement.getAliasFor(iterator.next()));
if(iterator.hasNext()) {
builder.append(',');
}
}
return builder.toString();
}
/**
* INTERNAL:
* Override this method if the platform supports sequence objects
* and it's possible to alter sequence object's increment in the database.
*/
@Override
public boolean isAlterSequenceObjectSupported() {
return true;
}
/**
* INTERNAL:
* Indicates whether SELECT DISTINCT ... FOR UPDATE is allowed by the platform (Oracle doesn't allow this).
*/
@Override
public boolean isForUpdateCompatibleWithDistinct() {
return false;
}
/**
* INTERNAL:
* Indicates whether SELECT DISTINCT lob FROM ... (where lob is BLOB or CLOB) is allowed by the platform (Oracle doesn't allow this).
*/
@Override
public boolean isLobCompatibleWithDistinct() {
return false;
}
/**
* Return true if the given exception occurred as a result of a lock
* time out exception (WAIT clause).
*/
@Override
public boolean isLockTimeoutException(DatabaseException e) {
return (e.getInternalException() instanceof java.sql.SQLException && ((java.sql.SQLException) e.getInternalException()).getErrorCode() == 30006);
}
/**
* INTERNAL:
* A call to this method will perform a platform based check on the connection and exception
* error code to determine if the connection is still valid or if a communication error has occurred.
* If a communication error has occurred then the query may be retried.
* If this platform is unable to determine if the error was communication based it will return
* false forcing the error to be thrown to the user.
*/
@Override
public boolean wasFailureCommunicationBased(SQLException exception, Connection connection, AbstractSession sessionForProfile){
if (exception != null){
if (exception.getErrorCode() == 17410){
return true;
}
if (exception.getErrorCode() == 17002){
return true;
}
if (exception.getErrorCode() == 2399){
return true;
}
if (exception.getErrorCode() == 2396){
return true;
}
}
return super.wasFailureCommunicationBased(exception, connection, sessionForProfile);
}
@Override
public boolean shouldPrintForUpdateClause() {
return shouldPrintForUpdateClause;
}
@Override
public Expression createExpressionFor(DatabaseField field, Expression builder, String fieldClassificationClassName) {
if (field.getType() == java.sql.Clob.class ||
field.getType() == java.sql.Blob.class ||
"java.sql.Clob".equals(fieldClassificationClassName) ||
"java.sql.Blob".equals(fieldClassificationClassName)) {
Expression subExp1 = builder.getField(field);
Expression subExp2 = builder.getParameter(field);
subExp1 = subExp1.getFunction("dbms_lob.compare", subExp2);
return subExp1.equal(0);
}
return super.createExpressionFor(field, builder, fieldClassificationClassName);
}
// Value of shouldCheckResultTableExistsQuery must be true.
/**
* INTERNAL:
* Returns query to check whether given table exists.
* Query execution returns a row when table exists or empty result otherwise.
* @param table database table meta-data
* @return query to check whether given table exists
*/
@Override
protected DataReadQuery getTableExistsQuery(final TableDefinition table) {
final DataReadQuery query = new DataReadQuery(
"SELECT table_name FROM user_tables WHERE table_name='" + table.getFullName() + "'");
query.setMaxRows(1);
return query;
}
/**
* INTERNAL:
* Executes and evaluates query to check whether given table exists.
* Returned value depends on returned result set being empty or not.
* @param session current database session
* @param table database table meta-data
* @param suppressLogging whether to suppress logging during query execution
* @return value of {@code true} if given table exists or {@code false} otherwise
*/
@Override
public boolean checkTableExists(final DatabaseSessionImpl session,
final TableDefinition table, final boolean suppressLogging) {
try {
session.setLoggingOff(suppressLogging);
final Vector result = (Vector)session.executeQuery(getTableExistsQuery(table));
return !result.isEmpty();
} catch (Exception notFound) {
return false;
}
}
@Override
public int getINClauseLimit() {
return 1000;
}
}