/*
* (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.Types;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.nuxeo.common.utils.StringUtils;
import org.nuxeo.ecm.core.NXCore;
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;
/**
* H2-specific dialect.
*
* @author Florent Guillaume
*/
public class DialectH2 extends Dialect {
protected static final String DEFAULT_USERS_SEPARATOR = ",";
private static final String DEFAULT_FULLTEXT_ANALYZER = "org.apache.lucene.analysis.standard.StandardAnalyzer";
protected final String fulltextAnalyzer;
protected final String usersSeparator;
public DialectH2(DatabaseMetaData metadata, RepositoryDescriptor repositoryDescriptor) {
super(metadata, repositoryDescriptor);
fulltextAnalyzer = repositoryDescriptor == null ? null
: repositoryDescriptor.getFulltextAnalyzer() == null ? DEFAULT_FULLTEXT_ANALYZER
: repositoryDescriptor.getFulltextAnalyzer();
usersSeparator = repositoryDescriptor == null ? null
: repositoryDescriptor.usersSeparatorKey == null ? DEFAULT_USERS_SEPARATOR
: repositoryDescriptor.usersSeparatorKey;
}
@Override
public boolean supportsIfExistsAfterTableName() {
return true;
}
@Override
public JDBCInfo getJDBCTypeAndString(ColumnType type) {
switch (type.spec) {
case STRING:
if (type.isUnconstrained()) {
return jdbcInfo("VARCHAR", Types.VARCHAR);
} else if (type.isClob()) {
return jdbcInfo("CLOB", Types.CLOB);
} else {
return jdbcInfo("VARCHAR(%d)", type.length, Types.VARCHAR);
}
case BOOLEAN:
return jdbcInfo("BOOLEAN", Types.BOOLEAN);
case LONG:
return jdbcInfo("BIGINT", Types.BIGINT);
case DOUBLE:
return jdbcInfo("DOUBLE", Types.DOUBLE);
case TIMESTAMP:
return jdbcInfo("TIMESTAMP", Types.TIMESTAMP);
case BLOBID:
return jdbcInfo("VARCHAR(250)", Types.VARCHAR);
// -----
case NODEID:
case NODEIDFK:
case NODEIDFKNP:
case NODEIDFKMUL:
case NODEIDFKNULL:
case NODEIDPK:
case NODEVAL:
return jdbcInfo("VARCHAR(36)", Types.VARCHAR);
case SYSNAME:
case SYSNAMEARRAY:
return jdbcInfo("VARCHAR(250)", Types.VARCHAR);
case TINYINT:
return jdbcInfo("TINYINT", Types.TINYINT);
case INTEGER:
return jdbcInfo("INTEGER", Types.INTEGER);
case AUTOINC:
return jdbcInfo("INTEGER AUTO_INCREMENT", Types.INTEGER);
case FTINDEXED:
throw new AssertionError(type);
case FTSTORED:
return jdbcInfo("CLOB", Types.CLOB);
case CLUSTERNODE:
return jdbcInfo("INTEGER", Types.INTEGER);
case CLUSTERFRAGS:
return jdbcInfo("VARCHAR", Types.VARCHAR);
}
throw new AssertionError(type);
}
@Override
public boolean isAllowedConversion(int expected, int actual, String actualName, int actualSize) {
// CLOB vs VARCHAR compatibility
if (expected == Types.VARCHAR && actual == Types.CLOB) {
return true;
}
if (expected == Types.CLOB && actual == Types.VARCHAR) {
return true;
}
// INTEGER vs BIGINT compatibility
if (expected == Types.BIGINT && actual == Types.INTEGER) {
return true;
}
if (expected == Types.INTEGER && actual == Types.BIGINT) {
return true;
}
return false;
}
@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.BOOLEAN:
ps.setBoolean(index, ((Boolean) value).booleanValue());
return;
case Types.TINYINT:
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.BOOLEAN:
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
public String getCreateFulltextIndexSql(String indexName, String quotedIndexName, Table table, List<Column> columns,
Model model) {
String columnNames = columns.stream()
.map(column -> "'" + column.getPhysicalName() + "'")
.collect(Collectors.joining(", "));
String fullIndexName = String.format("PUBLIC_%s_%s", table.getPhysicalName(), indexName);
return String.format("CALL NXFT_CREATE_INDEX('%s', 'PUBLIC', '%s', (%s), '%s')", fullIndexName,
table.getPhysicalName(), columnNames, fulltextAnalyzer);
}
@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", "NOT", "\"");
}
// SELECT ..., 1 as nxscore
// FROM ... LEFT JOIN NXFT_SEARCH('default', ?) nxfttbl
// .................. ON hierarchy.id = nxfttbl.KEY
// WHERE ... AND nxfttbl.KEY IS NOT NULL
// ORDER BY nxscore DESC
@Override
public FulltextMatchInfo getFulltextScoredMatchInfo(String fulltextQuery, String indexName, int nthMatch,
Column mainColumn, Model model, Database database) {
String phftname = database.getTable(Model.FULLTEXT_TABLE_NAME).getPhysicalName();
String fullIndexName = "PUBLIC_" + phftname + "_" + indexName;
String nthSuffix = nthMatch == 1 ? "" : String.valueOf(nthMatch);
String tableAlias = "_NXFTTBL" + nthSuffix;
String quotedTableAlias = openQuote() + tableAlias + closeQuote();
FulltextMatchInfo info = new FulltextMatchInfo();
info.joins = Collections.singletonList( //
new Join(Join.LEFT, //
String.format("NXFT_SEARCH('%s', ?)", fullIndexName), tableAlias, // alias
fulltextQuery, // param
String.format("%s.KEY", quotedTableAlias), // on1
mainColumn.getFullQuotedName() // on2
));
info.whereExpr = String.format("%s.KEY IS NOT NULL", quotedTableAlias);
info.scoreExpr = "1";
info.scoreAlias = "_NXSCORE" + nthSuffix;
info.scoreCol = new Column(mainColumn.getTable(), null, ColumnType.DOUBLE, null);
return info;
}
@Override
public boolean getMaterializeFulltextSyntheticColumn() {
return false;
}
@Override
public int getFulltextIndexedColumns() {
return 2;
}
@Override
public boolean supportsUpdateFrom() {
return false; // check this, unused
}
@Override
public boolean doesUpdateFromRepeatSelf() {
return true;
}
@Override
public String getClobCast(boolean inOrderBy) {
if (!inOrderBy) {
return "CAST(%s AS VARCHAR)";
}
return null;
}
@Override
public String getSecurityCheckSql(String idColumnName) {
return String.format("NX_ACCESS_ALLOWED(%s, ?, ?)", idColumnName);
}
@Override
public String getInTreeSql(String idColumnName, String id) {
return String.format("NX_IN_TREE(%s, ?)", idColumnName);
}
@Override
public boolean supportsArrays() {
return false;
}
@Override
public boolean isConcurrentUpdateException(Throwable t) {
while (t.getCause() != null) {
t = t.getCause();
}
if (t instanceof SQLException) {
String sqlState = ((SQLException) t).getSQLState();
if ("23503".equals(sqlState)) {
// Referential integrity violated child exists
return true;
}
if ("23505".equals(sqlState)) {
// Duplicate key
return true;
}
if ("23506".equals(sqlState)) {
// Referential integrity violated parent exists
return true;
}
if ("40001".equals(sqlState)) {
// Deadlock detected
return true;
}
if ("HYT00".equals(sqlState)) {
// Lock timeout
return true;
}
if ("90131".equals(sqlState)) {
// Concurrent update in table ...: another transaction has
// updated or deleted the same row
return true;
}
}
return false;
}
@Override
public String getSQLStatementsFilename() {
return "nuxeovcs/h2.sql.txt";
}
@Override
public String getTestSQLStatementsFilename() {
return "nuxeovcs/h2.test.sql.txt";
}
@Override
public Map<String, Serializable> getSQLStatementsProperties(Model model, Database database) {
Map<String, Serializable> properties = new HashMap<>();
properties.put("idType", "VARCHAR(36)");
String[] permissions = NXCore.getSecurityService().getPermissionsToCheck(SecurityConstants.BROWSE);
List<String> permsList = new LinkedList<>();
for (String perm : permissions) {
permsList.add("('" + perm + "')");
}
properties.put("fulltextEnabled", Boolean.valueOf(!fulltextDisabled));
properties.put("fulltextSearchEnabled", Boolean.valueOf(!fulltextSearchDisabled));
properties.put("clusteringEnabled", Boolean.valueOf(clusteringEnabled));
properties.put("readPermissions", String.join(", ", permsList));
properties.put("h2Functions", "org.nuxeo.ecm.core.storage.sql.db.H2Functions");
properties.put("h2Fulltext", "org.nuxeo.ecm.core.storage.sql.db.H2Fulltext");
properties.put("usersSeparator", getUsersSeparator());
return properties;
}
@Override
public boolean isClusteringSupported() {
return true;
}
@Override
public String getClusterInsertInvalidations() {
return "CALL NX_CLUSTER_INVAL(?, ?, ?, ?)";
}
@Override
public String getClusterGetInvalidations() {
return "SELECT * FROM NX_CLUSTER_GET_INVALS(?)";
}
@Override
public boolean supportsPaging() {
return true;
}
@Override
public String addPagingClause(String sql, long limit, long offset) {
return sql + String.format(" LIMIT %d OFFSET %d", limit, offset);
}
public String getUsersSeparator() {
if (usersSeparator == null) {
return DEFAULT_USERS_SEPARATOR;
}
return usersSeparator;
}
@Override
public String getBlobLengthFunction() {
return "LENGTH";
}
@Override
public String getAncestorsIdsSql() {
return "CALL NX_ANCESTORS(?)";
}
@Override
public List<String> checkStoredProcedure(String procName, String procCreate, String ddlMode, Connection connection,
JDBCLogger logger, Map<String, Serializable> properties) throws SQLException {
throw new UnsupportedOperationException();
}
}