Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,39 @@ public ComparisonCondition(String column, ComparisonOperator operator, Object va
this.operator = operator;
this.value = value;
}

public static Builder builder() {
return new Builder();
}

public static class Builder {
private String column;
private ComparisonOperator operator;
private Object value;

public Builder column(String column) {
this.column = column;
return this;
}

public Builder operator(ComparisonOperator operator) {
this.operator = operator;
return this;
}

public Builder value(Object value) {
this.value = value;
return this;
}

public ComparisonCondition build() {
if (column == null) {
throw new IllegalStateException("Column must be set");
}
if (operator == null) {
throw new IllegalStateException("Operator must be set");
}
return new ComparisonCondition(column, operator, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2024 OpenFacade 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.github.openfacade.table.api;

import lombok.Getter;

import java.util.ArrayList;
import java.util.List;

@Getter
public class CompositeCondition implements Condition {
private final LogicalOperator operator;
private final List<Condition> conditions;

private CompositeCondition(LogicalOperator operator, List<Condition> conditions) {
this.operator = operator;
this.conditions = conditions;
}

public static Builder builder() {
return new Builder();
}

public static class Builder {
private LogicalOperator operator;
private List<Condition> conditions = new ArrayList<>();

public Builder operator(LogicalOperator operator) {
this.operator = operator;
return this;
}

public Builder condition(Condition condition) {
this.conditions.add(condition);
return this;
}

public Builder conditions(List<Condition> conditions) {
this.conditions.addAll(conditions);
return this;
}

public CompositeCondition build() {
if (operator == null) {
throw new IllegalStateException("Operator must be set");
}
if (conditions.isEmpty()) {
throw new IllegalStateException("At least one condition must be added");
}
return new CompositeCondition(operator, conditions);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 OpenFacade 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.github.openfacade.table.api;

public enum LogicalOperator {
AND,
OR
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package io.github.openfacade.table.spring.reactive.mysql;

import io.github.openfacade.table.api.ComparisonCondition;
import io.github.openfacade.table.api.CompositeCondition;
import io.github.openfacade.table.api.Condition;
import io.github.openfacade.table.api.LogicalOperator;
import io.github.openfacade.table.spring.core.ReactiveBaseTableOperations;
import io.github.openfacade.table.spring.core.TableMetadata;
import io.github.openfacade.table.spring.util.TableMetadataUtil;
Expand Down Expand Up @@ -145,7 +147,10 @@ public <T> Mono<Long> update(Condition condition, Object[] pairs, Class<T> type,
.mapToObj(i -> MysqlUtil.quoteIdentifier((String) pairs[2 * i]) + " = ?")
.collect(Collectors.joining(", "));

String query = "UPDATE " + tableName + " SET " + setClause + " WHERE " + condition(condition);
StringBuilder conditionBuilder = new StringBuilder();
condition(condition, conditionBuilder);

String query = "UPDATE " + tableName + " SET " + setClause + " WHERE " + conditionBuilder;

DatabaseClient.GenericExecuteSpec spec = databaseClient.sql(query);
for (int i = 1; i < pairs.length; i += 2) {
Expand All @@ -162,7 +167,10 @@ public <T> Mono<T> find(Condition condition, Class<T> type, TableMetadata metada
.map(identifier -> MysqlUtil.quoteIdentifier(identifier))
.collect(Collectors.toList());

String query = "SELECT " + String.join(", ", escapedColumns) + " FROM " + tableName + " WHERE " + condition(condition);
StringBuilder conditionBuilder = new StringBuilder();
condition(condition, conditionBuilder);

String query = "SELECT " + String.join(", ", escapedColumns) + " FROM " + tableName + " WHERE " + conditionBuilder;

return databaseClient.sql(query)
.map((row, metadataAccessor) -> mapRowToEntity(row, type, metadata))
Expand Down Expand Up @@ -211,7 +219,11 @@ private <T> T mapRowToEntity(Row row, Class<T> type, TableMetadata metadata) {
@Override
public <T> Mono<Long> delete(Condition condition, Class<T> type, TableMetadata metadata) {
String tableName = MysqlUtil.quoteIdentifier(metadata.getTableName());
String query = "DELETE FROM " + tableName + " WHERE " + condition(condition);

StringBuilder conditionBuilder = new StringBuilder();
condition(condition, conditionBuilder);

String query = "DELETE FROM " + tableName + " WHERE " + conditionBuilder;

return databaseClient.sql(query)
.fetch()
Expand All @@ -230,9 +242,27 @@ public <T> Mono<Long> deleteAll(Class<T> type, TableMetadata metadata) {
.map(Long::valueOf);
}

private String condition(Condition condition) {
if (condition instanceof ComparisonCondition comparisonCondition) {
return MysqlUtil.quoteIdentifier(comparisonCondition.getColumn()) + " " + comparisonCondition.getOperator().symbol() + " " + escapeValue(comparisonCondition.getValue());
private void condition(Condition condition, StringBuilder sqlBuilder) {
if (condition instanceof ComparisonCondition) {
ComparisonCondition comparisonCondition = (ComparisonCondition) condition;
sqlBuilder.append(MysqlUtil.quoteIdentifier(comparisonCondition.getColumn()))
.append(" ")
.append(comparisonCondition.getOperator().symbol())
.append(" ")
.append(escapeValue(comparisonCondition.getValue()));
} else if (condition instanceof CompositeCondition) {
CompositeCondition compositeCondition = (CompositeCondition) condition;
List<Condition> conditions = compositeCondition.getConditions();
LogicalOperator operator = compositeCondition.getOperator();

sqlBuilder.append("(");
for (int i = 0; i < conditions.size(); i++) {
if (i > 0) {
sqlBuilder.append(" ").append(operator.name()).append(" ");
}
condition(conditions.get(i), sqlBuilder);
}
sqlBuilder.append(")");
} else {
throw new IllegalArgumentException("Unsupported condition type: " + condition.getClass().getName());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import io.github.openfacade.table.api.ComparisonCondition;
import io.github.openfacade.table.api.ComparisonOperator;
import io.github.openfacade.table.api.CompositeCondition;
import io.github.openfacade.table.api.LogicalOperator;
import io.github.openfacade.table.reactive.api.ReactiveTableOperations;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
Expand Down Expand Up @@ -192,4 +194,143 @@ void testFindByComparisonCondition() {
.expectNextCount(0)
.verifyComplete();
}

@Test
void testFindByCompositeCondition() {
TestMysqlEntity entityToInsert = new TestMysqlEntity();
entityToInsert.setId(3L);
entityToInsert.setTinyintBooleanField(false);
entityToInsert.setBlobBytesField("Composite Test Data".getBytes(StandardCharsets.UTF_8));
entityToInsert.setVarcharStringField("CompositeTest");

reactiveTableOperations.insert(entityToInsert)
.doOnSuccess(insertedEntity -> log.info("Inserted entity: {}", insertedEntity))
.block();

// Create a composite condition: id = 3 AND varchar_string_field = "CompositeTest"
ComparisonCondition idCondition = new ComparisonCondition("id", ComparisonOperator.EQ, 3L);
ComparisonCondition varcharCondition = new ComparisonCondition("varchar_string_field", ComparisonOperator.EQ, "CompositeTest");

CompositeCondition compositeCondition = CompositeCondition.builder()
.operator(LogicalOperator.AND)
.condition(idCondition)
.condition(varcharCondition)
.build();

TestMysqlEntity retrievedEntity = reactiveTableOperations.find(compositeCondition, TestMysqlEntity.class).block();

Assertions.assertNotNull(retrievedEntity, "Retrieved entity should not be null");
Assertions.assertEquals(3L, retrievedEntity.getId(), "Retrieved entity ID should match");
Assertions.assertFalse(retrievedEntity.isTinyintBooleanField());
Assertions.assertArrayEquals("Composite Test Data".getBytes(StandardCharsets.UTF_8), retrievedEntity.getBlobBytesField(), "Blob data should match");
Assertions.assertEquals("CompositeTest", retrievedEntity.getVarcharStringField());

// Test with OR condition: id = 3 OR varchar_string_field = "NonExistent"
CompositeCondition orCondition = CompositeCondition.builder()
.operator(LogicalOperator.OR)
.condition(idCondition)
.condition(new ComparisonCondition("varchar_string_field", ComparisonOperator.EQ, "NonExistent"))
.build();

TestMysqlEntity orRetrievedEntity = reactiveTableOperations.find(orCondition, TestMysqlEntity.class).block();

Assertions.assertNotNull(orRetrievedEntity, "Retrieved entity should not be null for OR condition");
Assertions.assertEquals(3L, orRetrievedEntity.getId(), "Retrieved entity ID should match for OR condition");

reactiveTableOperations.delete(compositeCondition, TestMysqlEntity.class)
.doOnSuccess(deletedCount -> log.info("Deleted {} entities", deletedCount))
.block();

reactiveTableOperations.findAll(TestMysqlEntity.class)
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}

@Test
void testFindByComplexCompositeCondition() {
// Insert test data
TestMysqlEntity entity1 = new TestMysqlEntity();
entity1.setId(4L);
entity1.setTinyintBooleanField(true);
entity1.setBlobBytesField("Complex Test Data 1".getBytes(StandardCharsets.UTF_8));
entity1.setVarcharStringField("ComplexTest1");

TestMysqlEntity entity2 = new TestMysqlEntity();
entity2.setId(5L);
entity2.setTinyintBooleanField(false);
entity2.setBlobBytesField("Complex Test Data 2".getBytes(StandardCharsets.UTF_8));
entity2.setVarcharStringField("ComplexTest2");

reactiveTableOperations.insert(entity1)
.doOnSuccess(insertedEntity -> log.info("Inserted entity1: {}", insertedEntity))
.block();

reactiveTableOperations.insert(entity2)
.doOnSuccess(insertedEntity -> log.info("Inserted entity2: {}", insertedEntity))
.block();

// Create a complex composite condition: (id = 4 AND varchar_string_field = "ComplexTest1") OR (id = 5 AND tinyint_boolean_field = true)
// This should return entity1 because the first part of OR condition matches
ComparisonCondition idCondition1 = new ComparisonCondition("id", ComparisonOperator.EQ, 4L);
ComparisonCondition varcharCondition1 = new ComparisonCondition("varchar_string_field", ComparisonOperator.EQ, "ComplexTest1");
CompositeCondition andCondition1 = CompositeCondition.builder()
.operator(LogicalOperator.AND)
.condition(idCondition1)
.condition(varcharCondition1)
.build();

ComparisonCondition idCondition2 = new ComparisonCondition("id", ComparisonOperator.EQ, 5L);
ComparisonCondition booleanCondition = new ComparisonCondition("tinyint_boolean_field", ComparisonOperator.EQ, true);
CompositeCondition andCondition2 = CompositeCondition.builder()
.operator(LogicalOperator.AND)
.condition(idCondition2)
.condition(booleanCondition)
.build();

CompositeCondition complexCondition = CompositeCondition.builder()
.operator(LogicalOperator.OR)
.condition(andCondition1)
.condition(andCondition2)
.build();

TestMysqlEntity retrievedEntity = reactiveTableOperations.find(complexCondition, TestMysqlEntity.class).block();

// Should find entity1 because (id = 4 AND varchar_string_field = "ComplexTest1") is true
Assertions.assertNotNull(retrievedEntity, "Retrieved entity should not be null");
Assertions.assertEquals(4L, retrievedEntity.getId(), "Retrieved entity ID should match");
Assertions.assertTrue(retrievedEntity.isTinyintBooleanField());
Assertions.assertArrayEquals("Complex Test Data 1".getBytes(StandardCharsets.UTF_8), retrievedEntity.getBlobBytesField(), "Blob data should match");
Assertions.assertEquals("ComplexTest1", retrievedEntity.getVarcharStringField());

// Create another complex composite condition: (id = 4 AND tinyint_boolean_field = false) OR (id = 5 AND tinyint_boolean_field = true)
// This should return null because neither part of OR condition is fully satisfied
ComparisonCondition booleanConditionFalse = new ComparisonCondition("tinyint_boolean_field", ComparisonOperator.EQ, false);
CompositeCondition andCondition1False = CompositeCondition.builder()
.operator(LogicalOperator.AND)
.condition(idCondition1)
.condition(booleanConditionFalse)
.build();

CompositeCondition complexConditionNoResult = CompositeCondition.builder()
.operator(LogicalOperator.OR)
.condition(andCondition1False)
.condition(andCondition2)
.build();

TestMysqlEntity noResultEntity = reactiveTableOperations.find(complexConditionNoResult, TestMysqlEntity.class).block();

// Should not find any entity because neither (id = 4 AND tinyint_boolean_field = false) nor (id = 5 AND tinyint_boolean_field = true) is fully satisfied
Assertions.assertNull(noResultEntity, "Should not find any entity for this condition");

// Clean up test data
reactiveTableOperations.deleteAll(TestMysqlEntity.class)
.doOnSuccess(deletedCount -> log.info("Deleted {} entities", deletedCount))
.block();

reactiveTableOperations.findAll(TestMysqlEntity.class)
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}
}
Loading