blob: 139dd4b18671ded01f2acd7c1c89a075a3cb2d8b [file] [log] [blame]
/*
* 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.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import java.util.Locale;
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.Index;
import org.apache.openjpa.jdbc.schema.PrimaryKey;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.schema.Unique;
import org.apache.openjpa.lib.jdbc.DelegatingConnection;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.StoreException;
/**
* Dictionary for Sybase.
* The main point of interest is that by default, every table
* that is created will have a unique column named "UNQ_INDEX" of
* the "IDENTITY" type. OpenJPA will not ever utilize this column. However,
* due to internal Sybase restrictions, this column is required
* in order to support pessimistic (datastore) locking, since Sybase
* requires that any tables in a "SELECT ... FOR UPDATE" clause have
* a unique index that is <strong>not</strong> included in the list
* of columns, as described in the
* <a href="http://www.sybase.com/detail/1,6904,1023075,00.html"
* >Sybase documentation</a>. This behavior can be surpressed by setting the
* dictionary property <code>CreateIdentityColumn=false</code>. The
* name of the unique column can be changed by setting the property
* <code>IdentityColumnName=COLUMN_NAME</code>.
* A good Sybase type reference is can be found <a
* href="http://www.ispirer.com/doc/sqlways36/sybase/syb_dtypes.html">here</a>.
*/
public class SybaseDictionary
extends AbstractSQLServerDictionary {
private static Localizer _loc = Localizer.forPackage
(SybaseDictionary.class);
public static String RIGHT_TRUNCATION_ON_SQL = "set string_rtruncation on";
public static String NUMERIC_TRUNCATION_OFF_SQL = "set arithabort numeric_truncation off";
/**
* If true, then whenever the <code>schematool</code> creates a
* table, it will append an additional IDENTITY column to the
* table's creation SQL. This is so Sybase will be able to
* perform <code>SELECT...FOR UPDATE</code> statements.
*/
public boolean createIdentityColumn = true;
/**
* If {@link #createIdentityColumn} is true, then the
* <code>identityColumnName</code> will be the name of the
* additional unique column that will be created.
*/
public String identityColumnName = "UNQ_INDEX";
/**
* If true, Sybase will ignore numeric truncation on insert or
* update operations. Otherwise, the operation will fail. The default
* value, false is in accordance with SQL92.
*/
public boolean ignoreNumericTruncation = false;
public SybaseDictionary() {
platform = "Sybase";
schemaCase = SCHEMA_CASE_PRESERVE;
forUpdateClause = "FOR UPDATE AT ISOLATION SERIALIZABLE";
supportsLockingWithDistinctClause = false;
supportsNullTableForGetColumns = false;
requiresAliasForSubselect = true;
requiresAutoCommitForMetaData = true;
maxTableNameLength = 30;
maxColumnNameLength = 30;
maxIndexNameLength = 30;
maxConstraintNameLength = 30;
bigintTypeName = "NUMERIC(38)";
bitTypeName = "TINYINT";
// Sybase doesn't understand "X CROSS JOIN Y", but it does understand
// the equivalent "X JOIN Y ON 1 = 1"
crossJoinClause = "JOIN";
requiresConditionForCrossJoin = true;
// these tables should not be reflected on
systemTableSet.addAll(Arrays.asList(new String[]{
"IJDBC_FUNCTION_ESCAPES", "JDBC_FUNCTION_ESCAPES",
"SPT_IJDBC_CONVERSION", "SPT_IJDBC_MDA", "SPT_IJDBC_TABLE_TYPES",
"SPT_JDBC_CONVERSION", "SPT_JDBC_TABLE_TYPES", "SPT_JTEXT",
"SPT_LIMIT_TYPES", "SPT_MDA", "SPT_MONITOR", "SPT_VALUES",
"SYBLICENSESLOG",
}));
// reserved words specified at:
// http://manuals.sybase.com/onlinebooks/group-as/asg1250e/refman/@Generic__BookTextView/26603
reservedWordSet.addAll(Arrays.asList(new String[]{
"ARITH_OVERFLOW", "BREAK", "BROWSE", "BULK", "CHAR_CONVERT",
"CHECKPOINT", "CLUSTERED", "COMPUTE", "CONFIRM", "CONTROLROW",
"DATABASE", "DBCC", "DETERMINISTIC", "DISK DISTINCT", "DUMMY",
"DUMP", "ENDTRAN", "ERRLVL", "ERRORDATA", "ERROREXIT", "EXCLUSIVE",
"EXIT", "EXP_ROW_SIZE", "FILLFACTOR", "FUNC", "FUNCTION",
"HOLDLOCK", "IDENTITY_GAP", "IDENTITY_INSERT", "IDENTITY_START",
"IF", "INDEX", "INOUT", "INSTALL", "INTERSECT", "JAR", "KILL",
"LINENO", "LOAD", "LOCK", "MAX_ROWS_PER_PAGE", "MIRROR",
"MIRROREXIT", "MODIFY", "NEW", "NOHOLDLOCK", "NONCLUSTERED",
"NUMERIC_TRUNCATION", "OFF", "OFFSETS", "ONCE", "ONLINE", "OUT",
"OVER", "PARTITION", "PERM", "PERMANENT", "PLAN", "PRINT", "PROC",
"PROCESSEXIT", "PROXY_TABLE", "QUIESCE", "RAISERROR", "READ",
"READPAST", "READTEXT", "RECONFIGURE", "REFERENCES REMOVE", "REORG",
"REPLACE", "REPLICATION", "RESERVEPAGEGAP", "RETURN", "RETURNS",
"ROLE", "ROWCOUNT", "RULE", "SAVE", "SETUSER", "SHARED",
"SHUTDOWN", "SOME", "STATISTICS", "STRINGSIZE", "STRIPE",
"SYB_IDENTITY", "SYB_RESTREE", "SYB_TERMINATE", "TEMP", "TEXTSIZE",
"TRAN", "TRIGGER", "TRUNCATE", "TSEQUAL", "UNPARTITION", "USE",
"USER_OPTION", "WAITFOR", "WHILE", "WRITETEXT",
}));
// Sybase does not allow reserved words to be used as column names.
invalidColumnWordSet.addAll(reservedWordSet);
// Sybase does not support foreign key delete/update action NULL,
// DEFAULT, CASCADE
supportsNullDeleteAction = false;
supportsDefaultDeleteAction = false;
supportsCascadeDeleteAction = false;
supportsNullUpdateAction = false;
supportsDefaultUpdateAction = false;
supportsCascadeUpdateAction = false;
fixedSizeTypeNameSet.remove("NUMERIC");
}
@Override
public int getJDBCType(int metaTypeCode, boolean lob) {
switch (metaTypeCode) {
// the default mapping for BYTE is a TINYINT, but Sybase's TINYINT
// type can't handle the complete range for a Java byte
case JavaTypes.BYTE:
case JavaTypes.BYTE_OBJ:
return getPreferredType(Types.SMALLINT);
default:
return super.getJDBCType(metaTypeCode, lob);
}
}
@Override
public void setBigInteger(PreparedStatement stmnt, int idx, BigInteger val,
Column col)
throws SQLException {
// setBigDecimal doesn't work here: in one case, a stored value
// of 7799438514924349440 turns into 7799438514924349400
// setObject gets around this in the Sybase JDBC drivers
setObject(stmnt, idx, new BigDecimal(val), Types.BIGINT, col);
}
@Override
public String[] getCreateTableSQL(Table table) {
if (!createIdentityColumn)
return super.getCreateTableSQL(table);
StringBuilder buf = new StringBuilder();
buf.append("CREATE TABLE ").append(getFullName(table, false)).
append(" (");
Column[] cols = table.getColumns();
boolean hasIdentity = false;
for (int i = 0; i < cols.length; i++) {
// can only have one identity column
if (cols[i].isAutoAssigned()) {
hasIdentity = true;
}
// The column may exist if dropping and recreating a table.
if(cols[i].getIdentifier().getName().equals(identityColumnName)) {
hasIdentity=true;
// column type may be lost when recreating - reset to NUMERIC
if(cols[i].getType() != Types.NUMERIC) { // should check if compatible
cols[i].setType(Types.NUMERIC);
}
}
buf.append(i == 0 ? "" : ", ");
buf.append(getDeclareColumnSQL(cols[i], false));
}
// add an identity column if we do not already have one
if (!hasIdentity)
buf.append(", ").append(identityColumnName).
append(" NUMERIC IDENTITY UNIQUE");
PrimaryKey pk = table.getPrimaryKey();
if (pk != null)
buf.append(", ").append(getPrimaryKeyConstraintSQL(pk));
Unique[] unqs = table.getUniques();
String unqStr;
for (Unique unq : unqs) {
unqStr = getUniqueConstraintSQL(unq);
if (unqStr != null)
buf.append(", ").append(unqStr);
}
buf.append(")");
return new String[]{ buf.toString() };
}
@Override
protected String getDeclareColumnSQL(Column col, boolean alter) {
StringBuilder buf = new StringBuilder();
buf.append(getColumnDBName(col)).append(" ");
buf.append(getTypeName(col));
// can't add constraints to a column we're adding after table
// creation, cause some data might already be inserted
if (!alter) {
if (col.getDefaultString() != null && !col.isAutoAssigned())
buf.append(" DEFAULT ").append(col.getDefaultString());
if (col.isAutoAssigned())
buf.append(" IDENTITY");
}
if (col.isNotNull())
buf.append(" NOT NULL");
else if (!col.isPrimaryKey()) {
// sybase forces you to explicitly specify that
// you will allow NULL values
buf.append(" NULL");
}
return buf.toString();
}
@Override
public String[] getDropColumnSQL(Column column) {
// Sybase uses "ALTER TABLE DROP <COLUMN_NAME>" rather than the
// usual "ALTER TABLE DROP COLUMN <COLUMN_NAME>"
return new String[]{ "ALTER TABLE "
+ getFullName(column.getTable(), false) + " DROP " + getColumnDBName(column) };
}
@Override
public void refSchemaComponents(Table table) {
// note that we use getColumns() rather than getting the column by name
// because under some circumstances this method is called under the
// dynamic schema factory, where getting a column by name creates
// that column
Column[] cols = table.getColumns();
for (Column col : cols)
if (identityColumnName.equalsIgnoreCase(col.getIdentifier().getName()))
col.ref();
}
@Override
public void endConfiguration() {
super.endConfiguration();
// warn about jdbc compliant flag
String url = conf.getConnectionURL();
if (!StringUtil.isEmpty(url)
&& url.toLowerCase(Locale.ENGLISH).indexOf("jdbc:sybase:tds") != -1
&& url.toLowerCase(Locale.ENGLISH).indexOf("be_as_jdbc_compliant_as_possible=")
== -1) {
log.warn(_loc.get("sybase-compliance", url));
}
}
@Override
public Connection decorate(Connection conn)
throws SQLException {
conn = super.decorate(conn);
Connection savedConn = conn;
// if(ignoreConnectionSetup) {
// if(conn instanceof DelegatingConnection) {
// conn = ((DelegatingConnection)conn).getInnermostDelegate();
// }
// }
// In order for Sybase to raise the truncation exception when the
// string length is greater than the column length for Char, VarChar,
// Binary, VarBinary, the "set string_rtruncation on" must be executed.
// This setting is effective for the duration of current connection.
if (setStringRightTruncationOn) {
PreparedStatement stmnt = prepareStatement(conn, RIGHT_TRUNCATION_ON_SQL);
stmnt.execute();
stmnt.close();
}
// By default, Sybase will fail to insert or update if a numeric
// truncation occurs as a result of, for example, loss of decimal
// precision. This setting specifies that the operation should not
// fail if a numeric truncation occurs.
if (ignoreNumericTruncation) {
PreparedStatement stmnt = prepareStatement(conn, NUMERIC_TRUNCATION_OFF_SQL);
stmnt.execute();
stmnt.close();
}
return new SybaseConnection(savedConn);
}
/**
* Helper method obtains a string value from a given column in a ResultSet. Strings provided are column names,
* jdbcName will be tried first if an SQLException occurs we'll try the sybase name.
*/
protected String getStringFromResultSet(ResultSet rs, String jdbcName, String sybaseName) throws SQLException {
try {
return rs.getString(jdbcName);
}
catch(SQLException sqle) {
// if the generic JDBC identifier isn't found an SQLException will be thrown
// try the Sybase specific id
return rs.getString(sybaseName);
}
}
/**
* Helper method obtains a boolean value from a given column in a ResultSet. Strings provided are column names,
* jdbcName will be tried first if an SQLException occurs we'll try the sybase name.
*/
protected boolean getBooleanFromResultSet(ResultSet rs, String jdbcName, String sybaseName) throws SQLException {
try {
return rs.getBoolean(jdbcName);
}
catch(SQLException sqle) {
// if the generic JDBC identifier isn't found an SQLException will be thrown
// try the Sybase specific id
return rs.getBoolean(sybaseName);
}
}
/**
* Create a new primary key from the information in the schema metadata.
*/
@Override
protected PrimaryKey newPrimaryKey(ResultSet pkMeta)
throws SQLException {
PrimaryKey pk = new PrimaryKey();
pk.setSchemaIdentifier(fromDBName(getStringFromResultSet(pkMeta, "TABLE_SCHEM", "table_owner"),
DBIdentifierType.SCHEMA));
pk.setTableIdentifier(fromDBName(getStringFromResultSet(pkMeta, "TABLE_NAME", "table_name"),
DBIdentifierType.TABLE));
pk.setColumnIdentifier(fromDBName(getStringFromResultSet(pkMeta, "COLUMN_NAME", "column_name"),
DBIdentifierType.COLUMN));
pk.setIdentifier(fromDBName(getStringFromResultSet(pkMeta, "PK_NAME", "index_name"),
DBIdentifierType.CONSTRAINT));
return pk;
}
/**
* Create a new index from the information in the index metadata.
*/
@Override
protected Index newIndex(ResultSet idxMeta)
throws SQLException {
Index idx = new Index();
idx.setSchemaIdentifier(fromDBName(getStringFromResultSet(idxMeta, "TABLE_SCHEM", "table_owner"),
DBIdentifierType.SCHEMA));
idx.setTableIdentifier(fromDBName(getStringFromResultSet(idxMeta, "TABLE_NAME", "table_name"),
DBIdentifierType.TABLE));
idx.setColumnIdentifier(fromDBName(getStringFromResultSet(idxMeta, "COLUMN_NAME", "column_name"),
DBIdentifierType.COLUMN));
idx.setIdentifier(fromDBName(getStringFromResultSet(idxMeta, "INDEX_NAME", "index_name"),
DBIdentifierType.INDEX));
idx.setUnique(!getBooleanFromResultSet(idxMeta, "NON_UNIQUE", "non_unique"));
return idx;
}
@Override
public boolean isFatalException(int subtype, SQLException ex) {
if (subtype == StoreException.LOCK) {
SQLException next = ex.getNextException();
if("JZ0TO".equals(next.getSQLState())) {
return false; // query timeout
}
}
return super.isFatalException(subtype, ex);
}
/**
* Connection wrapper to cache the {@link Connection#getCatalog} result,
* which takes a very long time with the Sybase Connection (and
* which we frequently invoke).
*/
protected static class SybaseConnection
extends DelegatingConnection {
private String _catalog = null;
public SybaseConnection(Connection conn) {
super(conn);
}
@Override
public String getCatalog()
throws SQLException {
if (_catalog == null)
_catalog = super.getCatalog();
return _catalog;
}
@Override
public void setAutoCommit(boolean autocommit)
throws SQLException {
// the sybase jdbc driver demands that the Connection always
// be rolled back before autocommit status changes. Failure to
// do so will yield "SET CHAINED command not allowed within
// multi-statement transaction." exceptions
try {
super.setAutoCommit(autocommit);
} catch (SQLException e) {
// failed for some reason: try rolling back and then
// setting autocommit again.
if (autocommit)
super.commit();
else
super.rollback();
super.setAutoCommit(autocommit);
}
}
}
@Override
public String getIsNullSQL(String colAlias, int colType) {
switch(colType) {
case Types.BLOB:
case Types.CLOB:
return String.format("datalength(%s) = 0", colAlias);
}
return super.getIsNullSQL(colAlias, colType);
}
@Override
public String getIsNotNullSQL(String colAlias, int colType) {
switch(colType) {
case Types.BLOB:
case Types.CLOB:
return String.format("datalength(%s) != 0", colAlias);
}
return super.getIsNotNullSQL(colAlias, colType);
}
@Override
public String getIdentityColumnName() {
return identityColumnName;
}
@Override
public void indexOf(SQLBuffer buf, FilterValue str, FilterValue find,
FilterValue start) {
buf.append("(CHARINDEX(");
find.appendTo(buf);
buf.append(", ");
if (start != null)
substring(buf, str, start, null);
else
str.appendTo(buf);
buf.append(")");
if (start != null) {
buf.append(" - 1 + ");
start.appendTo(buf);
}
buf.append(")");
}
}