diff --git a/ap/build.gradle.kts b/ap/build.gradle.kts index f6c6f52..d65dd98 100644 --- a/ap/build.gradle.kts +++ b/ap/build.gradle.kts @@ -17,10 +17,8 @@ dependencies { tasks.withType().configureEach { doFirst { // See: https://github.com/google/compile-testing/issues/222 - jvmArgs( - "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" - ) + jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED") + jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED") + jvmArgs("--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED") } } \ No newline at end of file diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/EntityManager.java b/ap/src/main/java/org/geysermc/databaseutils/processor/EntityManager.java index 30ae18b..867efbb 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/EntityManager.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/EntityManager.java @@ -139,7 +139,7 @@ EntityInfo processEntity(TypeElement type) { indexes.add(new IndexInfo("", keys.toArray(new CharSequence[0]), true)); } - var entityInfo = new EntityInfo(tableName, type.getQualifiedName(), columns, indexes, keys); + var entityInfo = new EntityInfo(tableName, type, columns, indexes, keys); entityInfoByClassName.put(type.getQualifiedName(), entityInfo); return entityInfo; } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/RepositoryProcessor.java b/ap/src/main/java/org/geysermc/databaseutils/processor/RepositoryProcessor.java index cbc59fa..12a7957 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/RepositoryProcessor.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/RepositoryProcessor.java @@ -33,7 +33,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.concurrent.CompletableFuture; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; @@ -102,7 +101,7 @@ public boolean process(Set annotations, RoundEnvironment for (int i = 0; i < result.size(); i++) { results.get(i).add(result.get(i)); } - } catch (InvalidRepositoryException exception) { + } catch (InvalidRepositoryException | IllegalStateException exception) { error(exception.getMessage()); errorOccurred = true; } @@ -195,17 +194,6 @@ private List processRepository(TypeElement repository) { continue; } - TypeElement returnType; - boolean async = false; - if (MoreTypes.isTypeOf(CompletableFuture.class, element.getReturnType())) { - async = true; - returnType = typeUtils.toBoxedTypeElement(MoreTypes.asDeclared(element.getReturnType()) - .getTypeArguments() - .get(0)); - } else { - returnType = typeUtils.toBoxedTypeElement(element.getReturnType()); - } - var name = element.getSimpleName().toString(); var result = new KeywordsReader(name, entity).read(); @@ -213,9 +201,9 @@ private List processRepository(TypeElement repository) { if (action == null) { throw new InvalidRepositoryException("No available actions for %s", name); } - var queryInfo = new QueryInfoCreator(result, element, entity, typeUtils).create(); + var queryInfo = new QueryInfoCreator(action, result, element, entity, typeUtils).create(); - action.addTo(generators, queryInfo, returnType, async, typeUtils); + action.addTo(generators, queryInfo); } return generators; diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/action/Action.java b/ap/src/main/java/org/geysermc/databaseutils/processor/action/Action.java index c169783..d570edc 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/action/Action.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/action/Action.java @@ -27,94 +27,117 @@ import com.squareup.javapoet.MethodSpec; import java.util.Collection; import java.util.List; -import javax.lang.model.element.TypeElement; +import java.util.function.Consumer; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import org.geysermc.databaseutils.processor.info.EntityInfo; import org.geysermc.databaseutils.processor.query.QueryInfo; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; import org.geysermc.databaseutils.processor.type.RepositoryGenerator; import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; import org.geysermc.databaseutils.processor.util.TypeUtils; public abstract class Action { private final String actionType; + private final boolean allowSelfCollectionArgument; private final boolean supportsFilter; + private final List supportedProjectionCategories; - protected Action(String actionType) { - this(actionType, true); - } - - protected Action(String actionType, boolean supportsFilter) { + protected Action( + String actionType, + boolean allowSelfCollectionArgument, + boolean supportsFilter, + ProjectionKeywordCategory... supportedProjectionCategories) { this.actionType = actionType; + this.allowSelfCollectionArgument = allowSelfCollectionArgument; this.supportsFilter = supportsFilter; + this.supportedProjectionCategories = List.of(supportedProjectionCategories); } public String actionType() { return actionType; } + public boolean allowSelfCollectionArgument() { + return allowSelfCollectionArgument; + } + public boolean supportsFilter() { return supportsFilter; } - protected abstract void addToSingle( - RepositoryGenerator generator, - QueryInfo info, - MethodSpec.Builder spec, - TypeElement returnType, - boolean async); + public List unsupportedProjectionCategories() { + return supportedProjectionCategories; + } + + protected abstract void addToSingle(RepositoryGenerator generator, QueryInfo info, MethodSpec.Builder spec); - protected boolean validateSingle(QueryInfo info, TypeElement returnType, TypeUtils typeUtils) { - return TypeUtils.isType(Void.class, returnType) || TypeUtils.isWholeNumberType(returnType); + protected boolean validateSingle( + EntityInfo info, CharSequence methodName, TypeMirror returnType, TypeUtils typeUtils) { + return typeUtils.isType(Void.class, returnType) || typeUtils.isWholeNumberType(returnType); } - protected boolean validateCollection(QueryInfo info, TypeElement elementType, TypeUtils typeUtils) { - return false; + protected boolean validateCollection( + EntityInfo info, CharSequence methodName, TypeMirror returnType, TypeUtils typeUtils) { + throw new InvalidRepositoryException( + "Collection return type (%s) is not supported for %s", returnType, actionType()); } - protected boolean validateEither(QueryInfo info, TypeElement elementType, boolean collection, TypeUtils typeUtils) { + protected boolean validateEither( + EntityInfo info, CharSequence methodName, TypeMirror returnType, boolean collection, TypeUtils typeUtils) { return false; } - protected void validate(QueryInfo info, TypeElement returnType, TypeUtils typeUtils) { - var elementType = returnType; - if (typeUtils.isAssignable(Collection.class, returnType.asType())) { - elementType = (TypeElement) returnType.getTypeParameters().get(0); + public void validate( + EntityInfo info, + CharSequence methodName, + TypeMirror returnType, + TypeUtils typeUtils, + Consumer customValidation) { + var passedCustomValidation = false; + if (typeUtils.isAssignable(returnType, Collection.class)) { + returnType = ((DeclaredType) returnType).getTypeArguments().get(0); if (!supportsFilter) { throw new InvalidRepositoryException("%s does not support a By section", actionType); } - if (validateCollection(info, elementType, typeUtils) - || validateEither(info, elementType, true, typeUtils)) { + if (customValidation != null) { + customValidation.accept(returnType); + passedCustomValidation = true; + } + + if (validateCollection(info, methodName, returnType, typeUtils) + || validateEither(info, methodName, returnType, true, typeUtils)) { return; } } else { - if (validateEither(info, elementType, false, typeUtils)) { + if (customValidation != null) { + customValidation.accept(returnType); + passedCustomValidation = true; + } + + if (validateEither(info, methodName, returnType, false, typeUtils)) { return; } - if (validateSingle(info, returnType, typeUtils)) { + if (validateSingle(info, methodName, returnType, typeUtils)) { return; } } - if (!typeUtils.isAssignable(info.entityType(), elementType.asType())) { - throw new InvalidRepositoryException( - "Unsupported return type %s for %s", - returnType.getSimpleName(), info.element().getSimpleName()); + if (!passedCustomValidation) { + throw new InvalidRepositoryException("Unsupported return type %s for %s", returnType, methodName); } } - public void addTo( - List generators, - QueryInfo info, - TypeElement returnType, - boolean async, - TypeUtils typeUtils) { - if (!info.hasBySection() && info.element().getParameters().size() != 1) { - throw new InvalidRepositoryException("Expected one parameter with type %s", info.entityType()); + public void addTo(List generators, QueryInfo info) { + if (!info.hasBySection() && !info.parametersInfo().isNoneOrAnySelf()) { + throw new InvalidRepositoryException("Expected at most one parameter, with type %s", info.entityType()); } - validate(info, returnType, typeUtils); for (RepositoryGenerator generator : generators) { - addToSingle(generator, info, MethodSpec.overriding(info.element()), returnType, async); + addToSingle( + generator, info, MethodSpec.overriding(info.parametersInfo().element())); } } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/action/DeleteAction.java b/ap/src/main/java/org/geysermc/databaseutils/processor/action/DeleteAction.java index 7822cef..0594e45 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/action/DeleteAction.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/action/DeleteAction.java @@ -25,7 +25,8 @@ package org.geysermc.databaseutils.processor.action; import com.squareup.javapoet.MethodSpec; -import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import org.geysermc.databaseutils.processor.info.EntityInfo; import org.geysermc.databaseutils.processor.query.QueryInfo; import org.geysermc.databaseutils.processor.type.RepositoryGenerator; import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; @@ -33,27 +34,21 @@ final class DeleteAction extends Action { DeleteAction() { - super("delete"); + super("delete", true, true); } @Override - protected boolean validateSingle(QueryInfo info, TypeElement returnType, TypeUtils typeUtils) { - // todo does it also support saying how many items were deleted? - if (!TypeUtils.isType(Void.class, returnType)) { - throw new InvalidRepositoryException( - "Expected Void as return type for %s, got %s", - info.element().getSimpleName(), returnType); - } - return true; + protected void addToSingle(RepositoryGenerator generator, QueryInfo info, MethodSpec.Builder spec) { + generator.addDelete(info, spec); } @Override - protected void addToSingle( - RepositoryGenerator generator, - QueryInfo info, - MethodSpec.Builder spec, - TypeElement returnType, - boolean async) { - generator.addDelete(info, spec, returnType, async); + protected boolean validateSingle( + EntityInfo info, CharSequence methodName, TypeMirror returnType, TypeUtils typeUtils) { + // todo does it also support saying how many items were deleted? + if (!typeUtils.isType(Void.class, returnType)) { + throw new InvalidRepositoryException("Expected Void as return type for %s, got %s", methodName, returnType); + } + return true; } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/action/ExistsAction.java b/ap/src/main/java/org/geysermc/databaseutils/processor/action/ExistsAction.java index 27b5658..5e02370 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/action/ExistsAction.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/action/ExistsAction.java @@ -25,34 +25,31 @@ package org.geysermc.databaseutils.processor.action; import com.squareup.javapoet.MethodSpec; -import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import org.geysermc.databaseutils.processor.info.EntityInfo; import org.geysermc.databaseutils.processor.query.QueryInfo; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; import org.geysermc.databaseutils.processor.type.RepositoryGenerator; import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; import org.geysermc.databaseutils.processor.util.TypeUtils; final class ExistsAction extends Action { ExistsAction() { - super("exists"); + super("exists", false, true, ProjectionKeywordCategory.UNIQUE); } @Override - protected boolean validateSingle(QueryInfo info, TypeElement returnType, TypeUtils typeUtils) { - if (!TypeUtils.isType(Boolean.class, returnType)) { + protected boolean validateSingle( + EntityInfo info, CharSequence methodName, TypeMirror returnType, TypeUtils typeUtils) { + if (!typeUtils.isType(Boolean.class, returnType)) { throw new InvalidRepositoryException( - "Expected Boolean as return type for %s, got %s", - info.element().getSimpleName(), returnType); + "Expected Boolean as return type for %s, got %s", methodName, returnType); } return true; } @Override - public void addToSingle( - RepositoryGenerator generator, - QueryInfo info, - MethodSpec.Builder spec, - TypeElement returnType, - boolean async) { - generator.addExists(info, spec, returnType, async); + public void addToSingle(RepositoryGenerator generator, QueryInfo info, MethodSpec.Builder spec) { + generator.addExists(info, spec); } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/action/FindAction.java b/ap/src/main/java/org/geysermc/databaseutils/processor/action/FindAction.java index fd43e36..d5c985f 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/action/FindAction.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/action/FindAction.java @@ -25,34 +25,43 @@ package org.geysermc.databaseutils.processor.action; import com.squareup.javapoet.MethodSpec; -import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import org.geysermc.databaseutils.processor.info.EntityInfo; import org.geysermc.databaseutils.processor.query.QueryInfo; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; import org.geysermc.databaseutils.processor.type.RepositoryGenerator; import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; import org.geysermc.databaseutils.processor.util.TypeUtils; final class FindAction extends Action { FindAction() { - super("find"); + super( + "find", + true, + true, + ProjectionKeywordCategory.UNIQUE, + ProjectionKeywordCategory.SUMMARY, + ProjectionKeywordCategory.LIMIT); } @Override - protected boolean validateEither(QueryInfo info, TypeElement returnType, boolean collection, TypeUtils typeUtils) { - if (!TypeUtils.isType(info.entityType(), returnType)) { + protected boolean validateEither( + EntityInfo info, CharSequence methodName, TypeMirror returnType, boolean collection, TypeUtils typeUtils) { + if (!typeUtils.isType(info.type().asType(), returnType)) { throw new InvalidRepositoryException( - "Expected %s as return type for %s, got %s", - info.entityType(), info.element().getSimpleName(), returnType); + "Expected %s as return type for %s, got %s", info.typeName(), methodName, returnType); } return true; } @Override - public void addToSingle( - RepositoryGenerator generator, - QueryInfo info, - MethodSpec.Builder spec, - TypeElement returnType, - boolean async) { - generator.addFind(info, spec, returnType, async); + protected boolean validateCollection( + EntityInfo info, CharSequence methodName, TypeMirror returnType, TypeUtils typeUtils) { + return false; + } + + @Override + public void addToSingle(RepositoryGenerator generator, QueryInfo info, MethodSpec.Builder spec) { + generator.addFind(info, spec); } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/action/InsertAction.java b/ap/src/main/java/org/geysermc/databaseutils/processor/action/InsertAction.java index 518e658..a46d838 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/action/InsertAction.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/action/InsertAction.java @@ -25,22 +25,16 @@ package org.geysermc.databaseutils.processor.action; import com.squareup.javapoet.MethodSpec; -import javax.lang.model.element.TypeElement; import org.geysermc.databaseutils.processor.query.QueryInfo; import org.geysermc.databaseutils.processor.type.RepositoryGenerator; final class InsertAction extends Action { InsertAction() { - super("insert", false); + super("insert", true, false); } @Override - protected void addToSingle( - RepositoryGenerator generator, - QueryInfo info, - MethodSpec.Builder spec, - TypeElement returnType, - boolean async) { - generator.addInsert(info, spec, returnType, async); + protected void addToSingle(RepositoryGenerator generator, QueryInfo info, MethodSpec.Builder spec) { + generator.addInsert(info, spec); } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/action/UpdateAction.java b/ap/src/main/java/org/geysermc/databaseutils/processor/action/UpdateAction.java index 533f8c4..21906a7 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/action/UpdateAction.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/action/UpdateAction.java @@ -25,34 +25,16 @@ package org.geysermc.databaseutils.processor.action; import com.squareup.javapoet.MethodSpec; -import javax.lang.model.element.TypeElement; import org.geysermc.databaseutils.processor.query.QueryInfo; import org.geysermc.databaseutils.processor.type.RepositoryGenerator; -import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; -import org.geysermc.databaseutils.processor.util.TypeUtils; final class UpdateAction extends Action { UpdateAction() { - super("update"); + super("update", true, true); } @Override - protected void addToSingle( - RepositoryGenerator generator, - QueryInfo info, - MethodSpec.Builder spec, - TypeElement returnType, - boolean async) { - generator.addUpdate(info, spec, returnType, async); - } - - @Override - protected boolean validateEither(QueryInfo info, TypeElement returnType, boolean collection, TypeUtils typeUtils) { - if (!collection) { - return false; - } - throw new InvalidRepositoryException( - "Unsupported return type %s for %s", - returnType.getSimpleName(), info.element().getSimpleName()); + protected void addToSingle(RepositoryGenerator generator, QueryInfo info, MethodSpec.Builder spec) { + generator.addUpdate(info, spec); } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/info/EntityInfo.java b/ap/src/main/java/org/geysermc/databaseutils/processor/info/EntityInfo.java index 3140dba..aa487a7 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/info/EntityInfo.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/info/EntityInfo.java @@ -27,13 +27,10 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import javax.lang.model.element.TypeElement; public record EntityInfo( - String name, - CharSequence className, - List columns, - List indexes, - List keys) { + String name, TypeElement type, List columns, List indexes, List keys) { public ColumnInfo columnFor(CharSequence columnName) { for (ColumnInfo column : columns) { if (column.name().contentEquals(columnName)) { @@ -43,6 +40,10 @@ public ColumnInfo columnFor(CharSequence columnName) { return null; } + public CharSequence typeName() { + return type.getQualifiedName(); + } + public List keyColumns() { return keys.stream().map(this::columnFor).collect(Collectors.toList()); } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReadResult.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReadResult.java index 0387dc3..b04f080 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReadResult.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReadResult.java @@ -26,7 +26,64 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.databaseutils.processor.query.section.BySection; -import org.geysermc.databaseutils.processor.query.section.order.OrderBySection; +import org.geysermc.databaseutils.processor.query.section.OrderBySection; +import org.geysermc.databaseutils.processor.query.section.ProjectionSection; public record KeywordsReadResult( - String actionName, boolean distinct, @Nullable BySection bySection, @Nullable OrderBySection orderBySection) {} + String actionName, + @Nullable ProjectionSection projection, + @Nullable BySection bySection, + @Nullable OrderBySection orderBySection) { + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private ProjectionSection projection; + private BySection bySection; + private OrderBySection orderBySection; + + private Builder() {} + + public ProjectionSection projection() { + return projection; + } + + public Builder projection(ProjectionSection projection) { + if (this.projection != null) { + throw new IllegalStateException("Cannot redefine projection!"); + } + this.projection = projection; + return this; + } + + public BySection bySection() { + return bySection; + } + + public Builder bySection(BySection bySection) { + if (this.bySection != null) { + throw new IllegalStateException("Cannot redefine by section!"); + } + this.bySection = bySection; + return this; + } + + public OrderBySection orderBySection() { + return orderBySection; + } + + public Builder orderBySection(OrderBySection orderBySection) { + if (this.orderBySection != null) { + throw new IllegalStateException("Cannot redefine orderBy section!"); + } + this.orderBySection = orderBySection; + return this; + } + + public KeywordsReadResult build(String actionName) { + return new KeywordsReadResult(actionName, projection, bySection, orderBySection); + } + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReader.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReader.java index f3e3212..b53ac89 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReader.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/KeywordsReader.java @@ -28,16 +28,25 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; import org.geysermc.databaseutils.processor.info.EntityInfo; import org.geysermc.databaseutils.processor.query.section.BySection; import org.geysermc.databaseutils.processor.query.section.FactorRegistry; +import org.geysermc.databaseutils.processor.query.section.OrderBySection; +import org.geysermc.databaseutils.processor.query.section.ProjectionSection; +import org.geysermc.databaseutils.processor.query.section.SectionType; import org.geysermc.databaseutils.processor.query.section.by.InputKeywordRegistry; import org.geysermc.databaseutils.processor.query.section.factor.Factor; +import org.geysermc.databaseutils.processor.query.section.factor.ProjectionFactor; import org.geysermc.databaseutils.processor.query.section.factor.VariableByFactor; import org.geysermc.databaseutils.processor.query.section.factor.VariableOrderByFactor; -import org.geysermc.databaseutils.processor.query.section.order.OrderBySection; import org.geysermc.databaseutils.processor.query.section.order.OrderDirection; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordRegistry; +import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; import org.geysermc.databaseutils.processor.util.StringUtils; +import org.geysermc.databaseutils.processor.util.StringUtils.LargestMatchResult; public class KeywordsReader { private final String name; @@ -70,182 +79,214 @@ public KeywordsReadResult read() { // In this result uniqueId matches, after that we look for something to do with the data. // In this example there is nothing, so it'll default to the 'equals' keyword - String action = null; - boolean isOrderBy = false; - boolean hadOrder = false; - - var bySections = new ArrayList(); - var orderBySections = new ArrayList(); + var sections = new ArrayList(); var workingSection = new StringBuilder(); for (int i = 0; i < name.length(); i++) { char current = name.charAt(i); if (Character.isUpperCase(current)) { - var finishedSection = workingSection.toString(); + sections.add(workingSection.toString()); workingSection = new StringBuilder(); - var shouldAdd = true; - - if (action == null) { - action = finishedSection; - } else { - if ("By".equals(finishedSection)) { - if (hadOrder) { - isOrderBy = true; - hadOrder = false; - shouldAdd = false; - bySections.remove(bySections.size() - 1); // Remove Order - if (!orderBySections.isEmpty()) { - throw new IllegalStateException("Can only have one OrderBy definition!"); - } - } else if (bySections.isEmpty()) { - // usually it starts with By (e.g. findByUsername), so ignore it - shouldAdd = false; - } - } else { - hadOrder = "Order".equals(finishedSection); - } - - if (shouldAdd) { - if (isOrderBy) { - orderBySections.add(finishedSection); - } else { - bySections.add(finishedSection); - } - } - } } workingSection.append(current); } if (!workingSection.isEmpty()) { - var finishedSection = workingSection.toString(); - if (action == null) { - action = finishedSection; - } else { - if (isOrderBy) { - orderBySections.add(finishedSection); - } else { - bySections.add(finishedSection); - } - } + sections.add(workingSection.toString()); } - return new KeywordsReadResult(action, false, formBySection(bySections), formOrderBySection(orderBySections)); - } + var action = sections.get(0); - public BySection formBySection(List sections) { - if (sections.isEmpty()) { - return null; + if (sections.size() == 1) { + return new KeywordsReadResult(action, null, null, null); } - var variables = new ArrayList(); - formBySections(sections, 0, variables); - return new BySection(variables); - } - private void formBySections(List bySections, int offset, List factors) { - var variableMatch = StringUtils.largestMatch(bySections, offset, input -> { - var variableName = StringUtils.uncapitalize(input); - return variableNames.contains(variableName) ? variableName : null; - }); + var builder = KeywordsReadResult.builder(); + var currentContext = new SectionContext(null, 1); + while ((currentContext = determineSection(sections, currentContext)).type != null) { + currentContext.offset = formSection(currentContext, sections, builder); + } - if (variableMatch == null) { - var factor = FactorRegistry.factorFor(bySections.get(offset)); - if (factor == null) { - throw new IllegalStateException( - "Expected a variable to match, but none did for " + join(bySections, offset)); - } - factors.add(factor); - // After a factor was found, restart the process to follow the correct variable -> keyword format - formBySections(bySections, offset + 1, factors); - return; + if (currentContext.offset != sections.size()) { + throw new InvalidRepositoryException( + "Unexpected remaining input: %s. %s sections left", + join(sections, currentContext.offset), sections.size() - currentContext.offset); } - // findByUsername = username, no keywords after the variable - if (variableMatch.offset() + 1 == bySections.size()) { - factors.add(new VariableByFactor(variableMatch.match())); - return; + return builder.build(action); + } + + private SectionContext determineSection(List sections, SectionContext currentContext) { + var offset = currentContext.offset; + // make sure that 'find' also works + if (sections.size() == offset) { + return new SectionContext(null, offset); } - // findByUsernameIsNotNull = IsNotNull - var keywordMatch = - StringUtils.largestMatch(bySections, variableMatch.offset() + 1, InputKeywordRegistry::findByName); - if (keywordMatch == null) { - var factor = FactorRegistry.factorFor(bySections.get(variableMatch.offset() + 1)); - if (factor == null) { - throw new IllegalStateException( - "Expected a keyword to match for " + join(bySections, variableMatch.offset())); + SectionType[] types = SectionType.VALUES; + // Projection always matches, so try matching backwards + types: + for (int typeIndex = types.length - 1; typeIndex >= 0; typeIndex--) { + SectionType type = types[typeIndex]; + + if (offset + type.sections().length > sections.size()) { + continue; } - // just like above, if no keyword is provided, assume equals - factors.add(new VariableByFactor(variableMatch.match())); - factors.add(factor); + for (int sectionIndex = 0; sectionIndex < type.sections().length; sectionIndex++) { + if (!sections.get(offset + sectionIndex).equals(type.sections()[sectionIndex])) { + continue types; + } + } - // We have to assume that after the factor something comes next - formBySections(bySections, variableMatch.offset() + 2, factors); - return; + // make sure you can't redefine sections + if (SectionType.isCorrectOrder(type, currentContext.type)) { + return new SectionContext(type, offset + type.sections().length); + } } - factors.add(new VariableByFactor(variableMatch.match(), keywordMatch.match())); - if (keywordMatch.offset() + 1 < bySections.size()) { - formBySections(bySections, keywordMatch.offset() + 1, factors); + return new SectionContext(null, offset); + } + + private int formSection(SectionContext context, List sections, KeywordsReadResult.Builder builder) { + var offset = context.offset; + if (sections.size() == offset) { + return offset; } + + var factors = new ArrayList(); + do { + offset = formSectionItem(context.type, sections, offset, factors); + } while (!isNextSection(sections, offset, context.type)); + + switch (context.type) { + case PROJECTION -> builder.projection(ProjectionSection.from(factors)); + case BY -> builder.bySection(new BySection(factors)); + case ORDER_BY -> builder.orderBySection(new OrderBySection(factors)); + } + return offset; } - public OrderBySection formOrderBySection(List sections) { - if (sections.isEmpty()) { - return null; + public boolean isNextSection(List sections, int offset, SectionType currentSection) { + if (sections.size() == offset) { + return true; } - var variables = new ArrayList(); - formOrderBySections(sections, 0, variables); - return new OrderBySection(variables); + var determinedType = determineSection(sections, new SectionContext(currentSection, offset)).type; + // projection matches on any var + return determinedType != null && determinedType != SectionType.PROJECTION; } - private void formOrderBySections(List orderBySections, int offset, List factors) { - var variableMatch = StringUtils.largestMatch(orderBySections, offset, input -> { - var variableName = StringUtils.uncapitalize(input); - return variableNames.contains(variableName) ? variableName : null; - }); + private int formSectionItem(SectionType type, List sections, int offset, List factors) { + // every section has a quite specific format: + // projection: keyword(s) - column name (either of them can be optional, not both) + // by: column name - optional keyword + // orderBy: column name - optional direction + + if (type == null) { + throw new IllegalStateException(String.format( + "Expected type to not be null! Remaining: %s. Offset: %s", join(sections, offset), offset)); + } + + if (type == SectionType.PROJECTION) { + ProjectionKeyword keyword; + var hadKeyword = false; + do { + keyword = ProjectionKeywordRegistry.findByName(sections.get(offset)); + if (keyword != null) { + hadKeyword = true; + factors.add(new ProjectionFactor(keyword, null)); + offset++; + } + } while (keyword != null); + + var variable = variableMatch(sections, offset); + if (variable == null) { + if (hadKeyword) { + return offset; + } + throw new IllegalStateException( + "Expected a projection, got nothing! Remaining: " + join(sections, offset - 1)); + } + factors.add(new ProjectionFactor(null, variable.match())); + return variable.offset() + 1; + } - if (variableMatch == null) { - var factor = FactorRegistry.factorFor(orderBySections.get(offset)); + var variable = variableMatch(sections, offset); + if (variable == null) { + var factor = FactorRegistry.factorFor(sections.get(offset)); if (factor == null) { throw new IllegalStateException( - "Expected a variable to match, but none did for " + join(orderBySections, offset)); + "Expected a variable to match, but none did for " + join(sections, offset)); } factors.add(factor); - // After a factor was found restart the process to follow the correct variable -> direction format - formOrderBySections(orderBySections, offset + 1, factors); - return; + return offset + 1; } - // use the default direction if none is provided - if (variableMatch.offset() + 1 == orderBySections.size()) { - factors.add(new VariableOrderByFactor(variableMatch.match(), OrderDirection.DEFAULT)); - return; + return switch (type) { + case BY -> createNormal( + type, sections, variable, factors, VariableByFactor::new, InputKeywordRegistry::findByName, true); + case ORDER_BY -> createNormal( + type, sections, variable, factors, VariableOrderByFactor::new, OrderDirection::byName, false); + default -> throw new IllegalStateException("Unexpected value: " + type); + }; + } + + private int createNormal( + SectionType type, + List sections, + LargestMatchResult variable, + List factors, + BiFunction creator, + Function matcher, + boolean largest) { + // specifying a direction / keyword is optional + if (variable.offset() + 1 == sections.size() || isNextSection(sections, variable.offset() + 1, type)) { + factors.add(creator.apply(variable.match(), null)); + return variable.offset() + 1; } - var directionString = StringUtils.uncapitalize(orderBySections.get(variableMatch.offset() + 1)); - var direction = OrderDirection.byName(directionString); - if (direction == null) { - var factor = FactorRegistry.factorFor(orderBySections.get(variableMatch.offset() + 1)); + LargestMatchResult keyword; + if (largest) { + keyword = StringUtils.largestMatch(sections, variable.offset() + 1, matcher); + } else { + var newIndex = variable.offset() + 1; + var result = matcher.apply(sections.get(newIndex)); + keyword = result == null ? null : new LargestMatchResult<>(result, newIndex); + } + + if (keyword == null) { + var factor = FactorRegistry.factorFor(sections.get(variable.offset() + 1)); if (factor == null) { - throw new IllegalStateException("Unknown order by direction " + directionString); + throw new IllegalStateException("Expected a keyword to match for " + join(sections, variable.offset())); } - // just like above, if no direction is provided, use default - factors.add(new VariableOrderByFactor(variableMatch.match(), OrderDirection.DEFAULT)); + // just like above, if no keyword is provided, use the default + factors.add(creator.apply(variable.match(), null)); factors.add(factor); // We have to assume that after the factor something comes next - formOrderBySections(orderBySections, variableMatch.offset() + 2, factors); - return; + return variable.offset() + 2; } - factors.add(new VariableOrderByFactor(variableMatch.match(), direction)); - // +2 because of the direction - if (variableMatch.offset() + 2 < orderBySections.size()) { - formOrderBySections(orderBySections, variableMatch.offset() + 2, factors); + factors.add(creator.apply(variable.match(), keyword.match())); + return keyword.offset() + 1; + } + + private LargestMatchResult variableMatch(List sections, int offset) { + return StringUtils.largestMatch(sections, offset, input -> { + var variableName = StringUtils.uncapitalize(input); + return variableNames.contains(variableName) ? variableName : null; + }); + } + + private static final class SectionContext { + private final SectionType type; + private int offset; + + SectionContext(SectionType type, int offset) { + this.type = type; + this.offset = offset; } } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfo.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfo.java index ed059f3..331a105 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfo.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfo.java @@ -24,22 +24,32 @@ */ package org.geysermc.databaseutils.processor.query; +import java.util.Collections; import java.util.List; import java.util.stream.Stream; -import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.geysermc.databaseutils.processor.info.ColumnInfo; import org.geysermc.databaseutils.processor.info.EntityInfo; import org.geysermc.databaseutils.processor.query.section.factor.Factor; import org.geysermc.databaseutils.processor.query.section.factor.VariableByFactor; +import org.geysermc.databaseutils.processor.query.type.ParametersTypeInfo; +import org.geysermc.databaseutils.processor.query.type.ReturnTypeInfo; +import org.geysermc.databaseutils.processor.util.TypeUtils; + +public record QueryInfo( + EntityInfo entityInfo, + KeywordsReadResult result, + ParametersTypeInfo parametersInfo, + ReturnTypeInfo returnInfo, + TypeUtils typeUtils) { -public record QueryInfo(EntityInfo entityInfo, KeywordsReadResult result, ExecutableElement element) { public String tableName() { return entityInfo.name(); } public CharSequence entityType() { - return entityInfo.className(); + return entityInfo.typeName(); } public List columns() { @@ -63,14 +73,10 @@ public boolean hasBySection() { return result.bySection() != null ? result.bySection().factors() : null; } - public void requireBySection() { - if (bySectionFactors() == null || bySectionFactors().isEmpty()) { - throw new IllegalStateException("This query requires a By section"); - } - } - public List byVariables() { - requireBySection(); + if (bySectionFactors() == null) { + return Collections.emptyList(); + } return bySectionFactors().stream() .flatMap(section -> { @@ -82,7 +88,7 @@ public List byVariables() { .toList(); } - public CharSequence parameterName(int index) { - return element.getParameters().get(index).getSimpleName(); + public TypeMirror returnType() { + return returnInfo.type(); } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfoCreator.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfoCreator.java index adf40b6..40b0ff3 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfoCreator.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/QueryInfoCreator.java @@ -24,12 +24,30 @@ */ package org.geysermc.databaseutils.processor.query; +import com.google.auto.common.MoreTypes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.databaseutils.processor.action.Action; +import org.geysermc.databaseutils.processor.info.ColumnInfo; import org.geysermc.databaseutils.processor.info.EntityInfo; +import org.geysermc.databaseutils.processor.query.section.SectionType; import org.geysermc.databaseutils.processor.query.section.factor.Factor; +import org.geysermc.databaseutils.processor.query.section.factor.ProjectionFactor; import org.geysermc.databaseutils.processor.query.section.factor.VariableByFactor; -import org.geysermc.databaseutils.processor.query.section.factor.VariableOrderByFactor; +import org.geysermc.databaseutils.processor.query.section.factor.VariableFactor; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; +import org.geysermc.databaseutils.processor.query.type.ParametersTypeInfo; +import org.geysermc.databaseutils.processor.query.type.ReturnTypeInfo; +import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; import org.geysermc.databaseutils.processor.util.TypeUtils; /** @@ -37,76 +55,156 @@ * Note that this will edit the provided readResult. It doesn't create a new instance. */ public class QueryInfoCreator { + private final Action action; private final KeywordsReadResult readResult; private final ExecutableElement element; private final EntityInfo info; private final TypeUtils typeUtils; + private final TypeMirror returnType; + private final boolean async; + private final boolean isCollection; + public QueryInfoCreator( - KeywordsReadResult readResult, ExecutableElement element, EntityInfo info, TypeUtils typeUtils) { + Action action, + KeywordsReadResult readResult, + ExecutableElement element, + EntityInfo info, + TypeUtils typeUtils) { + this.action = action; this.readResult = readResult; this.element = element; this.info = info; this.typeUtils = typeUtils; + + TypeMirror returnType; + boolean async = false; + if (MoreTypes.isTypeOf(CompletableFuture.class, element.getReturnType())) { + async = true; + returnType = typeUtils.toBoxedMirror( + ((DeclaredType) element.getReturnType()).getTypeArguments().get(0)); + } else { + returnType = typeUtils.toBoxedMirror(element.getReturnType()); + } + + this.returnType = returnType; + this.async = async; + this.isCollection = returnType != null && typeUtils.isAssignable(returnType, Collection.class); } public QueryInfo create() { - analyse(); - return new QueryInfo(info, readResult, element); + analyseAndValidate(); + return new QueryInfo( + info, + readResult, + new ParametersTypeInfo(element, info.typeName(), typeUtils), + new ReturnTypeInfo(async, returnType, typeUtils), + typeUtils); } - private void analyse() { + private void analyseAndValidate() { var parameterTypes = element.getParameters().stream().map(VariableElement::asType).toList(); var parameterNames = element.getParameters().stream() .map(VariableElement::getSimpleName) .toList(); - var handledInputs = 0; + AtomicInteger handledInputs = new AtomicInteger(); - if (readResult.bySection() != null) { - var section = readResult.bySection(); - for (Factor factor : section.factors()) { - if (factor instanceof VariableByFactor variable) { - var column = info.columnFor(variable.name()); - if (column == null) { - throw new IllegalStateException( - "Could not find column %s for entity %s".formatted(variable.name(), info.name())); - } - - var keyword = variable.keyword(); - keyword.validateTypes(column, parameterTypes, parameterNames, handledInputs, typeUtils); - handledInputs += keyword.inputCount(); + if (readResult.projection() != null) { + validateColumnNames(readResult.projection().projections(), SectionType.PROJECTION, null); + + var handledCategories = new ArrayList(); + for (@NonNull ProjectionFactor projection : readResult.projection().projections()) { + if (projection.keyword() == null) { + continue; + } + var category = projection.keyword().category(); + if (!handledCategories.add(category)) { + throw new InvalidRepositoryException( + "You can only provide one keyword of category %s, also got %s", + category, projection.keyword().name()); + } + if (category.requiresColumn() && readResult.projection().columnName() == null) { + throw new InvalidRepositoryException( + "Projection %s requires you to specify a column", + projection.keyword().name()); } } } + if (readResult.bySection() != null) { + validateColumnNames( + readResult.bySection().factors(), SectionType.BY, (VariableByFactor input, ColumnInfo column) -> { + var keyword = input.keyword(); + keyword.validateTypes(column, parameterTypes, parameterNames, handledInputs.get(), typeUtils); + handledInputs.addAndGet(keyword.inputCount()); + }); + } + if (readResult.orderBySection() != null) { - var section = readResult.orderBySection(); - for (Factor factor : section.factors()) { - if (factor instanceof VariableOrderByFactor variable) { - var column = info.columnFor(variable.name()); - if (column == null) { - throw new IllegalStateException( - "Could not find column %s for entity %s".formatted(variable.name(), info.name())); - } - } - } + validateColumnNames( + readResult.orderBySection().factors(), + SectionType.ORDER_BY, + ($, $$) -> handledInputs.incrementAndGet()); } - // if there is no By section and there are parameters, it should be the entity + // if there is no By section and there are parameters, it should be the entity or the provided projection if (readResult.bySection() == null && parameterTypes.size() == 1) { - if (!typeUtils.isAssignable(parameterTypes.get(0), info.className())) { - throw new IllegalStateException(String.format( - "Expected the only parameter %s to be assignable from entity %s with type %s!", - parameterTypes.get(0), info.name(), info.className())); + if (typeUtils.isAssignable(parameterTypes.get(0), Collection.class) + && !action.allowSelfCollectionArgument()) { + throw new InvalidRepositoryException( + "Action %s (for %s) doesn't support return a collection!", + action.actionType(), element.getSimpleName()); } + + if (readResult.projection() != null) { + var column = info.columnFor(readResult.projection().columnName()); + // specifying the columnName is optional + if (column != null) { + action.validate(info, element.getSimpleName(), returnType, typeUtils, type -> { + if (!typeUtils.isAssignable(type, column.typeName())) { + throw new InvalidRepositoryException( + "Expected response of %s to be assignable from %s", + element.getSimpleName(), column.typeName()); + } + }); + return; + } + } + + action.validate(info, element.getSimpleName(), returnType, typeUtils, null); return; } // Otherwise the expected parameter count should equal the actual - if (parameterTypes.size() != handledInputs) { + if (parameterTypes.size() != handledInputs.get()) { throw new IllegalStateException( "Expected %s parameters, received %s".formatted(handledInputs, parameterTypes)); } } + + private void validateColumnNames( + List factors, SectionType type, BiConsumer customValidation) { + var variableLast = true; + for (Factor factor : factors) { + if (factor instanceof VariableFactor variable) { + var column = info.columnFor(variable.columnName()); + if (column == null) { + throw new IllegalStateException( + "Could not find column %s for entity %s".formatted(variable.columnName(), info.name())); + } + variableLast = true; + if (customValidation != null) { + //noinspection unchecked + customValidation.accept((T) variable, column); + } + } else { + variableLast = false; + } + } + + if (!variableLast) { + throw new IllegalStateException("Cannot end a section (%s) with a factor!".formatted(type)); + } + } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderBySection.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/OrderBySection.java similarity index 95% rename from ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderBySection.java rename to ap/src/main/java/org/geysermc/databaseutils/processor/query/section/OrderBySection.java index f6d4efe..9e31382 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderBySection.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/OrderBySection.java @@ -22,7 +22,7 @@ * @author GeyserMC * @link https://github.com/GeyserMC/DatabaseUtils */ -package org.geysermc.databaseutils.processor.query.section.order; +package org.geysermc.databaseutils.processor.query.section; import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/ProjectionSection.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/ProjectionSection.java new file mode 100644 index 0000000..ce232b7 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/ProjectionSection.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section; + +import java.util.List; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.databaseutils.processor.query.section.factor.Factor; +import org.geysermc.databaseutils.processor.query.section.factor.ProjectionFactor; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.DistinctProjectionKeyword; + +public record ProjectionSection(List<@NonNull ProjectionFactor> projections) { + public ProjectionSection(@NonNull List<@NonNull ProjectionFactor> projections) { + this.projections = projections; + } + + public static ProjectionSection from(@NonNull List<@NonNull Factor> factors) { + if (factors.isEmpty()) { + return null; + } + return new ProjectionSection( + factors.stream().map(factor -> (ProjectionFactor) factor).toList()); + } + + public String columnName() { + return projections.stream() + .mapMulti((factor, results) -> { + if (factor.keyword() == null) { + results.accept(factor.columnName()); + } + }) + .findFirst() + .orElse(null); + } + + public DistinctProjectionKeyword distinct() { + return projections.stream() + .mapMulti((factor, results) -> { + if (factor.keyword() instanceof DistinctProjectionKeyword keyword) { + results.accept(keyword); + } + }) + .findFirst() + .orElse(null); + } + + public @NonNull List<@NonNull ProjectionKeyword> notDistinctProjectionKeywords() { + return projections.stream() + .mapMulti((factor, results) -> { + if (factor.columnName() != null && !(factor.keyword() instanceof DistinctProjectionKeyword)) { + results.accept(factor.keyword()); + } + }) + .toList(); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/SectionType.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/SectionType.java new file mode 100644 index 0000000..e2f51eb --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/SectionType.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section; + +public enum SectionType { + PROJECTION, + BY("By"), + ORDER_BY("Order", "By"); + + public static final SectionType[] VALUES = SectionType.values(); + private final String[] sections; + + SectionType(String... sections) { + this.sections = sections; + } + + public String[] sections() { + return sections; + } + + public static boolean isCorrectOrder(SectionType next, SectionType current) { + var nextIndex = indexFor(next); + var currentIndex = indexFor(current); + return nextIndex > currentIndex; + } + + public static int indexFor(SectionType type) { + if (type == null) return -1; + for (int i = 0; i < VALUES.length; i++) { + if (VALUES[i] == type) { + return i; + } + } + throw new IllegalStateException("Unrecognized section type: " + type); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/MultiInputKeyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeyword.java similarity index 96% rename from ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/MultiInputKeyword.java rename to ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeyword.java index ea9ffc0..bb9e16d 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/MultiInputKeyword.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeyword.java @@ -36,9 +36,11 @@ /** * A keyword that requires multiple inputs from the user. */ -public abstract class MultiInputKeyword implements Keyword { +public abstract class InputKeyword { private final List parameterNames = new ArrayList<>(); + public abstract @NonNull List<@NonNull String> names(); + /** * Returns for each input it's supported types */ @@ -96,7 +98,7 @@ public void addParameterName(@NonNull CharSequence parameterName) { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - MultiInputKeyword that = (MultiInputKeyword) o; + InputKeyword that = (InputKeyword) o; return Objects.equals(parameterNames, that.parameterNames); } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeywordRegistry.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeywordRegistry.java index a112969..b9484a4 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeywordRegistry.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/InputKeywordRegistry.java @@ -33,14 +33,14 @@ import org.geysermc.databaseutils.processor.query.section.by.keyword.LessThanKeyword; public class InputKeywordRegistry { - private static final Map> REGISTRY = new HashMap<>(); + private static final Map> REGISTRY = new HashMap<>(); - public static @Nullable MultiInputKeyword findByName(String keyword) { + public static @Nullable InputKeyword findByName(String keyword) { var supplier = REGISTRY.get(keyword); return supplier != null ? supplier.get() : null; } - private static void register(Supplier keywordSupplier) { + private static void register(Supplier keywordSupplier) { var instance = keywordSupplier.get(); for (@NonNull String name : instance.names()) { REGISTRY.put(name, keywordSupplier); diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/SingleInputKeyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/SingleInputKeyword.java index 40cd06b..89b58f3 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/SingleInputKeyword.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/SingleInputKeyword.java @@ -29,7 +29,7 @@ /** * A keyword that only supports a single input */ -public abstract class SingleInputKeyword extends MultiInputKeyword { +public abstract class SingleInputKeyword extends InputKeyword { /** * Returns the types the input supports */ diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/ProjectionFactor.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/ProjectionFactor.java new file mode 100644 index 0000000..81bd523 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/ProjectionFactor.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.factor; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; + +public record ProjectionFactor(@Nullable ProjectionKeyword keyword, @Nullable String columnName) + implements VariableFactor { + + public ProjectionFactor { + if (columnName == null && keyword == null) { + throw new IllegalArgumentException("Either the columnName or keyword must be specified"); + } + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableByFactor.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableByFactor.java index dc1d071..c3feab5 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableByFactor.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableByFactor.java @@ -25,11 +25,21 @@ package org.geysermc.databaseutils.processor.query.section.factor; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.databaseutils.processor.query.section.by.MultiInputKeyword; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.databaseutils.processor.query.section.by.InputKeyword; import org.geysermc.databaseutils.processor.query.section.by.keyword.EqualsKeyword; -public record VariableByFactor(@NonNull CharSequence name, @NonNull MultiInputKeyword keyword) implements Factor { - public VariableByFactor(CharSequence name) { - this(name, new EqualsKeyword()); +public record VariableByFactor(@NonNull CharSequence columnName, @NonNull InputKeyword keyword) + implements VariableFactor { + public VariableByFactor(@NonNull CharSequence columnName, @Nullable InputKeyword keyword) { + this.columnName = columnName; + if (keyword == null) { + keyword = new EqualsKeyword(); + } + this.keyword = keyword; + } + + public VariableByFactor(@NonNull CharSequence name) { + this(name, null); } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/Keyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableFactor.java similarity index 84% rename from ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/Keyword.java rename to ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableFactor.java index a780872..ac899ff 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/by/Keyword.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableFactor.java @@ -22,11 +22,10 @@ * @author GeyserMC * @link https://github.com/GeyserMC/DatabaseUtils */ -package org.geysermc.databaseutils.processor.query.section.by; +package org.geysermc.databaseutils.processor.query.section.factor; -import java.util.List; -import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; -public interface Keyword { - @NonNull List<@NonNull String> names(); +public interface VariableFactor extends Factor { + @Nullable CharSequence columnName(); } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableOrderByFactor.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableOrderByFactor.java index 3a4a9e4..8736323 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableOrderByFactor.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/factor/VariableOrderByFactor.java @@ -25,11 +25,17 @@ package org.geysermc.databaseutils.processor.query.section.factor; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.databaseutils.processor.query.section.order.OrderDirection; -public record VariableOrderByFactor(CharSequence name, OrderDirection direction) implements Factor { - public VariableOrderByFactor(@NonNull CharSequence name, @NonNull OrderDirection direction) { - this.name = name; +public record VariableOrderByFactor(@NonNull CharSequence columnName, @NonNull OrderDirection direction) + implements VariableFactor { + + public VariableOrderByFactor(@NonNull CharSequence columnName, @Nullable OrderDirection direction) { + this.columnName = columnName; + if (direction == null) { + direction = OrderDirection.DEFAULT; + } this.direction = direction; } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderDirection.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderDirection.java index 636459f..ce2837d 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderDirection.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/order/OrderDirection.java @@ -25,6 +25,7 @@ package org.geysermc.databaseutils.processor.query.section.order; import java.util.List; +import java.util.Locale; public enum OrderDirection { ASCENDING("asc", "ascending"), @@ -40,6 +41,7 @@ public enum OrderDirection { } public static OrderDirection byName(String name) { + name = name.toLowerCase(Locale.ROOT); for (OrderDirection value : VALUES) { if (value.names.contains(name)) { return value; diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeyword.java new file mode 100644 index 0000000..5fc1312 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeyword.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.projection; + +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; + +public abstract class ProjectionKeyword { + private final String name; + private final ProjectionKeywordCategory category; + + protected ProjectionKeyword(String name, ProjectionKeywordCategory category) { + this.name = name; + this.category = category; + } + + public @NonNull String name() { + return name; + } + + public ProjectionKeywordCategory category() { + return category; + } + + public void setValue(@NonNull String fullKeyword) {} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProjectionKeyword that = (ProjectionKeyword) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeywordCategory.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeywordCategory.java new file mode 100644 index 0000000..d28cfbb --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeywordCategory.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.projection; + +import java.util.Locale; + +public enum ProjectionKeywordCategory { + UNIQUE(false), + SUMMARY(true), + LIMIT(false); + + private final boolean requiresColumn; + + ProjectionKeywordCategory(boolean requiresColumn) { + this.requiresColumn = requiresColumn; + } + + public boolean requiresColumn() { + return requiresColumn; + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeywordRegistry.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeywordRegistry.java new file mode 100644 index 0000000..929aef0 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/ProjectionKeywordRegistry.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.projection; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.AvgProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.DistinctProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.TopProjectionKeyword; + +public class ProjectionKeywordRegistry { + private static final Map> REGISTRY = new HashMap<>(); + + public static @Nullable ProjectionKeyword findByName(String keyword) { + for (var entry : REGISTRY.entrySet()) { + if (entry.getKey().matcher(keyword).matches()) { + var instance = entry.getValue().get(); + instance.setValue(keyword); + return instance; + } + } + return null; + } + + private static void register(Supplier keywordSupplier) { + var instance = keywordSupplier.get(); + REGISTRY.put(Pattern.compile(instance.name()), keywordSupplier); + } + + static { + register(DistinctProjectionKeyword::new); + register(AvgProjectionKeyword::new); + register(TopProjectionKeyword::new); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/AvgProjectionKeyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/AvgProjectionKeyword.java new file mode 100644 index 0000000..5d15db4 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/AvgProjectionKeyword.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.projection.keyword; + +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; + +public class AvgProjectionKeyword extends ProjectionKeyword { + public AvgProjectionKeyword() { + super("Avg", ProjectionKeywordCategory.SUMMARY); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/DistinctProjectionKeyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/DistinctProjectionKeyword.java new file mode 100644 index 0000000..dfd80b5 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/DistinctProjectionKeyword.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.projection.keyword; + +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; + +public class DistinctProjectionKeyword extends ProjectionKeyword { + public DistinctProjectionKeyword() { + super("Distinct", ProjectionKeywordCategory.UNIQUE); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/TopProjectionKeyword.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/TopProjectionKeyword.java new file mode 100644 index 0000000..a9da38d --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/section/projection/keyword/TopProjectionKeyword.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.section.projection.keyword; + +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeywordCategory; + +public class TopProjectionKeyword extends ProjectionKeyword { + private int limit; + + public TopProjectionKeyword() { + super("Top[1-9][0-9]*", ProjectionKeywordCategory.LIMIT); + } + + public TopProjectionKeyword(int limit) { + this(); + limit(limit); + } + + public int limit() { + return limit; + } + + public void limit(int limit) { + this.limit = limit; + } + + public void setValue(@NonNull String fullKeyword) { + limit(Integer.parseInt(fullKeyword.substring(3))); + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + return ((TopProjectionKeyword) o).limit == limit; + } + + @Override + public int hashCode() { + return Objects.hash(name(), limit); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/type/ParametersTypeInfo.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/type/ParametersTypeInfo.java new file mode 100644 index 0000000..d21aad4 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/type/ParametersTypeInfo.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.type; + +import java.util.Collection; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import org.geysermc.databaseutils.processor.util.TypeUtils; + +public class ParametersTypeInfo { + private final ExecutableElement element; + private final boolean isSelf; + private final TypeMirror elementType; + private final boolean isSelfCollection; + + public ParametersTypeInfo(ExecutableElement element, CharSequence entityType, TypeUtils typeUtils) { + this.element = element; + this.isSelf = isSelf(entityType, typeUtils); + this.elementType = elementType(typeUtils); + this.isSelfCollection = elementType != null && typeUtils.isAssignable(elementType, entityType); + } + + public ExecutableElement element() { + return element; + } + + public boolean isSelf() { + return isSelf; + } + + public TypeMirror elementType() { + return elementType; + } + + public boolean isSelfCollection() { + return isSelfCollection; + } + + public CharSequence name(int index) { + return element.getParameters().get(index).getSimpleName(); + } + + public boolean isAnySelf() { + return isSelf() || isSelfCollection(); + } + + public boolean isNoneOrAnySelf() { + return element.getParameters().isEmpty() || isAnySelf(); + } + + private boolean isSelf(CharSequence entityType, TypeUtils typeUtils) { + if (element.getParameters().size() != 1) { + return false; + } + var first = element.getParameters().get(0); + return typeUtils.isAssignable(first.asType(), entityType); + } + + private TypeMirror elementType(TypeUtils typeUtils) { + if (element.getParameters().size() != 1) { + return null; + } + var first = element.getParameters().get(0); + if (!typeUtils.isAssignable(first.asType(), Collection.class)) { + return null; + } + return ((DeclaredType) first.asType()).getTypeArguments().get(0); + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/query/type/ReturnTypeInfo.java b/ap/src/main/java/org/geysermc/databaseutils/processor/query/type/ReturnTypeInfo.java new file mode 100644 index 0000000..a241413 --- /dev/null +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/query/type/ReturnTypeInfo.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 GeyserMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/DatabaseUtils + */ +package org.geysermc.databaseutils.processor.query.type; + +import java.util.Collection; +import java.util.Objects; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.geysermc.databaseutils.processor.util.TypeUtils; + +public final class ReturnTypeInfo { + private final boolean async; + private final TypeMirror type; + private final boolean isVoid; + private final TypeMirror elementType; + + public ReturnTypeInfo(boolean async, TypeMirror type, TypeUtils typeUtils) { + this.async = async; + this.type = type; + this.isVoid = isVoid(typeUtils); + this.elementType = elementType(typeUtils); + } + + public boolean async() { + return async; + } + + public TypeMirror type() { + return type; + } + + public boolean isVoid() { + return isVoid; + } + + public TypeMirror elementType() { + return elementType; + } + + public boolean isCollection() { + return elementType() != null; + } + + private boolean isVoid(TypeUtils typeUtils) { + if (type.getKind() == TypeKind.VOID) { + return true; + } + if (type.getKind() == TypeKind.DECLARED) { + return typeUtils.isType(Void.class, type); + } + return false; + } + + private TypeMirror elementType(TypeUtils typeUtils) { + if (type.getKind() != TypeKind.DECLARED) { + return null; + } + if (!typeUtils.isAssignable(type, Collection.class)) { + return null; + } + return ((DeclaredType) type).getTypeArguments().get(0); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ReturnTypeInfo) obj; + return Objects.equals(this.type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(type); + } + + @Override + public String toString() { + return "ReturnTypeInfo[" + "type=" + type + ']'; + } +} diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/type/RepositoryGenerator.java b/ap/src/main/java/org/geysermc/databaseutils/processor/type/RepositoryGenerator.java index 421c607..69605b2 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/type/RepositoryGenerator.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/type/RepositoryGenerator.java @@ -48,15 +48,15 @@ public abstract class RepositoryGenerator { protected void onConstructorBuilder(MethodSpec.Builder builder) {} - public abstract void addFind(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async); + public abstract void addFind(QueryInfo info, MethodSpec.Builder spec); - public abstract void addExists(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async); + public abstract void addExists(QueryInfo info, MethodSpec.Builder spec); - public abstract void addInsert(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async); + public abstract void addInsert(QueryInfo info, MethodSpec.Builder spec); - public abstract void addUpdate(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async); + public abstract void addUpdate(QueryInfo info, MethodSpec.Builder spec); - public abstract void addDelete(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async); + public abstract void addDelete(QueryInfo info, MethodSpec.Builder spec); public void init(TypeElement superType, EntityInfo entityInfo) { if (this.typeSpec != null) { diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/type/SqlRepositoryGenerator.java b/ap/src/main/java/org/geysermc/databaseutils/processor/type/SqlRepositoryGenerator.java index af47e81..e88df83 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/type/SqlRepositoryGenerator.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/type/SqlRepositoryGenerator.java @@ -42,7 +42,6 @@ import java.util.concurrent.CompletionException; import java.util.function.Supplier; import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.databaseutils.processor.info.ColumnInfo; import org.geysermc.databaseutils.processor.query.QueryInfo; @@ -52,6 +51,9 @@ import org.geysermc.databaseutils.processor.query.section.factor.Factor; import org.geysermc.databaseutils.processor.query.section.factor.OrFactor; import org.geysermc.databaseutils.processor.query.section.factor.VariableByFactor; +import org.geysermc.databaseutils.processor.query.section.projection.ProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.AvgProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.TopProjectionKeyword; import org.geysermc.databaseutils.processor.util.InvalidRepositoryException; import org.geysermc.databaseutils.processor.util.TypeUtils; @@ -68,19 +70,30 @@ protected void onConstructorBuilder(MethodSpec.Builder builder) { } @Override - public void addFind(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async) { - // todo make it work without a By section - var query = "select * from %s where %s".formatted(info.tableName(), createWhereForAll(info)); - addExecuteQueryData(spec, async, query, info, () -> { - spec.beginControlFlow("if (!result.next())"); - spec.addStatement("return null"); - spec.endControlFlow(); + public void addFind(QueryInfo info, MethodSpec.Builder spec) { + var query = "select %s from %s".formatted(createProjectionFor(info), info.tableName()); + if (info.hasBySection()) { + query += " where %s".formatted(createWhereForAll(info)); + } + + addExecuteQueryData(spec, query, info, () -> { + if (info.returnInfo().isCollection()) { + spec.addStatement( + "$T __responses = new $L<>()", + info.returnType(), + info.typeUtils().collectionImplementationFor(info.returnType())); + spec.beginControlFlow("while (__result.next())"); + } else { + spec.beginControlFlow("if (!__result.next())"); + spec.addStatement("return null"); + spec.endControlFlow(); + } var arguments = new ArrayList(); for (ColumnInfo column : info.columns()) { var columnType = ClassName.bestGuess(column.typeName().toString()); - var getFormat = jdbcGetFor(column.typeName(), "result.%s(%s)"); + var getFormat = jdbcGetFor(column.typeName(), "__result.%s(%s)"); if (TypeUtils.needsTypeCodec(column.typeName())) { getFormat = CodeBlock.of("this.__$L.decode($L)", column.name(), getFormat) .toString(); @@ -89,112 +102,99 @@ public void addFind(QueryInfo info, MethodSpec.Builder spec, TypeElement returnT spec.addStatement("$T _$L = %s".formatted(getFormat), columnType, column.name(), column.name()); arguments.add("_" + column.name()); } - spec.addStatement( - "return new $T($L)", - ClassName.bestGuess(info.entityType().toString()), - String.join(", ", arguments)); + + if (info.returnInfo().isCollection()) { + spec.addStatement( + "__responses.add(new $T($L))", + ClassName.bestGuess(info.entityType().toString()), + String.join(", ", arguments)); + spec.endControlFlow(); + spec.addStatement("return __responses"); + } else { + spec.addStatement( + "return new $T($L)", + ClassName.bestGuess(info.entityType().toString()), + String.join(", ", arguments)); + } }); } @Override - public void addExists(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async) { - // todo make it work without a By section - var query = "select 1 from %s where %s".formatted(info.tableName(), createWhereForAll(info)); - addExecuteQueryData(spec, async, query, info, () -> spec.addStatement("return result.next()")); + public void addExists(QueryInfo info, MethodSpec.Builder spec) { + var query = "select 1 from %s".formatted(info.tableName()); + if (info.hasBySection()) { + query += " where %s".formatted(createWhereForAll(info)); + } + addExecuteQueryData(spec, query, info, () -> spec.addStatement("return __result.next()")); } @Override - public void addInsert(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async) { + public void addInsert(QueryInfo info, MethodSpec.Builder spec) { var columnNames = String.join(",", info.columns().stream().map(ColumnInfo::name).toList()); var columnParameters = String.join(",", repeat("?", info.columns().size())); var query = "insert into %s (%s) values (%s)".formatted(info.tableName(), columnNames, columnParameters); - addUpdateQueryData(spec, returnType, async, query, info, info.columns()); + addUpdateQueryData(spec, query, info, info.columns()); } @Override - public void addUpdate(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async) { + public void addUpdate(QueryInfo info, MethodSpec.Builder spec) { // todo make it work with By section var query = "update %s set %s where %s".formatted(info.tableName(), createSetFor(info), createWhereForKeys(info)); - addUpdateQueryData( - spec, returnType, async, query, info, info.entityInfo().notKeyFirstColumns()); + addUpdateQueryData(spec, query, info, info.entityInfo().notKeyFirstColumns()); } @Override - public void addDelete(QueryInfo info, MethodSpec.Builder spec, TypeElement returnType, boolean async) { + public void addDelete(QueryInfo info, MethodSpec.Builder spec) { if (info.hasBySection()) { var query = "delete from %s where %s".formatted(info.tableName(), createWhereForAll(info)); - addUpdateQueryData(spec, Void.class.getCanonicalName(), async, query, info); + addUpdateQueryData(spec, query, info); return; } var query = "delete from %s where %s".formatted(info.tableName(), createWhereForKeys(info)); - addUpdateQueryData( - spec, returnType, async, query, info, info.entityInfo().keyColumns()); + addUpdateQueryData(spec, query, info, info.entityInfo().keyColumns()); } - private void addExecuteQueryData( - MethodSpec.Builder spec, boolean async, String query, QueryInfo info, Runnable content) { - addBySectionData(spec, async, query, info, () -> { - spec.beginControlFlow("try ($T result = statement.executeQuery())", ResultSet.class); + private void addExecuteQueryData(MethodSpec.Builder spec, String query, QueryInfo info, Runnable content) { + addBySectionData(spec, query, info, () -> { + spec.beginControlFlow("try ($T __result = __statement.executeQuery())", ResultSet.class); content.run(); spec.endControlFlow(); }); } - private void addUpdateQueryData( - MethodSpec.Builder spec, - TypeElement returnType, - boolean async, - String query, - QueryInfo info, - List columns) { - addNoBySectionData(spec, async, query, info, columns, () -> { - spec.addStatement("statement.executeUpdate()"); - if (TypeUtils.isType(Void.class, returnType)) { - spec.addStatement("return null"); - } else if (TypeUtils.isType(info.entityType(), returnType)) { + private void addUpdateQueryData(MethodSpec.Builder spec, String query, QueryInfo info) { + addBySectionData(spec, query, info, () -> { + spec.addStatement("__statement.executeUpdate()"); + if (info.typeUtils().isType(Void.class, info.returnType())) { + spec.addStatement("return " + (info.returnInfo().async() ? "null" : "")); + } else if (info.typeUtils().isType(info.entityType(), info.returnType())) { // todo support also creating an entity type from the given parameters - spec.addStatement("return $L", info.parameterName(0)); + spec.addStatement("return $L", info.parametersInfo().name(0)); } else { throw new InvalidRepositoryException( - "Return type can be either void or %s but got %s", - info.entityType(), returnType.getQualifiedName()); + "Return type can be either void or %s but got %s", info.entityType(), info.returnType()); } }); } - private void addUpdateQueryData( - MethodSpec.Builder spec, CharSequence returnType, boolean async, String query, QueryInfo info) { - addBySectionData(spec, async, query, info, () -> { - spec.addStatement("statement.executeUpdate()"); - if (TypeUtils.isType(Void.class, returnType)) { - spec.addStatement("return null"); - } else if (TypeUtils.isType(info.entityType(), returnType)) { - // todo support also creating an entity type from the given parameters - spec.addStatement("return $L", info.parameterName(0)); - } else { - throw new InvalidRepositoryException( - "Return type can be either void or %s but got %s", info.entityType(), returnType); - } - }); - } - - private void addNoBySectionData( - MethodSpec.Builder spec, - boolean async, - String query, - QueryInfo info, - List columns, - Runnable content) { - wrapInCompletableFuture(spec, async, () -> { - spec.beginControlFlow("try ($T connection = dataSource.getConnection())", Connection.class); + private void addUpdateQueryData(MethodSpec.Builder spec, String query, QueryInfo info, List columns) { + wrapInCompletableFuture(spec, info.returnInfo().async(), () -> { + spec.beginControlFlow("try ($T __connection = this.dataSource.getConnection())", Connection.class); spec.beginControlFlow( - "try ($T statement = connection.prepareStatement($S))", PreparedStatement.class, query); + "try ($T __statement = __connection.prepareStatement($S))", PreparedStatement.class, query); + + var parameterName = info.parametersInfo().name(0); + + if (info.parametersInfo().isSelfCollection()) { + spec.addStatement("int __count = 0"); + spec.beginControlFlow("for (var __element : $L)", parameterName); + parameterName = "__element"; + } // if it doesn't have a By section, we add all the requested columns - var parameterName = info.parameterName(0); int variableIndex = 0; for (ColumnInfo column : columns) { var columnName = column.name(); @@ -206,30 +206,57 @@ private void addNoBySectionData( .toString(); } // jdbc index starts at 1 - spec.addStatement(jdbcSetFor(columnType, "statement.%s($L, $L)"), ++variableIndex, input); + spec.addStatement(jdbcSetFor(columnType, "__statement.%s($L, $L)"), ++variableIndex, input); } - content.run(); + if (info.parametersInfo().isSelfCollection()) { + spec.addStatement("__statement.addBatch()"); + spec.beginControlFlow("if (__count % 250 == 0)"); + spec.addStatement("__statement.executeBatch()"); + spec.endControlFlow(); + + spec.endControlFlow(); + spec.addStatement("__statement.executeBatch()"); + spec.addStatement("__connection.commit()"); + } else { + spec.addStatement("__statement.executeUpdate()"); + } + + if (info.typeUtils().isType(Void.class, info.returnType())) { + spec.addStatement("return " + (info.returnInfo().async() ? "null" : "")); + } else if (info.typeUtils().isType(info.entityType(), info.returnType())) { + // todo support also creating an entity type from the given parameters + spec.addStatement("return $L", info.parametersInfo().name(0)); + } else { + throw new InvalidRepositoryException( + "Return type can be either void or %s but got %s", info.entityType(), info.returnType()); + } + + if (info.parametersInfo().isSelfCollection()) { + spec.nextControlFlow("catch ($T __exception)", SQLException.class); + spec.addStatement("__connection.rollback()"); + spec.addStatement("throw __exception"); + } spec.endControlFlow(); - spec.nextControlFlow("catch ($T exception)", SQLException.class); - spec.addStatement("throw new $T($S, exception)", CompletionException.class, "Unexpected error occurred"); + + spec.nextControlFlow("catch ($T __exception)", SQLException.class); + spec.addStatement("throw new $T($S, __exception)", CompletionException.class, "Unexpected error occurred"); spec.endControlFlow(); }); typeSpec.addMethod(spec.build()); } - private void addBySectionData( - MethodSpec.Builder spec, boolean async, String query, QueryInfo info, Runnable content) { - wrapInCompletableFuture(spec, async, () -> { - spec.beginControlFlow("try ($T connection = dataSource.getConnection())", Connection.class); + private void addBySectionData(MethodSpec.Builder spec, String query, QueryInfo info, Runnable content) { + wrapInCompletableFuture(spec, info.returnInfo().async(), () -> { + spec.beginControlFlow("try ($T __connection = this.dataSource.getConnection())", Connection.class); spec.beginControlFlow( - "try ($T statement = connection.prepareStatement($S))", PreparedStatement.class, query); + "try ($T __statement = __connection.prepareStatement($S))", PreparedStatement.class, query); // if it has a By section, everything is handled through the parameters int variableIndex = 0; for (VariableByFactor variable : info.byVariables()) { - var columnName = variable.name(); + var columnName = variable.columnName(); var columnType = Objects.requireNonNull(info.columnFor(columnName)).typeName(); @@ -240,15 +267,15 @@ private void addBySectionData( .toString(); } // jdbc index starts at 1 - spec.addStatement(jdbcSetFor(columnType, "statement.%s($L, $L)"), ++variableIndex, input); + spec.addStatement(jdbcSetFor(columnType, "__statement.%s($L, $L)"), ++variableIndex, input); } } content.run(); spec.endControlFlow(); - spec.nextControlFlow("catch ($T exception)", SQLException.class); - spec.addStatement("throw new $T($S, exception)", CompletionException.class, "Unexpected error occurred"); + spec.nextControlFlow("catch ($T __exception)", SQLException.class); + spec.addStatement("throw new $T($S, __exception)", CompletionException.class, "Unexpected error occurred"); spec.endControlFlow(); }); typeSpec.addMethod(spec.build()); @@ -299,7 +326,7 @@ private String createParametersForFactors(List factors, char separator) } var keyword = variable.keyword(); - builder.append(variable.name()); + builder.append(variable.columnName()); if (keyword instanceof EqualsKeyword) { builder.append("=?"); } else if (keyword instanceof LessThanKeyword) { @@ -310,4 +337,35 @@ private String createParametersForFactors(List factors, char separator) } return builder.toString(); } + + private String createProjectionFor(QueryInfo info) { + var section = info.result().projection(); + if (section == null) { + return "*"; + } + var distinct = section.distinct(); + var columnName = section.columnName(); + + var result = columnName != null ? columnName : "*"; + if (distinct != null) { + result = "distinct " + result; + } + String limit = null; + + for (ProjectionKeyword projection : section.notDistinctProjectionKeywords()) { + if (projection instanceof AvgProjectionKeyword) { + if (!info.typeUtils().isWholeNumberType(info.returnType())) { + result = "avg(%s)".formatted(result); + } + continue; + } + if (projection instanceof TopProjectionKeyword keyword) { + limit = "limit " + keyword.limit(); + continue; + } + throw new InvalidRepositoryException("Unsupported projection %s", projection.name()); + } + + return result + (limit != null ? ' ' + limit : ""); + } } diff --git a/ap/src/main/java/org/geysermc/databaseutils/processor/util/TypeUtils.java b/ap/src/main/java/org/geysermc/databaseutils/processor/util/TypeUtils.java index 2c60ab9..0ad19a4 100644 --- a/ap/src/main/java/org/geysermc/databaseutils/processor/util/TypeUtils.java +++ b/ap/src/main/java/org/geysermc/databaseutils/processor/util/TypeUtils.java @@ -25,9 +25,16 @@ package org.geysermc.databaseutils.processor.util; import com.google.auto.common.MoreTypes; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -41,8 +48,38 @@ public TypeUtils(Types typeUtils, Elements elementUtils) { this.elementUtils = elementUtils; } - public TypeElement toBoxedTypeElement(TypeMirror mirror) { + public Types typeUtils() { + return typeUtils; + } + + public Elements elementUtils() { + return elementUtils; + } + + public TypeElement elementFor(CharSequence name) { + return elementUtils.getTypeElement(name); + } + + public TypeElement elementFor(Class clazz) { + return elementFor(clazz.getCanonicalName()); + } + + public TypeMirror toBoxedMirror(TypeMirror mirror) { + if (mirror.getKind() == TypeKind.ERROR) { + throw new InvalidRepositoryException( + "Could not resolve a specific class! Please make sure you added all correct imports"); + } if (mirror.getKind().isPrimitive()) { + return typeUtils.boxedClass(MoreTypes.asPrimitiveType(mirror)).asType(); + } + if (mirror.getKind() == TypeKind.VOID) { + return elementFor(Void.class).asType(); + } + return mirror; + } + + public TypeElement toBoxedTypeElement(TypeMirror mirror) { + if (mirror.getKind().isPrimitive() || mirror.getKind() == TypeKind.VOID) { return typeUtils.boxedClass(MoreTypes.asPrimitiveType(mirror)); } return MoreTypes.asTypeElement(mirror); @@ -53,7 +90,7 @@ public boolean isAssignable(TypeMirror impl, TypeMirror base) { } public boolean isAssignable(CharSequence impl, TypeMirror base) { - return isAssignable(elementUtils.getTypeElement(impl).asType(), base); + return isAssignable(elementFor(impl).asType(), typeUtils.erasure(base)); } public boolean isAssignable(Class impl, TypeMirror base) { @@ -61,7 +98,7 @@ public boolean isAssignable(Class impl, TypeMirror base) { } public boolean isAssignable(TypeMirror impl, CharSequence base) { - return isAssignable(impl, elementUtils.getTypeElement(base).asType()); + return isAssignable(typeUtils.erasure(impl), elementFor(base).asType()); } public boolean isAssignable(TypeMirror impl, Class base) { @@ -72,20 +109,48 @@ public CharSequence canonicalName(TypeMirror mirror) { return toBoxedTypeElement(mirror).getQualifiedName(); } - public static boolean isType(Class clazz, TypeElement element) { - return isType(clazz, element.getQualifiedName()); + public CharSequence collectionImplementationFor(TypeMirror returnType) { + if (isType(Set.class, returnType)) { + return HashSet.class.getCanonicalName(); + } + if (isType(List.class, returnType)) { + return ArrayList.class.getCanonicalName(); + } + var implType = MoreTypes.asTypeElement(returnType); + for (Element element : implType.getEnclosedElements()) { + if (element.getSimpleName().contentEquals("")) { + var constructor = (ExecutableElement) element; + if (constructor.getParameters().isEmpty() + && constructor.getModifiers().contains(Modifier.PUBLIC)) { + return implType.getQualifiedName(); + } + } + } + throw new IllegalStateException("Cannot find an usable implementation for " + returnType); } - public static boolean isType(Class clazz, CharSequence canonicalName) { - return clazz.getCanonicalName().contentEquals(canonicalName); + public boolean isType(Class clazz, TypeMirror mirror) { + return isType(clazz.getCanonicalName(), mirror); + } + + public static boolean isType(CharSequence expected, TypeElement actual) { + return actual.getQualifiedName().contentEquals(expected); } - public static boolean isType(CharSequence name, TypeElement element) { - return element.getQualifiedName().contentEquals(name); + public boolean isType(CharSequence expected, TypeMirror actual) { + return isType(elementFor(expected).asType(), actual); } - public static boolean isType(CharSequence expected, TypeMirror actual) { - return isType(expected, MoreTypes.asTypeElement(actual)); + public boolean isType(TypeMirror expected, TypeMirror actual) { + return typeUtils.isSameType(typeUtils.erasure(expected), typeUtils.erasure(actual)); + } + + public static boolean isType(Class clazz, TypeElement element) { + return isType(clazz, element.getQualifiedName()); + } + + public static boolean isType(Class clazz, CharSequence canonicalName) { + return clazz.getCanonicalName().contentEquals(canonicalName); } public static boolean isType(CharSequence expected, CharSequence actual) { @@ -119,10 +184,10 @@ public static boolean needsTypeCodec(Name className) { .isEmpty(); } - public static boolean isWholeNumberType(TypeElement element) { - return isType(Byte.class, element) - || isType(Short.class, element) - || isType(Integer.class, element) - || isType(Long.class, element); + public boolean isWholeNumberType(TypeMirror mirror) { + return isType(Byte.class, mirror) + || isType(Short.class, mirror) + || isType(Integer.class, mirror) + || isType(Long.class, mirror); } } diff --git a/ap/src/test/java/org/geysermc/databaseutils/processor/query/KeywordsReaderTests.java b/ap/src/test/java/org/geysermc/databaseutils/processor/query/KeywordsReaderTests.java index 8a60fb2..80a2011 100644 --- a/ap/src/test/java/org/geysermc/databaseutils/processor/query/KeywordsReaderTests.java +++ b/ap/src/test/java/org/geysermc/databaseutils/processor/query/KeywordsReaderTests.java @@ -24,6 +24,7 @@ */ package org.geysermc.databaseutils.processor.query; +import static org.geysermc.databaseutils.processor.util.CollectionUtils.join; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.util.Collections; @@ -34,9 +35,12 @@ import org.geysermc.databaseutils.processor.query.section.factor.AndFactor; import org.geysermc.databaseutils.processor.query.section.factor.Factor; import org.geysermc.databaseutils.processor.query.section.factor.OrFactor; +import org.geysermc.databaseutils.processor.query.section.factor.ProjectionFactor; import org.geysermc.databaseutils.processor.query.section.factor.VariableByFactor; import org.geysermc.databaseutils.processor.query.section.factor.VariableOrderByFactor; import org.geysermc.databaseutils.processor.query.section.order.OrderDirection; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.DistinctProjectionKeyword; +import org.geysermc.databaseutils.processor.query.section.projection.keyword.TopProjectionKeyword; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -46,31 +50,61 @@ public class KeywordsReaderTests { @ParameterizedTest @MethodSource("okSimpleInputs") void okSimple( - String input, List variables, String action, List byFactors, List orderByFactors) { - commonOkLogic(input, variables, action, byFactors, orderByFactors); + String input, + List variables, + String action, + List projection, + List byFactors, + List orderByFactors) { + commonOkLogic(input, variables, action, projection, byFactors, orderByFactors); } @ParameterizedTest @MethodSource("okComplexInputs") void okComplex( - String input, List variables, String action, List byFactors, List orderByFactors) { - commonOkLogic(input, variables, action, byFactors, orderByFactors); + String input, + List variables, + String action, + List projection, + List byFactors, + List orderByFactors) { + commonOkLogic(input, variables, action, projection, byFactors, orderByFactors); } private void commonOkLogic( - String input, List variables, String action, List byFactors, List orderByFactors) { + String input, + List variables, + String action, + List projections, + List byFactors, + List orderByFactors) { var result = new KeywordsReader(input, variables).read(); Assertions.assertEquals(action, result.actionName()); + if (projections == null) { + Assertions.assertNull(result.projection()); + } else { + Assertions.assertNotNull(result.projection()); + var actualProjection = result.projection().projections(); + Assertions.assertEquals( + projections.size(), actualProjection.size(), () -> "For: %s\nExpected:\n%s\nActual:\n%s" + .formatted(input, join(projections), join(actualProjection))); + + for (int i = 0; i < projections.size(); i++) { + Assertions.assertEquals(projections.get(i), actualProjection.get(i)); + } + } + if (byFactors == null) { Assertions.assertNull(result.bySection()); } else { Assertions.assertNotNull(result.bySection()); - var actualVariables = result.bySection().factors(); - Assertions.assertEquals(byFactors.size(), actualVariables.size()); + var actualFactors = result.bySection().factors(); + Assertions.assertEquals(byFactors.size(), actualFactors.size(), () -> "For: %s\nExpected:\n%s\nActual:\n%s" + .formatted(input, join(byFactors), join(actualFactors))); for (int i = 0; i < byFactors.size(); i++) { - Assertions.assertEquals(byFactors.get(i), actualVariables.get(i)); + Assertions.assertEquals(byFactors.get(i), actualFactors.get(i)); } } @@ -78,47 +112,70 @@ private void commonOkLogic( Assertions.assertNull(result.orderBySection()); } else { Assertions.assertNotNull(result.orderBySection()); - var actualVariables = result.orderBySection().factors(); - Assertions.assertEquals(orderByFactors.size(), actualVariables.size()); + var actualFactors = result.orderBySection().factors(); + Assertions.assertEquals( + orderByFactors.size(), actualFactors.size(), () -> "For: %s\nExpected:\n%s\nActual:\n%s" + .formatted(input, join(orderByFactors), join(actualFactors))); for (int i = 0; i < orderByFactors.size(); i++) { - Assertions.assertEquals(orderByFactors.get(i), actualVariables.get(i)); + Assertions.assertEquals(orderByFactors.get(i), actualFactors.get(i)); } } } static Stream okSimpleInputs() { return Stream.of( - arguments("update", Collections.emptyList(), "update", null, null), + arguments("update", Collections.emptyList(), "update", null, null, null), + arguments("find", Collections.emptyList(), "find", null, null, null), + arguments( + "findTitle", + List.of("title"), + "find", + List.of(new ProjectionFactor(null, "title")), + null, + null), + arguments( + "findTop3Title", + List.of("title"), + "find", + List.of( + new ProjectionFactor(new TopProjectionKeyword(3), null), + new ProjectionFactor(null, "title")), + null, + null), arguments( "updateByUsername", List.of("username"), "update", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), null), - arguments("find", Collections.emptyList(), "find", null, null), arguments( "findByUsername", List.of("username"), "find", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), null), arguments( "findByUniqueId", List.of("uniqueId"), "find", + null, List.of(new VariableByFactor("uniqueId", new EqualsKeyword())), null), arguments( "findByUsernameLessThan", List.of("username"), "find", + null, List.of(new VariableByFactor("username", new LessThanKeyword())), null), arguments( "findByUniqueIdLessThan", List.of("uniqueId"), "find", + null, List.of(new VariableByFactor("uniqueId", new LessThanKeyword())), null), arguments( @@ -126,57 +183,76 @@ static Stream okSimpleInputs() { List.of("username"), "find", null, + null, List.of(new VariableOrderByFactor("username", OrderDirection.DEFAULT))), arguments( "findOrderByUsernameAsc", List.of("username"), "find", null, + null, List.of(new VariableOrderByFactor("username", OrderDirection.ASCENDING))), arguments( "findByUsernameOrderByUsername", List.of("username"), "find", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), List.of(new VariableOrderByFactor("username", OrderDirection.DEFAULT))), arguments( "findByUsernameOrderByUsernameAsc", List.of("username"), "find", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), List.of(new VariableOrderByFactor("username", OrderDirection.ASCENDING))), arguments( "findByUsernameOrderByUsernameDesc", List.of("username"), "find", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), List.of(new VariableOrderByFactor("username", OrderDirection.DESCENDING))), arguments( "findByUniqueIdOrderByUsername", List.of("uniqueId", "username"), "find", + null, List.of(new VariableByFactor("uniqueId", new EqualsKeyword())), List.of(new VariableOrderByFactor("username", OrderDirection.DEFAULT))), arguments( "findByUsernameLessThanOrderByUniqueId", List.of("username", "uniqueId"), "find", + null, List.of(new VariableByFactor("username", new LessThanKeyword())), List.of(new VariableOrderByFactor("uniqueId", OrderDirection.DEFAULT))), arguments( "findByUniqueIdLessThanOrderByUniqueId", List.of("uniqueId"), "find", + null, List.of(new VariableByFactor("uniqueId", new LessThanKeyword())), List.of(new VariableOrderByFactor("uniqueId", OrderDirection.DEFAULT)))); } static Stream okComplexInputs() { return Stream.of( + arguments( + "findTop3DistinctTitle", + List.of("title"), + "find", + List.of( + new ProjectionFactor(new TopProjectionKeyword(3), null), + new ProjectionFactor(new DistinctProjectionKeyword(), null), + new ProjectionFactor(null, "title")), + null, + null), arguments( "findByUsernameAndPassword", List.of("username", "password"), "find", + null, List.of( new VariableByFactor("username", new EqualsKeyword()), AndFactor.INSTANCE, @@ -186,6 +262,7 @@ static Stream okComplexInputs() { "findByUniqueIdAndPassword", List.of("uniqueId", "password"), "find", + null, List.of( new VariableByFactor("uniqueId", new EqualsKeyword()), AndFactor.INSTANCE, @@ -195,6 +272,7 @@ static Stream okComplexInputs() { "findByUniqueIdAndUniqueName", List.of("uniqueId", "uniqueName"), "find", + null, List.of( new VariableByFactor("uniqueId", new EqualsKeyword()), AndFactor.INSTANCE, @@ -204,6 +282,7 @@ static Stream okComplexInputs() { "findByUsernameLessThanAndPassword", List.of("username", "password"), "find", + null, List.of( new VariableByFactor("username", new LessThanKeyword()), AndFactor.INSTANCE, @@ -213,16 +292,40 @@ static Stream okComplexInputs() { "findByUniqueIdLessThanOrMyHash", List.of("uniqueId", "myHash"), "find", + null, List.of( new VariableByFactor("uniqueId", new LessThanKeyword()), OrFactor.INSTANCE, new VariableByFactor("myHash", new EqualsKeyword())), null), + arguments( + "findTop3TitleByUsername", + List.of("title", "username"), + "find", + List.of( + new ProjectionFactor(new TopProjectionKeyword(3), null), + new ProjectionFactor(null, "title")), + List.of(new VariableByFactor("username", new EqualsKeyword())), + null), + arguments( + "findTop3DistinctTitleByUsernameAndPassword", + List.of("title", "username", "password"), + "find", + List.of( + new ProjectionFactor(new TopProjectionKeyword(3), null), + new ProjectionFactor(new DistinctProjectionKeyword(), null), + new ProjectionFactor(null, "title")), + List.of( + new VariableByFactor("username", new EqualsKeyword()), + AndFactor.INSTANCE, + new VariableByFactor("password", new EqualsKeyword())), + null), arguments( "findOrderByUsernameOrEmail", List.of("username", "email"), "find", null, + null, List.of( new VariableOrderByFactor("username", OrderDirection.DEFAULT), OrFactor.INSTANCE, @@ -232,6 +335,7 @@ static Stream okComplexInputs() { List.of("username", "password"), "find", null, + null, List.of( new VariableOrderByFactor("username", OrderDirection.ASCENDING), OrFactor.INSTANCE, @@ -240,6 +344,7 @@ static Stream okComplexInputs() { "findByUsernameOrderByUsernameAndPassword", List.of("username", "password"), "find", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), List.of( new VariableOrderByFactor("username", OrderDirection.DEFAULT), @@ -249,6 +354,7 @@ static Stream okComplexInputs() { "findByUsernameOrderByUsernameAscOrPasswordDesc", List.of("username", "password"), "find", + null, List.of(new VariableByFactor("username", new EqualsKeyword())), List.of( new VariableOrderByFactor("username", OrderDirection.ASCENDING), @@ -258,6 +364,7 @@ static Stream okComplexInputs() { "findByUniqueIdLessThanOrUsernameOrderByUniqueIdAndPingDesc", List.of("uniqueId", "username", "ping"), "find", + null, List.of( new VariableByFactor("uniqueId", new LessThanKeyword()), OrFactor.INSTANCE, diff --git a/ap/src/test/resources/test/basic/BasicRepository.java b/ap/src/test/resources/test/basic/BasicRepository.java index 33a65f3..41d8875 100644 --- a/ap/src/test/resources/test/basic/BasicRepository.java +++ b/ap/src/test/resources/test/basic/BasicRepository.java @@ -1,20 +1,34 @@ package test.basic; +import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.geysermc.databaseutils.IRepository; import org.geysermc.databaseutils.meta.Repository; @Repository public interface BasicRepository extends IRepository { + CompletableFuture> find(); + CompletableFuture findByAAndB(int aa, String b); + Set find(Set entities); + + CompletableFuture exists(); + CompletableFuture existsByAOrB(int a, String bb); - TestEntity update(TestEntity entity); + void update(List entity); + + void update(TestEntity entity); CompletableFuture insert(TestEntity entity); + void insert(List entities); + CompletableFuture delete(TestEntity entity); + void delete(List entities); + CompletableFuture deleteByAAndB(int a, String b); } \ No newline at end of file diff --git a/ap/src/test/resources/test/basic/BasicRepositorySqlImpl.java b/ap/src/test/resources/test/basic/BasicRepositorySqlImpl.java index 00270fb..11ff3ba 100644 --- a/ap/src/test/resources/test/basic/BasicRepositorySqlImpl.java +++ b/ap/src/test/resources/test/basic/BasicRepositorySqlImpl.java @@ -10,6 +10,8 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -30,26 +32,85 @@ public BasicRepositorySqlImpl(SqlDatabase database, TypeCodecRegistry registry) this.__d = registry.requireCodecFor(UUID.class); } + @Override + public CompletableFuture> find() { + return CompletableFuture.supplyAsync(() -> { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("select * from hello")) { + try (ResultSet __result = __statement.executeQuery()) { + List __responses = new java.util.ArrayList<>(); + while (__result.next()) { + Integer _a = __result.getInt("a"); + String _b = __result.getString("b"); + String _c = __result.getString("c"); + UUID _d = this.__d.decode(__result.getBytes("d")); + __responses.add(new TestEntity(_a, _b, _c, _d)); + } + return __responses; + } + } + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); + } + } , this.database.executorService()); + } + @Override public CompletableFuture findByAAndB(int aa, String b) { return CompletableFuture.supplyAsync(() -> { - try (Connection connection = dataSource.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("select * from hello where a=? and b=?")) { - statement.setInt(1, aa); - statement.setString(2, b); - try (ResultSet result = statement.executeQuery()) { - if (!result.next()) { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("select * from hello where a=? and b=?")) { + __statement.setInt(1, aa); + __statement.setString(2, b); + try (ResultSet __result = __statement.executeQuery()) { + if (!__result.next()) { return null; } - Integer _a = result.getInt("a"); - String _b = result.getString("b"); - String _c = result.getString("c"); - UUID _d = this.__d.decode(result.getBytes("d")); + Integer _a = __result.getInt("a"); + String _b = __result.getString("b"); + String _c = __result.getString("c"); + UUID _d = this.__d.decode(__result.getBytes("d")); return new TestEntity(_a, _b, _c, _d); } } - } catch (SQLException exception) { - throw new CompletionException("Unexpected error occurred", exception); + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); + } + } , this.database.executorService()); + } + + @Override + public Set find(Set entities) { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("select * from hello")) { + try (ResultSet __result = __statement.executeQuery()) { + Set __responses = new java.util.HashSet<>(); + while (__result.next()) { + Integer _a = __result.getInt("a"); + String _b = __result.getString("b"); + String _c = __result.getString("c"); + UUID _d = this.__d.decode(__result.getBytes("d")); + __responses.add(new TestEntity(_a, _b, _c, _d)); + } + return __responses; + } + } + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); + } + } + + @Override + public CompletableFuture exists() { + return CompletableFuture.supplyAsync(() -> { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("select 1 from hello")) { + try (ResultSet __result = __statement.executeQuery()) { + return __result.next(); + } + } + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); } } , this.database.executorService()); } @@ -57,82 +118,161 @@ public CompletableFuture findByAAndB(int aa, String b) { @Override public CompletableFuture existsByAOrB(int a, String bb) { return CompletableFuture.supplyAsync(() -> { - try (Connection connection = dataSource.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("select 1 from hello where a=? or b=?")) { - statement.setInt(1, a); - statement.setString(2, bb); - try (ResultSet result = statement.executeQuery()) { - return result.next(); + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("select 1 from hello where a=? or b=?")) { + __statement.setInt(1, a); + __statement.setString(2, bb); + try (ResultSet __result = __statement.executeQuery()) { + return __result.next(); } } - } catch (SQLException exception) { - throw new CompletionException("Unexpected error occurred", exception); + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); } } , this.database.executorService()); } @Override - public TestEntity update(TestEntity entity) { - try (Connection connection = dataSource.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("update hello set c=?,d=? where a=? and b=?")) { - statement.setString(1, entity.c()); - statement.setBytes(2, this.__d.encode(entity.d())); - statement.setInt(3, entity.a()); - statement.setString(4, entity.b()); - statement.executeUpdate(); - return entity; + public void update(List entity) { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("update hello set c=?,d=? where a=? and b=?")) { + int __count = 0; + for (var __element : entity) { + __statement.setString(1, __element.c()); + __statement.setBytes(2, this.__d.encode(__element.d())); + __statement.setInt(3, __element.a()); + __statement.setString(4, __element.b()); + __statement.addBatch(); + if (__count % 250 == 0) { + __statement.executeBatch(); + } + } + __statement.executeBatch(); + __connection.commit(); + return; + } catch (SQLException __exception) { + __connection.rollback(); + throw __exception; } - } catch (SQLException exception) { - throw new CompletionException("Unexpected error occurred", exception); + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); + } + } + + @Override + public void update(TestEntity entity) { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("update hello set c=?,d=? where a=? and b=?")) { + __statement.setString(1, entity.c()); + __statement.setBytes(2, this.__d.encode(entity.d())); + __statement.setInt(3, entity.a()); + __statement.setString(4, entity.b()); + __statement.executeUpdate(); + return; + } + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); } } @Override public CompletableFuture insert(TestEntity entity) { return CompletableFuture.supplyAsync(() -> { - try (Connection connection = dataSource.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("insert into hello (a,b,c,d) values (?,?,?,?)")) { - statement.setInt(1, entity.a()); - statement.setString(2, entity.b()); - statement.setString(3, entity.c()); - statement.setBytes(4, this.__d.encode(entity.d())); - statement.executeUpdate(); + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("insert into hello (a,b,c,d) values (?,?,?,?)")) { + __statement.setInt(1, entity.a()); + __statement.setString(2, entity.b()); + __statement.setString(3, entity.c()); + __statement.setBytes(4, this.__d.encode(entity.d())); + __statement.executeUpdate(); return null; } - } catch (SQLException exception) { - throw new CompletionException("Unexpected error occurred", exception); + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); } } , this.database.executorService()); } + @Override + public void insert(List entities) { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("insert into hello (a,b,c,d) values (?,?,?,?)")) { + int __count = 0; + for (var __element : entities) { + __statement.setInt(1, __element.a()); + __statement.setString(2, __element.b()); + __statement.setString(3, __element.c()); + __statement.setBytes(4, this.__d.encode(__element.d())); + __statement.addBatch(); + if (__count % 250 == 0) { + __statement.executeBatch(); + } + } + __statement.executeBatch(); + __connection.commit(); + return ; + } catch (SQLException __exception) { + __connection.rollback(); + throw __exception; + } + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); + } + } + @Override public CompletableFuture delete(TestEntity entity) { return CompletableFuture.supplyAsync(() -> { - try (Connection connection = dataSource.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("delete from hello where a=? and b=?")) { - statement.setInt(1, entity.a()); - statement.setString(2, entity.b()); - statement.executeUpdate(); + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("delete from hello where a=? and b=?")) { + __statement.setInt(1, entity.a()); + __statement.setString(2, entity.b()); + __statement.executeUpdate(); return null; } - } catch (SQLException exception) { - throw new CompletionException("Unexpected error occurred", exception); + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); } } , this.database.executorService()); } + @Override + public void delete(List entities) { + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("delete from hello where a=? and b=?")) { + int __count = 0; + for (var __element : entities) { + __statement.setInt(1, __element.a()); + __statement.setString(2, __element.b()); + __statement.addBatch(); + if (__count % 250 == 0) { + __statement.executeBatch(); + } + } + __statement.executeBatch(); + __connection.commit(); + return ; + } catch (SQLException __exception) { + __connection.rollback(); + throw __exception; + } + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); + } + } + @Override public CompletableFuture deleteByAAndB(int a, String b) { return CompletableFuture.supplyAsync(() -> { - try (Connection connection = dataSource.getConnection()) { - try (PreparedStatement statement = connection.prepareStatement("delete from hello where a=? and b=?")) { - statement.setInt(1, a); - statement.setString(2, b); - statement.executeUpdate(); + try (Connection __connection = this.dataSource.getConnection()) { + try (PreparedStatement __statement = __connection.prepareStatement("delete from hello where a=? and b=?")) { + __statement.setInt(1, a); + __statement.setString(2, b); + __statement.executeUpdate(); return null; } - } catch (SQLException exception) { - throw new CompletionException("Unexpected error occurred", exception); + } catch (SQLException __exception) { + throw new CompletionException("Unexpected error occurred", __exception); } } , this.database.executorService()); } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b26b48..a5ac3b1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,8 +6,6 @@ rootProject.name = "databaseutils-parent" dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - mavenLocal() - mavenCentral() maven("https://repo.opencollab.dev/main") }