| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.openjpa.jdbc.sql; |
| |
| import java.sql.Connection; |
| import java.sql.DatabaseMetaData; |
| import java.sql.PreparedStatement; |
| import java.sql.ResultSet; |
| import java.sql.SQLException; |
| import java.sql.Statement; |
| import java.sql.Types; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength; |
| import org.apache.openjpa.jdbc.identifier.DBIdentifier; |
| import org.apache.openjpa.jdbc.identifier.DBIdentifier.DBIdentifierType; |
| import org.apache.openjpa.jdbc.kernel.exps.FilterValue; |
| import org.apache.openjpa.jdbc.schema.Column; |
| import org.apache.openjpa.jdbc.schema.ForeignKey; |
| import org.apache.openjpa.jdbc.schema.Index; |
| import org.apache.openjpa.jdbc.schema.PrimaryKey; |
| import org.apache.openjpa.jdbc.schema.Table; |
| import org.apache.openjpa.lib.util.Localizer; |
| import org.apache.openjpa.lib.util.ReferenceHashSet; |
| import org.apache.openjpa.util.StoreException; |
| import org.apache.openjpa.util.UnsupportedException; |
| |
| /** |
| * Dictionary for Informix database. Notable features: |
| * <ul> |
| * <li>Informix does not allow pessimistic locking on scrollable result |
| * sets.</li> |
| * <li>SET LOCK MODE TO WAIT N statements are issued to wait on locks. See |
| * {@link #lockWaitSeconds} and {@link #lockModeEnabled}.</li> |
| * <li>LOCK MODE ROW is used by default for table creation to allow the |
| * maximum concurrency.</li> |
| * </ul> |
| */ |
| public class InformixDictionary |
| extends DBDictionary { |
| |
| public static final String VENDOR_IBM = "ibm"; |
| |
| /** |
| * If true, then we will issue a "SET LOCK MODE TO WAIT N" |
| * statement whenever we create a {@link Connection}, in order |
| * allow waiting on locks. |
| */ |
| public boolean lockModeEnabled = false; |
| |
| /** |
| * If {@link #lockModeEnabled} is <code>true</code>, then this |
| * parameter specifies the number of seconds we will wait to |
| * obtain a lock for inserts and pessimistic locking. |
| */ |
| public int lockWaitSeconds = 30; |
| |
| /** |
| * Informix JDBC metadata for all known drivers returns with the |
| * table catalog and the table schema name swapped. A <code>true</code> |
| * value for this property indicates that they should be reversed. |
| */ |
| public boolean swapSchemaAndCatalog = true; |
| |
| protected boolean useJCC = false; |
| // weak set of connections we've already executed lock mode sql on |
| private final Collection _seenConnections = Collections.synchronizedSet( |
| new ReferenceHashSet(ReferenceStrength.WEAK)); |
| |
| public boolean disableRetainUpdateLocksSQL=false; |
| |
| private static final Localizer _loc = Localizer.forPackage |
| (InformixDictionary.class); |
| |
| public InformixDictionary() { |
| platform = "Informix"; |
| validationSQL = "SELECT FIRST 1 CURRENT TIMESTAMP " |
| + "FROM informix.systables"; |
| |
| supportsAutoAssign = true; |
| autoAssignTypeName = "serial"; |
| lastGeneratedKeyQuery = "SELECT FIRST 1 DBINFO('sqlca.sqlerrd1') " |
| + "FROM informix.systables"; |
| |
| // informix actually does support deferred constraints, but not |
| // in the table definition; deferred constraints can be activated by |
| // invoking "set constraints all deferred" on the connection, which |
| // we don't do yet |
| supportsDeferredConstraints = false; |
| constraintNameMode = CONS_NAME_AFTER; |
| |
| // informix supports "CLOB" type, but any attempt to insert |
| // into them raises: "java.sql.SQLException: Can't convert fromnull" |
| useGetStringForClobs = true; |
| longVarcharTypeName = "TEXT"; |
| clobTypeName = "TEXT"; |
| smallintTypeName = "INT8"; |
| tinyintTypeName = "INT8"; |
| floatTypeName = "FLOAT"; |
| bitTypeName = "BOOLEAN"; |
| blobTypeName = "BYTE"; |
| doubleTypeName = "NUMERIC(32,20)"; |
| dateTypeName = "DATE"; |
| timeTypeName = "DATETIME HOUR TO SECOND"; |
| timestampTypeName = "DATETIME YEAR TO FRACTION(3)"; |
| doubleTypeName = "NUMERIC(32,20)"; |
| floatTypeName = "REAL"; |
| bigintTypeName = "NUMERIC(32,0)"; |
| doubleTypeName = "DOUBLE PRECISION"; |
| fixedSizeTypeNameSet.addAll(Arrays.asList(new String[]{ |
| "BYTE", "DOUBLE PRECISION", "INTERVAL", "SMALLFLOAT", "TEXT", |
| "INT8", |
| })); |
| |
| // OpenJPA-2045: NAME has been removed from common reserved words to |
| // only specific dictionaries |
| reservedWordSet.add("NAME"); |
| |
| supportsLockingWithDistinctClause = false; |
| supportsLockingWithMultipleTables = false; |
| supportsLockingWithOrderClause = false; |
| |
| // the informix JDBC drivers have problems with using the |
| // schema name in reflection on columns and tables |
| supportsSchemaForGetColumns = false; |
| supportsSchemaForGetTables = false; |
| |
| // Informix doesn't support aliases in deletes if the table has an index |
| allowsAliasInBulkClause = false; |
| |
| // Informix doesn't understand "X CROSS JOIN Y", but it does understand |
| // the equivalent "X JOIN Y ON 1 = 1" |
| crossJoinClause = "JOIN"; |
| requiresConditionForCrossJoin = true; |
| |
| concatenateFunction = "CONCAT({0},{1})"; |
| nextSequenceQuery = "SELECT {0}.NEXTVAL FROM SYSTABLES WHERE TABID=1"; |
| supportsCorrelatedSubselect = false; |
| swapSchemaAndCatalog = false; |
| |
| // Informix does not support foreign key delete action NULL or DEFAULT |
| supportsNullDeleteAction = false; |
| supportsDefaultDeleteAction = false; |
| |
| trimSchemaName = true; |
| } |
| |
| @Override |
| public void connectedConfiguration(Connection conn) |
| throws SQLException { |
| super.connectedConfiguration(conn); |
| |
| DatabaseMetaData meta = conn.getMetaData(); |
| String driverName = meta.getDriverName(); |
| if (driverName != null) { |
| if (driverName.equals("IBM DB2 JDBC Universal Driver Architecture")) |
| { |
| driverVendor = VENDOR_IBM; |
| useJCC = true; |
| setIdentifierCase(meta); |
| } |
| else if (driverName.equals("IBM Informix JDBC Driver for IBM Informix Dynamic Server")) { |
| setIdentifierCase(meta); |
| driverVendor = VENDOR_IBM; |
| } |
| else if ("Informix".equalsIgnoreCase(driverName)) |
| driverVendor = VENDOR_DATADIRECT; |
| else |
| driverVendor = VENDOR_OTHER; |
| } else |
| driverVendor = VENDOR_OTHER; |
| |
| if (isJDBC3) { |
| conn.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); |
| if (log.isTraceEnabled()) |
| log.trace(_loc.get("connection-defaults", new Object[]{ |
| conn.getAutoCommit(), conn.getHoldability(), |
| conn.getTransactionIsolation()})); |
| } |
| } |
| |
| private void setIdentifierCase(DatabaseMetaData meta) { |
| try { |
| // lower case identifiers is the default for the JCC and newer |
| // Informix JDBC drivers |
| if (meta.storesLowerCaseIdentifiers()) { |
| schemaCase = SCHEMA_CASE_LOWER; |
| } |
| else if (meta.storesMixedCaseIdentifiers()) { |
| schemaCase = SCHEMA_CASE_PRESERVE; |
| } |
| // otherwise, use the default (upper) |
| } |
| catch (SQLException e) { |
| getLog().warn("cannot-determine-identifier-base-case"); |
| if (getLog().isTraceEnabled()) { |
| getLog().trace(e.toString(), e); |
| } |
| } |
| } |
| |
| @Override |
| public Column[] getColumns(DatabaseMetaData meta, String catalog, |
| String schemaName, String tableName, String columnName, Connection conn) |
| throws SQLException { |
| return getColumns(meta, DBIdentifier.newCatalog(catalog), |
| DBIdentifier.newSchema(schemaName),DBIdentifier.newTable(tableName), |
| DBIdentifier.newColumn(columnName), conn); |
| } |
| |
| @Override |
| public Column[] getColumns(DatabaseMetaData meta, DBIdentifier catalog, |
| DBIdentifier schemaName, DBIdentifier tableName, DBIdentifier columnName, Connection conn) |
| throws SQLException { |
| Column[] cols = super.getColumns(meta, catalog, schemaName, tableName, |
| columnName, conn); |
| |
| // treat logvarchar as clob |
| for (int i = 0; cols != null && i < cols.length; i++) |
| if (cols[i].getType() == Types.LONGVARCHAR) |
| cols[i].setType(Types.CLOB); |
| return cols; |
| } |
| |
| @Override |
| public Column newColumn(ResultSet colMeta) |
| throws SQLException { |
| Column col = super.newColumn(colMeta); |
| if (swapSchemaAndCatalog) |
| col.setSchemaIdentifier(fromDBName(colMeta.getString("TABLE_CAT"), DBIdentifierType.CATALOG)); |
| return col; |
| } |
| |
| @Override |
| public PrimaryKey newPrimaryKey(ResultSet pkMeta) |
| throws SQLException { |
| PrimaryKey pk = super.newPrimaryKey(pkMeta); |
| if (swapSchemaAndCatalog) |
| pk.setSchemaIdentifier(fromDBName(pkMeta.getString("TABLE_CAT"), DBIdentifierType.CATALOG)); |
| return pk; |
| } |
| |
| @Override |
| public Index newIndex(ResultSet idxMeta) |
| throws SQLException { |
| Index idx = super.newIndex(idxMeta); |
| if (swapSchemaAndCatalog) |
| idx.setSchemaIdentifier(fromDBName(idxMeta.getString("TABLE_CAT"), DBIdentifierType.CATALOG)); |
| return idx; |
| } |
| |
| @Override |
| public void setBoolean(PreparedStatement stmnt, int idx, boolean val, |
| Column col) |
| throws SQLException { |
| // informix actually requires that a boolean be set: it cannot |
| // handle a numeric argument |
| stmnt.setString(idx, val ? "t" : "f"); |
| } |
| |
| @Override |
| public String[] getCreateTableSQL(Table table) { |
| String[] create = super.getCreateTableSQL(table); |
| create[0] = create[0] + " LOCK MODE ROW"; |
| return create; |
| } |
| |
| @Override |
| public String[] getAddPrimaryKeySQL(PrimaryKey pk) { |
| String pksql = getPrimaryKeyConstraintSQL(pk); |
| if (pksql == null) |
| return new String[0]; |
| return new String[]{ "ALTER TABLE " |
| + getFullName(pk.getTable(), false) + " ADD CONSTRAINT " + pksql }; |
| } |
| |
| @Override |
| public String[] getAddForeignKeySQL(ForeignKey fk) { |
| String fksql = getForeignKeyConstraintSQL(fk); |
| if (fksql == null) |
| return new String[0]; |
| return new String[]{ "ALTER TABLE " |
| + getFullName(fk.getTable(), false) + " ADD CONSTRAINT " + fksql }; |
| } |
| |
| @Override |
| public boolean supportsRandomAccessResultSet(Select sel, |
| boolean forUpdate) { |
| return !forUpdate && !sel.isLob() |
| && super.supportsRandomAccessResultSet(sel, forUpdate); |
| } |
| |
| @Override |
| public Connection decorate(Connection conn) |
| throws SQLException { |
| conn = super.decorate(conn); |
| if (isJDBC3 && conn.getHoldability() != |
| ResultSet.HOLD_CURSORS_OVER_COMMIT) { |
| conn.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); |
| if (log.isTraceEnabled()) { |
| log.trace(_loc.get("connection-defaults", new Object[]{ |
| conn.getAutoCommit(), conn.getHoldability(), |
| conn.getTransactionIsolation()})); |
| } |
| } |
| |
| // if we haven't already done so, initialize the lock mode of the |
| // connection |
| if (_seenConnections.add(conn)) { |
| if (lockModeEnabled) { |
| String sql = "SET LOCK MODE TO WAIT"; |
| if (lockWaitSeconds > 0) |
| sql = sql + " " + lockWaitSeconds; |
| execute(sql, conn, true); |
| } |
| |
| if (!disableRetainUpdateLocksSQL){ |
| String sql = "SET ENVIRONMENT RETAINUPDATELOCKS 'ALL'"; |
| execute(sql, conn, false); |
| } |
| } |
| |
| // the datadirect driver requires that we issue a rollback before using |
| // each connection |
| if (VENDOR_DATADIRECT.equalsIgnoreCase(driverVendor)) |
| try { |
| conn.rollback(); |
| } catch (SQLException se) { |
| } |
| return conn; |
| } |
| |
| private void execute(String sql, Connection conn, boolean throwExc) { |
| Statement stmnt = null; |
| try { |
| stmnt = conn.createStatement(); |
| stmnt.executeUpdate(sql); |
| } catch (SQLException se) { |
| if (throwExc) |
| throw SQLExceptions.getStore(se, this); |
| else { |
| if (log.isTraceEnabled()) |
| log.trace(_loc.get("can-not-execute", sql)); |
| } |
| } finally { |
| if (stmnt != null) |
| try { |
| stmnt.close(); |
| } catch (SQLException se) { |
| } |
| } |
| } |
| |
| @Override |
| public void indexOf(SQLBuffer buf, FilterValue str, FilterValue find, |
| FilterValue start) { |
| throw new UnsupportedException(_loc.get("function-not-supported", |
| getClass(), "LOCATE")); |
| } |
| |
| @Override |
| public boolean needsToCreateIndex(Index idx, Table table) { |
| // Informix will automatically create a unique index for the |
| // primary key, so don't create another index again |
| PrimaryKey pk = table.getPrimaryKey(); |
| if (pk != null && idx.columnsMatch(pk.getColumns())) |
| return false; |
| return true; |
| } |
| |
| public boolean useJCC() { |
| return useJCC; |
| } |
| |
| /** |
| * Return DB specific schemaCase |
| */ |
| @Override |
| public String getSchemaCase(){ |
| return schemaCase; |
| } |
| |
| @Override |
| public boolean isFatalException(int subtype, SQLException ex) { |
| |
| // SQL State of IX000 is a general purpose Informix error code |
| // category, so only return Boolean.TRUE if we match SQL Codes |
| // recoverable = Boolean.FALSE; |
| if ((subtype == StoreException.LOCK && checkNestedErrorCodes(ex, "IX000", -154)) |
| ||(subtype == StoreException.QUERY && ex.getErrorCode() == -213)) { |
| return false; |
| } |
| |
| return super.isFatalException(subtype, ex); |
| } |
| |
| /** |
| * Specialized matchErrorState method for Informix. Informix exceptions are |
| * typically nested multiple levels deep. Correct determination of the exception type requires |
| * inspection of nested exceptions to determine the root cause. A list of Informix (IDS v10) error codes |
| * can be found here: |
| * |
| * http://publib.boulder.ibm.com/infocenter/idshelp/v10/index.jsp?topic=/com.ibm.em.doc/errors_ids100.html |
| * |
| * @param errorStates classification of SQL error states by their specific nature. The keys of the |
| * map represent one of the constants defined in {@link StoreException}. The value corresponding to |
| * a key represent the set of SQL Error States representing specific category of database error. |
| * This supplied map is sourced from <code>sql-error-state-codes.xml</xml> and filtered the |
| * error states for the current database. |
| * |
| * @param ex original SQL Exception as raised by the database driver. |
| * |
| * @return A constant indicating the category of error as defined in {@link StoreException}. |
| */ |
| @Override |
| protected int matchErrorState(Map<Integer,Set<String>> errorStates, SQLException ex) { |
| // Informix SQLState IX000 is a general SQLState that applies to many possible conditions |
| // If the underlying cause is also an IX000 with error code: |
| // -107 ISAM error: record is locked. || -154 ISAM error: Lock Timeout Expired. |
| // the exception type is LOCK. |
| if (checkNestedErrorCodes(ex, "IX000", -107, -154)) { |
| return StoreException.LOCK; |
| } |
| return super.matchErrorState(errorStates, ex); |
| } |
| |
| private boolean checkNestedErrorCodes(SQLException ex, String sqlState, int...errorCodes) { |
| SQLException cause = ex; |
| int level = 0; |
| // Query at most 5 exceptions deep to prevent infinite iteration exception loops |
| // Typically, the root exception is at level 3. |
| while (cause != null && level < 5) { |
| String errorState = cause.getSQLState(); |
| if (sqlState == null || sqlState.equals(errorState)) { |
| for (int ec : errorCodes) { |
| if (cause.getErrorCode() == ec) { |
| return true; |
| } |
| } |
| } |
| cause = cause.getNextException(); |
| level++; |
| } |
| return false; |
| } |
| } |
| |