diff --git a/deploy.sh b/bin/deploy.sh similarity index 100% rename from deploy.sh rename to bin/deploy.sh diff --git a/bin/format.sh b/bin/format.sh new file mode 100755 index 0000000..10b2bf0 --- /dev/null +++ b/bin/format.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +for f in $(find ./src -name \*.java); do + echo Formatting $f + java -jar ./lib/google-java-format-1.3-all-deps.jar --replace $f +done + diff --git a/sign.sh b/bin/sign.sh similarity index 100% rename from sign.sh rename to bin/sign.sh diff --git a/lib/google-java-format-1.3-all-deps.jar b/lib/google-java-format-1.3-all-deps.jar new file mode 100644 index 0000000..859a3ca Binary files /dev/null and b/lib/google-java-format-1.3-all-deps.jar differ diff --git a/src/main/java/com/datastax/driver/core/querybuilder/IsNotNullClause.java b/src/main/java/com/datastax/driver/core/querybuilder/IsNotNullClause.java new file mode 100644 index 0000000..9ff2596 --- /dev/null +++ b/src/main/java/com/datastax/driver/core/querybuilder/IsNotNullClause.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 The Helenus 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 com.datastax.driver.core.querybuilder; + +import com.datastax.driver.core.CodecRegistry; +import java.util.List; + +public class IsNotNullClause extends Clause { + + final String name; + + public IsNotNullClause(String name) { + this.name = name; + } + + @Override + String name() { + return name; + } + + @Override + Object firstValue() { + return null; + } + + @Override + void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { + Utils.appendName(name, sb).append(" IS NOT NULL"); + } + + @Override + boolean containsBindMarker() { + return false; + } +} diff --git a/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java b/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java new file mode 100644 index 0000000..919becc --- /dev/null +++ b/src/main/java/com/datastax/driver/core/schemabuilder/CreateMaterializedView.java @@ -0,0 +1,53 @@ +package com.datastax.driver.core.schemabuilder; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.querybuilder.Select; + +public class CreateMaterializedView extends Create { + + private String viewName; + private Select.Where selection; + private String primaryKey; + private String clustering; + + public CreateMaterializedView( + String keyspaceName, String viewName, Select.Where selection, String primaryKey, String clustering) { + super(keyspaceName, viewName); + this.viewName = viewName; + this.selection = selection; + this.primaryKey = primaryKey; + this.clustering = clustering; + } + + public String getQueryString(CodecRegistry codecRegistry) { + return buildInternal(); + } + + public String buildInternal() { + StringBuilder createStatement = + new StringBuilder(STATEMENT_START).append("CREATE MATERIALIZED VIEW"); + if (ifNotExists) { + createStatement.append(" IF NOT EXISTS"); + } + createStatement.append(" "); + if (keyspaceName.isPresent()) { + createStatement.append(keyspaceName.get()).append("."); + } + createStatement.append(viewName); + createStatement.append(" AS "); + createStatement.append(selection.getQueryString()); + createStatement.setLength(createStatement.length() - 1); + createStatement.append(" "); + createStatement.append(primaryKey); + if (clustering != null) { + createStatement.append(" ").append(clustering); + } + createStatement.append(";"); + + return createStatement.toString(); + } + + public String toString() { + return buildInternal(); + } +} diff --git a/src/main/java/com/datastax/driver/core/schemabuilder/DropMaterializedView.java b/src/main/java/com/datastax/driver/core/schemabuilder/DropMaterializedView.java new file mode 100644 index 0000000..7eca05d --- /dev/null +++ b/src/main/java/com/datastax/driver/core/schemabuilder/DropMaterializedView.java @@ -0,0 +1,53 @@ +package com.datastax.driver.core.schemabuilder; + +import com.google.common.base.Optional; + +public class DropMaterializedView extends Drop { + + enum DroppedItem { + TABLE, + TYPE, + INDEX, + MATERIALIZED_VIEW + } + + private Optional keyspaceName = Optional.absent(); + private String itemName; + private boolean ifExists = true; + private final String itemType = "MATERIALIZED VIEW"; + + public DropMaterializedView(String keyspaceName, String viewName) { + this(keyspaceName, viewName, DroppedItem.MATERIALIZED_VIEW); + } + + private DropMaterializedView(String keyspaceName, String viewName, DroppedItem itemType) { + super(keyspaceName, viewName, Drop.DroppedItem.TABLE); + validateNotEmpty(keyspaceName, "Keyspace name"); + this.keyspaceName = Optional.fromNullable(keyspaceName); + this.itemName = viewName; + } + + /** + * Add the 'IF EXISTS' condition to this DROP statement. + * + * @return this statement. + */ + public Drop ifExists() { + this.ifExists = true; + return this; + } + + @Override + public String buildInternal() { + StringBuilder dropStatement = new StringBuilder("DROP " + itemType + " "); + if (ifExists) { + dropStatement.append("IF EXISTS "); + } + if (keyspaceName.isPresent()) { + dropStatement.append(keyspaceName.get()).append("."); + } + + dropStatement.append(itemName); + return dropStatement.toString(); + } +} diff --git a/src/main/java/net/helenus/core/HelenusSession.java b/src/main/java/net/helenus/core/HelenusSession.java index 8fe9f90..601a773 100644 --- a/src/main/java/net/helenus/core/HelenusSession.java +++ b/src/main/java/net/helenus/core/HelenusSession.java @@ -182,11 +182,11 @@ public final class HelenusSession extends AbstractSessionOperations implements C return metadata; } - public synchronized T begin() { + public synchronized UnitOfWork begin() { return begin(null); } - public synchronized T begin(T parent) { + public synchronized UnitOfWork begin(UnitOfWork parent) { try { Class clazz = unitOfWorkClass; Constructor ctor = @@ -195,7 +195,7 @@ public final class HelenusSession extends AbstractSessionOperations implements C if (parent != null) { parent.addNestedUnitOfWork(uow); } - return (T) uow.begin(); + return uow.begin(); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException diff --git a/src/main/java/net/helenus/core/SchemaUtil.java b/src/main/java/net/helenus/core/SchemaUtil.java index e2bb681..5e33b3d 100644 --- a/src/main/java/net/helenus/core/SchemaUtil.java +++ b/src/main/java/net/helenus/core/SchemaUtil.java @@ -17,16 +17,22 @@ package net.helenus.core; import com.datastax.driver.core.*; import com.datastax.driver.core.IndexMetadata; +import com.datastax.driver.core.querybuilder.IsNotNullClause; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.schemabuilder.*; import com.datastax.driver.core.schemabuilder.Create.Options; import java.util.*; import java.util.stream.Collectors; +import net.helenus.core.reflect.HelenusPropertyNode; import net.helenus.mapping.*; import net.helenus.mapping.ColumnType; +import net.helenus.mapping.annotation.ClusteringColumn; import net.helenus.mapping.type.OptionalColumnMetadata; import net.helenus.support.CqlUtil; import net.helenus.support.HelenusMappingException; + public final class SchemaUtil { private SchemaUtil() {} @@ -143,6 +149,79 @@ public final class SchemaUtil { return SchemaBuilder.dropType(type.getTypeName()).ifExists(); } + public static SchemaStatement createMaterializedView( + String keyspace, String viewName, HelenusEntity entity) { + if (entity.getType() != HelenusEntityType.VIEW) { + throw new HelenusMappingException("expected view entity " + entity); + } + + if (entity == null) { + throw new HelenusMappingException("no entity or table to select data"); + } + + List props = new ArrayList(); + entity + .getOrderedProperties() + .stream() + .map(p -> new HelenusPropertyNode(p, Optional.empty())) + .forEach(p -> props.add(p)); + + Select.Selection selection = QueryBuilder.select(); + + for (HelenusPropertyNode prop : props) { + String columnName = prop.getColumnName(); + selection = selection.column(columnName); + } + Class iface = entity.getMappingInterface(); + String tableName = Helenus.entity(iface.getInterfaces()[0]).getName().toCql(); + Select.Where where = selection.from(tableName).where(); + List p = new ArrayList(props.size()); + List c = new ArrayList(props.size()); + List o = new ArrayList(props.size()); + + for (HelenusPropertyNode prop : props) { + String columnName = prop.getColumnName(); + switch (prop.getProperty().getColumnType()) { + case PARTITION_KEY: + p.add(columnName); + where = where.and(new IsNotNullClause(columnName)); + break; + + case CLUSTERING_COLUMN: + c.add(columnName); + where = where.and(new IsNotNullClause(columnName)); + + ClusteringColumn clusteringColumn = prop.getProperty().getGetterMethod().getAnnotation(ClusteringColumn.class); + if (clusteringColumn != null && clusteringColumn.ordering() != null) { + o.add(columnName + " " + clusteringColumn.ordering().cql()); + } + break; + default: + break; + } + } + + String primaryKey = + "PRIMARY KEY (" + + ((p.size() > 1) ? "(" + String.join(", ", p) + ")" : p.get(0)) + + ((c.size() > 0) + ? ", " + ((c.size() > 1) ? "(" + String.join(", ", c) + ")" : c.get(0)) + : "") + + ")"; + + String clustering = ""; + if (o.size() > 0) { + clustering = "WITH CLUSTERING ORDER BY (" + String.join(", ", o) + ")"; + } + return new CreateMaterializedView(keyspace, viewName, where, primaryKey, clustering) + .ifNotExists(); + } + + public static SchemaStatement dropMaterializedView( + String keyspace, String viewName, HelenusEntity entity) { + return new DropMaterializedView(keyspace, viewName); + } + public static SchemaStatement createTable(HelenusEntity entity) { if (entity.getType() != HelenusEntityType.TABLE) { diff --git a/src/main/java/net/helenus/core/SessionInitializer.java b/src/main/java/net/helenus/core/SessionInitializer.java index 27a0312..f6ecfd6 100644 --- a/src/main/java/net/helenus/core/SessionInitializer.java +++ b/src/main/java/net/helenus/core/SessionInitializer.java @@ -277,7 +277,7 @@ public final class SessionInitializer extends AbstractSessionOperations { } DslExportable dsl = (DslExportable) Helenus.dsl(iface); - dsl.setMetadata(session.getCluster().getMetadata()); + dsl.setCassandraMetadataForHelenusSesion(session.getCluster().getMetadata()); sessionRepository.add(dsl); }); @@ -287,8 +287,16 @@ public final class SessionInitializer extends AbstractSessionOperations { switch (autoDdl) { case CREATE_DROP: - // Drop tables first, otherwise a `DROP TYPE ...` will fail as the type is still referenced - // by a table. + // Drop view first, otherwise a `DROP TABLE ...` will fail as the type is still referenced + // by a view. + sessionRepository + .entities() + .stream() + .filter(e -> e.getType() == HelenusEntityType.VIEW) + .forEach(e -> tableOps.dropView(e)); + + // Drop tables second, before DROP TYPE otherwise a `DROP TYPE ...` will fail as the type is + // still referenced by a table. sessionRepository .entities() .stream() @@ -307,6 +315,12 @@ public final class SessionInitializer extends AbstractSessionOperations { .filter(e -> e.getType() == HelenusEntityType.TABLE) .forEach(e -> tableOps.createTable(e)); + sessionRepository + .entities() + .stream() + .filter(e -> e.getType() == HelenusEntityType.VIEW) + .forEach(e -> tableOps.createView(e)); + break; case VALIDATE: @@ -317,16 +331,29 @@ public final class SessionInitializer extends AbstractSessionOperations { .stream() .filter(e -> e.getType() == HelenusEntityType.TABLE) .forEach(e -> tableOps.validateTable(getTableMetadata(e), e)); + break; case UPDATE: eachUserTypeInOrder(userTypeOps, e -> userTypeOps.updateUserType(getUserType(e), e)); + sessionRepository + .entities() + .stream() + .filter(e -> e.getType() == HelenusEntityType.VIEW) + .forEach(e -> tableOps.dropView(e)); + sessionRepository .entities() .stream() .filter(e -> e.getType() == HelenusEntityType.TABLE) .forEach(e -> tableOps.updateTable(getTableMetadata(e), e)); + + sessionRepository + .entities() + .stream() + .filter(e -> e.getType() == HelenusEntityType.VIEW) + .forEach(e -> tableOps.createView(e)); break; } diff --git a/src/main/java/net/helenus/core/TableOperations.java b/src/main/java/net/helenus/core/TableOperations.java index f9c83d0..3c602d6 100644 --- a/src/main/java/net/helenus/core/TableOperations.java +++ b/src/main/java/net/helenus/core/TableOperations.java @@ -35,14 +35,11 @@ public final class TableOperations { } public void createTable(HelenusEntity entity) { - sessionOps.execute(SchemaUtil.createTable(entity), true); - executeBatch(SchemaUtil.createIndexes(entity)); } public void dropTable(HelenusEntity entity) { - sessionOps.execute(SchemaUtil.dropTable(entity), true); } @@ -50,7 +47,10 @@ public final class TableOperations { if (tmd == null) { throw new HelenusException( - "table not exists " + entity.getName() + "for entity " + entity.getMappingInterface()); + "table does not exists " + + entity.getName() + + "for entity " + + entity.getMappingInterface()); } List list = SchemaUtil.alterTable(tmd, entity, dropUnusedColumns); @@ -67,7 +67,31 @@ public final class TableOperations { } public void updateTable(TableMetadata tmd, HelenusEntity entity) { + if (tmd == null) { + createTable(entity); + return; + } + executeBatch(SchemaUtil.alterTable(tmd, entity, dropUnusedColumns)); + executeBatch(SchemaUtil.alterIndexes(tmd, entity, dropUnusedIndexes)); + } + + public void createView(HelenusEntity entity) { + sessionOps.execute( + SchemaUtil.createMaterializedView( + sessionOps.usingKeyspace(), entity.getName().toCql(), entity), + true); + // executeBatch(SchemaUtil.createIndexes(entity)); NOTE: Unfortunately C* 3.10 does not yet support 2i on materialized views. + } + + public void dropView(HelenusEntity entity) { + sessionOps.execute( + SchemaUtil.dropMaterializedView( + sessionOps.usingKeyspace(), entity.getName().toCql(), entity), + true); + } + + public void updateView(TableMetadata tmd, HelenusEntity entity) { if (tmd == null) { createTable(entity); return; diff --git a/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java b/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java index 221fcf7..0827105 100644 --- a/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java +++ b/src/main/java/net/helenus/core/operation/AbstractOptionalOperation.java @@ -21,13 +21,11 @@ import com.datastax.driver.core.ResultSet; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; - import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; - import net.helenus.core.AbstractSessionOperations; import net.helenus.core.Filter; import net.helenus.core.Helenus; @@ -173,5 +171,4 @@ public abstract class AbstractOptionalOperation>supplyAsync(() -> sync(uow)); } - } diff --git a/src/main/java/net/helenus/core/operation/InsertOperation.java b/src/main/java/net/helenus/core/operation/InsertOperation.java index 3ee0c8a..53abaee 100644 --- a/src/main/java/net/helenus/core/operation/InsertOperation.java +++ b/src/main/java/net/helenus/core/operation/InsertOperation.java @@ -83,9 +83,18 @@ public final class InsertOperation extends AbstractOperation keys = (mutations == null) ? null : mutations; for (HelenusProperty prop : properties) { + boolean addProp = false; - if (keys == null || keys.contains(prop.getPropertyName())) { + switch (prop.getColumnType()) { + case PARTITION_KEY: + case CLUSTERING_COLUMN: + addProp = true; + break; + default: + addProp = (keys == null || keys.contains(prop.getPropertyName())); + } + if (addProp) { Object value = BeanColumnValueProvider.INSTANCE.getColumnValue(pojo, -1, prop); value = sessionOps.getValuePreparer().prepareColumnValue(value, prop); diff --git a/src/main/java/net/helenus/core/operation/SelectOperation.java b/src/main/java/net/helenus/core/operation/SelectOperation.java index 4204c29..cfc54f2 100644 --- a/src/main/java/net/helenus/core/operation/SelectOperation.java +++ b/src/main/java/net/helenus/core/operation/SelectOperation.java @@ -22,6 +22,7 @@ import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select.Selection; import com.datastax.driver.core.querybuilder.Select.Where; +import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import java.util.*; import java.util.function.Function; @@ -47,6 +48,7 @@ public final class SelectOperation extends AbstractFilterStreamOperation ordering = null; protected Integer limit = null; protected boolean allowFiltering = false; + protected String alternateTableName = null; @SuppressWarnings("unchecked") public SelectOperation(AbstractSessionOperations sessionOperations) { @@ -128,6 +130,19 @@ public final class SelectOperation extends AbstractFilterStreamOperation SelectOperation from(Class materializedViewClass) { + Objects.requireNonNull(materializedViewClass); + HelenusEntity entity = Helenus.entity(materializedViewClass); + this.alternateTableName = entity.getName().toCql(); + this.props.clear(); + entity + .getOrderedProperties() + .stream() + .map(p -> new HelenusPropertyNode(p, Optional.empty())) + .forEach(p -> this.props.add(p)); + return this; + } + public SelectFirstOperation single() { limit(1); return new SelectFirstOperation(this); @@ -231,6 +246,7 @@ public final class SelectOperation extends AbstractFilterStreamOperation extends AbstractFilterStreamOperation implements InvocationHandler { this.classLoader = classLoader; } - public void setMetadata(Metadata metadata) { + public void setCassandraMetadataForHelenusSesion(Metadata metadata) { if (metadata != null) { this.metadata = metadata; entity = init(metadata); @@ -130,7 +130,7 @@ public class DslInvocationHandler implements InvocationHandler { && args.length == 1 && args[0] instanceof Metadata) { if (metadata == null) { - this.setMetadata((Metadata) args[0]); + this.setCassandraMetadataForHelenusSesion((Metadata) args[0]); } return null; } diff --git a/src/main/java/net/helenus/mapping/HelenusEntityType.java b/src/main/java/net/helenus/mapping/HelenusEntityType.java index 1d93991..2ef8d63 100644 --- a/src/main/java/net/helenus/mapping/HelenusEntityType.java +++ b/src/main/java/net/helenus/mapping/HelenusEntityType.java @@ -17,6 +17,7 @@ package net.helenus.mapping; public enum HelenusEntityType { TABLE, + VIEW, TUPLE, UDT; } diff --git a/src/main/java/net/helenus/mapping/HelenusMappingEntity.java b/src/main/java/net/helenus/mapping/HelenusMappingEntity.java index b7fd8c3..ceb9722 100644 --- a/src/main/java/net/helenus/mapping/HelenusMappingEntity.java +++ b/src/main/java/net/helenus/mapping/HelenusMappingEntity.java @@ -185,6 +185,9 @@ public final class HelenusMappingEntity implements HelenusEntity { case TABLE: return MappingUtil.getTableName(iface, true); + case VIEW: + return MappingUtil.getViewName(iface, true); + case TUPLE: return IdentityName.of(MappingUtil.getDefaultEntityName(iface), false); @@ -201,6 +204,8 @@ public final class HelenusMappingEntity implements HelenusEntity { if (null != iface.getDeclaredAnnotation(Table.class)) { return HelenusEntityType.TABLE; + } else if (null != iface.getDeclaredAnnotation(MaterializedView.class)) { + return HelenusEntityType.VIEW; } else if (null != iface.getDeclaredAnnotation(Tuple.class)) { return HelenusEntityType.TUPLE; } else if (null != iface.getDeclaredAnnotation(UDT.class)) { diff --git a/src/main/java/net/helenus/mapping/MappingUtil.java b/src/main/java/net/helenus/mapping/MappingUtil.java index 197acd3..cf84c05 100644 --- a/src/main/java/net/helenus/mapping/MappingUtil.java +++ b/src/main/java/net/helenus/mapping/MappingUtil.java @@ -25,10 +25,7 @@ import javax.validation.ConstraintValidator; import net.helenus.core.Getter; import net.helenus.core.Helenus; import net.helenus.core.reflect.*; -import net.helenus.mapping.annotation.Index; -import net.helenus.mapping.annotation.Table; -import net.helenus.mapping.annotation.Tuple; -import net.helenus.mapping.annotation.UDT; +import net.helenus.mapping.annotation.*; import net.helenus.support.DslPropertyException; import net.helenus.support.HelenusMappingException; @@ -172,6 +169,28 @@ public final class MappingUtil { return udt != null; } + public static IdentityName getViewName(Class iface, boolean required) { + + String viewName = null; + boolean forceQuote = false; + + MaterializedView view = iface.getDeclaredAnnotation(MaterializedView.class); + + if (view != null) { + viewName = view.value(); + forceQuote = view.forceQuote(); + + } else if (required) { + throw new HelenusMappingException("entity must have annotation @Table " + iface); + } + + if (viewName == null || viewName.isEmpty()) { + viewName = getDefaultEntityName(iface); + } + + return new IdentityName(viewName, forceQuote); + } + public static IdentityName getTableName(Class iface, boolean required) { String tableName = null; @@ -222,6 +241,7 @@ public final class MappingUtil { } if (iface.getDeclaredAnnotation(Table.class) != null + || iface.getDeclaredAnnotation(MaterializedView.class) != null || iface.getDeclaredAnnotation(UDT.class) != null || iface.getDeclaredAnnotation(Tuple.class) != null) { diff --git a/src/main/java/net/helenus/mapping/annotation/CoveringIndex.java b/src/main/java/net/helenus/mapping/annotation/CoveringIndex.java new file mode 100644 index 0000000..6271e8e --- /dev/null +++ b/src/main/java/net/helenus/mapping/annotation/CoveringIndex.java @@ -0,0 +1,50 @@ +package net.helenus.mapping.annotation; + +import java.lang.annotation.*; + +/** + * CoveringIndex annotation is using under the specific column or method in entity interface + * with @Table annotation. + * + *

A corresponding materialized view will be created based on the underline @Table for the + * specific column. + * + *

This is useful when you need to perform IN or SORT/ORDER-BY queries and to do so you'll need + * different materialized table on disk in Cassandra. + * + *

For each @Table annotated interface Helenus will create/update/verify Cassandra Materialized + * Views and some indexes if needed on startup. + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface CoveringIndex { + + /** + * Defined the name of the index. By default the entity name with column name as suffix. + * + * @return name of the covering index + */ + String name() default ""; + + /** + * Set of fields in this entity to replicate in the index. + * + * @return array of the string names of the fields. + */ + String[] covering() default ""; + + /** + * Set of fields to use as the partition keys for this projection. + * + * @return array of the string names of the fields. + */ + String[] partitionKeys() default ""; + + /** + * Set of fields to use as the clustering columns for this projection. + * + * @return array of the string names of the fields. + */ + String[] clusteringColumns() default ""; +} diff --git a/src/main/java/net/helenus/mapping/annotation/MaterializedView.java b/src/main/java/net/helenus/mapping/annotation/MaterializedView.java new file mode 100644 index 0000000..112d8ce --- /dev/null +++ b/src/main/java/net/helenus/mapping/annotation/MaterializedView.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 The Helenus 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 net.helenus.mapping.annotation; + +import java.lang.annotation.*; + +/** + * Materialized alternate view of another Entity annotation + * + *

MaterializedView annotation is used to define different mapping to some other Table interface + * + *

This is useful when you need to perform IN or SORT/ORDER-BY queries and to do so you'll need + * different materialized table on disk in Cassandra. + * + *

For each @Table annotated interface Helenus will create/update/verify Cassandra Materialized + * Views and some indexes if needed on startup. + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface MaterializedView { + + /** + * Default value is the SimpleName of the interface normalized to underscore + * + * @return name of the type + */ + String value() default ""; + + /** + * For reserved words in Cassandra we need quotation in CQL queries. This property marks that the + * name of the type needs to be quoted. + * + *

Default value is false, we are quoting only selected names. + * + * @return true if name have to be quoted + */ + boolean forceQuote() default false; +} diff --git a/src/test/java/net/helenus/test/integration/core/views/Cyclist.java b/src/test/java/net/helenus/test/integration/core/views/Cyclist.java new file mode 100644 index 0000000..0ab5a73 --- /dev/null +++ b/src/test/java/net/helenus/test/integration/core/views/Cyclist.java @@ -0,0 +1,29 @@ +package net.helenus.test.integration.core.views; + +import java.util.Date; +import java.util.UUID; +import net.helenus.mapping.annotation.ClusteringColumn; +import net.helenus.mapping.annotation.CoveringIndex; +import net.helenus.mapping.annotation.PartitionKey; +import net.helenus.mapping.annotation.Table; + +@Table +@CoveringIndex( + name = "cyclist_mv", + covering = {"age", "birthday", "country"}, + partitionKeys = {"age", "cid"}, + clusteringColumns = {} +) +public interface Cyclist { + @ClusteringColumn + UUID cid(); + + String name(); + + @PartitionKey + int age(); + + Date birthday(); + + String country(); +} diff --git a/src/test/java/net/helenus/test/integration/core/views/CyclistsByAge.java b/src/test/java/net/helenus/test/integration/core/views/CyclistsByAge.java new file mode 100644 index 0000000..02b414c --- /dev/null +++ b/src/test/java/net/helenus/test/integration/core/views/CyclistsByAge.java @@ -0,0 +1,21 @@ +package net.helenus.test.integration.core.views; + +import java.util.Date; +import java.util.UUID; + +import net.helenus.mapping.OrderingDirection; +import net.helenus.mapping.annotation.*; + +@MaterializedView +public interface CyclistsByAge extends Cyclist { + @PartitionKey + UUID cid(); + + @ClusteringColumn(ordering = OrderingDirection.ASC) + int age(); + + Date birthday(); + + @Index + String country(); +} diff --git a/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java b/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java new file mode 100644 index 0000000..41c8eff --- /dev/null +++ b/src/test/java/net/helenus/test/integration/core/views/MaterializedViewTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 The Helenus 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 net.helenus.test.integration.core.views; + +import static net.helenus.core.Query.eq; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import net.helenus.core.Helenus; +import net.helenus.core.HelenusSession; +import net.helenus.test.integration.build.AbstractEmbeddedCassandraTest; +import org.junit.BeforeClass; +import org.junit.Test; + +// See: https://docs.datastax.com/en/cql/3.3/cql/cql_using/useCreateMV.html +// https://docs.datastax.com/en/cql/3.3/cql/cql_reference/cqlCreateMaterializedView.html +// https://www.datastax.com/dev/blog/materialized-view-performance-in-cassandra-3-x +// https://cassandra-zone.com/materialized-views/ +public class MaterializedViewTest extends AbstractEmbeddedCassandraTest { + + static Cyclist cyclist; + static HelenusSession session; + + static Date dateFromString(String dateInString) { + SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy"); + try { + return formatter.parse(dateInString); + } catch (ParseException e) { + e.printStackTrace(); + } + return null; + } + + @BeforeClass + public static void beforeTest() { + session = + Helenus.init(getSession()) + .showCql() + .add(Cyclist.class) + .add(CyclistsByAge.class) + .autoCreateDrop() + .get(); + cyclist = session.dsl(Cyclist.class); + + session + .insert(cyclist) + .value(cyclist::cid, UUID.randomUUID()) + .value(cyclist::age, 18) + .value(cyclist::birthday, dateFromString("1997-02-08")) + .value(cyclist::country, "Netherlands") + .value(cyclist::name, "Pascal EENKHOORN") + .sync(); + } + + @Test + public void testMv() throws Exception { + session + .select(Cyclist.class) + .from(CyclistsByAge.class) + .where(cyclist::age, eq(18)) + .sync(); + } +}