001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 * Benoit Delbosc 012 */ 013package org.nuxeo.ecm.core.storage.sql.jdbc.dialect; 014 015import java.io.Serializable; 016import java.sql.Connection; 017import java.sql.DatabaseMetaData; 018import java.sql.PreparedStatement; 019import java.sql.ResultSet; 020import java.sql.SQLException; 021import java.sql.Statement; 022import java.sql.Types; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.Iterator; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Map; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.common.utils.StringUtils; 034import org.nuxeo.ecm.core.NXCore; 035import org.nuxeo.ecm.core.api.NuxeoException; 036import org.nuxeo.ecm.core.api.security.SecurityConstants; 037import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer; 038import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer.FulltextQuery; 039import org.nuxeo.ecm.core.storage.sql.ColumnType; 040import org.nuxeo.ecm.core.storage.sql.Model; 041import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor; 042import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; 043import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database; 044import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join; 045import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; 046 047/** 048 * Microsoft SQL Server-specific dialect. 049 * 050 * @author Florent Guillaume 051 */ 052public class DialectSQLServer extends Dialect { 053 054 private static final Log log = LogFactory.getLog(DialectSQLServer.class); 055 056 private static final String DEFAULT_FULLTEXT_ANALYZER = "english"; 057 058 private static final String DEFAULT_FULLTEXT_CATALOG = "nuxeo"; 059 060 /** 061 * Column containing an IDENTITY used to create a clustered index. 062 */ 063 public static final String CLUSTER_INDEX_COL = "_oid"; 064 065 protected final String fulltextAnalyzer; 066 067 protected final String fulltextCatalog; 068 069 private static final String DEFAULT_USERS_SEPARATOR = "|"; 070 071 protected final String usersSeparator; 072 073 protected final DialectIdType idType; 074 075 protected String idSequenceName; 076 077 protected boolean pathOptimizationsEnabled; 078 079 /** 9 = SQL Server 2005, 10 = SQL Server 2008, 11 = SQL Server 2012 / Azure */ 080 protected int majorVersion; 081 082 // http://msdn.microsoft.com/en-us/library/ms174396.aspx 083 /** 5 = Azure */ 084 protected int engineEdition; 085 086 protected boolean azure; 087 088 public DialectSQLServer(DatabaseMetaData metadata, RepositoryDescriptor repositoryDescriptor) { 089 super(metadata, repositoryDescriptor); 090 try { 091 checkDatabaseConfiguration(metadata.getConnection()); 092 majorVersion = metadata.getDatabaseMajorVersion(); 093 engineEdition = getEngineEdition(metadata.getConnection()); 094 095 } catch (SQLException e) { 096 throw new NuxeoException(e); 097 } 098 if (engineEdition == 5) { // 5 = SQL Azure 099 azure = true; 100 fulltextDisabled = true; 101 fulltextSearchDisabled = true; 102 if (repositoryDescriptor != null) { 103 repositoryDescriptor.setFulltextDisabled(true); 104 } 105 } 106 fulltextAnalyzer = repositoryDescriptor == null ? null 107 : repositoryDescriptor.fulltextAnalyzer == null ? DEFAULT_FULLTEXT_ANALYZER 108 : repositoryDescriptor.fulltextAnalyzer; 109 fulltextCatalog = repositoryDescriptor == null ? null 110 : repositoryDescriptor.fulltextCatalog == null ? DEFAULT_FULLTEXT_CATALOG 111 : repositoryDescriptor.fulltextCatalog; 112 usersSeparator = repositoryDescriptor == null ? null 113 : repositoryDescriptor.usersSeparatorKey == null ? DEFAULT_USERS_SEPARATOR 114 : repositoryDescriptor.usersSeparatorKey; 115 pathOptimizationsEnabled = repositoryDescriptor == null ? false 116 : repositoryDescriptor.getPathOptimizationsEnabled(); 117 String idt = repositoryDescriptor == null ? null : repositoryDescriptor.idType; 118 if (idt == null || "".equals(idt) || "varchar".equalsIgnoreCase(idt)) { 119 idType = DialectIdType.VARCHAR; 120 } else if (idt.toLowerCase().startsWith("sequence")) { 121 idType = DialectIdType.SEQUENCE; 122 if (idt.toLowerCase().startsWith("sequence:")) { 123 String[] split = idt.split(":"); 124 idSequenceName = split[1]; 125 } else { 126 idSequenceName = "hierarchy_seq"; 127 } 128 } else { 129 throw new NuxeoException("Unknown id type: '" + idt + "'"); 130 } 131 132 } 133 134 @Override 135 public boolean supportsPaging() { 136 // available since SQL Server 2012 137 return (majorVersion >= 11); 138 } 139 140 @Override 141 public String addPagingClause(String sql, long limit, long offset) { 142 if (!sql.contains("ORDER")) { 143 // Order is required to use the offset operation 144 sql += " ORDER BY 1"; 145 } 146 return sql + String.format(" OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", offset, limit); 147 } 148 149 protected int getEngineEdition(Connection connection) throws SQLException { 150 Statement st = connection.createStatement(); 151 try { 152 ResultSet rs = st.executeQuery("SELECT CONVERT(NVARCHAR(100), SERVERPROPERTY('EngineEdition'))"); 153 rs.next(); 154 return rs.getInt(1); 155 } finally { 156 st.close(); 157 } 158 } 159 160 protected void checkDatabaseConfiguration(Connection connection) throws SQLException { 161 Statement stmt = connection.createStatement(); 162 try { 163 String sql = "SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = db_name()"; 164 if (log.isTraceEnabled()) { 165 log.trace("SQL: " + sql); 166 } 167 ResultSet rs = stmt.executeQuery(sql); 168 if (!rs.next()) { 169 throw new SQLException("Cannot detect whether READ_COMMITTED_SNAPSHOT is on"); 170 } 171 int on = rs.getInt(1); 172 if (on != 1) { 173 throw new SQLException("Incorrect database configuration, you must enable READ_COMMITTED_SNAPSHOT"); 174 } 175 rs.close(); 176 } finally { 177 stmt.close(); 178 } 179 } 180 181 @Override 182 public char openQuote() { 183 return '['; 184 } 185 186 @Override 187 public char closeQuote() { 188 return ']'; 189 } 190 191 @Override 192 public String getNoColumnsInsertString() { 193 return "DEFAULT VALUES"; 194 } 195 196 @Override 197 public String getNullColumnString() { 198 return " NULL"; 199 } 200 201 @Override 202 public boolean qualifyIndexName() { 203 return false; 204 } 205 206 @Override 207 public String getAddColumnString() { 208 return "ADD"; 209 } 210 211 @Override 212 public JDBCInfo getJDBCTypeAndString(ColumnType type) { 213 switch (type.spec) { 214 case STRING: 215 if (type.isUnconstrained()) { 216 return jdbcInfo("NVARCHAR(4000)", Types.VARCHAR); 217 } else if (type.isClob() || type.length > 4000) { 218 return jdbcInfo("NVARCHAR(MAX)", Types.CLOB); 219 } else { 220 return jdbcInfo("NVARCHAR(%d)", type.length, Types.VARCHAR); 221 } 222 case BOOLEAN: 223 return jdbcInfo("BIT", Types.BIT); 224 case LONG: 225 return jdbcInfo("BIGINT", Types.BIGINT); 226 case DOUBLE: 227 return jdbcInfo("DOUBLE PRECISION", Types.DOUBLE); 228 case TIMESTAMP: 229 return jdbcInfo("DATETIME", Types.TIMESTAMP); 230 case BLOBID: 231 return jdbcInfo("NVARCHAR(250)", Types.VARCHAR); 232 // ----- 233 case NODEID: 234 case NODEIDFK: 235 case NODEIDFKNP: 236 case NODEIDFKMUL: 237 case NODEIDFKNULL: 238 case NODEIDPK: 239 case NODEVAL: 240 switch (idType) { 241 case VARCHAR: 242 return jdbcInfo("NVARCHAR(36)", Types.VARCHAR); 243 case SEQUENCE: 244 return jdbcInfo("BIGINT", Types.BIGINT); 245 } 246 case SYSNAME: 247 case SYSNAMEARRAY: 248 return jdbcInfo("NVARCHAR(256)", Types.VARCHAR); 249 case TINYINT: 250 return jdbcInfo("TINYINT", Types.TINYINT); 251 case INTEGER: 252 return jdbcInfo("INT", Types.INTEGER); 253 case AUTOINC: 254 return jdbcInfo("INT IDENTITY", Types.INTEGER); 255 case FTINDEXED: 256 throw new AssertionError(type); 257 case FTSTORED: 258 return jdbcInfo("NVARCHAR(MAX)", Types.CLOB); 259 case CLUSTERNODE: 260 return jdbcInfo("SMALLINT", Types.SMALLINT); 261 case CLUSTERFRAGS: 262 return jdbcInfo("NVARCHAR(4000)", Types.VARCHAR); 263 } 264 throw new AssertionError(type); 265 } 266 267 @Override 268 public boolean isAllowedConversion(int expected, int actual, String actualName, int actualSize) { 269 // The jTDS JDBC driver uses VARCHAR / CLOB 270 // The Microsoft JDBC driver uses NVARCHAR / LONGNVARCHAR 271 if (expected == Types.VARCHAR && actual == Types.CLOB) { 272 return true; 273 } 274 if (expected == Types.VARCHAR && actual == Types.NVARCHAR) { 275 return true; 276 } 277 if (expected == Types.VARCHAR && actual == Types.LONGNVARCHAR) { 278 return true; 279 } 280 if (expected == Types.CLOB && actual == Types.VARCHAR) { 281 return true; 282 } 283 if (expected == Types.CLOB && actual == Types.NVARCHAR) { 284 return true; 285 } 286 if (expected == Types.CLOB && actual == Types.LONGNVARCHAR) { 287 return true; 288 } 289 if (expected == Types.BIGINT && actual == Types.INTEGER) { 290 return true; 291 } 292 if (expected == Types.INTEGER && actual == Types.BIGINT) { 293 return true; 294 } 295 return false; 296 } 297 298 @Override 299 public void setId(PreparedStatement ps, int index, Serializable value) throws SQLException { 300 switch (idType) { 301 case VARCHAR: 302 ps.setObject(index, value, Types.VARCHAR); 303 break; 304 case SEQUENCE: 305 setIdLong(ps, index, value); 306 break; 307 } 308 } 309 310 @Override 311 public void setToPreparedStatement(PreparedStatement ps, int index, Serializable value, Column column) 312 throws SQLException { 313 switch (column.getJdbcType()) { 314 case Types.VARCHAR: 315 case Types.CLOB: 316 setToPreparedStatementString(ps, index, value, column); 317 return; 318 case Types.BIT: 319 ps.setBoolean(index, ((Boolean) value).booleanValue()); 320 return; 321 case Types.TINYINT: 322 case Types.SMALLINT: 323 case Types.INTEGER: 324 case Types.BIGINT: 325 ps.setLong(index, ((Number) value).longValue()); 326 return; 327 case Types.DOUBLE: 328 ps.setDouble(index, ((Double) value).doubleValue()); 329 return; 330 case Types.TIMESTAMP: 331 setToPreparedStatementTimestamp(ps, index, value, column); 332 return; 333 default: 334 throw new SQLException("Unhandled JDBC type: " + column.getJdbcType()); 335 } 336 } 337 338 @Override 339 @SuppressWarnings("boxing") 340 public Serializable getFromResultSet(ResultSet rs, int index, Column column) throws SQLException { 341 switch (column.getJdbcType()) { 342 case Types.VARCHAR: 343 case Types.CLOB: 344 return getFromResultSetString(rs, index, column); 345 case Types.BIT: 346 return rs.getBoolean(index); 347 case Types.TINYINT: 348 case Types.INTEGER: 349 case Types.BIGINT: 350 return rs.getLong(index); 351 case Types.DOUBLE: 352 return rs.getDouble(index); 353 case Types.TIMESTAMP: 354 return getFromResultSetTimestamp(rs, index, column); 355 } 356 throw new SQLException("Unhandled JDBC type: " + column.getJdbcType()); 357 } 358 359 @Override 360 protected int getMaxNameSize() { 361 return 128; 362 } 363 364 @Override 365 public boolean getMaterializeFulltextSyntheticColumn() { 366 return false; 367 } 368 369 @Override 370 public int getFulltextIndexedColumns() { 371 return 2; 372 } 373 374 @Override 375 public boolean supportsMultipleFulltextIndexes() { 376 // With SQL Server, only one full-text index is allowed per table... 377 return false; 378 } 379 380 @Override 381 public String getCreateFulltextIndexSql(String indexName, String quotedIndexName, Table table, 382 List<Column> columns, Model model) { 383 StringBuilder buf = new StringBuilder(); 384 buf.append(String.format("CREATE FULLTEXT INDEX ON %s (", table.getQuotedName())); 385 Iterator<Column> it = columns.iterator(); 386 while (it.hasNext()) { 387 buf.append(String.format("%s LANGUAGE %s", it.next().getQuotedName(), getQuotedFulltextAnalyzer())); 388 if (it.hasNext()) { 389 buf.append(", "); 390 } 391 } 392 String fulltextUniqueIndex = "[fulltext_pk]"; 393 buf.append(String.format(") KEY INDEX %s ON [%s]", fulltextUniqueIndex, fulltextCatalog)); 394 return buf.toString(); 395 } 396 397 @Override 398 public String getDialectFulltextQuery(String query) { 399 query = query.replace("%", "*"); 400 FulltextQuery ft = FulltextQueryAnalyzer.analyzeFulltextQuery(query); 401 if (ft == null) { 402 return "DONTMATCHANYTHINGFOREMPTYQUERY"; 403 } 404 return FulltextQueryAnalyzer.translateFulltext(ft, "OR", "AND", "AND NOT", "\"", "\"", 405 Collections.<Character> emptySet(), "\"", "\"", false); 406 } 407 408 // SELECT ..., FTTBL.RANK / 1000.0 409 // FROM ... LEFT JOIN [fulltext] ON [fulltext].[id] = [hierarchy].[id] 410 // ........ LEFT JOIN CONTAINSTABLE([fulltext], *, ?, LANGUAGE 'english') 411 // .................. AS FTTBL 412 // .................. ON [fulltext].[id] = FTTBL.[KEY] 413 // WHERE ... AND FTTBL.[KEY] IS NOT NULL 414 // ORDER BY FTTBL.RANK DESC 415 @Override 416 public FulltextMatchInfo getFulltextScoredMatchInfo(String fulltextQuery, String indexName, int nthMatch, 417 Column mainColumn, Model model, Database database) { 418 // TODO multiple indexes 419 Table ft = database.getTable(model.FULLTEXT_TABLE_NAME); 420 Column ftMain = ft.getColumn(model.MAIN_KEY); 421 String nthSuffix = nthMatch == 1 ? "" : String.valueOf(nthMatch); 422 String tableAlias = "_nxfttbl" + nthSuffix; 423 FulltextMatchInfo info = new FulltextMatchInfo(); 424 // there are two left joins here 425 info.joins = new ArrayList<Join>(); 426 if (nthMatch == 1) { 427 // Need only one JOIN involving the fulltext table 428 info.joins.add(new Join(Join.LEFT, ft.getQuotedName(), null, null, ftMain.getFullQuotedName(), 429 mainColumn.getFullQuotedName())); 430 } 431 info.joins.add(new Join( 432 Join.LEFT, // 433 String.format("CONTAINSTABLE(%s, *, ?, LANGUAGE %s)", ft.getQuotedName(), getQuotedFulltextAnalyzer()), 434 tableAlias, // alias 435 fulltextQuery, // param 436 ftMain.getFullQuotedName(), // on1 437 String.format("%s.[KEY]", tableAlias) // on2 438 )); 439 info.whereExpr = String.format("%s.[KEY] IS NOT NULL", tableAlias); 440 info.scoreExpr = String.format("(%s.RANK / 1000.0)", tableAlias); 441 info.scoreAlias = "_nxscore" + nthSuffix; 442 info.scoreCol = new Column(mainColumn.getTable(), null, ColumnType.DOUBLE, null); 443 return info; 444 } 445 446 protected String getQuotedFulltextAnalyzer() { 447 if (!Character.isDigit(fulltextAnalyzer.charAt(0))) { 448 return String.format("'%s'", fulltextAnalyzer); 449 } 450 return fulltextAnalyzer; 451 } 452 453 @Override 454 public String getLikeEscaping() { 455 return " ESCAPE '\\'"; 456 } 457 458 @Override 459 public boolean supportsCircularCascadeDeleteConstraints() { 460 // See http://support.microsoft.com/kb/321843 461 // Msg 1785 Introducing FOREIGN KEY constraint 462 // 'hierarchy_parentid_hierarchy_fk' on table 'hierarchy' may cause 463 // cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON 464 // UPDATE NO ACTION, or modify other FOREIGN KEY constraints. 465 // Instead we use a trigger "INSTEAD OF DELETE" to do the recursion. 466 return false; 467 } 468 469 @Override 470 public boolean supportsUpdateFrom() { 471 return true; 472 } 473 474 @Override 475 public boolean doesUpdateFromRepeatSelf() { 476 return true; 477 } 478 479 @Override 480 public boolean needsAliasForDerivedTable() { 481 return true; 482 } 483 484 @Override 485 public boolean needsOriginalColumnInGroupBy() { 486 // http://msdn.microsoft.com/en-us/library/ms177673.aspx 487 // A column alias that is defined in the SELECT list cannot be used to 488 // specify a grouping column. 489 return true; 490 } 491 492 @Override 493 public String getSecurityCheckSql(String idColumnName) { 494 return String.format("dbo.NX_ACCESS_ALLOWED(%s, ?, ?) = 1", idColumnName); 495 } 496 497 @Override 498 public boolean supportsFastDescendants() { 499 return pathOptimizationsEnabled; 500 } 501 502 @Override 503 public String getInTreeSql(String idColumnName, String id) { 504 String idParam; 505 switch (idType) { 506 case VARCHAR: 507 idParam = "?"; 508 break; 509 case SEQUENCE: 510 // check that it's really an integer 511 if (id != null && !org.apache.commons.lang.StringUtils.isNumeric(id)) { 512 return null; 513 } 514 idParam = "CONVERT(BIGINT, ?)"; 515 break; 516 default: 517 throw new AssertionError("Unknown id type: " + idType); 518 } 519 520 if (pathOptimizationsEnabled) { 521 return String.format("EXISTS(SELECT 1 FROM ancestors WHERE hierarchy_id = %s AND ancestor = %s)", 522 idColumnName, idParam); 523 } 524 return String.format("%s IN (SELECT * FROM dbo.nx_children(%s))", idColumnName, idParam); 525 } 526 527 @Override 528 public String getSQLStatementsFilename() { 529 return "nuxeovcs/sqlserver.sql.txt"; 530 } 531 532 @Override 533 public String getTestSQLStatementsFilename() { 534 return "nuxeovcs/sqlserver.test.sql.txt"; 535 } 536 537 @Override 538 public Map<String, Serializable> getSQLStatementsProperties(Model model, Database database) { 539 Map<String, Serializable> properties = new HashMap<String, Serializable>(); 540 switch (idType) { 541 case VARCHAR: 542 properties.put("idType", "NVARCHAR(36)"); 543 properties.put("idTypeParam", "NVARCHAR"); 544 properties.put("idNotPresent", "'-'"); 545 properties.put("sequenceEnabled", Boolean.FALSE); 546 break; 547 case SEQUENCE: 548 properties.put("idType", "BIGINT"); 549 properties.put("idTypeParam", "BIGINT"); 550 properties.put("idNotPresent", "-1"); 551 properties.put("sequenceEnabled", Boolean.TRUE); 552 properties.put("idSequenceName", idSequenceName); 553 } 554 properties.put("lockEscalationDisabled", Boolean.valueOf(supportsLockEscalationDisable())); 555 properties.put("md5HashString", getMd5HashString()); 556 properties.put("reseedAclrModified", azure ? "" : "DBCC CHECKIDENT('aclr_modified', RESEED, 0);"); 557 properties.put("fulltextEnabled", Boolean.valueOf(!fulltextSearchDisabled)); 558 properties.put("fulltextCatalog", fulltextCatalog); 559 properties.put("aclOptimizationsEnabled", Boolean.valueOf(aclOptimizationsEnabled)); 560 properties.put("pathOptimizationsEnabled", Boolean.valueOf(pathOptimizationsEnabled)); 561 properties.put("clusteringEnabled", Boolean.valueOf(clusteringEnabled)); 562 properties.put("proxiesEnabled", Boolean.valueOf(proxiesEnabled)); 563 properties.put("softDeleteEnabled", Boolean.valueOf(softDeleteEnabled)); 564 String[] permissions = NXCore.getSecurityService().getPermissionsToCheck(SecurityConstants.BROWSE); 565 List<String> permsList = new LinkedList<String>(); 566 for (String perm : permissions) { 567 permsList.add(String.format(" SELECT '%s' ", perm)); 568 } 569 properties.put("readPermissions", StringUtils.join(permsList, " UNION ALL ")); 570 properties.put("usersSeparator", getUsersSeparator()); 571 return properties; 572 } 573 574 protected String getMd5HashString() { 575 if (majorVersion <= 9) { 576 // this is an internal function and doesn't work on Azure 577 return "SUBSTRING(master.dbo.fn_varbintohexstr(HashBytes('MD5', @string)), 3, 32)"; 578 } else { 579 // this doesn't work on SQL Server 2005 580 return "SUBSTRING(CONVERT(VARCHAR(34), HashBytes('MD5', @string), 1), 3, 32)"; 581 } 582 } 583 584 protected boolean supportsLockEscalationDisable() { 585 // not supported on SQL Server 2005 586 return majorVersion > 9; 587 } 588 589 @Override 590 public boolean supportsReadAcl() { 591 return aclOptimizationsEnabled; 592 } 593 594 @Override 595 public String getPrepareUserReadAclsSql() { 596 return "EXEC nx_prepare_user_read_acls ?"; 597 } 598 599 @Override 600 public String getReadAclsCheckSql(String userIdCol) { 601 return String.format("%s = dbo.nx_md5(?)", userIdCol); 602 } 603 604 @Override 605 public String getUpdateReadAclsSql() { 606 return "EXEC dbo.nx_update_read_acls"; 607 } 608 609 @Override 610 public String getRebuildReadAclsSql() { 611 return "EXEC dbo.nx_rebuild_read_acls"; 612 } 613 614 @Override 615 public boolean isClusteringSupported() { 616 return true; 617 } 618 619 @Override 620 public String getClusterInsertInvalidations() { 621 return "EXEC dbo.NX_CLUSTER_INVAL ?, ?, ?, ?"; 622 } 623 624 @Override 625 public String getClusterGetInvalidations() { 626 return "SELECT [id], [fragments], [kind] FROM [cluster_invals] WHERE [nodeid] = ?"; 627 } 628 629 @Override 630 public boolean isConcurrentUpdateException(Throwable t) { 631 while (t.getCause() != null) { 632 t = t.getCause(); 633 } 634 if (t instanceof SQLException) { 635 switch (((SQLException) t).getErrorCode()) { 636 case 547: // The INSERT statement conflicted with the FOREIGN KEY 637 // constraint ... 638 case 1205: // Transaction (Process ID ...) was deadlocked on ... 639 // resources with another process and has been chosen as 640 // the deadlock victim. Rerun the transaction 641 case 2627: // Violation of UNIQUE KEY constraint 642 // Violation of PRIMARY KEY constraint 643 return true; 644 } 645 } 646 return false; 647 } 648 649 @Override 650 public String getBlobLengthFunction() { 651 return "DATALENGTH"; 652 } 653 654 public String getUsersSeparator() { 655 if (usersSeparator == null) { 656 return DEFAULT_USERS_SEPARATOR; 657 } 658 return usersSeparator; 659 } 660 661 @Override 662 public Serializable getGeneratedId(Connection connection) throws SQLException { 663 if (idType != DialectIdType.SEQUENCE) { 664 return super.getGeneratedId(connection); 665 } 666 String sql = String.format("SELECT NEXT VALUE FOR [%s]", idSequenceName); 667 Statement s = connection.createStatement(); 668 try { 669 ResultSet rs = s.executeQuery(sql); 670 rs.next(); 671 return Long.valueOf(rs.getLong(1)); 672 } finally { 673 s.close(); 674 } 675 } 676 677 /** 678 * Set transaction isolation level to snapshot 679 */ 680 @Override 681 public void performPostOpenStatements(Connection connection) throws SQLException { 682 Statement stmt = connection.createStatement(); 683 try { 684 stmt.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED"); 685 } finally { 686 stmt.close(); 687 } 688 } 689 690 @Override 691 public String getAncestorsIdsSql() { 692 return "SELECT id FROM dbo.NX_ANCESTORS(?)"; 693 } 694 695 @Override 696 public String getDateCast() { 697 if (majorVersion <= 9) { 698 // SQL Server 2005 doesn't have a DATE type. At all. Sigh. 699 // Style 112 is YYYYMMDD 700 return "CONVERT(DATETIME, CONVERT(VARCHAR, %s, 112), 112)"; 701 } 702 return super.getDateCast(); 703 } 704 705 @Override 706 public String castIdToVarchar(String expr) { 707 switch (idType) { 708 case VARCHAR: 709 return expr; 710 case SEQUENCE: 711 return "CONVERT(VARCHAR, " + expr + ")"; 712 default: 713 throw new AssertionError("Unknown id type: " + idType); 714 } 715 } 716 717 @Override 718 public DialectIdType getIdType() { 719 return idType; 720 } 721 722 @Override 723 public List<String> getIgnoredColumns(Table table) { 724 return Collections.singletonList(CLUSTER_INDEX_COL); 725 } 726 727 /** 728 * Tables created for directories don't need a clustered column automatically defined. 729 */ 730 protected boolean needsClusteredColumn(Table table) { 731 if (idType == DialectIdType.SEQUENCE) { 732 // good enough for a clustered index 733 // no need to add another column 734 return false; 735 } 736 for (Column col : table.getColumns()) { 737 if (col.getType().isId()) { 738 return true; 739 } 740 } 741 return false; 742 } 743 744 @Override 745 public String getCustomColumnDefinition(Table table) { 746 if (!needsClusteredColumn(table)) { 747 return null; 748 } 749 return String.format("[%s] INT NOT NULL IDENTITY", CLUSTER_INDEX_COL); 750 } 751 752 @Override 753 public List<String> getCustomPostCreateSqls(Table table) { 754 if (!needsClusteredColumn(table)) { 755 return Collections.emptyList(); 756 } 757 String quotedIndexName = getIndexName(table.getKey(), Collections.singletonList(CLUSTER_INDEX_COL)); 758 String sql = String.format("CREATE UNIQUE CLUSTERED INDEX [%s] ON %s ([%s])", quotedIndexName, 759 table.getQuotedName(), CLUSTER_INDEX_COL); 760 return Collections.singletonList(sql); 761 } 762 763 @Override 764 public String getSoftDeleteSql() { 765 return "EXEC dbo.NX_DELETE ?, ?"; 766 } 767 768 @Override 769 public String getSoftDeleteCleanupSql() { 770 return "{?= call dbo.NX_DELETE_PURGE(?, ?)}"; 771 } 772 773}