diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlClusterFactoryBean.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlClusterFactoryBean.java deleted file mode 100644 index 9f0ae2430..000000000 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraCqlClusterFactoryBean.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2013-2020 the 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 - * - * https://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.springframework.data.cassandra.config; - -/** - * Spring CQL extension of CassandraCqlClusterFactoryBean. This class exists only in the name of symmetry, based on the - * other CassandraData*FactoryBean classes. - * - * @author Matthew T. Adams - * @author Mark Paluch - */ -public class CassandraCqlClusterFactoryBean extends CassandraClusterFactoryBean {} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/AsyncResultStream.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/AsyncResultStream.java new file mode 100644 index 000000000..684be5a7b --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/AsyncResultStream.java @@ -0,0 +1,235 @@ +/* + * Copyright 2019 the 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 + * + * https://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.springframework.data.cassandra.core.cql; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Collector; + +import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.Row; + +/** + * Asynchronous supplied sequence of elements supporting sequential operations over a {@link AsyncResultSet a result + * set}. An asynchronous stream represents a pipeline of operations to process a {@link AsyncResultSet}. + * + * @author Mark Paluch + * @since 3.0 + */ +class AsyncResultStream { + + private final AsyncResultSet resultSet; + + private final RowMapper mapper; + + private AsyncResultStream(AsyncResultSet resultSet, RowMapper mapper) { + this.resultSet = resultSet; + this.mapper = mapper; + } + + /** + * Creates a {@link AsyncResultStream} given {@link AsyncResultSet}. + * + * @param resultSet the result set to process. + * @return a new {@link AsyncResultStream} instance. + */ + static AsyncResultStream from(AsyncResultSet resultSet) { + + Assert.notNull(resultSet, "AsyncResultSet must not be null"); + + return new AsyncResultStream<>(resultSet, (row, rowNum) -> row); + } + + /** + * Returns a stream consisting of the results of applying the given function to the elements of this stream. + *

