blob: 43e98363552e0d07074696ddd2d27712971a0978 [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.io.InputStream;
import java.io.Reader;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Types;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Locale;
import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.kernel.exps.FilterValue;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.kernel.Filters;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.StoreException;
/**
* Dictionary for Microsoft SQL Server.
*/
public class SQLServerDictionary extends AbstractSQLServerDictionary {
public static final String VENDOR_MICROSOFT = "microsoft";
public static final String VENDOR_NETDIRECT = "netdirect";
public static final String VENDOR_JTDS = "jtds";
private static final Localizer _loc =
Localizer.forPackage(SQLServerDictionary.class);
private String schemaCase = SCHEMA_CASE_PRESERVE;
/**
* Flag whether to treat UNIQUEIDENTIFIER as VARBINARY or VARCHAR
*/
public boolean uniqueIdentifierAsVarbinary = true;
/**
* SQLServer doesn't like a java.sql.Time as default.
* So either we send it as String or people configure sendTimeAsDatetime=false on the Connection.
* This is depending how the Database actually is setup.
* To mitigate misconfiguration we can work around by sending the time as String to the JDBC driver.
*/
public Boolean sendTimeAsString = null;
public SQLServerDictionary() {
platform = "Microsoft SQL Server";
// SQLServer locks on a table-by-table basis
forUpdateClause = null;
tableForUpdateClause = "WITH (UPDLOCK)";
supportsNullTableForGetColumns = false;
requiresAliasForSubselect = true;
stringLengthFunction = "LEN({0})";
timeWithZoneTypeName = "TIME";
timestampWithZoneTypeName = "DATETIMEOFFSET";
indexPhysicalForeignKeys = true; // MS-SQLServer does not automatically create an index for a foreign key so we will
// reservedWordSet subset that CANNOT be used as valid column names
// (i.e., without surrounding them with double-quotes)
// generated at 2021-05-02T16:15:30.630 via org.apache.openjpa.reservedwords.ReservedWordsIT
invalidColumnWordSet.addAll(Arrays.asList(new String[] {
"ADD", "ALL", "ALTER", "AND", "ANY", "AS", "ASC", "AUTHORIZATION", "BACKUP", "BEGIN", "BETWEEN", "BREAK", "BROWSE",
"BULK", "BY", "CASCADE", "CASE", "CHECK", "CHECKPOINT", "CLOSE", "CLUSTERED", "COALESCE", "COLLATE", "COLUMN",
"COMMIT", "COMPUTE", "CONSTRAINT", "CONTAINS", "CONTAINSTABLE", "CONTINUE", "CONVERT", "CREATE", "CROSS", "CURRENT",
"CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "CURSOR", "DATABASE", "DBCC", "DEALLOCATE",
"DECLARE", "DEFAULT", "DELETE", "DENY", "DESC", "DISTINCT", "DISTRIBUTED", "DOUBLE", "DROP", "ELSE", "END", "END-EXEC",
"ERRLVL", "ESCAPE", "EXCEPT", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXTERNAL", "FETCH", "FILE", "FILLFACTOR", "FOR",
"FOREIGN", "FREETEXT", "FREETEXTTABLE", "FROM", "FULL", "FUNCTION", "GOTO", "GRANT", "GROUP", "HAVING", "HOLDLOCK",
"IDENTITY", "IDENTITY_INSERT", "IDENTITYCOL", "IF", "IN", "INDEX", "INNER", "INSERT", "INTERSECT", "INTO", "IS",
"JOIN", "KEY", "KILL", "LEFT", "LIKE", "LINENO", "MERGE", "NATIONAL", "NOCHECK", "NONCLUSTERED", "NOT", "NULL",
"NULLIF", "OF", "OFF", "OFFSETS", "ON", "OPEN", "OPENDATASOURCE", "OPENQUERY", "OPENROWSET", "OPENXML", "OPTION",
"OR", "ORDER", "OUTER", "OVER", "PERCENT", "PLAN", "PRIMARY", "PRINT", "PROC", "PROCEDURE", "PUBLIC", "RAISERROR",
"READ", "READTEXT", "RECONFIGURE", "REFERENCES", "REPLICATION", "RESTORE", "RESTRICT", "RETURN", "REVOKE", "RIGHT",
"ROLLBACK", "ROWCOUNT", "ROWGUIDCOL", "RULE", "SAVE", "SCHEMA", "SELECT", "SESSION_USER", "SET", "SETUSER", "SHUTDOWN",
"SOME", "STATISTICS", "SYSTEM_USER", "TABLE", "TABLESAMPLE", "TEXTSIZE", "THEN", "TO", "TOP", "TRAN", "TRANSACTION",
"TRIGGER", "TRUNCATE", "TSEQUAL", "UNION", "UNIQUE", "UPDATE", "UPDATETEXT", "USE", "USER", "VALUES", "VARYING",
"VIEW", "WAITFOR", "WHEN", "WHERE", "WHILE", "WITH", "WRITETEXT",
}));
}
@Override
public void connectedConfiguration(Connection conn) throws SQLException {
super.connectedConfiguration(conn);
boolean requiresWarnings = true;
DatabaseMetaData meta = conn.getMetaData();
String driverName = meta.getDriverName();
String url = meta.getURL();
if (driverVendor == null) {
// serverMajorVersion of 8==2000, 9==2005, 10==2008, 11==2012
if (meta.getDatabaseMajorVersion() >= 9) {
setSupportsXMLColumn(true);
if (sendTimeAsString == null) {
sendTimeAsString = Boolean.FALSE;
}
}
if (meta.getDatabaseMajorVersion() >= 10) {
// MSSQL 2008 supports new date, time and datetime2 types
// Use DATETIME2 which has 100ns vs. 3.333msec precision
dateTypeName = "DATE";
timeTypeName = "TIME";
timestampTypeName = "DATETIME2";
datePrecision = MICRO / 10;
}
if (meta.getDatabaseMajorVersion() >= 11) {
//SQLServer 2012 supports range select
rangePosition = RANGE_POST_SELECT;
supportsSelectStartIndex = true;
supportsSelectEndIndex = true;
}
if (driverName != null) {
if (driverName.startsWith("Microsoft SQL Server")) {
// v1.1, 1.2, 2.0 or 3.0 driver
driverVendor = VENDOR_MICROSOFT;
if (meta.getDriverMajorVersion() >= 2) {
// see http://blogs.msdn.com/jdbcteam/archive/2007/05/\
// 02/what-is-adaptive-response-buffering-and-why-\
// should-i-use-it.aspx
// 2.0 driver connectURL automatically includes
// responseBuffering=adaptive
// and disableStatementPooling=true
requiresWarnings = false;
}
} else {
if ("NetDirect JSQLConnect".equals(driverName))
driverVendor = VENDOR_NETDIRECT;
else if (driverName.startsWith("jTDS"))
driverVendor = VENDOR_JTDS;
else if ("SQLServer".equals(driverName)) {
if (url != null &&
url.startsWith("jdbc:microsoft:sqlserver:"))
driverVendor = VENDOR_MICROSOFT;
else if (url != null &&
url.startsWith("jdbc:datadirect:sqlserver:"))
driverVendor = VENDOR_DATADIRECT;
else
driverVendor = VENDOR_OTHER;
}
// old way of determining xml support
if (driverName.indexOf(platform) != -1) {
String versionString =
driverName.substring(platform.length() + 1);
if (versionString.indexOf(" ") != -1)
versionString = versionString.substring(0,
versionString.indexOf(" "));
int version = Integer.parseInt(versionString);
if (version >= 2005)
setSupportsXMLColumn(true);
}
}
} else {
driverVendor = VENDOR_OTHER;
}
}
// warn about not using cursors for pre-2.0 MS driver
// as connectURL includes selectMethod=direct
if (((VENDOR_MICROSOFT.equalsIgnoreCase(driverVendor) &&
requiresWarnings) ||
VENDOR_DATADIRECT.equalsIgnoreCase(driverVendor)) &&
(url.toLowerCase(Locale.ENGLISH).indexOf("selectmethod=cursor") == -1))
log.warn(_loc.get("sqlserver-cursor", url));
// warn about prepared statement caching if using pre-2.0 MS drivers
// as connectURL includes responseBuffering=full
String props = conf.getConnectionFactoryProperties();
if ((props != null) &&
VENDOR_MICROSOFT.equalsIgnoreCase(driverVendor) &&
requiresWarnings &&
(props.toLowerCase(Locale.ENGLISH).indexOf("maxcachedstatements=0") == -1))
log.warn(_loc.get("sqlserver-cachedstmnts"));
}
@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);
// for opta driver, which reports nvarchar as unknown type
for (int i = 0; cols != null && i < cols.length; i++) {
String typeName = cols[i].getTypeIdentifier().getName();
if (typeName == null)
continue;
typeName = typeName.toUpperCase(Locale.ENGLISH);
if ("NVARCHAR".equals(typeName))
cols[i].setType(Types.VARCHAR);
else if ("UNIQUEIDENTIFIER".equals(typeName)) {
if (uniqueIdentifierAsVarbinary)
cols[i].setType(Types.VARBINARY);
else
cols[i].setType(Types.VARCHAR);
} else if ("NCHAR".equals(typeName))
cols[i].setType(Types.CHAR);
else if ("NTEXT".equals(typeName))
cols[i].setType(Types.CLOB);
}
return cols;
}
@Override
protected void appendLength(SQLBuffer buf, int type) {
if (type == Types.VARCHAR)
buf.append("(").append(Integer.toString(characterColumnSize))
.append(")");
}
/**
* If this dictionary supports XML type, use this method to append xml
* predicate.
*
* @param buf the SQL buffer to write the comparison
* @param op the comparison operation to perform
* @param lhs the left hand side of the comparison
* @param rhs the right hand side of the comparison
* @param lhsxml indicates whether the left operand maps to xml
* @param rhsxml indicates whether the right operand maps to xml
*/
@Override
public void appendXmlComparison(SQLBuffer buf, String op, FilterValue lhs,
FilterValue rhs, boolean lhsxml, boolean rhsxml) {
super.appendXmlComparison(buf, op, lhs, rhs, lhsxml, rhsxml);
if (lhsxml && rhsxml)
appendXmlComparison2(buf, op, lhs, rhs);
else if (lhsxml)
appendXmlComparison1(buf, op, lhs, rhs);
else
appendXmlComparison1(buf, op, rhs, lhs);
}
/**
* Append an xml comparison predicate
*
* @param buf the SQL buffer to write the comparison
* @param op the comparison operation to perform
* @param lhs the left hand side of the comparison (maps to xml column)
* @param rhs the right hand side of the comparison
*/
private void appendXmlComparison1(SQLBuffer buf, String op,
FilterValue lhs, FilterValue rhs) {
boolean castrhs = rhs.isConstant();
if (castrhs)
appendXmlValue(buf, lhs);
else
appendXmlExist(buf, lhs);
buf.append(" ").append(op).append(" ");
if (castrhs)
rhs.appendTo(buf);
else {
buf.append("sql:column(\"");
rhs.appendTo(buf);
buf.append("\")").append("]') = 1");
}
}
private void appendXmlExist(SQLBuffer buf, FilterValue lhs) {
buf.append(lhs.getColumnAlias(lhs.getFieldMapping().getColumns()[0]))
.append(".exist('").append("/*[");
lhs.appendTo(buf);
}
/**
* Append an xml comparison predicate (both operands map to xml column)
*
* @param buf the SQL buffer to write the comparison
* @param op the comparison operation to perform
* @param lhs the left hand side of the comparison (maps to xml column)
* @param rhs the right hand side of the comparison (maps to xml column)
*/
private void appendXmlComparison2(SQLBuffer buf, String op,
FilterValue lhs, FilterValue rhs) {
appendXmlValue(buf, lhs);
buf.append(" ").append(op).append(" ");
appendXmlValue(buf, rhs);
}
private void appendXmlValue(SQLBuffer buf, FilterValue val) {
Class<?> rc = Filters.wrap(val.getType());
int type = getJDBCType(JavaTypes.getTypeCode(rc), false);
boolean isXmlAttribute = (val.getXmlMapping() == null)
? false : val.getXmlMapping().isXmlAttribute();
buf.append(val.getColumnAlias(val.getFieldMapping().getColumns()[0]))
.append(".value(").append("'(/*/");
val.appendTo(buf);
if (!isXmlAttribute)
buf.append("/text()");
buf.append(")[1]','").append(getTypeName(type));
appendLength(buf, type);
buf.append("')");
}
/**
* Return DB specific schemaCase
*/
@Override
public String getSchemaCase() {
return schemaCase;
}
@Override
public void setSupportsXMLColumn(boolean b) {
super.setSupportsXMLColumn(b);
// MS SQL Server requires XML data in UTF-16 or UCS-2 instead of JAXB default of UTF-8
super.setXMLTypeEncoding("UTF-16");
}
@Override
public boolean isFatalException(int subtype, SQLException ex) {
String errorState = ex.getSQLState();
if ((subtype == StoreException.LOCK || subtype == StoreException.QUERY)
&&("1222".equals(errorState) || "HY008".equals(errorState) || "HYT00".equals(errorState)))
return false;
return super.isFatalException(subtype, ex);
}
/**
* Obtain an {@link InputStream} by using {@link ResultSet#getBlob(int)} and
* {@link Blob#getBinaryStream()}.
* Unfortunately this will load entire BLOB into memory.
* The alternative {@link ResultSet#getBinaryStream(int)} provides true streaming but
* the stream can be consumed only as long as {@link ResultSet} is open.
*/
@Override
public InputStream getLOBStream(JDBCStore store, ResultSet rs, int column) throws SQLException {
Blob blob = rs.getBlob(column);
if (blob == null) {
return null;
}
return blob.getBinaryStream();
}
/**
* Obtain a {@link Reader} by using {@link ResultSet#getClob(int)} and
* {@link Clob#getCharacterStream()}.
* Unfortunately this will load entire CLOB into memory.
* The alternative {@link ResultSet#getCharacterStream(int)} provides true streaming but
* the stream can be consumed only as long as {@link ResultSet} is open.
*/
@Override
public Reader getCharacterStream(ResultSet rs, int column) throws SQLException {
Clob clob = rs.getClob(column);
if (clob == null) {
return null;
}
return clob.getCharacterStream();
}
@Override
public LocalDate getLocalDate(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, LocalDate.class);
}
@Override
public void setLocalTime(PreparedStatement stmnt, int idx, LocalTime val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
@Override
public LocalTime getLocalTime(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, LocalTime.class);
}
@Override
public void setLocalDateTime(PreparedStatement stmnt, int idx, LocalDateTime val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
@Override
public LocalDateTime getLocalDateTime(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, LocalDateTime.class);
}
@Override
public void setOffsetDateTime(PreparedStatement stmnt, int idx, OffsetDateTime val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
/**
* h2 does intentionally not support {@code getTimestamp()} for 'TIME WITH TIME ZONE' columns.
* See h2 ticket #413.
*/
@Override
public OffsetDateTime getOffsetDateTime(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, OffsetDateTime.class);
}
@Override
public void setTime(PreparedStatement stmnt, int idx, Time val, Calendar cal, Column col) throws SQLException {
if (sendTimeAsString) {
stmnt.setString(idx, val.toString());
}
else {
// use Time
super.setTime(stmnt, idx, val, cal, col);
}
}
@Override
public void indexOf(SQLBuffer buf, FilterValue str, FilterValue find,
FilterValue start) {
buf.append("CHARINDEX(");
find.appendTo(buf);
buf.append(", ");
str.appendTo(buf);
if (start != null) {
buf.append(", ");
start.appendTo(buf);
}
buf.append(")");
}
@Override
protected void appendSelectRange(SQLBuffer buf, long start, long end, boolean subselect) {
//SQL Server 2012 supports range select
if (this.getMajorVersion() >= 11) {
//we need an order by clause....
if (!buf.getSQL().contains(" ORDER BY ")) {
buf.append(" ORDER BY 1 ");
}
buf.append(" OFFSET ").append(Long.toString(start)).append(" ROWS ").
append(" FETCH NEXT ").append(Long.toString(end - start)).append(" ROWS ONLY ");
} else {
super.appendSelectRange(buf, start, end, subselect);
}
}
}