diff --git a/.github/workflows/junit.yml b/.github/workflows/junit.yml new file mode 100644 index 0000000..8bef2b6 --- /dev/null +++ b/.github/workflows/junit.yml @@ -0,0 +1,33 @@ +name: JUnit + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + - name: Shutdown default MySQL + run: sudo service mysql stop + - name: Setup MySQL + uses: mirromutth/mysql-action@v1.1 + with: + mysql database: 'test' + mysql user: 'test' + mysql password: 'test' + mysql port: 3306 + - name: Gradle Build + run: ./gradlew build -x test + - name: Gradle Test + run: ./gradlew test + - name: JUnit Report Action + uses: mikepenz/action-junit-report@v3.7.1 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' \ No newline at end of file diff --git a/LICENSE b/LICENSE index 5c645f8..d683356 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 ZorTik +Copyright (c) 2022-2023 ZorTik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 81bcbf7..73d56cb 100644 --- a/README.md +++ b/README.md @@ -15,37 +15,8 @@ Before documentation is done, here is a Installation on Wiki + +> Wiki in progress! diff --git a/api/build.gradle b/api/build.gradle index 317744c..4d18e15 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -10,6 +10,7 @@ repositories { } dependencies { + implementation project(":shared") implementation group: 'org.jetbrains', name: 'annotations', version: '20.1.0' implementation 'com.google.code.gson:gson:2.9.0' compileOnly 'org.projectlombok:lombok:1.18.24' diff --git a/api/settings.gradle b/api/settings.gradle new file mode 100644 index 0000000..c2269b4 --- /dev/null +++ b/api/settings.gradle @@ -0,0 +1,2 @@ +include ":shared" +project(":shared").projectDir = file("../shared") \ No newline at end of file diff --git a/api/src/main/java/me/zort/sqllib/api/Executive.java b/api/src/main/java/me/zort/sqllib/api/Executive.java index 13fc0b1..cee7a62 100644 --- a/api/src/main/java/me/zort/sqllib/api/Executive.java +++ b/api/src/main/java/me/zort/sqllib/api/Executive.java @@ -2,6 +2,6 @@ public interface Executive { - SQLDatabaseConnection getConnection(); + SQLConnection getConnection(); } diff --git a/api/src/main/java/me/zort/sqllib/api/Query.java b/api/src/main/java/me/zort/sqllib/api/Query.java index f882cd2..76c4c2d 100644 --- a/api/src/main/java/me/zort/sqllib/api/Query.java +++ b/api/src/main/java/me/zort/sqllib/api/Query.java @@ -17,4 +17,8 @@ default Query getAncestor() { return this; } + default boolean isAncestor() { + return getAncestor() == this; + } + } diff --git a/api/src/main/java/me/zort/sqllib/api/SQLDatabaseConnection.java b/api/src/main/java/me/zort/sqllib/api/SQLDatabaseConnection.java deleted file mode 100644 index 50ed44d..0000000 --- a/api/src/main/java/me/zort/sqllib/api/SQLDatabaseConnection.java +++ /dev/null @@ -1,84 +0,0 @@ -package me.zort.sqllib.api; - -import me.zort.sqllib.api.data.QueryResult; -import me.zort.sqllib.api.data.QueryRowsResult; -import me.zort.sqllib.api.data.Row; -import me.zort.sqllib.internal.query.*; -import org.jetbrains.annotations.Nullable; - -/** - * Database connection object able to handle queries - * from this library. - * - * @author ZorTik - */ -public interface SQLDatabaseConnection extends SQLConnection { - - /** - * Saves this mapping object into database using upsert query. - *

- * All mapping strategies are described in: - * {@link SQLDatabaseConnection#query(Query, Class)}. - * - * @param table Table to save into. - * @param obj The object to save. - * @return Result of the query. - */ - QueryResult save(String table, Object obj); - - /** - * Performs new query and returns the result. This result is never null. - * See: {@link QueryRowsResult#isSuccessful()} - * - * Examples: - *

- * query(Select.of().from("players"), Player.class) - * .stream() - * .map(Player::getNickname) - * .forEach(System.out::println); - *

- * query(() -> "SELECT * FROM players;"); - * - * @param query The query to use while constructing query string. - * @param typeClass Type class of object which will be instantinated and - * populated with column values. - * @param Type of objects in result. - * - * @return Collection of row objects. - */ - QueryRowsResult query(Query query, Class typeClass); - - /** - * @see SQLDatabaseConnection#query(Query, Class) - */ - QueryRowsResult query(Query query); - - /** - * Executes given query and returns execution result. - * This result does not contain any rows. If you want to - * execute query return result of rows, see method - * {@link SQLDatabaseConnection#query(Query)} - * - * @param query Query to use for building query string. - * @return Blank rows result that only informs - * about success state of the request. - */ - QueryResult exec(Query query); - - SelectQuery select(String... cols); - - UpdateQuery update(); - - UpdateQuery update(@Nullable String table); - - InsertQuery insert(); - - InsertQuery insert(@Nullable String table); - - UpsertQuery upsert(); - - UpsertQuery upsert(@Nullable String table); - - DeleteQuery delete(); - -} diff --git a/api/src/main/java/me/zort/sqllib/api/StatementFactory.java b/api/src/main/java/me/zort/sqllib/api/StatementFactory.java new file mode 100644 index 0000000..81c5388 --- /dev/null +++ b/api/src/main/java/me/zort/sqllib/api/StatementFactory.java @@ -0,0 +1,11 @@ +package me.zort.sqllib.api; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +public interface StatementFactory { + + T prepare(Connection connection) throws SQLException; + +} diff --git a/api/src/main/java/me/zort/sqllib/api/data/QueryResult.java b/api/src/main/java/me/zort/sqllib/api/data/QueryResult.java index ad59027..abcfba6 100644 --- a/api/src/main/java/me/zort/sqllib/api/data/QueryResult.java +++ b/api/src/main/java/me/zort/sqllib/api/data/QueryResult.java @@ -1,7 +1,11 @@ package me.zort.sqllib.api.data; +import org.jetbrains.annotations.Nullable; + public interface QueryResult { boolean isSuccessful(); + @Nullable + String getRejectMessage(); } diff --git a/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java b/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java index bc02311..a465136 100644 --- a/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java +++ b/api/src/main/java/me/zort/sqllib/api/data/QueryRowsResult.java @@ -4,13 +4,27 @@ import java.util.ArrayList; +@Getter public class QueryRowsResult extends ArrayList implements QueryResult { - @Getter private final boolean successful; + private String rejectMessage = null; public QueryRowsResult(boolean successful) { + this(successful, null); + } + + public QueryRowsResult(boolean successful, String rejectMessage) { this.successful = successful; + rejectMessage(rejectMessage); + } + + public QueryRowsResult rejectMessage(String message) { + if (rejectMessage != null) + throw new RuntimeException("Reject message is already set!"); + + this.rejectMessage = message; + return this; } } diff --git a/api/src/main/java/me/zort/sqllib/internal/annotation/Id.java b/api/src/main/java/me/zort/sqllib/internal/annotation/Id.java index 2a1bdff..a543b5a 100644 --- a/api/src/main/java/me/zort/sqllib/internal/annotation/Id.java +++ b/api/src/main/java/me/zort/sqllib/internal/annotation/Id.java @@ -8,7 +8,7 @@ /** * Serves as definitive identity of entity id field. * This is useful if you have different PrimaryKey than - * you want to use as id in {@link me.zort.sqllib.api.repository.SQLTableRepository}, + * you want to use as id in SQLTableRepository, * otherwise a {@link PrimaryKey} is used as ID. */ @Retention(RetentionPolicy.RUNTIME) diff --git a/api/src/main/java/me/zort/sqllib/internal/exception/InvalidConnectionInstanceException.java b/api/src/main/java/me/zort/sqllib/internal/exception/InvalidConnectionInstanceException.java new file mode 100644 index 0000000..414c941 --- /dev/null +++ b/api/src/main/java/me/zort/sqllib/internal/exception/InvalidConnectionInstanceException.java @@ -0,0 +1,15 @@ +package me.zort.sqllib.internal.exception; + +import lombok.Getter; +import me.zort.sqllib.api.SQLConnection; + +public class InvalidConnectionInstanceException extends RuntimeException { + + @Getter + private final SQLConnection invalid; + + public InvalidConnectionInstanceException(SQLConnection invalid) { + super(String.format("Invalid connection instance %s!", invalid.getClass().getName())); + this.invalid = invalid; + } +} diff --git a/api/src/main/java/me/zort/sqllib/internal/query/QueryPart.java b/api/src/main/java/me/zort/sqllib/internal/query/QueryPart.java deleted file mode 100644 index 90cf81a..0000000 --- a/api/src/main/java/me/zort/sqllib/internal/query/QueryPart.java +++ /dev/null @@ -1,111 +0,0 @@ -package me.zort.sqllib.internal.query; - -import lombok.Getter; -import me.zort.sqllib.api.Executive; -import me.zort.sqllib.api.Query; -import me.zort.sqllib.api.SQLDatabaseConnection; -import me.zort.sqllib.api.data.QueryResult; -import me.zort.sqllib.internal.exception.NoLinkedConnectionException; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Getter -public abstract class QueryPart