+ * This is an intermediate operation. + * + * @param The element type of the new stream + * @param mapper a non-interfering, stateless {@link RowMapper}. + */ + AsyncResultStream map(RowMapper mapper) { + + Assert.notNull(mapper, "RowMapper must not be null"); + + return new AsyncResultStream<>(resultSet, mapper); + } + + /** + * Performs a mutable reduction operation on the elements of this stream using a {@link Collector} resulting in a + * {@link ListenableFuture}. + *

+ * This is a terminal operation. + * + * @param the type of the result + * @param the intermediate accumulation type of the {@link Collector} + * @param collector the {@link Collector} describing the reduction + * @return the result of the reduction + */ + ListenableFuture collect(Collector collector) { + + Assert.notNull(collector, "Collector must not be null"); + + SettableListenableFuture future = new SettableListenableFuture<>(); + CollectState collectState = new CollectState<>(collector); + + collectState.collectAsync(future, this.resultSet); + + return future; + } + + /** + * Performs an action for each element of this stream. This method returns a {@link ListenableFuture} that completes + * without a value ({@code null}) once all elements have been processed. + *

+ * This is a terminal operation. + *

+ * If the action accesses shared state, it is responsible for providing the required synchronization. + * + * @param action a non-interfering action to perform on the elements. + */ + ListenableFuture forEach(Consumer action) { + + Assert.notNull(action, "Action must not be null"); + + SettableListenableFuture future = new SettableListenableFuture<>(); + ForwardLoopState loopState = new ForwardLoopState(action); + + loopState.forEachAsync(future, this.resultSet); + + return future; + } + + /** + * State object for forward-looping using {@code forEach}. + */ + class ForwardLoopState { + + private final AtomicInteger rowNumber = new AtomicInteger(); + private final Consumer consumer; + + ForwardLoopState(Consumer consumer) { + this.consumer = consumer; + } + + void peekRow(Iterable rows) { + rows.forEach(row -> consumer.accept(mapper.mapRow(row, rowNumber.incrementAndGet()))); + } + + /** + * Recursive async iteration. + * + * @param target + * @param resultSet + */ + void forEachAsync(SettableListenableFuture target, AsyncResultSet resultSet) { + + if (target.isCancelled()) { + return; + } + + try { + peekRow(resultSet.currentPage()); + } catch (RuntimeException e) { + target.setException(e); + return; + } + + if (!resultSet.hasMorePages()) { + target.set(null); + } else { + + CompletionStage nextPage = resultSet.fetchNextPage(); + + nextPage.whenComplete((nextResultSet, throwable) -> { + + if (throwable != null) { + target.setException(throwable); + } else { + forEachAsync(target, nextResultSet); + } + }); + } + } + } + + /** + * State object for collecting rows using {@code collect}. + */ + class CollectState { + + private final AtomicInteger rowNumber = new AtomicInteger(); + private volatile A intermediate; + private final Collector collector; + + CollectState(Collector collector) { + this.collector = collector; + this.intermediate = collector.supplier().get(); + } + + void collectPage(Iterable rows) { + + for (Row row : rows) { + collector.accumulator().accept(intermediate, mapper.mapRow(row, rowNumber.incrementAndGet())); + } + } + + R finish() { + return collector.finisher().apply(intermediate); + } + + /** + * Recursive collection. + * + * @param target + * @param resultSet + */ + void collectAsync(SettableListenableFuture target, AsyncResultSet resultSet) { + + if (target.isCancelled()) { + return; + } + + try { + collectPage(resultSet.currentPage()); + } catch (RuntimeException e) { + target.setException(e); + return; + } + + if (!resultSet.hasMorePages()) { + target.set(finish()); + } else { + + CompletionStage nextPage = resultSet.fetchNextPage(); + + nextPage.whenComplete((nextResultSet, throwable) -> { + + if (throwable != null) { + target.setException(throwable); + } else { + collectAsync(target, nextResultSet); + } + }); + } + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/StatementBuilder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/StatementBuilder.java new file mode 100644 index 000000000..053ab6d4c --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/StatementBuilder.java @@ -0,0 +1,267 @@ +/* + * Copyright 2019 the 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 + * + * https://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.springframework.data.cassandra.core.cql.util; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; + +import org.springframework.util.Assert; + +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; + +/** + * Functional builder for Cassandra {@link BuildableQuery statements}. Statements are built by applying + * {@link UnaryOperator builder functions} that get applied when {@link #build() building the actual + * {@link SimpleStatement statement}. The {@code StatmentBuilder} provides a mutable container for statement creation + * allowing a functional declaration of actions that are necessary to build a statement. This class helps building CQL + * statements as a {@link BuildableQuery} classes are typically immutable and require return value tracking across + * methods that want to apply modifications to a statment. + *

+ * Building a statement consists of three phases: + *

    + *
  1. Creation of the {@link StatementBuilder} with a {@link BuildableQuery query stub}
  2. + *
  3. Functional declaration applying {@link UnaryOperator builder functions}, {@link BindFunction bind functions} and + * {@link Consumer on build signals}
  4. + *
  5. Building the statement using {@link #build()}
  6. + *
+ * The initial {@link BuildableQuery query stub} is used as base object for all built queries. Builder functions are + * applied each time a statement is built allowing to build multiple statement instances while evolving the actual + * statement. + *

+ * The builder can be used for structural evolution and value evolution of statements. Values are bound through + * {@link BindFunction binding functions} that accept the statement and a {@link TermFactory}. Values can be bound + * inline or through bind markers when {@link #build(ParameterHandling, CodecRegistry) building} the statement. All + * functions are applied in the order of their declaration. + *

+ * All methods returning {@link StatementBuilder} point to the same instance. This class is intended for internal use. + * + * @author Mark Paluch + * @param Statement type + * @since 3.0 + */ +public class StatementBuilder { + + private S statement; + + private List> queryActions = new ArrayList<>(); + private List> onBuild = new ArrayList<>(); + + private StatementBuilder(S statement) { + this.statement = statement; + } + + /** + * Create a new {@link StatementBuilder} with the given {@link BuildableQuery query stub}. The stub is used as base + * for the built query so each query inherits properties of this stub. + * + * @param stub the query stub to use. + * @param query type. + * @return the {@link StatementBuilder} for the {@link BuildableQuery query stub}. + */ + public static StatementBuilder of(S stub) { + + Assert.notNull(stub, "Query stub must not be null"); + + return new StatementBuilder<>(stub); + } + + /** + * Apply a {@link BindFunction} to the statement. Bind functions are applied on {@link #build()}. + * + * @param action the bind function to be applied to the statement. + * @return {@code this} {@link StatementBuilder}. + */ + public StatementBuilder bind(BindFunction action) { + + Assert.notNull(action, "BindFunction must not be null"); + + queryActions.add(action::bind); + return this; + } + + /** + * Apply a {@link UnaryOperator builder function} to the statement. Builder functions are applied on {@link #build()}. + * + * @param action the builder function to be applied to the statement. + * @return {@code this} {@link StatementBuilder}. + */ + public StatementBuilder apply(UnaryOperator action) { + + Assert.notNull(action, "BindFunction must not be null"); + + queryActions.add((source, termFactory) -> action.apply(source)); + return this; + } + + /** + * Add behavior when the statement is built. The {@link Consumer} gets invoked with a {@link SimpleStatementBuilder} + * allowing association of the final statement with additional settings. The {@link Consumer} is applied on + * {@link #build()}. + * + * @param action the {@link Consumer} function that gets notified on {@link #build()}. + * @return {@code this} {@link StatementBuilder}. + */ + public StatementBuilder onBuild(Consumer action) { + + Assert.notNull(action, "Consumer must not be null"); + + onBuild.add(action); + return this; + } + + /** + * Build a {@link SimpleStatement statement} by applying builder and bind functions using the default + * {@link CodecRegistry} and {@link ParameterHandling#INLINE} parameter rendering. + * + * @return the built {@link SimpleStatement}. + */ + public SimpleStatement build() { + return build(ParameterHandling.INLINE, CodecRegistry.DEFAULT); + } + + /** + * Build a {@link SimpleStatement statement} by applying builder and bind functions using the given + * {@link ParameterHandling}. + * + * @param parameterHandling + * @return the built {@link SimpleStatement}. + */ + public SimpleStatement build(ParameterHandling parameterHandling) { + return build(parameterHandling, CodecRegistry.DEFAULT); + } + + /** + * Build a {@link SimpleStatement statement} by applying builder and bind functions using the given + * {@link CodecRegistry} and {@link ParameterHandling}. + * + * @param parameterHandling + * @param codecRegistry + * @return the built {@link SimpleStatement}. + */ + public SimpleStatement build(ParameterHandling parameterHandling, CodecRegistry codecRegistry) { + + Assert.notNull(parameterHandling, "ParameterHandling must not be null"); + Assert.notNull(codecRegistry, "CodecRegistry must not be null"); + + S statement = this.statement; + + if (parameterHandling == ParameterHandling.INLINE) { + + TermFactory termFactory = value -> QueryBuilder.literal(value, codecRegistry); + + for (BuilderRunnable runnable : queryActions) { + statement = runnable.run(statement, termFactory); + } + + return onBuild(statement.builder()).build(); + } + + if (parameterHandling == ParameterHandling.BY_INDEX) { + + List values = new ArrayList<>(); + TermFactory termFactory = value -> { + values.add(value); + return QueryBuilder.bindMarker(); + }; + + for (BuilderRunnable runnable : queryActions) { + statement = runnable.run(statement, termFactory); + } + + return onBuild(statement.builder().addPositionalValues(values)).build(); + } + + if (parameterHandling == ParameterHandling.BY_NAME) { + + Map values = new LinkedHashMap<>(); + TermFactory termFactory = value -> { + String name = "p" + values.size(); + values.put(name, value); + return QueryBuilder.bindMarker(name); + }; + + for (BuilderRunnable runnable : queryActions) { + statement = runnable.run(statement, termFactory); + } + + SimpleStatementBuilder builder = statement.builder(); + values.forEach(builder::addNamedValue); + + return onBuild(builder).build(); + } + + throw new UnsupportedOperationException(String.format("ParameterHandling %s not supported", parameterHandling)); + } + + private SimpleStatementBuilder onBuild(SimpleStatementBuilder statementBuilder) { + + onBuild.forEach(it -> it.accept(statementBuilder)); + + return statementBuilder; + } + + /** + * Binding function. This function gets called with the current statement and {@link TermFactory}. + * + * @param + */ + @FunctionalInterface + public interface BindFunction { + + /** + * Apply a binding operation on the {@link BuildableQuery statement} and return the modified statement instance. + * + * @param statement the initial statement instance. + * @param factory factory to create {@link com.datastax.oss.driver.api.querybuilder.term.Term} objects. + * @return the modified statement instance. + */ + S bind(S statement, TermFactory factory); + } + + @FunctionalInterface + interface BuilderRunnable { + S run(S source, TermFactory termFactory); + } + + /** + * Enumeration to represent how parameters are rendered. + */ + public enum ParameterHandling { + + /** + * CQL inline rendering as literals. + */ + INLINE, + + /** + * Index-based bind markers. + */ + BY_INDEX, + + /** + * Named bind markers. + */ + BY_NAME; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/TermFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/TermFactory.java new file mode 100644 index 000000000..1b60d5a9c --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/TermFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 the 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 + * + * https://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.springframework.data.cassandra.core.cql.util; + +import org.springframework.lang.Nullable; + +import com.datastax.oss.driver.api.querybuilder.term.Term; + +/** + * Factory for {@link Term} objects encapsulating a binding {@code value}. Classes implementing this factory interface + * may return inline terms to render values as part of the query string, or bind markers to supply parameters on + * statement creation/parameter binding. + *

+ * A {@ling TermFactory} is typically used with {@link StatementBuilder}. + * + * @author Mark Paluch + * @since 3.0 + */ +@FunctionalInterface +public interface TermFactory { + + /** + * Create a {@link Term} for the given {@code value}. + * + * @param value the value to bind, can be {@literal null}. + * @return the {@link Term} for the given {@code value}. + */ + Term create(@Nullable Object value); +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/package-info.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/package-info.java new file mode 100644 index 000000000..89a275c0d --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/util/package-info.java @@ -0,0 +1,7 @@ +/** + * Utility classes for basic CQL interaction. + */ +@NonNullApi +package org.springframework.data.cassandra.core.cql.util; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java new file mode 100644 index 000000000..dca78c3e8 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2019 the 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 + * + * https://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.springframework.data.cassandra.core.cql; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.junit.Test; + +import org.springframework.util.concurrent.ListenableFuture; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.Row; + +/** + * Unit tests for {@link AsyncResultStream}. + * + * @author Mark Paluch + */ +public class AsyncResultStreamUnitTests { + + AsyncResultSet first = mock(AsyncResultSet.class); + AsyncResultSet last = mock(AsyncResultSet.class); + Row row1 = mock(Row.class); + Row row2 = mock(Row.class); + + @Test // DATACASS-656 + public void shouldIterateFirstPage() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + List rows = new ArrayList<>(); + + AsyncResultStream.from(first).forEach(rows::add); + + assertThat(rows).containsOnly(row1); + } + + @Test // DATACASS-656 + public void shouldIterateMappedFirstPage() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + List rows = new ArrayList<>(); + + AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum).forEach(rows::add); + + assertThat(rows).containsOnly("row-1"); + } + + @Test // DATACASS-656 + public void shouldIterateMappedPages() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + when(last.currentPage()).thenReturn(Collections.singletonList(row2)); + when(first.fetchNextPage()).thenReturn(CompletableFuture.completedFuture(last)); + when(first.hasMorePages()).thenReturn(true); + + List rows = new ArrayList<>(); + + AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum).forEach(rows::add); + + assertThat(rows).containsOnly("row-1", "row-2"); + } + + @Test // DATACASS-656 + public void shouldPropagateExceptionOnIterate() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(new RuntimeException("boo")); + when(first.fetchNextPage()).thenReturn(failed); + when(first.hasMorePages()).thenReturn(true); + + List rows = new ArrayList<>(); + + ListenableFuture completion = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) + .forEach(rows::add); + + assertThatThrownBy(completion::get).hasRootCauseInstanceOf(RuntimeException.class); + } + + @Test // DATACASS-656 + public void shouldCollectFirstPage() throws ExecutionException, InterruptedException { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + ListenableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); + + assertThat(collect.get()).containsOnly(row1); + } + + @Test // DATACASS-656 + public void shouldCollectMappedPages() throws ExecutionException, InterruptedException { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + when(last.currentPage()).thenReturn(Collections.singletonList(row2)); + when(first.fetchNextPage()).thenReturn(CompletableFuture.completedFuture(last)); + when(first.hasMorePages()).thenReturn(true); + + ListenableFuture> rows = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) + .collect(Collectors.toList()); + + assertThat(rows.get()).containsOnly("row-1", "row-2"); + } + + @Test // DATACASS-656 + public void shouldPropagateExceptionOnCollect() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(new RuntimeException("boo")); + when(first.fetchNextPage()).thenReturn(failed); + when(first.hasMorePages()).thenReturn(true); + + ListenableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); + + assertThatThrownBy(collect::get).hasRootCauseInstanceOf(RuntimeException.class); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/util/StatementBuilderUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/util/StatementBuilderUnitTests.java new file mode 100644 index 000000000..766cfdd29 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/util/StatementBuilderUnitTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 the 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 + * + * https://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.springframework.data.cassandra.core.cql.util; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; + +/** + * Unit tests for {@link StatementBuilder}. + * + * @author Mark Paluch + */ +public class StatementBuilderUnitTests { + + @Test // DATACASS-656 + public void shouldCreateSimpleStatement() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()).build(); + + assertThat(statement.getQuery()).isEqualTo("SELECT * FROM person"); + } + + @Test // DATACASS-656 + public void shouldApplyBuilderFunction() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()) + .apply(select -> select.orderBy("foo", ClusteringOrder.ASC)).build(); + + assertThat(statement.getQuery()).isEqualTo("SELECT * FROM person ORDER BY foo ASC"); + } + + @Test // DATACASS-656 + public void shouldApplyBindFunction() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()) + .bind((select, factory) -> select.where(Relation.column("foo").isEqualTo(factory.create("bar")))).build(); + + assertThat(statement.getQuery()).isEqualTo("SELECT * FROM person WHERE foo='bar'"); + } + + @Test // DATACASS-656 + public void shouldBindByIndex() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()) + .bind((select, factory) -> select.where(Relation.column("foo").isEqualTo(factory.create("bar")))) + .build(StatementBuilder.ParameterHandling.BY_INDEX); + + assertThat(statement.getQuery()).isEqualTo("SELECT * FROM person WHERE foo=?"); + assertThat(statement.getPositionalValues()).containsOnly("bar"); + } + + @Test // DATACASS-656 + public void shouldBindByName() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()) + .bind((select, factory) -> select.where(Relation.column("foo").isEqualTo(factory.create("bar")))) + .build(StatementBuilder.ParameterHandling.BY_NAME); + + assertThat(statement.getQuery()).isEqualTo("SELECT * FROM person WHERE foo=:p0"); + assertThat(statement.getNamedValues()).containsEntry(CqlIdentifier.fromCql("p0"), "bar"); + } + + @Test // DATACASS-656 + public void shouldApplyFunctionsInOrder() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()) + .bind((select, factory) -> select.where(Relation.column("foo").isEqualTo(factory.create("bar")))) + .apply(select -> select.orderBy("one", ClusteringOrder.ASC)) + .bind((select, factory) -> select.where(Relation.column("bar").isEqualTo(factory.create("baz")))) + .apply(select -> select.orderBy("two", ClusteringOrder.ASC)).build(); + + assertThat(statement.getQuery()) + .isEqualTo("SELECT * FROM person WHERE foo='bar' AND bar='baz' ORDER BY one ASC,two ASC"); + } + + @Test // DATACASS-656 + public void shouldNotifyOnBuild() { + + SimpleStatement statement = StatementBuilder.of(QueryBuilder.selectFrom("person").all()) + .onBuild(statementBuilder -> statementBuilder.addPositionalValue("foo")) + .build(StatementBuilder.ParameterHandling.BY_NAME); + + assertThat(statement.getQuery()).isEqualTo("SELECT * FROM person"); + assertThat(statement.getPositionalValues()).hasSize(1); + } +}