/* * 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.cyclop.service.cassandra.intern; import static org.cyclop.common.Gullectors.toImmutableMap; import static org.cyclop.common.Gullectors.toNaturalImmutableSortedSet; import static org.cyclop.common.QueryHelper.extractSpace; import static org.cyclop.common.QueryHelper.extractTableName; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.StreamSupport; import javax.inject.Inject; import javax.inject.Named; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.cyclop.common.AppConfig; import org.cyclop.model.CassandraVersion; import org.cyclop.model.CqlColumnName; import org.cyclop.model.CqlColumnType; import org.cyclop.model.CqlDataType; import org.cyclop.model.CqlExtendedColumnName; import org.cyclop.model.CqlIndex; import org.cyclop.model.CqlKeySpace; import org.cyclop.model.CqlKeyword; import org.cyclop.model.CqlPartitionKey; import org.cyclop.model.CqlQuery; import org.cyclop.model.CqlQueryResult; import org.cyclop.model.CqlQueryType; import org.cyclop.model.CqlRowMetadata; import org.cyclop.model.CqlTable; import org.cyclop.model.QueryEntry; import org.cyclop.model.exception.QueryException; import org.cyclop.service.cassandra.QueryService; import org.cyclop.service.queryprotocoling.HistoryService; import org.cyclop.validation.EnableValidation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.datastax.driver.core.ColumnDefinitions; import com.datastax.driver.core.DataType; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; /** @author Maciej Miklas */ @EnableValidation @Named @CassandraVersionQualifier(CassandraVersion.VER_2_0) class QueryServiceImpl implements QueryService { private final static Logger LOG = LoggerFactory.getLogger(QueryServiceImpl.class); @Inject protected AppConfig config; @Inject protected CassandraSessionImpl session; @Inject protected QueryScopeImpl queryScope; @Inject private HistoryService historyService; @Override public boolean checkTableExists(CqlTable table) { Validate.notNull(table, "null CqlTable"); StringBuilder cql = new StringBuilder("select columnfamily_name from system.schema_columnfamilies"); cql.append(" where columnfamily_name='").append(table.partLc).append("' allow filtering"); Optional<ResultSet> result = executeSilent(cql.toString()); boolean tableExists = result.filter(r -> !r.isExhausted()).isPresent(); return tableExists; } @Override public ImmutableSortedSet<CqlIndex> findAllIndexes(Optional<CqlKeySpace> keySpace) { StringBuilder cql = new StringBuilder("SELECT index_name FROM system.schema_columns"); if (keySpace.isPresent()) { cql.append(" where keyspace_name='").append(keySpace.get().partLc).append("'"); } Optional<ResultSet> result = executeSilent(cql.toString()); if (!result.isPresent()) { LOG.debug("No indexes found for keyspace: " + keySpace); return ImmutableSortedSet.of(); } ImmutableSortedSet<CqlIndex> res = map(result, "index_name", CqlIndex::new); return res; } @Override public ImmutableSortedSet<CqlKeySpace> findAllKeySpaces() { Optional<ResultSet> result = executeSilent("select keyspace_name from system.schema_keyspaces"); if (!result.isPresent()) { LOG.debug("Cannot readIdentifier keyspace info"); return ImmutableSortedSet.of(); } ImmutableSortedSet<CqlKeySpace> res = map(result, "keyspace_name", CqlKeySpace::new); return res; } @Override public ImmutableSortedSet<CqlTable> findTableNames(Optional<CqlKeySpace> keySpace) { StringBuilder cql = new StringBuilder("select columnfamily_name from system.schema_columnfamilies"); if (keySpace.isPresent()) { cql.append(" where keyspace_name='").append(keySpace.get().partLc).append("'"); } Optional<ResultSet> result = executeSilent(cql.toString()); if (!result.isPresent()) { LOG.debug("No table names found for keyspace: " + keySpace); return ImmutableSortedSet.of(); } ImmutableSortedSet<CqlTable> res = map(result, "columnfamily_name", CqlTable::new); return res; } private void setActiveKeySpace(CqlQuery query) { Optional<CqlKeySpace> space = extractSpace(query); queryScope.setActiveKeySpace(space); } @Override public CqlQueryResult execute(CqlQuery query) { return execute(query, true); } @Override public void executeSimple(CqlQuery query, boolean updateHistory) { long startTime = System.currentTimeMillis(); execute(query.part); if (updateHistory) { updateHistory(query, startTime); } } @Override public CqlQueryResult execute(CqlQuery query, boolean updateHistory) { long startTime = System.currentTimeMillis(); CqlQueryResult result = executeIntern(query); if (updateHistory) { updateHistory(query, startTime); } return result; } private CqlQueryResult executeIntern(CqlQuery query) { LOG.debug("Executing CQL: {}", query); if (query.type == CqlQueryType.USE) { setActiveKeySpace(query); } ResultSet cqlResult = execute(query.part); if (cqlResult == null || cqlResult.isExhausted()) { return CqlQueryResult.EMPTY; } Map<String, CqlColumnType> typeMap = createTypeMap(query); Row firstRow = cqlResult.one(); CqlRowMetadata rowMetadata = extractRowMetadata(firstRow, typeMap); RowIterator rowIterator = new RowIterator(cqlResult.iterator(), firstRow); CqlQueryResult result = new CqlQueryResult(rowIterator, rowMetadata); return result; } private void updateHistory(CqlQuery query, long startTime) { long runTime = System.currentTimeMillis() - startTime; QueryEntry entry = new QueryEntry(query, runTime); historyService.addAndStore(entry); } private CqlRowMetadata extractRowMetadata(Row row, Map<String, CqlColumnType> typeMap) { // collect and count all columns ColumnDefinitions definitions = row.getColumnDefinitions(); ImmutableList.Builder<CqlExtendedColumnName> columnsBuild = ImmutableList.builder(); CqlPartitionKey partitionKey = null; for (int colIndex = 0; colIndex < definitions.size(); colIndex++) { if (colIndex > config.cassandra.columnsLimit) { LOG.debug("Reached columns limit: {}", config.cassandra.columnsLimit); break; } if (row.isNull(colIndex)) { continue; } DataType dataType = definitions.getType(colIndex); String columnNameText = definitions.getName(colIndex); CqlColumnType columnType = typeMap.get(columnNameText.toLowerCase()); if (columnType == null) { columnType = CqlColumnType.REGULAR; LOG.debug("Column type not found for: {} - using regular", columnNameText); } CqlExtendedColumnName columnName = new CqlExtendedColumnName(columnType, CqlDataType.create(dataType), columnNameText); if (columnType == CqlColumnType.PARTITION_KEY) { partitionKey = CqlPartitionKey.fromColumn(columnName); } columnsBuild.add(columnName); } CqlRowMetadata metadata = new CqlRowMetadata(columnsBuild.build(), partitionKey); return metadata; } private <T extends Comparable<?>> ImmutableSortedSet<T> map(Optional<ResultSet> result, String columnName, Function<String, T> mapper) { ImmutableSortedSet<T> res = StreamSupport.stream(result.get().spliterator(), false) .map(r -> r.getString(columnName)).map(StringUtils::trimToNull).filter(Objects::nonNull).map(mapper) .collect(toNaturalImmutableSortedSet()); return res; } protected ImmutableMap<String, CqlColumnType> createTypeMap(CqlQuery query) { Optional<CqlTable> table = extractTableName(CqlKeyword.Def.FROM.value, query); if (!table.isPresent()) { LOG.warn("Could not extract table name from: {}. Column type information is not available."); return ImmutableMap.of(); } Optional<ResultSet> result = executeSilent("select column_name, type from system.schema_columns where " + "columnfamily_name='" + table.get().part + "' allow filtering"); if (!result.isPresent()) { LOG.warn("Could not readIdentifier types for columns of table: " + table); return ImmutableMap.of(); } ImmutableMap<String, CqlColumnType> typesMap = StreamSupport.stream(result.get().spliterator(), false) .map(r -> new TypeTransfer(r.getString("type"), r.getString("column_name"))) .filter(TypeTransfer::isCorrect).collect(toImmutableMap(t -> t.name.toLowerCase(), t -> extractType(t.type))); return typesMap; } private final class TypeTransfer { public final String type; public final String name; public TypeTransfer(String type, String name) { this.type = StringUtils.trimToNull(type); this.name = StringUtils.trimToNull(name); } public boolean isCorrect() { return type != null && name != null; } @Override public String toString() { return "TypeTransfer [type=" + type + ", name=" + name + "]"; } } @Override public ImmutableSortedSet<CqlColumnName> findColumnNames(Optional<CqlTable> table) { StringBuilder buf = new StringBuilder("select column_name from system.schema_columns"); if (table.isPresent()) { buf.append(" where columnfamily_name='"); buf.append(table.get().partLc); buf.append("'"); } buf.append(" limit "); buf.append(config.cassandra.columnsLimit); buf.append(" allow filtering"); Optional<ResultSet> result = executeSilent(buf.toString()); if (!result.isPresent()) { LOG.warn("Cannot readIdentifier column names"); return ImmutableSortedSet.of(); } ImmutableSortedSet.Builder<CqlColumnName> cqlColumnNames = ImmutableSortedSet.naturalOrder(); for (Row row : result.get()) { String name = StringUtils.trimToNull(row.getString("column_name")); if (name == null) { continue; } cqlColumnNames.add(new CqlColumnName(CqlDataType.create(DataType.text()), name)); } loadPartitionKeyNames(table, cqlColumnNames); return cqlColumnNames.build(); } // required only for cassandra 1.x protected void loadPartitionKeyNames(Optional<CqlTable> table, ImmutableSortedSet.Builder<CqlColumnName> cqlColumnNames) { } protected CqlColumnType extractType(String typeText) { CqlColumnType type; try { type = CqlColumnType.valueOf(typeText.toUpperCase()); } catch (IllegalArgumentException ia) { LOG.warn("Read unsupported column type: {}", typeText, ia); type = CqlColumnType.REGULAR; } return type; } @Override public ImmutableSortedSet<CqlColumnName> findAllColumnNames() { return findColumnNames(Optional.empty()); } protected Optional<ResultSet> executeSilent(String cql) { LOG.debug("Executing: {}", cql); ResultSet resultSet = null; try { resultSet = session.getSession().execute(cql); } catch (Exception e) { LOG.warn("Error executing CQL: '" + cql + "', reason: " + e.getMessage()); LOG.debug(e.getMessage(), e); } return Optional.ofNullable(resultSet); } protected ResultSet execute(String cql) { LOG.debug("Executing: {} ", cql); ResultSet resultSet; try { resultSet = session.getSession().execute(cql); } catch (Exception e) { throw new QueryException("Error executing CQL: '" + cql + "', reason: " + e.getMessage(), e); } return resultSet; } private class RowIterator implements Iterator<Row> { private final Iterator<Row> wrapped; private final Row firstRow; private int read = 0; private RowIterator(Iterator<Row> wrapped, Row firstRow) { this.wrapped = wrapped; this.firstRow = firstRow; } @Override public boolean hasNext() { boolean has; if (read == 0 && firstRow != null) { has = true; } else { has = wrapped.hasNext(); } return has; } @Override public Row next() { Row next = read == 0 ? firstRow : wrapped.next(); if (next != null) { read++; } return next; } @Override public void remove() { throw new UnsupportedOperationException("Remove is not supported"); } } }