> implements Query { - - @Getter(onMethod_ = {@Nullable}) - private final P parent; - private final List> children; - private final int priority; - - public QueryPart(@Nullable P parent, List> initial) { - this(parent, initial, QueryPriority.GENERAL); - } - - public QueryPart(@Nullable P parent, List> initial, QueryPriority priority) { - this(parent, initial, priority.getPrior()); - } - - public QueryPart(@Nullable P parent, List> initial, int priority) { - this.parent = parent; - this.children = initial; - this.priority = priority; - } - - public > QueryPart then(QueryPart part) { - this.children.add(part); - return part; - } - - public QueryPart then(String part) { - int maxPriority = children.stream() - .map(QueryPart::getPriority) - .max(Comparator.naturalOrder()) - .orElse(0); - then(new LocalQueryPart(this, maxPriority + 1, part)); - return this; - } - - public P also() { - return parent; - } - - public String buildInnerQuery() { - List> children = new ArrayList<>(this.children); - Collections.sort(children, Comparator.comparingInt(QueryPart::getPriority)); - return !children.isEmpty() ? String.join(" ", children - .stream() - .map(QueryPart::buildQuery) - .collect(Collectors.toList())) : ""; - } - - public QueryResult execute() { - return invokeToConnection(connection -> connection.exec(getAncestor())); - } - - @Nullable - protected T invokeToConnection(Function func) throws NoLinkedConnectionException { - QueryPart current = this; - while(current.getParent() != null && !(current instanceof Executive)) { - current = current.getParent(); - } - T result; - if(current instanceof Executive) { - SQLDatabaseConnection connection = ((Executive) current).getConnection(); - result = func.apply(connection); - } else { - throw new NoLinkedConnectionException(this); - } - return result; - } - - public QueryPart getAncestor() { - QueryPart current = this; - while(current.getParent() != null) { - current = current.getParent(); - } - return current; - } - - private static class LocalQueryPart extends QueryPart { - - private final String queryPartString; - - public LocalQueryPart(@Nullable QueryPart parent, int priority, String queryPartString) { - super(parent, Collections.emptyList(), priority); - this.queryPartString = queryPartString; - } - - @Override - public String buildQuery() { - return queryPartString; - } - - } - -} diff --git a/api/src/main/java/me/zort/sqllib/internal/query/part/LimitStatement.java b/api/src/main/java/me/zort/sqllib/internal/query/part/LimitStatement.java deleted file mode 100644 index b41aa05..0000000 --- a/api/src/main/java/me/zort/sqllib/internal/query/part/LimitStatement.java +++ /dev/null @@ -1,28 +0,0 @@ -package me.zort.sqllib.internal.query.part; - -import me.zort.sqllib.internal.query.QueryPart; -import me.zort.sqllib.internal.query.QueryPartQuery; -import me.zort.sqllib.internal.query.QueryPriority; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class LimitStatement

> extends QueryPartQuery

{ - - private final int limit; - - public LimitStatement(@Nullable P parent, List> initial, int limit) { - super(parent, initial, Integer.MAX_VALUE); - this.limit = limit; - } - - @Override - public String buildQuery() { - return " LIMIT " + Math.max(limit, 0); - } - - @Override - public LimitStatement

then(String part) { - return (LimitStatement

) super.then(part); - } -} diff --git a/api/src/main/java/me/zort/sqllib/internal/query/part/WhereStatement.java b/api/src/main/java/me/zort/sqllib/internal/query/part/WhereStatement.java deleted file mode 100644 index 5201550..0000000 --- a/api/src/main/java/me/zort/sqllib/internal/query/part/WhereStatement.java +++ /dev/null @@ -1,93 +0,0 @@ -package me.zort.sqllib.internal.query.part; - -import me.zort.sqllib.internal.exception.IllegalStatementOperationException; -import me.zort.sqllib.internal.query.QueryPart; -import me.zort.sqllib.internal.query.QueryPartQuery; -import me.zort.sqllib.internal.query.QueryPriority; -import me.zort.sqllib.util.Util; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -public class WhereStatement

> extends QueryPartQuery

