/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Florent Guillaume
* Benoit Delbosc
*/
package org.nuxeo.ecm.core.storage.sql.jdbc.dialect;
import java.io.Serializable;
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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.StringUtils;
import org.nuxeo.ecm.core.NXCore;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer;
import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer.FulltextQuery;
import org.nuxeo.ecm.core.storage.sql.ColumnType;
import org.nuxeo.ecm.core.storage.sql.Model;
import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor;
import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
/**
* Microsoft SQL Server-specific dialect.
*
* @author Florent Guillaume
*/
public class DialectSQLServer extends Dialect {
private static final Log log = LogFactory.getLog(DialectSQLServer.class);
private static final String DEFAULT_FULLTEXT_ANALYZER = "english";
private static final String DEFAULT_FULLTEXT_CATALOG = "nuxeo";
/**
* Column containing an IDENTITY used to create a clustered index.
*/
public static final String CLUSTER_INDEX_COL = "_oid";
protected final String fulltextAnalyzer;
protected final String fulltextCatalog;
private static final String DEFAULT_USERS_SEPARATOR = "|";
protected final String usersSeparator;
protected final DialectIdType idType;
protected String idSequenceName;
protected boolean pathOptimizationsEnabled;
/** 9 = SQL Server 2005, 10 = SQL Server 2008, 11 = SQL Server 2012 / Azure */
protected int majorVersion;
// http://msdn.microsoft.com/en-us/library/ms174396.aspx
/** 5 = Azure */
protected int engineEdition;
protected boolean azure;
public DialectSQLServer(DatabaseMetaData metadata, RepositoryDescriptor repositoryDescriptor) {
super(metadata, repositoryDescriptor);
try {
checkDatabaseConfiguration(metadata.getConnection());
majorVersion = metadata.getDatabaseMajorVersion();
engineEdition = getEngineEdition(metadata.getConnection());
} catch (SQLException e) {
throw new NuxeoException(e);
}
if (engineEdition == 5) { // 5 = SQL Azure
azure = true;
fulltextDisabled = true;
fulltextSearchDisabled = true;
if (repositoryDescriptor != null) {
repositoryDescriptor.setFulltextDisabled(true);
}
}
fulltextAnalyzer = repositoryDescriptor == null ? null
: repositoryDescriptor.getFulltextAnalyzer() == null ? DEFAULT_FULLTEXT_ANALYZER
: repositoryDescriptor.getFulltextAnalyzer();
fulltextCatalog = repositoryDescriptor == null ? null
: repositoryDescriptor.getFulltextCatalog() == null ? DEFAULT_FULLTEXT_CATALOG
: repositoryDescriptor.getFulltextCatalog();
usersSeparator = repositoryDescriptor == null ? null
: repositoryDescriptor.usersSeparatorKey == null ? DEFAULT_USERS_SEPARATOR
: repositoryDescriptor.usersSeparatorKey;
pathOptimizationsEnabled = repositoryDescriptor != null && repositoryDescriptor.getPathOptimizationsEnabled();
String idt = repositoryDescriptor == null ? null : repositoryDescriptor.idType;
if (idt == null || "".equals(idt) || "varchar".equalsIgnoreCase(idt)) {
idType = DialectIdType.VARCHAR;
} else if (idt.toLowerCase().startsWith("sequence")) {
idType = DialectIdType.SEQUENCE;
if (idt.toLowerCase().startsWith("sequence:")) {
String[] split = idt.split(":");
idSequenceName = split[1];
} else {
idSequenceName = "hierarchy_seq";
}
} else {
throw new NuxeoException("Unknown id type: '" + idt + "'");
}
}
@Override
public boolean supportsPaging() {
// available since SQL Server 2012
return (majorVersion >= 11);
}
@Override
public String addPagingClause(String sql, long limit, long offset) {
if (!sql.contains("ORDER")) {
// Order is required to use the offset operation
sql += " ORDER BY 1";
}
return sql + String.format(" OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", offset, limit);
}
protected int getEngineEdition(Connection connection) throws SQLException {
try (Statement st = connection.createStatement()) {
ResultSet rs = st.executeQuery("SELECT CONVERT(NVARCHAR(100), SERVERPROPERTY('EngineEdition'))");
rs.next();
return rs.getInt(1);
}
}
protected void checkDatabaseConfiguration(Connection connection) throws SQLException {
try (Statement stmt = connection.createStatement()) {
String sql = "SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = db_name()";
if (log.isTraceEnabled()) {
log.trace("SQL: " + sql);
}
ResultSet rs = stmt.executeQuery(sql);
if (!rs.next()) {
throw new SQLException("Cannot detect whether READ_COMMITTED_SNAPSHOT is on");
}
int on = rs.getInt(1);
if (on != 1) {
throw new SQLException("Incorrect database configuration, you must enable READ_COMMITTED_SNAPSHOT");
}
rs.close();
}
}
@Override
public char openQuote() {
return '[';
}
@Override
public char closeQuote() {
return ']';
}
@Override
public String getNoColumnsInsertString(Column idColumn) {
return "DEFAULT VALUES";
}
@Override
public String getNullColumnString() {
return " NULL";
}
@Override
public boolean qualifyIndexName() {
return false;
}
@Override
public String getAddColumnString() {
return "ADD";
}
@Override
public JDBCInfo getJDBCTypeAndString(ColumnType type) {
switch (type.spec) {
case STRING:
if (type.isUnconstrained()) {
return jdbcInfo("NVARCHAR(4000)", Types.VARCHAR);
} else if (type.isClob() || type.length > 4000) {
return jdbcInfo("NVARCHAR(MAX)", Types.CLOB);
} else {
return jdbcInfo("NVARCHAR(%d)", type.length, Types.VARCHAR);
}
case BOOLEAN:
return jdbcInfo("BIT", Types.BIT);
case LONG:
return jdbcInfo("BIGINT", Types.BIGINT);
case DOUBLE:
return jdbcInfo("DOUBLE PRECISION", Types.DOUBLE);
case TIMESTAMP:
return jdbcInfo("DATETIME2(3)", Types.TIMESTAMP);
case BLOBID:
return jdbcInfo("NVARCHAR(250)", Types.VARCHAR);
// -----
case NODEID:
case NODEIDFK:
case NODEIDFKNP:
case NODEIDFKMUL:
case NODEIDFKNULL:
case NODEIDPK:
case NODEVAL:
switch (idType) {
case VARCHAR:
return jdbcInfo("NVARCHAR(36)", Types.VARCHAR);
case SEQUENCE:
return jdbcInfo("BIGINT", Types.BIGINT);
}
case SYSNAME:
case SYSNAMEARRAY:
return jdbcInfo("NVARCHAR(256)", Types.VARCHAR);
case TINYINT:
return jdbcInfo("TINYINT", Types.TINYINT);
case INTEGER:
return jdbcInfo("INT", Types.INTEGER);
case AUTOINC:
return jdbcInfo("INT IDENTITY", Types.INTEGER);
case FTINDEXED:
throw new AssertionError(type);
case FTSTORED:
return jdbcInfo("NVARCHAR(MAX)", Types.CLOB);
case CLUSTERNODE:
return jdbcInfo("SMALLINT", Types.SMALLINT);
case CLUSTERFRAGS:
return jdbcInfo("NVARCHAR(4000)", Types.VARCHAR);
}
throw new AssertionError(type);
}
@Override
public boolean isAllowedConversion(int expected, int actual, String actualName, int actualSize) {
// The jTDS JDBC driver uses VARCHAR / CLOB
// The Microsoft JDBC driver uses NVARCHAR / LONGNVARCHAR
if (expected == Types.VARCHAR && actual == Types.CLOB) {
return true;
}
if (expected == Types.VARCHAR && actual == Types.NVARCHAR) {
return true;
}
if (expected == Types.VARCHAR && actual == Types.LONGNVARCHAR) {
return true;
}
if (expected == Types.CLOB && actual == Types.VARCHAR) {
return true;
}
if (expected == Types.CLOB && actual == Types.NVARCHAR) {
return true;
}
if (expected == Types.CLOB && actual == Types.LONGNVARCHAR) {
return true;
}
if (expected == Types.BIGINT && actual == Types.INTEGER) {
return true;
}
if (expected == Types.INTEGER && actual == Types.BIGINT) {
return true;
}
return false;
}
@Override
public void setId(PreparedStatement ps, int index, Serializable value) throws SQLException {
switch (idType) {
case VARCHAR:
ps.setObject(index, value, Types.VARCHAR);
break;
case SEQUENCE:
setIdLong(ps, index, value);
break;
}
}
@Override
public void setToPreparedStatement(PreparedStatement ps, int index, Serializable value, Column column)
throws SQLException {
switch (column.getJdbcType()) {
case Types.VARCHAR:
case Types.CLOB:
setToPreparedStatementString(ps, index, value, column);
return;
case Types.BIT:
ps.setBoolean(index, ((Boolean) value).booleanValue());
return;
case Types.TINYINT:
case Types.SMALLINT:
case Types.INTEGER:
case Types.BIGINT:
ps.setLong(index, ((Number) value).longValue());
return;
case Types.DOUBLE:
ps.setDouble(index, ((Double) value).doubleValue());
return;
case Types.TIMESTAMP:
setToPreparedStatementTimestamp(ps, index, value, column);
return;
default:
throw new SQLException("Unhandled JDBC type: " + column.getJdbcType());
}
}
@Override
@SuppressWarnings("boxing")
public Serializable getFromResultSet(ResultSet rs, int index, Column column) throws SQLException {
switch (column.getJdbcType()) {
case Types.VARCHAR:
case Types.CLOB:
return getFromResultSetString(rs, index, column);
case Types.BIT:
return rs.getBoolean(index);
case Types.TINYINT:
case Types.INTEGER:
case Types.BIGINT:
return rs.getLong(index);
case Types.DOUBLE:
return rs.getDouble(index);
case Types.TIMESTAMP:
return getFromResultSetTimestamp(rs, index, column);
}
throw new SQLException("Unhandled JDBC type: " + column.getJdbcType());
}
@Override
protected int getMaxNameSize() {
return 128;
}
@Override
public boolean getMaterializeFulltextSyntheticColumn() {
return false;
}
@Override
public int getFulltextIndexedColumns() {
return 2;
}
@Override
public boolean supportsMultipleFulltextIndexes() {
// With SQL Server, only one full-text index is allowed per table...
return false;
}
@Override
public String getCreateFulltextIndexSql(String indexName, String quotedIndexName, Table table, List<Column> columns,
Model model) {
StringBuilder buf = new StringBuilder();
buf.append(String.format("CREATE FULLTEXT INDEX ON %s (", table.getQuotedName()));
Iterator<Column> it = columns.iterator();
while (it.hasNext()) {
buf.append(String.format("%s LANGUAGE %s", it.next().getQuotedName(), getQuotedFulltextAnalyzer()));
if (it.hasNext()) {
buf.append(", ");
}
}
String fulltextUniqueIndex = "[fulltext_pk]";
buf.append(String.format(") KEY INDEX %s ON [%s]", fulltextUniqueIndex, fulltextCatalog));
return buf.toString();
}
@Override
public String getDialectFulltextQuery(String query) {
query = query.replace("%", "*");
FulltextQuery ft = FulltextQueryAnalyzer.analyzeFulltextQuery(query);
if (ft == null) {
return "DONTMATCHANYTHINGFOREMPTYQUERY";
}
return FulltextQueryAnalyzer.translateFulltext(ft, "OR", "AND", "AND NOT", "\"", "\"",
Collections.<Character> emptySet(), "\"", "\"", false);
}
// SELECT ..., FTTBL.RANK / 1000.0
// FROM ... LEFT JOIN [fulltext] ON [fulltext].[id] = [hierarchy].[id]
// ........ LEFT JOIN CONTAINSTABLE([fulltext], *, ?, LANGUAGE 'english')
// .................. AS FTTBL
// .................. ON [fulltext].[id] = FTTBL.[KEY]
// WHERE ... AND FTTBL.[KEY] IS NOT NULL
// ORDER BY FTTBL.RANK DESC
@Override
public FulltextMatchInfo getFulltextScoredMatchInfo(String fulltextQuery, String indexName, int nthMatch,
Column mainColumn, Model model, Database database) {
// TODO multiple indexes
Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME);
Column ftMain = ft.getColumn(Model.MAIN_KEY);
String nthSuffix = nthMatch == 1 ? "" : String.valueOf(nthMatch);
String tableAlias = "_nxfttbl" + nthSuffix;
FulltextMatchInfo info = new FulltextMatchInfo();
// there are two left joins here
info.joins = new ArrayList<>();
if (nthMatch == 1) {
// Need only one JOIN involving the fulltext table
info.joins.add(new Join(Join.LEFT, ft.getQuotedName(), null, null, ftMain.getFullQuotedName(),
mainColumn.getFullQuotedName()));
}
info.joins.add(
new Join(Join.LEFT, //
String.format("CONTAINSTABLE(%s, *, ?, LANGUAGE %s)", ft.getQuotedName(),
getQuotedFulltextAnalyzer()),
tableAlias, // alias
fulltextQuery, // param
ftMain.getFullQuotedName(), // on1
String.format("%s.[KEY]", tableAlias) // on2
));
info.whereExpr = String.format("%s.[KEY] IS NOT NULL", tableAlias);
info.scoreExpr = String.format("(%s.RANK / 1000.0)", tableAlias);
info.scoreAlias = "_nxscore" + nthSuffix;
info.scoreCol = new Column(mainColumn.getTable(), null, ColumnType.DOUBLE, null);
return info;
}
protected String getQuotedFulltextAnalyzer() {
if (!Character.isDigit(fulltextAnalyzer.charAt(0))) {
return String.format("'%s'", fulltextAnalyzer);
}
return fulltextAnalyzer;
}
@Override
public String getLikeEscaping() {
return " ESCAPE '\\'";
}
@Override
public boolean supportsCircularCascadeDeleteConstraints() {
// See http://support.microsoft.com/kb/321843
// Msg 1785 Introducing FOREIGN KEY constraint
// 'hierarchy_parentid_hierarchy_fk' on table 'hierarchy' may cause
// cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON
// UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
// Instead we use a trigger "INSTEAD OF DELETE" to do the recursion.
return false;
}
@Override
public boolean supportsUpdateFrom() {
return true;
}
@Override
public boolean doesUpdateFromRepeatSelf() {
return true;
}
@Override
public boolean needsAliasForDerivedTable() {
return true;
}
@Override
public boolean needsOriginalColumnInGroupBy() {
// http://msdn.microsoft.com/en-us/library/ms177673.aspx
// A column alias that is defined in the SELECT list cannot be used to
// specify a grouping column.
return true;
}
@Override
public String getSecurityCheckSql(String idColumnName) {
return String.format("dbo.NX_ACCESS_ALLOWED(%s, ?, ?) = 1", idColumnName);
}
@Override
public boolean supportsFastDescendants() {
return pathOptimizationsEnabled;
}
@Override
public String getInTreeSql(String idColumnName, String id) {
String idParam;
switch (idType) {
case VARCHAR:
idParam = "?";
break;
case SEQUENCE:
// check that it's really an integer
if (id != null && !org.apache.commons.lang.StringUtils.isNumeric(id)) {
return null;
}
idParam = "CONVERT(BIGINT, ?)";
break;
default:
throw new AssertionError("Unknown id type: " + idType);
}
if (pathOptimizationsEnabled) {
return String.format("EXISTS(SELECT 1 FROM ancestors WHERE hierarchy_id = %s AND ancestor = %s)",
idColumnName, idParam);
}
return String.format("%s IN (SELECT * FROM dbo.nx_children(%s))", idColumnName, idParam);
}
@Override
public String getSQLStatementsFilename() {
return "nuxeovcs/sqlserver.sql.txt";
}
@Override
public String getTestSQLStatementsFilename() {
return "nuxeovcs/sqlserver.test.sql.txt";
}
@Override
public Map<String, Serializable> getSQLStatementsProperties(Model model, Database database) {
Map<String, Serializable> properties = new HashMap<>();
switch (idType) {
case VARCHAR:
properties.put("idType", "NVARCHAR(36)");
properties.put("idTypeParam", "NVARCHAR");
properties.put("idNotPresent", "'-'");
properties.put("sequenceEnabled", Boolean.FALSE);
break;
case SEQUENCE:
properties.put("idType", "BIGINT");
properties.put("idTypeParam", "BIGINT");
properties.put("idNotPresent", "-1");
properties.put("sequenceEnabled", Boolean.TRUE);
properties.put("idSequenceName", idSequenceName);
}
properties.put("lockEscalationDisabled", Boolean.valueOf(supportsLockEscalationDisable()));
properties.put("md5HashString", getMd5HashString());
properties.put("reseedAclrModified", azure ? "" : "DBCC CHECKIDENT('aclr_modified', RESEED, 0);");
properties.put("fulltextEnabled", Boolean.valueOf(!fulltextDisabled));
properties.put("fulltextSearchEnabled", Boolean.valueOf(!fulltextSearchDisabled));
properties.put("fulltextCatalog", fulltextCatalog);
properties.put("aclOptimizationsEnabled", Boolean.valueOf(aclOptimizationsEnabled));
properties.put("pathOptimizationsEnabled", Boolean.valueOf(pathOptimizationsEnabled));
properties.put("clusteringEnabled", Boolean.valueOf(clusteringEnabled));
properties.put("proxiesEnabled", Boolean.valueOf(proxiesEnabled));
properties.put("softDeleteEnabled", Boolean.valueOf(softDeleteEnabled));
String[] permissions = NXCore.getSecurityService().getPermissionsToCheck(SecurityConstants.BROWSE);
List<String> permsList = new LinkedList<>();
for (String perm : permissions) {
permsList.add(String.format(" SELECT '%s' ", perm));
}
properties.put("readPermissions", String.join(" UNION ALL ", permsList));
properties.put("usersSeparator", getUsersSeparator());
return properties;
}
protected String getMd5HashString() {
if (majorVersion <= 9) {
// this is an internal function and doesn't work on Azure
return "SUBSTRING(master.dbo.fn_varbintohexstr(HashBytes('MD5', @string)), 3, 32)";
} else {
// this doesn't work on SQL Server 2005
return "SUBSTRING(CONVERT(VARCHAR(34), HashBytes('MD5', @string), 1), 3, 32)";
}
}
protected boolean supportsLockEscalationDisable() {
// not supported on SQL Server 2005
return majorVersion > 9;
}
@Override
public boolean supportsReadAcl() {
return aclOptimizationsEnabled;
}
@Override
public String getPrepareUserReadAclsSql() {
return "EXEC nx_prepare_user_read_acls ?";
}
@Override
public String getReadAclsCheckSql(String userIdCol) {
return String.format("%s = dbo.nx_md5(?)", userIdCol);
}
@Override
public String getUpdateReadAclsSql() {
return "EXEC dbo.nx_update_read_acls";
}
@Override
public String getRebuildReadAclsSql() {
return "EXEC dbo.nx_rebuild_read_acls";
}
@Override
public List<String> getStartupSqls(Model model, Database database) {
if (aclOptimizationsEnabled) {
log.info("Vacuuming tables used by optimized acls");
return Collections.singletonList("EXEC nx_vacuum_read_acls");
}
return Collections.emptyList();
}
@Override
public boolean isClusteringSupported() {
return true;
}
@Override
public String getClusterInsertInvalidations() {
return "EXEC dbo.NX_CLUSTER_INVAL ?, ?, ?, ?";
}
@Override
public String getClusterGetInvalidations() {
return "SELECT [id], [fragments], [kind] FROM [cluster_invals] WHERE [nodeid] = ?";
}
@Override
public boolean isConcurrentUpdateException(Throwable t) {
while (t.getCause() != null) {
t = t.getCause();
}
if (t instanceof SQLException) {
switch (((SQLException) t).getErrorCode()) {
case 547: // The INSERT statement conflicted with the FOREIGN KEY
// constraint ...
case 1205: // Transaction (Process ID ...) was deadlocked on ...
// resources with another process and has been chosen as
// the deadlock victim. Rerun the transaction
case 2627: // Violation of UNIQUE KEY constraint
// Violation of PRIMARY KEY constraint
return true;
}
}
return false;
}
@Override
public String getBlobLengthFunction() {
return "DATALENGTH";
}
public String getUsersSeparator() {
if (usersSeparator == null) {
return DEFAULT_USERS_SEPARATOR;
}
return usersSeparator;
}
@Override
public Serializable getGeneratedId(Connection connection) throws SQLException {
if (idType != DialectIdType.SEQUENCE) {
return super.getGeneratedId(connection);
}
String sql = String.format("SELECT NEXT VALUE FOR [%s]", idSequenceName);
try (Statement s = connection.createStatement()) {
ResultSet rs = s.executeQuery(sql);
rs.next();
return Long.valueOf(rs.getLong(1));
}
}
/**
* Set transaction isolation level to snapshot
*/
@Override
public void performPostOpenStatements(Connection connection) throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
}
}
@Override
public String getAncestorsIdsSql() {
return "SELECT id FROM dbo.NX_ANCESTORS(?)";
}
@Override
public String getDateCast() {
if (majorVersion <= 9) {
// SQL Server 2005 doesn't have a DATE type. At all. Sigh.
// Style 112 is YYYYMMDD
return "CONVERT(DATETIME, CONVERT(VARCHAR, %s, 112), 112)";
}
return super.getDateCast();
}
@Override
public String castIdToVarchar(String expr) {
switch (idType) {
case VARCHAR:
return expr;
case SEQUENCE:
return "CONVERT(VARCHAR, " + expr + ")";
default:
throw new AssertionError("Unknown id type: " + idType);
}
}
@Override
public DialectIdType getIdType() {
return idType;
}
@Override
public List<String> getIgnoredColumns(Table table) {
return Collections.singletonList(CLUSTER_INDEX_COL);
}
/**
* Tables created for directories don't need a clustered column automatically defined.
*/
protected boolean needsClusteredColumn(Table table) {
if (idType == DialectIdType.SEQUENCE) {
// good enough for a clustered index
// no need to add another column
return false;
}
for (Column col : table.getColumns()) {
if (col.getType().isId()) {
return true;
}
}
return false;
}
@Override
public String getCustomColumnDefinition(Table table) {
if (!needsClusteredColumn(table)) {
return null;
}
return String.format("[%s] INT NOT NULL IDENTITY", CLUSTER_INDEX_COL);
}
@Override
public List<String> getCustomPostCreateSqls(Table table) {
if (!needsClusteredColumn(table)) {
return Collections.emptyList();
}
String quotedIndexName = getIndexName(table.getKey(), Collections.singletonList(CLUSTER_INDEX_COL));
String sql = String.format("CREATE UNIQUE CLUSTERED INDEX [%s] ON %s ([%s])", quotedIndexName,
table.getQuotedName(), CLUSTER_INDEX_COL);
return Collections.singletonList(sql);
}
@Override
public String getSoftDeleteSql() {
return "EXEC dbo.NX_DELETE ?, ?";
}
@Override
public String getSoftDeleteCleanupSql() {
return "{?= call dbo.NX_DELETE_PURGE(?, ?)}";
}
@Override
public List<String> checkStoredProcedure(String procName, String procCreate, String ddlMode, Connection connection,
JDBCLogger logger, Map<String, Serializable> properties) throws SQLException {
boolean compatCheck = ddlMode.contains(RepositoryDescriptor.DDL_MODE_COMPAT);
String procCreateLower = procCreate.toLowerCase();
String procDrop;
if (procCreateLower.startsWith("create function ")) {
procDrop = "DROP FUNCTION " + procName;
} else if (procCreateLower.startsWith("create procedure ")) {
procDrop = "DROP PROCEDURE " + procName;
} else {
procDrop = "DROP TRIGGER " + procName;
}
if (compatCheck) {
procDrop = "IF OBJECT_ID('" + procName + "') IS NOT NULL " + procDrop;
return Arrays.asList(procDrop, procCreate);
}
try (Statement st = connection.createStatement()) {
String getBody = "SELECT OBJECT_DEFINITION(OBJECT_ID('" + procName + "'))";
logger.log(getBody);
try (ResultSet rs = st.executeQuery(getBody)) {
rs.next();
String body = rs.getString(1);
if (body == null) {
logger.log(" -> missing");
return Collections.singletonList(procCreate);
} else if (normalizeString(procCreate).contains(normalizeString(body))) {
logger.log(" -> exists, unchanged");
return Collections.emptyList();
} else {
logger.log(" -> exists, old");
return Arrays.asList(procDrop, procCreate);
}
}
}
}
protected static String normalizeString(String string) {
return string.replaceAll("[ \n\r\t]+", " ").trim();
}
@Override
public String getSQLForDump(String sql) {
return sql + "\nGO";
}
}