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;
+ }
+ }
+
+}