/** * Copyright 2014 Sunny Gleason and original author or authors * * 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. */ package io.kazuki.v0.store.index; import io.kazuki.v0.internal.hash.LongHash; import io.kazuki.v0.internal.hash.MurmurHash; import io.kazuki.v0.internal.helper.OpaquePaginationHelper; import io.kazuki.v0.internal.helper.SqlParamBindings; import io.kazuki.v0.internal.helper.SqlTypeHelper; import io.kazuki.v0.internal.helper.StringHelper; import io.kazuki.v0.internal.v2schema.compact.FieldTransform; import io.kazuki.v0.store.KazukiException; import io.kazuki.v0.store.index.query.QueryOperator; import io.kazuki.v0.store.index.query.QueryTerm; import io.kazuki.v0.store.index.query.ValueHolder; import io.kazuki.v0.store.index.query.ValueType; import io.kazuki.v0.store.keyvalue.KeyValueStoreIteration.SortDirection; import io.kazuki.v0.store.schema.model.Attribute; import io.kazuki.v0.store.schema.model.IndexAttribute; import io.kazuki.v0.store.schema.model.IndexDefinition; import io.kazuki.v0.store.schema.model.Schema; import io.kazuki.v0.store.sequence.KeyImpl; import io.kazuki.v0.store.sequence.SequenceService; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.IDBI; import org.skife.jdbi.v2.TransactionCallback; import org.skife.jdbi.v2.TransactionStatus; import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; import com.google.common.base.Throwables; public class SecondaryIndexTableHelper { private final SqlTypeHelper typeHelper; private final LongHash longHash; private final SequenceService sequences; private final String prefix; @Inject public SecondaryIndexTableHelper(SqlTypeHelper typeHelper, SequenceService sequences) { this.typeHelper = typeHelper; this.longHash = new MurmurHash(); this.sequences = sequences; this.prefix = typeHelper.getPrefix(); } public String getPrefix() { return prefix; } public void createIndex(IDBI database, final String type, final String indexName, final Schema schema, final String groupName, final String storeName, final String partitionName) { database.inTransaction(new TransactionCallback<Void>() { @Override public Void inTransaction(Handle handle, TransactionStatus arg1) throws Exception { try { handle .createStatement(prefix + "drop_index") .define("table_name", getTableName(type, indexName, groupName, storeName, partitionName)) .define("index_name", getIndexName(type, indexName, groupName, storeName, partitionName)).execute(); } catch (UnableToExecuteStatementException ok) { // expected case in mysql - this is just best-effort anyway } handle.createStatement( getTableDefinition(type, indexName, schema, groupName, storeName, partitionName)) .execute(); handle.createStatement( getIndexDefinition(type, indexName, schema, groupName, storeName, partitionName)) .execute(); return null; } }); } public void dropTableAndIndex(Handle handle, final String type, final String indexName, String groupName, String storeName, String partitionName) { handle.createStatement(getTableDrop(type, indexName, groupName, storeName, partitionName)) .execute(); try { handle.createStatement(prefix + "drop_index") .define("table_name", getTableName(type, indexName, groupName, storeName, partitionName)) .define("index_name", getIndexName(type, indexName, groupName, storeName, partitionName)) .execute(); } catch (UnableToExecuteStatementException ok) { // expected case in mysql - this is just best-effort anyway } } public void dropTableAndIndex(IDBI database, final String type, final String indexName, final String groupName, final String storeName, final String partitionName) { database.inTransaction(new TransactionCallback<Void>() { @Override public Void inTransaction(Handle handle, TransactionStatus arg1) throws Exception { dropTableAndIndex(handle, type, indexName, groupName, storeName, partitionName); return null; } }); } public String getInsertStatement(String type, String indexName, Schema schema, SqlParamBindings bindings, String groupName, String storeName, String partitionName) { IndexDefinition indexDefinition = schema.getIndexMap().get(indexName); List<String> cols = new ArrayList<String>(); List<String> params = new ArrayList<String>(); Set<String> already = new HashSet<String>(); cols.add(getColumnName("id")); params.add(bindings.bind("id", Attribute.Type.U64)); already.add("id"); for (IndexAttribute attr : indexDefinition.getIndexAttributes()) { if (already.contains(attr.getName())) { continue; } cols.add(getColumnName(attr.getName())); params.add(bindings.bind(attr.getName(), schema.getAttribute(attr.getName()).getType())); } cols.add(typeHelper.quote("quarantined")); params.add(bindings.bind("quarantined", "N", Attribute.Type.CHAR_ONE)); StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("insert into "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" ("); sqlBuilder.append(StringHelper.join(", ", cols)); sqlBuilder.append(") values ("); sqlBuilder.append(StringHelper.join(", ", params)); sqlBuilder.append(")"); return sqlBuilder.toString(); } public String getUpdateStatement(String type, String indexName, Schema schema, SqlParamBindings bindings, String groupName, String storeName, String partitionName) { IndexDefinition indexDefinition = schema.getIndexMap().get(indexName); List<String> sets = new ArrayList<String>(); for (IndexAttribute attr : indexDefinition.getIndexAttributes()) { if ("id".equals(attr.getName())) { continue; } sets.add(getColumnName(attr.getName()) + " = " + bindings.bind(attr.getName(), schema.getAttribute(attr.getName()).getType())); } StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("update "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" set "); sqlBuilder.append(StringHelper.join(", ", sets)); sqlBuilder.append(" where "); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" = "); sqlBuilder.append(bindings.bind("id", Attribute.Type.U64)); return sqlBuilder.toString(); } public String getDeleteStatement(String type, String indexName, SqlParamBindings bindings, String groupName, String storeName, String partitionName) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("delete from "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" where "); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" = "); sqlBuilder.append(bindings.bind("id", Attribute.Type.U64)); return sqlBuilder.toString(); } public String getQuarantineStatement(String type, String indexName, SqlParamBindings bindings, boolean isQuarantined, String groupName, String storeName, String partitionName) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("update "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" set "); sqlBuilder.append(typeHelper.quote("quarantined")); sqlBuilder.append(" = "); sqlBuilder.append(bindings.bind("is_quarantined", isQuarantined ? "Y" : "N", Attribute.Type.CHAR_ONE)); sqlBuilder.append(" where "); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" = "); sqlBuilder.append(bindings.bind("id", Attribute.Type.U64)); return sqlBuilder.toString(); } public void truncateIndexTable(Handle handle, final String type, final String indexName, String groupName, String storeName, String partitionName) { String indexTableName = getTableName(type, indexName, groupName, storeName, partitionName); handle.createStatement(prefix + "truncate_table").define("table_name", indexTableName) .execute(); } public String getSqlOperator(QueryOperator operator, ValueHolder value) { switch (operator) { case EQ: return (value.getValueType().equals(ValueType.NULL)) ? "is null" : "="; case NE: return (value.getValueType().equals(ValueType.NULL)) ? "is not null" : "<>"; case GT: return ">"; case GE: return ">="; case LT: return "<"; case LE: return "<="; case IN: return "in"; default: throw new IllegalArgumentException("Unknown operator: " + operator); } } public Object transformAttributeValue(Object value, IndexAttribute attr) { Object toBind = value; if (toBind != null) { switch (attr.getTransform()) { case UPPERCASE: toBind = toBind.toString().toUpperCase(); break; case LOWERCASE: toBind = toBind.toString().toLowerCase(); break; default: break; } } return toBind; } public boolean isConstraintViolation(UnableToExecuteStatementException e) { if (e.getCause() != null) { String message = e.getCause().getMessage().toLowerCase(); return message.contains("constraint violation") || message.contains("duplicate entry") || message.contains("unique index or primary key violation"); } return false; } public String getColumnName(String attributeName, boolean doQuote) { return doQuote ? getColumnName(attributeName) : "_" + attributeName; } public String getColumnName(String attributeName) { return typeHelper.quote("_" + attributeName + ""); } public String getTableName(String type, String index, String groupName, String storeName, String partitionName) { try { Integer typeId = sequences.getTypeId(type, false); if (typeId == null) { return null; } String truncType = truncateString(type, 4); String truncIndex = truncateString(index, 10); return typeHelper.quote("_" + groupName + "_" + storeName + "__idxtbl__" + partitionName + "_" + String.format("%04d", typeId) + "__" + getIndexHexId(index) + "_" + truncType + "_" + truncIndex); } catch (Exception e) { throw new RuntimeException(e); } } public String getIndexName(String type, String index, String groupName, String storeName, String partitionName) { try { Integer typeId = sequences.getTypeId(type, false); if (typeId == null) { return null; } String truncType = truncateString(type, 4); String truncIndex = truncateString(index, 10); return typeHelper.quote("_" + groupName + "_" + storeName + "__idxidx__" + partitionName + "_" + String.format("%04d", typeId) + "__" + getIndexHexId(index) + "_" + truncType + "_" + truncIndex); } catch (Exception e) { throw new RuntimeException(e); } } private String getIndexHexId(String index) { return String.format("%016x", longHash.getLongHashCode(index)); } public String getTableDrop(String type, String indexName, String groupName, String storeName, String partitionName) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("drop table if exists "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); return sqlBuilder.toString(); } public String getIndexDrop(String type, String indexName, String groupName, String storeName, String partitionName) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("drop index "); sqlBuilder.append(getIndexName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" on "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); return sqlBuilder.toString(); } public String getTableDefinition(String type, String indexName, Schema schema, String groupName, String storeName, String partitionName) { IndexDefinition indexDefinition = schema.getIndexMap().get(indexName); if (indexDefinition == null) { throw new IllegalArgumentException("schema or index not found " + type + "." + indexName); } StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("create table if not exists "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" ("); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" "); sqlBuilder.append(typeHelper.getSqlType(Attribute.Type.U64)); sqlBuilder.append(" PRIMARY KEY"); for (IndexAttribute column : indexDefinition.getIndexAttributes()) { Attribute attribute = schema.getAttribute(column.getName()); if (attribute == null && !column.getName().equals("id")) { throw new IllegalArgumentException("Unknown attribute : " + column.getName()); } if (column.getName().equals("id")) { continue; } sqlBuilder.append(", "); sqlBuilder.append(getColumnName(column.getName())); sqlBuilder.append(" "); sqlBuilder.append(typeHelper.getSqlType(attribute.getType())); } sqlBuilder.append(", "); sqlBuilder.append(typeHelper.quote("quarantined")); sqlBuilder.append(" "); sqlBuilder.append(typeHelper.getSqlType(Attribute.Type.CHAR_ONE)); sqlBuilder.append(")"); sqlBuilder.append(typeHelper.getTableOptions()); return sqlBuilder.toString(); } public String getIndexDefinition(String type, String indexName, Schema schema, String groupName, String storeName, String partitionName) { IndexDefinition indexDefinition = schema.getIndexMap().get(indexName); if (indexDefinition == null) { throw new IllegalArgumentException("schema or index not found " + type + "." + indexName); } Iterator<IndexAttribute> iter = indexDefinition.getIndexAttributes().iterator(); List<String> colDefs = new ArrayList<String>(); while (iter.hasNext()) { IndexAttribute column = iter.next(); Attribute attribute = schema.getAttribute(column.getName()); if (attribute == null && !column.getName().equals("id")) { throw new IllegalArgumentException("Unknown attribute : " + column.getName()); } if (indexDefinition.isUnique() && column.getName().equals("id")) { continue; } String sortDirection = column.getSortDirection().equals(SortDirection.ASCENDING) ? "ASC" : "DESC"; colDefs.add(getColumnName(column.getName()) + " " + sortDirection); } if (!indexDefinition.isUnique()) { colDefs.add(getColumnName("id") + " " + "ASC"); } StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("create "); if (indexDefinition.isUnique()) { sqlBuilder.append("unique "); } sqlBuilder.append("index "); sqlBuilder.append(getIndexName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" on "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" ("); sqlBuilder.append(StringHelper.join(", ", colDefs)); sqlBuilder.append(")"); return sqlBuilder.toString(); } public String getIndexQuery(String type, String indexName, SortDirection sortDirection, List<QueryTerm> queryTerms, Long offset, Long pageSize, boolean includeQuarantine, Schema schema, SqlParamBindings bindings, String groupName, String storeName, String partitionName) throws Exception { IndexDefinition indexDef = schema.getIndex(indexName); if (indexDef == null) { throw new IllegalArgumentException("schema or index not found " + type + "." + indexName); } return getIndexQuery(type, indexName, sortTerms(indexDef, queryTerms), sortDirection, offset, pageSize, includeQuarantine, indexDef, schema, new FieldTransform(schema), bindings, groupName, storeName, partitionName); } public String getIndexQuery(String type, String indexName, Map<String, List<QueryTerm>> termMap, SortDirection sortDirection, Long offset, Long pageSize, boolean includeQuarantine, IndexDefinition indexDefinition, Schema schema, FieldTransform transform, SqlParamBindings bindings, String groupName, String storeName, String partitionName) throws Exception { List<QueryTerm> firstTerm = termMap.get(indexDefinition.getIndexAttributes().get(0).getName()); if (firstTerm == null || firstTerm.isEmpty()) { throw new IllegalArgumentException("missing query term for first attribute of index"); } List<String> clauses = new ArrayList<String>(); int param = 0; for (IndexAttribute attribute : indexDefinition.getIndexAttributes()) { String attrName = attribute.getName(); List<QueryTerm> termList = termMap.get(attrName); if (termList == null || termList.isEmpty()) { continue; } for (QueryTerm term : termList) { String maybeParam = ""; QueryOperator op = term.getOperator(); if (op.equals(QueryOperator.IN)) { List<ValueHolder> valueList = term.getValueList().getValueList(); String sqlOperator = getSqlOperator(term.getOperator(), valueList.get(0)); List<String> paramNames = new ArrayList<String>(); for (ValueHolder value : valueList) { String boundParam = bindParam(attribute, schema, transform, bindings, param, attrName, value); if (boundParam != null) { maybeParam = " " + boundParam; paramNames.add(maybeParam); param += 1; } else { maybeParam = ""; } } clauses.add(getColumnName(term.getField()) + " " + sqlOperator + "(" + StringHelper.join(", ", paramNames) + ")"); } else { String boundParam = bindParam(attribute, schema, transform, bindings, param, attrName, term.getValue()); if (boundParam != null) { maybeParam = " " + boundParam; param += 1; } clauses.add(getColumnName(term.getField()) + " " + getSqlOperator(term.getOperator(), term.getValue()) + maybeParam); } } } List<String> sortOrders = new ArrayList<String>(); for (IndexAttribute attr : indexDefinition.getIndexAttributes()) { String colName = getColumnName(attr.getName()); String colSortDirection = null; if (sortDirection.equals(attr.getSortDirection())) { colSortDirection = sortDirection.equals(SortDirection.ASCENDING) ? "ASC" : "DESC"; } else { colSortDirection = sortDirection.equals(SortDirection.DESCENDING) ? "DESC" : "ASC"; } sortOrders.add(colName + " " + colSortDirection); } sortOrders.add(getColumnName("id") + " " + (sortDirection.equals(SortDirection.ASCENDING) ? "ASC" : "DESC")); offset = offset != null ? offset : 0L; Long limit = pageSize != null ? pageSize + 1L : -1; StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("select "); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" from "); sqlBuilder.append(getTableName(type, indexName, groupName, storeName, partitionName)); sqlBuilder.append(" where "); if (!includeQuarantine) { sqlBuilder.append(typeHelper.quote("quarantined")); sqlBuilder.append(" = 'N' AND "); } sqlBuilder.append(StringHelper.join(" AND ", clauses)); sqlBuilder.append(" order by "); sqlBuilder.append(StringHelper.join(", ", sortOrders)); sqlBuilder.append(" limit "); sqlBuilder.append(limit); sqlBuilder.append(" offset "); sqlBuilder.append(offset); return sqlBuilder.toString(); } public String getIndexAllQuery(String type, String token, Long pageSize, boolean includeQuarantine) throws Exception { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("select "); sqlBuilder.append(typeHelper.quote("_key_id")); sqlBuilder.append(" as "); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" from "); sqlBuilder.append(typeHelper.quote("_key_values")); sqlBuilder.append(" where "); sqlBuilder.append(typeHelper.quote("_key_type")); sqlBuilder.append(" = "); sqlBuilder.append(sequences.getTypeId(type, false)); sqlBuilder.append(" AND "); if (!includeQuarantine) { sqlBuilder.append(typeHelper.quote("_is_deleted")); sqlBuilder.append(" = 'N'"); } else { sqlBuilder.append(typeHelper.quote("_is_deleted")); sqlBuilder.append(" != 'Y'"); } sqlBuilder.append(" order by "); sqlBuilder.append(typeHelper.quote("_id")); sqlBuilder.append(" limit "); sqlBuilder.append(pageSize + 1L); sqlBuilder.append(" offset "); sqlBuilder.append(OpaquePaginationHelper.decodeOpaqueCursor(token)); return sqlBuilder.toString(); } public String computeIndexKey(String type, String indexName, IndexDefinition indexDefinition, Map<String, Object> value) { StringBuilder theKey = new StringBuilder(); try { theKey.append("idx:"); theKey.append(getUniqueIndexIdentifier(type, indexName)); theKey.append(":"); Iterator<IndexAttribute> indexAttrIter = indexDefinition.getIndexAttributes().iterator(); while (indexAttrIter.hasNext()) { IndexAttribute attr = indexAttrIter.next(); String attrName = attr.getName(); if ("id".equals(attrName)) { continue; } Object attrValue = value.get(attrName); Object transformed = (attrValue == null) ? null : transformAttributeValue(attrValue, attr); String attrValueString = (transformed != null) ? URLEncoder.encode(transformed.toString(), "UTF-8") : "$"; theKey.append(attrValueString); if (indexAttrIter.hasNext()) { theKey.append("|"); } } } catch (Exception shouldntHappen) { throw new RuntimeException(shouldntHappen); } return theKey.toString(); } public Map<String, List<QueryTerm>> sortTerms(IndexDefinition indexDef, List<QueryTerm> terms) throws KazukiException { Map<String, List<QueryTerm>> termMap = new LinkedHashMap<String, List<QueryTerm>>(); Set<String> termFields = new HashSet<String>(); for (QueryTerm term : terms) { String attrName = term.getField(); termFields.add(attrName); if (!indexDef.getAttributeNames().contains(attrName)) { throw new KazukiException("'" + attrName + "' not in index"); } } for (IndexAttribute attribute : indexDef.getIndexAttributes()) { String attrName = attribute.getName(); if (indexDef.isUnique() && !attrName.equals("id") && !termFields.contains(attrName)) { throw new KazukiException("unique index query must specify all fields"); } for (QueryTerm term : terms) { if (term.getField().equals(attrName)) { List<QueryTerm> attrTerms = termMap.get(attrName); if (attrTerms == null) { attrTerms = new ArrayList<QueryTerm>(); termMap.put(attrName, attrTerms); } attrTerms.add(term); } } } return termMap; } public static String getUniqueIndexKey(String type, Schema schema, String indexName, Map<String, Object> valMap) { IndexDefinition indexDef = schema.getIndex(indexName); StringBuilder builder = new StringBuilder(); builder.append(type); builder.append("."); builder.append(indexName); builder.append(":"); for (IndexAttribute attr : indexDef.getIndexAttributes()) { try { builder.append("/"); builder.append(URLEncoder.encode(String.valueOf(valMap.get(attr.getName())), "UTF-8")); } catch (UnsupportedEncodingException e) { throw Throwables.propagate(e); } } return builder.toString(); } private String bindParam(IndexAttribute attribute, Schema schema, FieldTransform transform, SqlParamBindings bindings, int param, String attrName, ValueHolder value) throws KazukiException { if (!value.getValueType().equals(ValueType.NULL)) { Object instance = value.getValue(); Object transformed = transform.transformValue(attrName, instance); if (transformed != null) { transformed = transformed.toString(); } if (attrName.equals("id")) { try { transformed = KeyImpl.valueOf(transformed.toString()); } catch (Exception e) { throw new KazukiException("invalid id: '" + instance.toString() + "'"); } } return bindings.bind( "p" + param, transformAttributeValue(transformed, attribute), "id".equals(attribute.getName()) ? Attribute.Type.U64 : schema.getAttributeMap() .get(attribute.getName()).getType()); } return null; } private String getUniqueIndexIdentifier(String type, String index) throws Exception { return String.format("%04d", sequences.getTypeId(type, false)) + "__" + getIndexHexId(index); } private String truncateString(String value, int desired) { int len = value.length(); return len >= desired ? value.substring(0, desired) : value; } }