{ - - private final List conditions = new ArrayList<>(); - - public WhereStatement(@Nullable P parent, List> initial) { - super(parent, initial, QueryPriority.CONDITION.getPrior()); - } - - public WhereStatement(@Nullable P parent, List> initial, int priority) { - super(parent, initial, priority); - } - - public WhereStatement

isEqual(String column, Object value) { - conditions.add(column + " = " + Util.buildQuoted(value)); - return this; - } - - public WhereStatement

bt(String column, long value) { - conditions.add(column + " > " + value); - return this; - } - - public WhereStatement

lt(String column, long value) { - conditions.add(column + " < " + value); - return this; - } - - public WhereStatement

in(String column, Object... objs) { - return in(column, Arrays.asList(objs)); - } - - public WhereStatement

in(String column, List objs) { - if(objs.isEmpty()) return this; - conditions.add(column + " IN (" + objs.stream() - .map(Util::buildQuoted) - .collect(Collectors.joining(", ")) + ")"); - return this; - } - - public WhereStatement

like(String column, String placeholder) { - conditions.add(column + " LIKE " + Util.buildQuoted(placeholder)); - return this; - } - - @Override - public > QueryPart then(QueryPart part) { - throw new IllegalStatementOperationException("Where statement can't have inner parts!"); - } - - public WhereStatement

and() { - return this; - } - - public WhereStatement

or() { - conditions.add(" OR "); - return this; - } - - @Override - public String buildQuery() { - StringBuilder stmt = new StringBuilder(" WHERE "); - if(conditions.isEmpty()) { - // We don't have any conditions, so where statement should be true. - return stmt + "TRUE"; - } - for(String condition : conditions) { - if(!stmt.toString().equals(" WHERE ") && !condition.equals(" OR ") && !stmt.toString().endsWith(" OR ")) { - stmt.append(" AND "); - } - stmt.append(condition); - } - return stmt.toString(); - } - - @Override - public WhereStatement

then(String part) { - return (WhereStatement

) super.then(part); - } -} diff --git a/api/src/main/java/me/zort/sqllib/util/Util.java b/api/src/main/java/me/zort/sqllib/util/Util.java deleted file mode 100644 index 6b8b47a..0000000 --- a/api/src/main/java/me/zort/sqllib/util/Util.java +++ /dev/null @@ -1,12 +0,0 @@ -package me.zort.sqllib.util; - -public final class Util { - - public static String buildQuoted(Object obj) { - obj = obj instanceof String - ? String.format("'%s'", obj) - : String.valueOf(obj); - return (String) obj; - } - -} diff --git a/build.gradle b/build.gradle index 69992c9..4e97e8d 100644 --- a/build.gradle +++ b/build.gradle @@ -15,10 +15,26 @@ repositories { dependencies { implementation group: 'org.jetbrains', name: 'annotations', version: '20.1.0' implementation 'com.google.code.gson:gson:2.9.0' - compileOnly 'org.projectlombok:lombok:1.18.24' - annotationProcessor 'org.projectlombok:lombok:1.18.24' implementation project(":api") implementation project(":core") + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testImplementation project(":api") + testImplementation project(":core") + testImplementation project(":shared") + testImplementation 'com.mysql:mysql-connector-j:8.0.32' + testImplementation 'org.apache.logging.log4j:log4j-core:2.19.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testRuntimeOnly 'com.google.guava:guava:31.0-jre' + testRuntimeOnly 'commons-lang:commons-lang:2.6' + testCompileOnly 'org.projectlombok:lombok:1.18.24' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.24' +} + +test { + useJUnitPlatform() } jar { diff --git a/core/build.gradle b/core/build.gradle index a38335e..ef00bcd 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -11,6 +11,7 @@ repositories { dependencies { implementation project(":api") + implementation project(":shared") implementation group: 'org.jetbrains', name: 'annotations', version: '20.1.0' implementation 'com.google.code.gson:gson:2.9.0' compileOnly 'org.projectlombok:lombok:1.18.24' diff --git a/core/settings.gradle b/core/settings.gradle index 09cc34a..77a3839 100644 --- a/core/settings.gradle +++ b/core/settings.gradle @@ -1,2 +1,3 @@ include ":api" -project(":api").projectDir = file("../api") \ No newline at end of file +project(":api").projectDir = file("../api") +project(":shared").projectDir = file("../shared") \ No newline at end of file diff --git a/core/src/main/java/me/zort/sqllib/Logger.java b/core/src/main/java/me/zort/sqllib/Logger.java new file mode 100644 index 0000000..3fb61a7 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/Logger.java @@ -0,0 +1,21 @@ +package me.zort.sqllib; + +import java.sql.Connection; + +public final class Logger { + + private Logger() { + } + + public static void debug(Connection connection, String message) { + SQLConnectionPool.find(connection) + .filter(c -> c instanceof SQLDatabaseConnectionImpl) + .ifPresent(c -> debug((SQLDatabaseConnectionImpl) c, message)); + } + + public static void debug(SQLDatabaseConnectionImpl connection, String message) { + if (connection.isDebug()) + connection.debug(message); + } + +} diff --git a/core/src/main/java/me/zort/sqllib/SQLClient.java b/core/src/main/java/me/zort/sqllib/SQLClient.java new file mode 100644 index 0000000..ff0515d --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/SQLClient.java @@ -0,0 +1,16 @@ +package me.zort.sqllib; + +import java.sql.Connection; + +public final class SQLClient { + + private SQLClient() { + } + + public static boolean isDebug(Connection connection) { + return SQLConnectionPool.find(connection) + .map(SQLDatabaseConnection::isDebug) + .orElse(false); + } + +} diff --git a/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java b/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java index a1aa8d6..7f88789 100644 --- a/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java +++ b/core/src/main/java/me/zort/sqllib/SQLConnectionBuilder.java @@ -79,14 +79,14 @@ public SQLConnectionBuilder withDriver(String driver) { } public SQLDatabaseConnectionImpl build() { - return build(new SQLDatabaseOptions()); + return build(null); } - public SQLDatabaseConnectionImpl build(SQLDatabaseOptions options) { + public SQLDatabaseConnectionImpl build(@Nullable SQLDatabaseOptions options) { return build(driver, options); } - public SQLDatabaseConnectionImpl build(@Nullable String driver, SQLDatabaseOptions options) { + public SQLDatabaseConnectionImpl build(@Nullable String driver, @Nullable SQLDatabaseOptions options) { Objects.requireNonNull(endpoint, "Endpoint must be set!"); Objects.requireNonNull(jdbc); if(driver == null) { diff --git a/core/src/main/java/me/zort/sqllib/SQLConnectionPool.java b/core/src/main/java/me/zort/sqllib/SQLConnectionPool.java new file mode 100644 index 0000000..b7c0bb7 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/SQLConnectionPool.java @@ -0,0 +1,23 @@ +package me.zort.sqllib; + +import java.sql.Connection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class SQLConnectionPool { + + private SQLConnectionPool() { + } + + private static final List CONNECTIONS = new CopyOnWriteArrayList<>(); + + static void register(SQLDatabaseConnection connection) { + CONNECTIONS.add(connection); + } + + static Optional find(Connection connection) { + return CONNECTIONS.stream().filter(c -> c.getConnection() == connection).findFirst(); + } + +} diff --git a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java new file mode 100644 index 0000000..297a784 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnection.java @@ -0,0 +1,129 @@ +package me.zort.sqllib; + +import lombok.Getter; +import me.zort.sqllib.api.Query; +import me.zort.sqllib.api.SQLConnection; +import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.api.data.QueryRowsResult; +import me.zort.sqllib.api.data.Row; +import me.zort.sqllib.internal.factory.SQLConnectionFactory; +import me.zort.sqllib.internal.query.*; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Database connection object able to handle queries + * from this library. + * + * @author ZorTik + */ +public abstract class SQLDatabaseConnection implements SQLConnection { + + private final SQLConnectionFactory connectionFactory; + @Getter(onMethod_ = {@Nullable}) + private Connection connection; + + public SQLDatabaseConnection(SQLConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + this.connection = null; + + SQLConnectionPool.register(this); + } + + /** + * Saves this mapping object into database using upsert query. + *

+ * All mapping strategies are described in: + * {@link SQLDatabaseConnection#query(Query, Class)}. + * + * @param table Table to save into. + * @param obj The object to save. + * @return Result of the query. + */ + public abstract QueryResult save(String table, Object obj); + + /** + * Performs new query and returns the result. This result is never null. + * See: {@link QueryRowsResult#isSuccessful()} + * + * Examples: + *

+ * query(Select.of().from("players"), Player.class) + * .stream() + * .map(Player::getNickname) + * .forEach(System.out::println); + *

+ * query(() -> "SELECT * FROM players;"); + * + * @param query The query to use while constructing query string. + * @param typeClass Type class of object which will be instantinated and + * populated with column values. + * @param Type of objects in result. + * + * @return Collection of row objects. + */ + public abstract QueryRowsResult query(Query query, Class typeClass); + + /** + * @see SQLDatabaseConnection#query(Query, Class) + */ + public abstract QueryRowsResult query(Query query); + + /** + * Executes given query and returns execution result. + * This result does not contain any rows. If you want to + * execute query return result of rows, see method + * {@link SQLDatabaseConnection#query(Query)} + * + * @param query Query to use for building query string. + * @return Blank rows result that only informs + * about success state of the request. + */ + public abstract QueryResult exec(Query query); + public abstract boolean isLogSqlErrors(); + public abstract boolean isDebug(); + + public abstract SelectQuery select(String... cols); + public abstract UpdateQuery update(); + public abstract UpdateQuery update(@Nullable String table); + public abstract InsertQuery insert(); + public abstract InsertQuery insert(@Nullable String table); + public abstract UpsertQuery upsert(); + public abstract UpsertQuery upsert(@Nullable String table); + public abstract DeleteQuery delete(); + + @Override + public boolean connect() { + if(isConnected()) { + disconnect(); + } + + try { + connection = connectionFactory.connect(); + } catch (SQLException e) { + logSqlError(e); + connection = null; + } + return isConnected(); + } + + @Override + public void disconnect() { + if(isConnected()) { + try { + connection.close(); + } catch (SQLException e) { + logSqlError(e); + } + } + } + + protected void logSqlError(Exception e) { + if(isLogSqlErrors()) { + e.printStackTrace(); + } + } + +} diff --git a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java index 1d48318..76d696d 100644 --- a/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java +++ b/core/src/main/java/me/zort/sqllib/SQLDatabaseConnectionImpl.java @@ -4,20 +4,24 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; +import lombok.RequiredArgsConstructor; import me.zort.sqllib.api.Query; -import me.zort.sqllib.api.SQLConnection; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.api.StatementFactory; import me.zort.sqllib.api.data.QueryResult; import me.zort.sqllib.api.data.QueryRowsResult; import me.zort.sqllib.api.data.Row; +import me.zort.sqllib.api.options.NamingStrategy; +import me.zort.sqllib.internal.Defaults; import me.zort.sqllib.internal.annotation.JsonField; import me.zort.sqllib.internal.factory.SQLConnectionFactory; import me.zort.sqllib.internal.fieldResolver.LinkedOneFieldResolver; +import me.zort.sqllib.internal.impl.DefaultNamingStrategy; import me.zort.sqllib.internal.impl.QueryResultImpl; import me.zort.sqllib.internal.query.*; import me.zort.sqllib.internal.query.part.SetStatement; import me.zort.sqllib.util.Pair; import me.zort.sqllib.util.Validator; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.*; @@ -31,18 +35,20 @@ * * @author ZorTik */ -public class SQLDatabaseConnectionImpl implements SQLDatabaseConnection { +public class SQLDatabaseConnectionImpl extends SQLDatabaseConnection { + + public static boolean DEFAULT_AUTO_RECONNECT = true; + public static boolean DEFAULT_DEBUG = false; + public static boolean DEFAULT_LOG_SQL_ERRORS = true; + public static NamingStrategy DEFAULT_NAMING_STRATEGY = new DefaultNamingStrategy(); + public static Gson DEFAULT_GSON = Defaults.DEFAULT_GSON; - private final SQLConnectionFactory connectionFactory; @Getter private final SQLDatabaseOptions options; // Resolvers used after no value is found for the field // in mapped object as backup. private final List backupValueResolvers; - @Getter(onMethod_ = {@Nullable}) - private Connection connection; - /** * Constructs new instance of this implementation with default * options. @@ -50,7 +56,7 @@ public class SQLDatabaseConnectionImpl implements SQLDatabaseConnection { * @see SQLDatabaseConnectionImpl#SQLDatabaseConnectionImpl(SQLConnectionFactory, SQLDatabaseOptions) */ public SQLDatabaseConnectionImpl(SQLConnectionFactory connectionFactory) { - this(connectionFactory, new SQLDatabaseOptions()); + this(connectionFactory, null); } /** @@ -59,17 +65,28 @@ public SQLDatabaseConnectionImpl(SQLConnectionFactory connectionFactory) { * @param connectionFactory Factory to use while opening connection. * @param options Client options to use. */ - public SQLDatabaseConnectionImpl(SQLConnectionFactory connectionFactory, SQLDatabaseOptions options) { - this.connectionFactory = connectionFactory; + public SQLDatabaseConnectionImpl(SQLConnectionFactory connectionFactory, @Nullable SQLDatabaseOptions options) { + super(connectionFactory); + + if (options == null) + options = new SQLDatabaseOptions( + DEFAULT_AUTO_RECONNECT, + DEFAULT_DEBUG, + DEFAULT_LOG_SQL_ERRORS, + DEFAULT_NAMING_STRATEGY, + DEFAULT_GSON + ); + this.options = options; this.backupValueResolvers = Collections.synchronizedList(new ArrayList<>()); - this.connection = null; // Default backup value resolvers. registerBackupValueResolver(new LinkedOneFieldResolver()); } - public void registerBackupValueResolver(FieldValueResolver resolver) { + public void registerBackupValueResolver(@NotNull FieldValueResolver resolver) { + Objects.requireNonNull(resolver, "Resolver cannot be null!"); + backupValueResolvers.add(resolver); } @@ -77,7 +94,7 @@ public void registerBackupValueResolver(FieldValueResolver resolver) { * @see SQLDatabaseConnection#save(String, Object) */ @Override - public QueryResult save(String table, Object obj) { + public QueryResult save(String table, Object obj) { // by default, it creates and upsert request. Pair defsValsPair = buildDefsVals(obj); if(defsValsPair == null) { return new QueryResultImpl(false); @@ -98,6 +115,8 @@ public QueryResult save(String table, Object obj) { @Nullable protected Pair buildDefsVals(Object obj) { + Objects.requireNonNull(obj); + Class aClass = obj.getClass(); Map fields = new HashMap<>(); @@ -140,7 +159,7 @@ protected Pair buildDefsVals(Object obj) { */ @Override public QueryRowsResult query(Query query, Class typeClass) { - QueryRowsResult resultRows = query(query); + QueryRowsResult resultRows = query(query.getAncestor()); QueryRowsResult result = new QueryRowsResult<>(resultRows.isSuccessful()); for(Row row : resultRows) { Optional.ofNullable(assignValues(row, typeClass)) @@ -154,14 +173,16 @@ public QueryRowsResult query(Query query, Class typeClass) { */ @Override public QueryRowsResult query(Query query) { + Objects.requireNonNull(query); + if(!handleAutoReconnect()) { - return new QueryRowsResult<>(false); + return new QueryRowsResult<>(false, "Cannot connect to database!"); } - String queryString = query.getAncestor().buildQuery(); - debug("Query string: " + queryString); - try(PreparedStatement stmt = connection.prepareStatement(queryString); + + try(PreparedStatement stmt = buildStatement(query); ResultSet resultSet = stmt.executeQuery()) { QueryRowsResult result = new QueryRowsResult<>(true); + while(resultSet.next()) { ResultSetMetaData meta = resultSet.getMetaData(); Row row = new Row(); @@ -174,10 +195,11 @@ public QueryRowsResult query(Query query) { } result.add(row); } + return result; } catch (SQLException e) { logSqlError(e); - return new QueryRowsResult<>(false); + return new QueryRowsResult<>(false, e.getMessage()); } } @@ -186,16 +208,14 @@ public QueryRowsResult query(Query query) { */ public QueryResult exec(Query query) { if(!handleAutoReconnect()) { - return new QueryResultImpl(false); + return new QueryResultImpl(false, "Cannot connect to database!"); } - String queryString = query.getAncestor().buildQuery(); - debug("Query string: " + queryString); - try(PreparedStatement stmt = connection.prepareStatement(queryString)) { + try(PreparedStatement stmt = buildStatement(query)) { stmt.execute(); return new QueryResultImpl(true); } catch (SQLException e) { logSqlError(e); - return new QueryResultImpl(false); + return new QueryResultImpl(false, e.getMessage()); } } @@ -205,6 +225,7 @@ private T assignValues(Row row, Class typeClass) { try { try { Constructor c = typeClass.getConstructor(); + c.setAccessible(true); instance = c.newInstance(); } catch (NoSuchMethodException e) { for(Constructor c : typeClass.getConstructors()) { @@ -255,7 +276,7 @@ private Object buildElementValue(AnnotatedElement element, Row row) { if(element instanceof Field) { name = ((Field) element).getName(); type = ((Field) element).getGenericType(); - } else if(element instanceof Parameter) { + } else if(element instanceof Parameter) { // TODO: Parameter names are arg[a-zA-Z0-9]+, use different strategy. name = ((Parameter) element).getName(); type = ((Parameter) element).getType(); } else { @@ -298,37 +319,6 @@ private boolean handleAutoReconnect() { return true; } - /** - * @see SQLConnection#connect() - */ - @Override - public boolean connect() { - if(isConnected()) { - disconnect(); - } - try { - connection = connectionFactory.connect(); - } catch (SQLException e) { - logSqlError(e); - connection = null; - } - return isConnected(); - } - - /** - * @see SQLConnection#disconnect() - */ - @Override - public void disconnect() { - if(isConnected()) { - try { - connection.close(); - } catch (SQLException e) { - logSqlError(e); - } - } - } - public SelectQuery select(String... cols) { return new SelectQuery(this, cols); } @@ -367,9 +357,33 @@ public void debug(String message) { } } - protected void logSqlError(Exception e) { - if(options.isLogSqlErrors()) { - e.printStackTrace(); + @Override + public boolean isLogSqlErrors() { + return options.isLogSqlErrors(); + } + + @Override + public boolean isDebug() { + return options.isDebug(); + } + + @SuppressWarnings("unchecked") + private PreparedStatement buildStatement(Query query) throws SQLException { + StatementFactory factory = new DefaultStatementFactory(query); + if (query instanceof StatementFactory) + factory = (StatementFactory) query; + + return factory.prepare(getConnection()); + } + + @RequiredArgsConstructor + private static class DefaultStatementFactory implements StatementFactory { + + private final Query query; + + @Override + public PreparedStatement prepare(Connection connection) throws SQLException { + return connection.prepareStatement(query.getAncestor().buildQuery()); } } diff --git a/core/src/main/java/me/zort/sqllib/SQLDatabaseOptions.java b/core/src/main/java/me/zort/sqllib/SQLDatabaseOptions.java index 14a6d01..0ddddb9 100644 --- a/core/src/main/java/me/zort/sqllib/SQLDatabaseOptions.java +++ b/core/src/main/java/me/zort/sqllib/SQLDatabaseOptions.java @@ -1,26 +1,22 @@ package me.zort.sqllib; import com.google.gson.Gson; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import me.zort.sqllib.api.options.NamingStrategy; import me.zort.sqllib.internal.Defaults; import me.zort.sqllib.internal.impl.DefaultNamingStrategy; +@AllArgsConstructor +@NoArgsConstructor @Data public class SQLDatabaseOptions { - private boolean autoReconnect; - private boolean debug; - private boolean logSqlErrors; - private NamingStrategy namingStrategy; - private Gson gson; - - public SQLDatabaseOptions() { - this.autoReconnect = true; - this.debug = false; - this.logSqlErrors = true; - this.namingStrategy = new DefaultNamingStrategy(); - this.gson = Defaults.DEFAULT_GSON; - } + private boolean autoReconnect = true; + private boolean debug = false; + private boolean logSqlErrors = true; + private NamingStrategy namingStrategy = new DefaultNamingStrategy(); + private Gson gson = Defaults.DEFAULT_GSON; } diff --git a/core/src/main/java/me/zort/sqllib/SQLTableRepositoryBuilder.java b/core/src/main/java/me/zort/sqllib/SQLTableRepositoryBuilder.java index 2715b04..da79173 100644 --- a/core/src/main/java/me/zort/sqllib/SQLTableRepositoryBuilder.java +++ b/core/src/main/java/me/zort/sqllib/SQLTableRepositoryBuilder.java @@ -1,12 +1,10 @@ package me.zort.sqllib; import com.google.gson.internal.Primitives; -import me.zort.sqllib.api.SQLDatabaseConnection; import me.zort.sqllib.api.repository.CachingSQLTableRepository; import me.zort.sqllib.api.repository.SQLTableRepository; import me.zort.sqllib.internal.annotation.JsonField; import me.zort.sqllib.internal.annotation.NullableField; -import me.zort.sqllib.internal.annotation.PrimaryKey; import me.zort.sqllib.util.Arrays; import me.zort.sqllib.util.Validator; import org.jetbrains.annotations.ApiStatus; diff --git a/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java b/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java index 4ad307b..7257400 100644 --- a/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java +++ b/core/src/main/java/me/zort/sqllib/SQLiteDatabaseConnectionImpl.java @@ -18,6 +18,12 @@ import java.lang.reflect.Modifier; import java.util.Arrays; +/** + * SQLite database connection that changes some operations + * since SQLite does not have support for some SQL statements. + * + * @author ZorTik + */ public class SQLiteDatabaseConnectionImpl extends SQLDatabaseConnectionImpl { public SQLiteDatabaseConnectionImpl(SQLConnectionFactory connectionFactory) { @@ -30,7 +36,7 @@ public SQLiteDatabaseConnectionImpl(SQLConnectionFactory connectionFactory, SQLD /** * Performs an upsert query for defined object - * as stated in {@link me.zort.sqllib.api.SQLDatabaseConnection#save(String, Object)}. + * as stated in {@link SQLDatabaseConnection#save(String, Object)}. *

* Object needs to have {@link me.zort.sqllib.internal.annotation.PrimaryKey} annotation * set to determine which column is a primary key. @@ -95,6 +101,18 @@ public QueryResult save(String table, Object obj) { return upsert(table, primaryKey, insert, update); } + /** + * Simulates upsert query for SQLite. + * It selects rows with limit 1 to check whether there is a row present + * matching the current request and then it performs either insert or + * update according to the result. + * + * @param table Table to upsert into. + * @param primaryKey Primary key of the object. + * @param insert Insert query. + * @param update Update query. + * @return Result of the query. + */ public QueryResult upsert(String table, PrimaryKey primaryKey, InsertQuery insert, UpdateQuery update) { QueryRowsResult slct = select("*") .from(table) diff --git a/api/src/main/java/me/zort/sqllib/api/provider/Delete.java b/core/src/main/java/me/zort/sqllib/api/provider/Delete.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/api/provider/Delete.java rename to core/src/main/java/me/zort/sqllib/api/provider/Delete.java diff --git a/api/src/main/java/me/zort/sqllib/api/provider/Select.java b/core/src/main/java/me/zort/sqllib/api/provider/Select.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/api/provider/Select.java rename to core/src/main/java/me/zort/sqllib/api/provider/Select.java diff --git a/api/src/main/java/me/zort/sqllib/api/repository/CachingSQLTableRepository.java b/core/src/main/java/me/zort/sqllib/api/repository/CachingSQLTableRepository.java similarity index 96% rename from api/src/main/java/me/zort/sqllib/api/repository/CachingSQLTableRepository.java rename to core/src/main/java/me/zort/sqllib/api/repository/CachingSQLTableRepository.java index ac95bd1..9a8b50a 100644 --- a/api/src/main/java/me/zort/sqllib/api/repository/CachingSQLTableRepository.java +++ b/core/src/main/java/me/zort/sqllib/api/repository/CachingSQLTableRepository.java @@ -2,7 +2,7 @@ import lombok.Getter; import lombok.Setter; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import java.util.Map; import java.util.Optional; diff --git a/api/src/main/java/me/zort/sqllib/api/repository/SQLTableRepository.java b/core/src/main/java/me/zort/sqllib/api/repository/SQLTableRepository.java similarity index 98% rename from api/src/main/java/me/zort/sqllib/api/repository/SQLTableRepository.java rename to core/src/main/java/me/zort/sqllib/api/repository/SQLTableRepository.java index 78f0057..23cac6a 100644 --- a/api/src/main/java/me/zort/sqllib/api/repository/SQLTableRepository.java +++ b/core/src/main/java/me/zort/sqllib/api/repository/SQLTableRepository.java @@ -4,7 +4,7 @@ import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import me.zort.sqllib.api.provider.Select; import me.zort.sqllib.internal.annotation.Id; import me.zort.sqllib.internal.annotation.PrimaryKey; diff --git a/core/src/main/java/me/zort/sqllib/internal/impl/QueryResultImpl.java b/core/src/main/java/me/zort/sqllib/internal/impl/QueryResultImpl.java index 1cb4f94..9441e2f 100644 --- a/core/src/main/java/me/zort/sqllib/internal/impl/QueryResultImpl.java +++ b/core/src/main/java/me/zort/sqllib/internal/impl/QueryResultImpl.java @@ -3,11 +3,26 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.api.data.QueryRowsResult; @RequiredArgsConstructor +@Getter public class QueryResultImpl implements QueryResult { - @Getter private final boolean successful; + private String rejectMessage = null; + + public QueryResultImpl(boolean successful, String rejectMessage) { + this.successful = successful; + rejectMessage(rejectMessage); + } + + public QueryResultImpl rejectMessage(String message) { + if (rejectMessage != null) + throw new RuntimeException("Reject message is already set!"); + + this.rejectMessage = message; + return this; + } } diff --git a/api/src/main/java/me/zort/sqllib/internal/query/Conditional.java b/core/src/main/java/me/zort/sqllib/internal/query/Conditional.java similarity index 80% rename from api/src/main/java/me/zort/sqllib/internal/query/Conditional.java rename to core/src/main/java/me/zort/sqllib/internal/query/Conditional.java index 00ca447..33f80f9 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/Conditional.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/Conditional.java @@ -5,18 +5,18 @@ import java.util.ArrayList; -public interface Conditional

& Conditional

> { +public interface Conditional

& Conditional

> { default WhereStatement

where() { return where(QueryPriority.CONDITION.getPrior()); } default WhereStatement

where(int priority) { - if(!(this instanceof QueryPart)) { + if(!(this instanceof QueryNode)) { throw new IllegalStatementOperationException("This instance is not query part!"); } WhereStatement

stmt = new WhereStatement<>((P) this, new ArrayList<>(), priority); - ((QueryPart) this).then(stmt); + ((QueryNode) this).then(stmt); return stmt; } diff --git a/api/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java similarity index 76% rename from api/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java rename to core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java index a5c8aa9..37772cd 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/DeleteQuery.java @@ -2,13 +2,13 @@ import lombok.Getter; import me.zort.sqllib.api.Executive; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Objects; -public class DeleteQuery extends QueryPart> implements Executive, Conditional { +public class DeleteQuery extends QueryNode> implements Executive, Conditional { private String table; @@ -31,9 +31,12 @@ public DeleteQuery from(String table) { } @Override - public String buildQuery() { + public QueryDetails buildQueryDetails() { Objects.requireNonNull(table, "Table cannot be null!"); - return String.format("DELETE FROM %s%s;", table, buildInnerQuery()); + + return new QueryDetails.Builder("DELETE FROM " + table) + .build() + .append(buildInnerQuery()); } @Override diff --git a/api/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java similarity index 66% rename from api/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java rename to core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java index 43f2b0c..a9c0034 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/InsertQuery.java @@ -2,7 +2,7 @@ import lombok.Getter; import me.zort.sqllib.api.Executive; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import me.zort.sqllib.internal.exception.IllegalStatementOperationException; import me.zort.sqllib.util.Encoding; import me.zort.sqllib.util.Util; @@ -10,14 +10,14 @@ import java.util.ArrayList; import java.util.Objects; -import java.util.StringJoiner; -public class InsertQuery extends QueryPart> implements Executive, Conditional { +public class InsertQuery extends QueryNode> implements Executive, Conditional { @Getter private String table; private String[] defs; private String[] values; + private int currPhIndex = 0; @Getter private final SQLDatabaseConnection connection; @@ -69,20 +69,41 @@ private String handleVal(Object obj) { } @Override - public String buildQuery() { + public QueryDetails buildQueryDetails() { Objects.requireNonNull(table, "Table cannot be null!"); if(defs.length != values.length) { throw new IllegalStatementOperationException("Definition count must be same as values count!"); } - return String.format("INSERT INTO %s %s VALUES %s%s;", table, joinArr(defs), joinArr(values), buildInnerQuery()); + + QueryDetails details = new QueryDetails.Builder(String.format("INSERT INTO %s ", table)).build(); + + insertArray(details, defs, false); + details.append(" VALUES "); + insertArray(details, values, true); + + return details; } - private String joinArr(String[] arr) { - StringJoiner joiner = new StringJoiner(", ", "(", ")"); - for(String str : arr) { - joiner.add(str); + private void insertArray(QueryDetails details, String[] array, boolean usePlaceholders) { + details.append("("); + for (String obj : array) { + String placeholder = nextPlaceholder(); + if (!details.getQueryStr().endsWith("(")) + details.append(", "); + + if (usePlaceholders) { + details.append(new QueryDetails.Builder("<" + placeholder + ">") + .placeholder(placeholder, obj) + .build()); + } else { + details.append(obj); + } } - return joiner.toString(); + details.append(")"); + } + + private String nextPlaceholder() { + return "insert_" + currPhIndex++; } @Override diff --git a/core/src/main/java/me/zort/sqllib/internal/query/QueryDetails.java b/core/src/main/java/me/zort/sqllib/internal/query/QueryDetails.java new file mode 100644 index 0000000..dda061c --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/internal/query/QueryDetails.java @@ -0,0 +1,152 @@ +package me.zort.sqllib.internal.query; + +import lombok.*; +import me.zort.sqllib.Logger; +import me.zort.sqllib.util.Pair; +import me.zort.sqllib.util.Util; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.IntConsumer; + +@AllArgsConstructor +@Getter +public class QueryDetails { + + public static QueryDetails empty() { + return new QueryDetails(); + } + + @Setter(AccessLevel.PROTECTED) + private String queryStr; + private final Map values; + + public QueryDetails() { // Equiv to empty() + this("", new HashMap<>()); + } + + public QueryDetails append(QueryDetails other) { + return append("", other); + } + + public QueryDetails append(String prefix, QueryDetails other) { + Objects.requireNonNull(other, "QueryDetails cannot be null!"); + + append(prefix + other.queryStr); + other.values.forEach(values::putIfAbsent); + return this; + } + + public QueryDetails append(String s) { + queryStr += s; + return this; + } + + // Creates prepared statement for execution in SQlDatabaseConnectionImpl class. + protected PreparedStatement prepare(Connection connection) throws SQLException { + Pair requirements = buildStatementDetails(); + + // Shows plain query for prepared statement. + Logger.debug(connection, String.format("P-Query: %s", requirements.getFirst())); + Logger.debug(connection, String.format("P-Values: %s", Arrays.toString(requirements.getSecond()))); + + PreparedStatement statement = connection.prepareStatement(requirements.getFirst()); + Object[] values = requirements.getSecond(); + for (int i = 0; i < values.length; i++) { + set(statement, i + 1, values[i]); + } + return statement; + } + + protected Pair buildStatementDetails() { + String query = queryStr; + Map valuesUnsorted = new HashMap<>(); + + int i = 0; + for (String placeholder : this.values.keySet()) { + Object value = this.values.get(placeholder); + + placeholder = String.format("<%s>", placeholder); + + if (Util.count(queryStr, placeholder) != 1) + throw new RuntimeException("Placeholder " + placeholder + " is not unique in query " + queryStr); + + valuesUnsorted.put(query.indexOf(placeholder), value); + query = query.replaceAll(placeholder, "?"); + + i++; + } + + Object[] values = new Object[valuesUnsorted.size()]; + valuesUnsorted.keySet() + .stream() + .mapToInt(Integer::intValue) + .sorted().forEach(new IntConsumer() { + private int index = 0; + @Override + public void accept(int value) { + values[index] = valuesUnsorted.get(value); + index++; + } + }); + + return new Pair<>(query, values); + } + + private static void set(PreparedStatement statement, int index, Object value) throws SQLException { + switch(value.getClass().getSimpleName().toLowerCase()) { + case "string": + statement.setString(index, (String) value); + break; + case "integer": + case "int": + statement.setInt(index, (int) value); + break; + case "long": + statement.setLong(index, (long) value); + break; + case "double": + statement.setDouble(index, (double) value); + break; + case "float": + statement.setFloat(index, (float) value); + break; + case "boolean": + statement.setBoolean(index, (boolean) value); + break; + default: + statement.setObject(index, value); + } + } + + public int length() { + return queryStr.length(); + } + + @RequiredArgsConstructor + public static class Builder { + + private final String query; + private final Map values = new HashMap<>(); + + // Name without brackets + public Builder placeholder(String name, Object value) { + if(!query.contains("<" + name + ">")) + throw new IllegalArgumentException("Placeholder <" + name + "> not found in query!"); + + values.put(name, value); + return this; + } + + public QueryDetails build() { + return new QueryDetails(query, values); + } + + } + +} diff --git a/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java b/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java new file mode 100644 index 0000000..7880058 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/internal/query/QueryNode.java @@ -0,0 +1,158 @@ +package me.zort.sqllib.internal.query; + +import lombok.Getter; +import me.zort.sqllib.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnectionImpl; +import me.zort.sqllib.api.*; +import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.internal.exception.InvalidConnectionInstanceException; +import me.zort.sqllib.internal.exception.NoLinkedConnectionException; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +@Getter +public abstract class QueryNode

> implements Query, StatementFactory { + + @Getter(onMethod_ = {@Nullable}) + private final P parent; + private final List> children; + private final int priority; + private final Map details; + + public QueryNode(@Nullable P parent, List> initial) { + this(parent, initial, QueryPriority.GENERAL); + } + + public QueryNode(@Nullable P parent, List> initial, QueryPriority priority) { + this(parent, initial, priority.getPrior()); + } + + public QueryNode(@Nullable P parent, List> initial, int priority) { + this.parent = parent; + this.children = initial; + this.priority = priority; + this.details = new ConcurrentHashMap<>(); + } + + public abstract QueryDetails buildQueryDetails(); + + @Override + public PreparedStatement prepare(Connection connection) throws SQLException { + return details.remove(buildQuery()).prepare(connection); + } + + @Override + public String buildQuery() { + QueryDetails queryDetails = buildQueryDetails(); + + if (isAncestor()) + debug(String.format("Query: %s", queryDetails.getQueryStr())); + + String uuid = UUID.randomUUID().toString(); + details.put(uuid, queryDetails); + return uuid; + } + + public QueryDetails buildInnerQuery() { + List> children = new ArrayList<>(this.children); + Collections.sort(children, Comparator.comparingInt(QueryNode::getPriority)); + + QueryDetails details = new QueryDetails("", new HashMap<>()); + + if (children.isEmpty()) { + return QueryDetails.empty(); + } + + for (QueryNode inner : children) { + if (details.length() > 0) + details.append(" "); + + QueryDetails innerDetails = inner.getDetails().get(inner.buildQuery()); + details.append(innerDetails); + } + + return details; + } + + @Nullable + protected T invokeToConnection(Function func) + throws NoLinkedConnectionException, InvalidConnectionInstanceException { + QueryNode current = this; + while(current.getParent() != null && !(current instanceof Executive)) { + current = current.getParent(); + } + T result; + if(current instanceof Executive) { + SQLConnection connection = ((Executive) current).getConnection(); + if(!(connection instanceof SQLDatabaseConnection)) { + throw new InvalidConnectionInstanceException(connection); + } + + result = func.apply((SQLDatabaseConnection) connection); + } else { + throw new NoLinkedConnectionException(this); + } + return result; + } + + @SuppressWarnings("unchecked") + public QueryNode then(String part) { + int maxPriority = children.stream() + .map(QueryNode::getPriority) + .max(Comparator.naturalOrder()) + .orElse(0); + + then(new LocalQueryNode(this, maxPriority + 1, part)); + return this; + } + + public > QueryNode then(QueryNode part) { + this.children.add(part); + return part; + } + + public P also() { + return parent; + } + + public QueryResult execute() { + return invokeToConnection(connection -> connection.exec(getAncestor())); + } + + public QueryNode getAncestor() { + QueryNode current = this; + while(current.getParent() != null) { + current = current.getParent(); + } + return current; + } + + private void debug(String message) { + if (getAncestor() instanceof Executive + && ((Executive) getAncestor()).getConnection() instanceof SQLDatabaseConnectionImpl) { + ((SQLDatabaseConnectionImpl) ((Executive) getAncestor()).getConnection()).debug(message); + } + } + + private static class LocalQueryNode extends QueryNode { + + private final String queryPartString; + + public LocalQueryNode(@Nullable QueryNode parent, int priority, String queryPartString) { + super(parent, Collections.emptyList(), priority); + this.queryPartString = queryPartString; + } + + @Override + public QueryDetails buildQueryDetails() { + return new QueryDetails(queryPartString, new HashMap<>()); + } + } + +} diff --git a/api/src/main/java/me/zort/sqllib/internal/query/QueryPartQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/QueryNodeR.java similarity index 68% rename from api/src/main/java/me/zort/sqllib/internal/query/QueryPartQuery.java rename to core/src/main/java/me/zort/sqllib/internal/query/QueryNodeR.java index f156cf0..28fc453 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/QueryPartQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/QueryNodeR.java @@ -7,17 +7,17 @@ import java.util.List; import java.util.Optional; -public abstract class QueryPartQuery

> extends QueryPart

{ +public abstract class QueryNodeR

> extends QueryNode

{ - public QueryPartQuery(@Nullable P parent, List> initial) { + public QueryNodeR(@Nullable P parent, List> initial) { super(parent, initial); } - public QueryPartQuery(@Nullable P parent, List> initial, QueryPriority priority) { + public QueryNodeR(@Nullable P parent, List> initial, QueryPriority priority) { super(parent, initial, priority); } - public QueryPartQuery(@Nullable P parent, List> initial, int priority) { + public QueryNodeR(@Nullable P parent, List> initial, int priority) { super(parent, initial, priority); } diff --git a/api/src/main/java/me/zort/sqllib/internal/query/QueryPriority.java b/core/src/main/java/me/zort/sqllib/internal/query/QueryPriority.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/internal/query/QueryPriority.java rename to core/src/main/java/me/zort/sqllib/internal/query/QueryPriority.java diff --git a/api/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java similarity index 73% rename from api/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java rename to core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java index 10fd2f0..964c65c 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/SelectQuery.java @@ -2,7 +2,7 @@ import lombok.Getter; import me.zort.sqllib.api.Executive; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import me.zort.sqllib.internal.query.part.LimitStatement; import org.jetbrains.annotations.Nullable; @@ -11,7 +11,7 @@ import java.util.List; import java.util.Objects; -public class SelectQuery extends QueryPartQuery> implements Executive, Conditional { +public class SelectQuery extends QueryNodeR> implements Executive, Conditional { private final List cols; private String table; @@ -41,10 +41,15 @@ public SelectQuery limit(int limit) { } @Override - public String buildQuery() { + public QueryDetails buildQueryDetails() { Objects.requireNonNull(table, "Table cannot be null!"); - String cols = this.cols.isEmpty() ? "*" : String.join(", ", this.cols); - return String.format("SELECT %s FROM %s%s;", cols, table, buildInnerQuery()); + + QueryDetails details = new QueryDetails.Builder(String.format("SELECT %s FROM %s", + this.cols.isEmpty() ? "*" : String.join(", ", this.cols), + table)) + .build(); + + return details.append(buildInnerQuery()); } @Override diff --git a/api/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java similarity index 87% rename from api/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java rename to core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java index 39e485f..51ede8b 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/UpdateQuery.java @@ -2,7 +2,7 @@ import lombok.Getter; import me.zort.sqllib.api.Executive; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import me.zort.sqllib.internal.query.part.SetStatement; import me.zort.sqllib.internal.query.part.WhereStatement; import org.jetbrains.annotations.Nullable; @@ -10,7 +10,7 @@ import java.util.ArrayList; import java.util.Objects; -public class UpdateQuery extends QueryPart> implements Executive, Conditional { +public class UpdateQuery extends QueryNode> implements Executive, Conditional { private String table; @@ -54,9 +54,10 @@ public WhereStatement where() { } @Override - public String buildQuery() { + public QueryDetails buildQueryDetails() { Objects.requireNonNull(table, "Table cannot be null!"); - return String.format("UPDATE %s%s;", table, buildInnerQuery()); + + return new QueryDetails.Builder("UPDATE " + table).build().append(buildInnerQuery()); } @Override diff --git a/api/src/main/java/me/zort/sqllib/internal/query/UpsertQuery.java b/core/src/main/java/me/zort/sqllib/internal/query/UpsertQuery.java similarity index 73% rename from api/src/main/java/me/zort/sqllib/internal/query/UpsertQuery.java rename to core/src/main/java/me/zort/sqllib/internal/query/UpsertQuery.java index c5912e1..749c269 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/UpsertQuery.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/UpsertQuery.java @@ -1,9 +1,11 @@ package me.zort.sqllib.internal.query; -import me.zort.sqllib.api.SQLDatabaseConnection; +import me.zort.sqllib.SQLDatabaseConnection; import me.zort.sqllib.internal.query.part.SetStatement; import org.jetbrains.annotations.Nullable; +import java.util.HashMap; + public class UpsertQuery extends InsertQuery { public UpsertQuery(SQLDatabaseConnection connection) { @@ -33,8 +35,14 @@ public SetStatement onDuplicateKey(String column, Object value) { public SetStatement onDuplicateKey() { SetStatement stmt = new SetStatement(this, 3) { @Override - public String buildQuery() { - return " ON DUPLICATE KEY UPDATE" + super.buildQuery().replaceAll("SET ", ""); + public QueryDetails buildQueryDetails() { + QueryDetails details = new QueryDetails(" ON DUPLICATE KEY UPDATE", new HashMap<>()); + + QueryDetails superDetails = super.buildQueryDetails(); + superDetails.setQueryStr(superDetails.getQueryStr().replaceAll("SET ", "")); + details.append(superDetails); + + return details; } }; then(stmt); diff --git a/core/src/main/java/me/zort/sqllib/internal/query/part/LimitStatement.java b/core/src/main/java/me/zort/sqllib/internal/query/part/LimitStatement.java new file mode 100644 index 0000000..1e400b9 --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/internal/query/part/LimitStatement.java @@ -0,0 +1,31 @@ +package me.zort.sqllib.internal.query.part; + +import me.zort.sqllib.internal.query.QueryDetails; +import me.zort.sqllib.internal.query.QueryNode; +import me.zort.sqllib.internal.query.QueryNodeR; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class LimitStatement

> extends QueryNodeR

{ + + private final int limit; + + public LimitStatement(@Nullable P parent, List> initial, int limit) { + super(parent, initial, Integer.MAX_VALUE); + this.limit = limit; + } + + @Override + public QueryDetails buildQueryDetails() { + return new QueryDetails(" LIMIT " + Math.max(limit, 0), new HashMap<>()); + } + + @SuppressWarnings("unchecked") + @Override + public LimitStatement

then(String part) { + return (LimitStatement

) super.then(part); + } +} diff --git a/api/src/main/java/me/zort/sqllib/internal/query/part/SetStatement.java b/core/src/main/java/me/zort/sqllib/internal/query/part/SetStatement.java similarity index 56% rename from api/src/main/java/me/zort/sqllib/internal/query/part/SetStatement.java rename to core/src/main/java/me/zort/sqllib/internal/query/part/SetStatement.java index e25cae6..d5a2504 100644 --- a/api/src/main/java/me/zort/sqllib/internal/query/part/SetStatement.java +++ b/core/src/main/java/me/zort/sqllib/internal/query/part/SetStatement.java @@ -2,19 +2,19 @@ import me.zort.sqllib.internal.exception.IllegalStatementOperationException; import me.zort.sqllib.internal.query.Conditional; -import me.zort.sqllib.internal.query.QueryPart; -import me.zort.sqllib.util.Encoding; +import me.zort.sqllib.internal.query.QueryDetails; +import me.zort.sqllib.internal.query.QueryNode; import me.zort.sqllib.util.Pair; import me.zort.sqllib.util.Pairs; -import me.zort.sqllib.util.Util; import org.jetbrains.annotations.Nullable; import java.util.Collections; -import java.util.stream.Collectors; +import java.util.HashMap; -public class SetStatement

& Conditional

> extends QueryPart

implements Conditional

{ +public class SetStatement

& Conditional

> extends QueryNode

implements Conditional

{ private final Pairs update; + private int currPhIndex = 0; public SetStatement(@Nullable P parent) { this(parent, 1); @@ -49,22 +49,35 @@ public WhereStatement

where(int priority) { } @Override - public String buildQuery() { + public QueryDetails buildQueryDetails() { if(update.isEmpty()) { - return ""; + return QueryDetails.empty(); } - return " SET " + update - .stream() - .map(pair -> { - Object obj = pair.getSecond(); - if(obj instanceof String) { - obj = Encoding.handleTo((String) obj); - } - return pair.getFirst() + " = " + Util.buildQuoted(obj); - }) - .collect(Collectors.joining(", ")); + + QueryDetails details = new QueryDetails(" SET ", new HashMap<>()); + + for (Pair pair : update) { + String name = pair.getFirst(); + Object value = pair.getSecond(); + + String placeholder = nextPlaceholder(); + + if (!details.getQueryStr().equals(" SET ")) + details.append(", "); + + details.append(new QueryDetails.Builder(String.format("%s = <%s>", name, placeholder)) + .placeholder(placeholder, value) + .build()); + } + + return details; + } + + private String nextPlaceholder() { + return "set_" + currPhIndex++; } + @SuppressWarnings("unchecked") @Override public SetStatement

then(String part) { return (SetStatement

) super.then(part); diff --git a/core/src/main/java/me/zort/sqllib/internal/query/part/WhereStatement.java b/core/src/main/java/me/zort/sqllib/internal/query/part/WhereStatement.java new file mode 100644 index 0000000..14b086e --- /dev/null +++ b/core/src/main/java/me/zort/sqllib/internal/query/part/WhereStatement.java @@ -0,0 +1,127 @@ +package me.zort.sqllib.internal.query.part; + +import me.zort.sqllib.internal.exception.IllegalStatementOperationException; +import me.zort.sqllib.internal.query.QueryDetails; +import me.zort.sqllib.internal.query.QueryNode; +import me.zort.sqllib.internal.query.QueryNodeR; +import me.zort.sqllib.internal.query.QueryPriority; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class WhereStatement

> extends QueryNodeR

{ + + private final List conditions = new ArrayList<>(); + private int currPhIndex = 0; + + public WhereStatement(@Nullable P parent, List> initial) { + super(parent, initial, QueryPriority.CONDITION.getPrior()); + } + + public WhereStatement(@Nullable P parent, List> initial, int priority) { + super(parent, initial, priority); + } + + public WhereStatement

isEqual(String column, Object value) { + String placeholder = nextPlaceholder(); + conditions.add(new QueryDetails.Builder(String.format("%s = <%s>", column, placeholder)) + .placeholder(placeholder, value) + .build()); + return this; + } + + public WhereStatement

bt(String column, long value) { + String placeholder = nextPlaceholder(); + conditions.add(new QueryDetails.Builder(String.format("%s > <%s>", column, placeholder)) + .placeholder(placeholder, value) + .build()); + return this; + } + + public WhereStatement

lt(String column, long value) { + String placeholder = nextPlaceholder(); + conditions.add(new QueryDetails.Builder(String.format("%s < <%s>", column, placeholder)) + .placeholder(placeholder, value) + .build()); + return this; + } + + public WhereStatement

in(String column, Object... objs) { + return in(column, Arrays.asList(objs)); + } + + public WhereStatement

in(String column, List objs) { + if(objs.isEmpty()) return this; + + QueryDetails details = new QueryDetails(column + " IN (", new HashMap<>()); + for (Object obj : objs) { + + if (!details.getQueryStr().endsWith("(")) + details.append(", "); + + String placeholder = nextPlaceholder(); + details.append(new QueryDetails.Builder(String.format("<%s>", placeholder)) + .placeholder(placeholder, obj) + .build()); + } + details.append(")"); + + conditions.add(details); + + return this; + } + + public WhereStatement

like(String column, String paramPlaceholder) { + String placeholder = nextPlaceholder(); + conditions.add(new QueryDetails.Builder(String.format("%s LIKE <%s>", column, placeholder)) + .placeholder(placeholder, paramPlaceholder) + .build()); + return this; + } + + private String nextPlaceholder() { + return "where_" + currPhIndex++; + } + + @Override + public > QueryNode then(QueryNode part) { + throw new IllegalStatementOperationException("Where statement can't have inner parts!"); + } + + public WhereStatement

and() { + return this; + } + + public WhereStatement

or() { + conditions.add(new QueryDetails(" OR ", new HashMap<>())); + return this; + } + + @Override + public QueryDetails buildQueryDetails() { + QueryDetails details = new QueryDetails(" WHERE ", new HashMap<>()); + + if(conditions.isEmpty()) { + // We don't have any conditions, so where statement should be true. + details.append("TRUE"); + } else { + for(QueryDetails _details : conditions) { + String condition = _details.getQueryStr(); + + if(!details.getQueryStr().equals(" WHERE ") && !condition.equals(" OR ") && !details.getQueryStr().endsWith(" OR ")) { + details.append(" AND "); + } + + details.append(_details); + } + } + + return details; + } + + @SuppressWarnings("unchecked") + @Override + public WhereStatement

then(String part) { + return (WhereStatement

) super.then(part); + } +} diff --git a/api/src/main/java/me/zort/sqllib/util/Validator.java b/core/src/main/java/me/zort/sqllib/util/Validator.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/Validator.java rename to core/src/main/java/me/zort/sqllib/util/Validator.java diff --git a/examples/build.gradle b/examples/build.gradle new file mode 100644 index 0000000..7e518e5 --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group 'com.github.ZorTik' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation project(":api") + implementation project(":shared") + implementation project(":core") + implementation group: 'org.jetbrains', name: 'annotations', version: '20.1.0' + implementation 'com.google.code.gson:gson:2.9.0' + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' +} \ No newline at end of file diff --git a/examples/settings.gradle b/examples/settings.gradle new file mode 100644 index 0000000..c633ef2 --- /dev/null +++ b/examples/settings.gradle @@ -0,0 +1,4 @@ +include ":api", ":shared", ":core" +project(":api").projectDir = file("../api") +project(":shared").projectDir = file("../shared") +project(":core").projectDir = file("../core") \ No newline at end of file diff --git a/core/src/main/java/me/zort/sqllib/Example.java b/examples/src/main/java/me/zort/sqllib/Example.java similarity index 100% rename from core/src/main/java/me/zort/sqllib/Example.java rename to examples/src/main/java/me/zort/sqllib/Example.java diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index 5b5eadc..66de96d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,10 @@ rootProject.name = 'AdvancedSQLClient' include ':api' include ':core' +include 'shared' +include 'examples' project(":api").projectDir = file("api") -project(":core").projectDir = file("core") \ No newline at end of file +project(":core").projectDir = file("core") +project(":shared").projectDir = file("shared") +project(":examples").projectDir = file("examples") \ No newline at end of file diff --git a/shared/build.gradle b/shared/build.gradle new file mode 100644 index 0000000..317744c --- /dev/null +++ b/shared/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' +} + +group 'com.github.ZorTik' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation group: 'org.jetbrains', name: 'annotations', version: '20.1.0' + implementation 'com.google.code.gson:gson:2.9.0' + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' +} \ No newline at end of file diff --git a/api/src/main/java/me/zort/sqllib/util/Arrays.java b/shared/src/main/java/me/zort/sqllib/util/Arrays.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/Arrays.java rename to shared/src/main/java/me/zort/sqllib/util/Arrays.java diff --git a/api/src/main/java/me/zort/sqllib/util/Encoding.java b/shared/src/main/java/me/zort/sqllib/util/Encoding.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/Encoding.java rename to shared/src/main/java/me/zort/sqllib/util/Encoding.java diff --git a/api/src/main/java/me/zort/sqllib/util/ListBuilder.java b/shared/src/main/java/me/zort/sqllib/util/ListBuilder.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/ListBuilder.java rename to shared/src/main/java/me/zort/sqllib/util/ListBuilder.java diff --git a/api/src/main/java/me/zort/sqllib/util/Pair.java b/shared/src/main/java/me/zort/sqllib/util/Pair.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/Pair.java rename to shared/src/main/java/me/zort/sqllib/util/Pair.java diff --git a/api/src/main/java/me/zort/sqllib/util/Pairs.java b/shared/src/main/java/me/zort/sqllib/util/Pairs.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/Pairs.java rename to shared/src/main/java/me/zort/sqllib/util/Pairs.java diff --git a/api/src/main/java/me/zort/sqllib/util/PrimaryKey.java b/shared/src/main/java/me/zort/sqllib/util/PrimaryKey.java similarity index 100% rename from api/src/main/java/me/zort/sqllib/util/PrimaryKey.java rename to shared/src/main/java/me/zort/sqllib/util/PrimaryKey.java diff --git a/shared/src/main/java/me/zort/sqllib/util/Util.java b/shared/src/main/java/me/zort/sqllib/util/Util.java new file mode 100644 index 0000000..e4250bb --- /dev/null +++ b/shared/src/main/java/me/zort/sqllib/util/Util.java @@ -0,0 +1,25 @@ +package me.zort.sqllib.util; + +public final class Util { + + public static String buildQuoted(Object obj) { + obj = obj instanceof String + //? String.format("'%s'", obj) // No longer needed, because I use prepared statements. + ? String.format("%s", obj) + : String.valueOf(obj); + return (String) obj; + } + + public static int count(String str, String substr) { + String copy = str; + int count = 0; + + while (copy.contains(substr)) { + copy = copy.replaceFirst(substr, ""); + count++; + } + + return count; + } + +} diff --git a/src/test/java/me/zort/sqllib/test/TestCase1.java b/src/test/java/me/zort/sqllib/test/TestCase1.java new file mode 100644 index 0000000..051647a --- /dev/null +++ b/src/test/java/me/zort/sqllib/test/TestCase1.java @@ -0,0 +1,176 @@ +package me.zort.sqllib.test; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; +import me.zort.sqllib.SQLConnectionBuilder; +import me.zort.sqllib.SQLDatabaseOptions; +import me.zort.sqllib.SQLDatabaseConnection; +import me.zort.sqllib.api.data.QueryResult; +import me.zort.sqllib.api.data.QueryRowsResult; +import me.zort.sqllib.api.data.Row; +import me.zort.sqllib.api.provider.Select; +import me.zort.sqllib.internal.impl.DefaultSQLEndpoint; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@EnabledOnOs(value = {OS.LINUX, OS.WINDOWS}) +@TestMethodOrder(MethodOrderer.MethodName.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TestCase1 { + + private SQLDatabaseConnection connection; + private static final String TABLE_NAME = "users"; + private final User user1 = new User("User1", 100); + private final User user2 = new User("User2", 200); + + @BeforeAll + public void prepareLogging() { + Configurator.setAllLevels("", Level.ALL); + } + + @Timeout(15) + @BeforeAll + public void prepare() { + System.out.println("Preparing test case..."); + + String host = System.getenv("D_MYSQL_HOST"); + + if(host == null) + host = "localhost"; + + SQLDatabaseOptions options = new SQLDatabaseOptions(); + options.setDebug(true); + + DefaultSQLEndpoint endpoint = new DefaultSQLEndpoint(String.format("%s:3306", host), "test", "test", "test"); + + connection = SQLConnectionBuilder.of(endpoint) + .withDriver("com.mysql.cj.jdbc.Driver") + .build(options); + + System.out.println("Connection prepared, connecting..."); + + assertEquals(endpoint.buildJdbc(), String.format("jdbc:mysql://%s:3306/test", host)); + assertTrue(connection.connect()); + assertTrue(connection.isConnected()); + + System.out.println("Connection established, preparing tables..."); + + assertNull(connection.exec(() -> "CREATE TABLE IF NOT EXISTS users (nickname VARCHAR(16) PRIMARY KEY NOT NULL, points INT NOT NULL);").getRejectMessage()); + assertNull(connection.exec(() -> "TRUNCATE TABLE users;").getRejectMessage()); + + System.out.println("Tables prepared, test cases ready"); + } + + @Timeout(10) + @Test + public void test1_Upsert() { + System.out.println("Testing upsert (save)..."); + assertTrue(connection.save(TABLE_NAME, user1).isSuccessful()); + System.out.println("Save successful"); + System.out.println("Testing upsert..."); + assertTrue(connection.upsert() + .into(TABLE_NAME, "nickname", "points") + .values(user2.getNickname(), user2.getPoints()) + .onDuplicateKey() + .and("nickname", user2.getNickname()) + .and("points", user2.getPoints()) + .execute().isSuccessful()); + System.out.println("Upsert successful"); + } + + @Timeout(10) + @Test + public void test2_Select() { + System.out.println("Testing select..."); + QueryRowsResult result = connection.query(Select.of().from(TABLE_NAME) + .where() + .isEqual("nickname", "User1"), User.class); + + assertNull(result.getRejectMessage()); + assertEquals(1, result.size()); + assertEquals(user1, result.get(0)); + System.out.println("Select successful"); + } + + @Timeout(10) + @Test + public void test3_Update() { + System.out.println("Testing update..."); + assertNull(connection.update() + .table(TABLE_NAME) + .set("points", 300) + .where() + .isEqual("nickname", user1.getNickname()) + .execute().getRejectMessage()); + Optional rowOptional = connection.select("points") + .from(TABLE_NAME) + .where() + .isEqual("nickname", user1.getNickname()) + .obtainOne(); + + assertTrue(rowOptional.isPresent()); + assertEquals(300, rowOptional.get().get("points")); + } + + @Timeout(10) + @Test + public void test4_Delete() { + QueryResult result = connection.delete() + .from(TABLE_NAME) + .where() + .isEqual("nickname", "User1").execute(); + + assertNull(result.getRejectMessage()); + } + + @Timeout(5) + @Test + public void test5_Close() { + System.out.println("Closing connection..."); + connection.disconnect(); + System.out.println("Connection closed"); + } + + @AllArgsConstructor + @NoArgsConstructor + private static class User { + private String nickname; + private int points; + + public String getNickname() { + return nickname; + } + + public int getPoints() { + return points; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + User user = (User) o; + + if (points != user.points) return false; + return nickname.equals(user.nickname); + } + + @Override + public int hashCode() { + int result = nickname.hashCode(); + result = 31 * result + points; + return result; + } + } + +}