add database support
parent
82f5a125bd
commit
bfae7c9802
@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
id("java-library")
|
||||||
|
id("chat-bot.java-conventions")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "eu.jonahbauer.chat"
|
||||||
|
version = "0.1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(libs.annotations)
|
||||||
|
implementation(libs.sqlite)
|
||||||
|
implementation(libs.hikari)
|
||||||
|
implementation(libs.log4j2.api)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database;
|
||||||
|
|
||||||
|
import lombok.experimental.StandardException;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
@StandardException
|
||||||
|
public class BindException extends SQLException {
|
||||||
|
}
|
@ -0,0 +1,310 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.Binder;
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.Helper;
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.Inserter;
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.Updater;
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.types.TypeAdapter;
|
||||||
|
import lombok.Lombok;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.intellij.lang.annotations.Language;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@SuppressWarnings({"SqlSourceToSinkFlow", "unused"})
|
||||||
|
public class Database implements AutoCloseable {
|
||||||
|
private static final ScopedValue<Connection> CONNECTION = ScopedValue.newInstance();
|
||||||
|
private final @NotNull HikariDataSource ds;
|
||||||
|
|
||||||
|
public Database(@NotNull String url) {
|
||||||
|
this(url, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Database(@NotNull String url, @Nullable String user, @Nullable String password) {
|
||||||
|
var hikariConfig = new HikariConfig();
|
||||||
|
hikariConfig.setJdbcUrl(url);
|
||||||
|
hikariConfig.setUsername(user);
|
||||||
|
hikariConfig.setPassword(password);
|
||||||
|
hikariConfig.addDataSourceProperty("enforceForeignKeys", true);
|
||||||
|
this.ds = new HikariDataSource(hikariConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("RedundantThrows")
|
||||||
|
public <T extends Throwable> void transaction(@NotNull ThrowingRunnable<T> runnable) throws SQLException, T {
|
||||||
|
if (CONNECTION.isBound()) throw new IllegalStateException("nested transactions are not supported");
|
||||||
|
|
||||||
|
try (var connection = getConnection0()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
ScopedValue.runWhere(CONNECTION, connection, () -> {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw Lombok.sneakyThrow(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connection.commit();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
connection.rollback();
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("RedundantThrows")
|
||||||
|
public <R, T extends Throwable> R transaction(@NotNull ThrowingCallable<R, T> callable) throws SQLException, T {
|
||||||
|
if (CONNECTION.isBound()) throw new IllegalStateException("nested transactions are not supported");
|
||||||
|
|
||||||
|
try (var connection = getConnection0()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
var result = ScopedValue.callWhere(CONNECTION, connection, () -> {
|
||||||
|
try {
|
||||||
|
return callable.call();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw Lombok.sneakyThrow(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connection.commit();
|
||||||
|
return result;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
connection.rollback();
|
||||||
|
throw Lombok.sneakyThrow(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull List<T> findAll(@NotNull Class<T> type) throws SQLException {
|
||||||
|
return executeQuery(type, "SELECT * FROM " + Helper.getQuotedTableName(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record & Entity<T>> @NotNull Optional<T> findById(@NotNull Class<T> type, long id) throws SQLException {
|
||||||
|
return findWhere(type, "id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("Convert2MethodRef")
|
||||||
|
public <T extends Record> @NotNull List<T> findAllWhere(@NotNull Class<T> type, @NotNull String component, Object value) throws SQLException {
|
||||||
|
return findWhere0(type, component, value, (t, query, customizer) -> executeQuery(t, query, customizer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("Convert2MethodRef")
|
||||||
|
public <T extends Record> @NotNull Optional<T> findWhere(@NotNull Class<T> type, @NotNull String component, Object value) throws SQLException {
|
||||||
|
return findWhere0(type, component, value, (t, query, customizer) -> executeUniqueQuery(t, query, customizer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows(NoSuchMethodException.class)
|
||||||
|
private <T extends Record, R> R findWhere0(@NotNull Class<T> type, @NotNull String component, Object value, @NotNull FindWhere<T, R> finder) throws SQLException {
|
||||||
|
var table = Helper.getQuotedTableName(type);
|
||||||
|
var column = Helper.getColumnName(type, component);
|
||||||
|
var componentType = type.getMethod(component).getReturnType();
|
||||||
|
var adapter = TypeAdapter.get(componentType);
|
||||||
|
return finder.find(
|
||||||
|
type,
|
||||||
|
"SELECT * FROM " + table + " WHERE " + column + " " + (value == null ? "IS NULL" : "= ?"),
|
||||||
|
stmt -> {
|
||||||
|
if (value != null) adapter.prepareUnsafe(stmt, 1, value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface FindWhere<T extends Record, R> {
|
||||||
|
R find(@NotNull Class<T> type, @Language("SQL") @NotNull String query, @NotNull ThrowingConsumer<PreparedStatement, SQLException> customizer) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull List<T> bindAll(@NotNull Class<T> type, @NotNull ResultSet result) throws SQLException {
|
||||||
|
return bindAll(type, result, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull List<T> bindAll(@NotNull Class<T> type, @NotNull ResultSet result, @NotNull String prefix) throws SQLException {
|
||||||
|
var out = new ArrayList<T>();
|
||||||
|
var binder = Binder.get(type);
|
||||||
|
while (result.next()) {
|
||||||
|
out.add(binder.bind(result, prefix));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull Optional<T> bind(@NotNull Class<T> type, @NotNull ResultSet result) throws SQLException {
|
||||||
|
return bind(type, result, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull Optional<T> bind(@NotNull Class<T> type, @NotNull ResultSet result, @NotNull String prefix) throws SQLException {
|
||||||
|
if (result.next()) {
|
||||||
|
var binder = Binder.get(type);
|
||||||
|
var out = Optional.of(binder.bind(result, prefix));
|
||||||
|
if (result.next()) throw new NonUniqueResultException();
|
||||||
|
return out;
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T extends Record & Entity<T>> boolean delete(@NotNull T entity) throws SQLException {
|
||||||
|
return delete((Class<T>) entity.getClass(), entity.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record & Entity<T>> boolean delete(@NotNull Class<T> type, long id) throws SQLException {
|
||||||
|
return 1 == executeUpdate("DELETE FROM " + Helper.getQuotedTableName(type) + " WHERE id = ?", stmt -> stmt.setLong(1, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long insert(@NotNull Record record) throws SQLException {
|
||||||
|
return insert0(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends Record> long insert0(@NotNull T record) throws SQLException {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var type = (Class<T>) record.getClass();
|
||||||
|
var connection = getConnection0();
|
||||||
|
try {
|
||||||
|
return Inserter.get(type).insert(connection, record);
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> T save(@NotNull T record) throws SQLException {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var type = (Class<T>) record.getClass();
|
||||||
|
var connection = getConnection0();
|
||||||
|
try {
|
||||||
|
var id = Inserter.get(type).insert(connection, record);
|
||||||
|
if (record instanceof Entity<?> entity) {
|
||||||
|
return (T) entity.withId(id);
|
||||||
|
} else {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record & Entity<T>> boolean update(@NotNull T entity) throws SQLException {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
var type = (Class<T>) entity.getClass();
|
||||||
|
var connection = getConnection0();
|
||||||
|
try {
|
||||||
|
return Updater.get(type).update(connection, entity);
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SqlWithoutWhere")
|
||||||
|
public int truncate(@NotNull Class<? extends Record> type) throws SQLException {
|
||||||
|
return executeUpdate("DELETE FROM " + Helper.getQuotedTableName(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull List<T> executeQuery(@NotNull Class<T> type, @NotNull @Language("SQL") String query) throws SQLException {
|
||||||
|
var connection = getConnection0();
|
||||||
|
try (var stmt = connection.createStatement()) {
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var result = stmt.executeQuery(query)) {
|
||||||
|
return bindAll(type, result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <R extends Record, T extends Throwable> @NotNull List<R> executeQuery(@NotNull Class<R> type, @NotNull @Language("SQL") String query, @NotNull ThrowingConsumer<@NotNull PreparedStatement, T> customizer) throws SQLException, T {
|
||||||
|
var connection = getConnection0();
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var stmt = connection.prepareStatement(query)) {
|
||||||
|
customizer.accept(stmt);
|
||||||
|
try (var result = stmt.executeQuery()) {
|
||||||
|
return bindAll(type, result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Record> @NotNull Optional<T> executeUniqueQuery(@NotNull Class<T> type, @NotNull @Language("SQL") String query) throws SQLException {
|
||||||
|
var connection = getConnection0();
|
||||||
|
try (var stmt = connection.createStatement()) {
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var result = stmt.executeQuery(query)) {
|
||||||
|
return bind(type, result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <R extends Record, T extends Throwable> @NotNull Optional<R> executeUniqueQuery(@NotNull Class<R> type, @NotNull @Language("SQL") String query, @NotNull ThrowingConsumer<@NotNull PreparedStatement, T> customizer) throws SQLException, T {
|
||||||
|
var connection = getConnection0();
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var stmt = connection.prepareStatement(query)) {
|
||||||
|
customizer.accept(stmt);
|
||||||
|
try (var result = stmt.executeQuery()) {
|
||||||
|
return bind(type, result);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int executeUpdate(@NotNull @Language("SQL") String query) throws SQLException {
|
||||||
|
var connection = getConnection0();
|
||||||
|
try (var stmt = connection.createStatement()) {
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
return stmt.executeUpdate(query);
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Throwable> int executeUpdate(@NotNull @Language("SQL") String query, @NotNull ThrowingConsumer<@NotNull PreparedStatement, T> customizer) throws SQLException, T {
|
||||||
|
var connection = getConnection0();
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var stmt = connection.prepareStatement(query)) {
|
||||||
|
customizer.accept(stmt);
|
||||||
|
return stmt.executeUpdate();
|
||||||
|
} finally {
|
||||||
|
close(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Connection getConnection() throws SQLException {
|
||||||
|
return ds.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Connection getConnection0() throws SQLException {
|
||||||
|
return CONNECTION.isBound() ? CONNECTION.get() : ds.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void close(@NotNull Connection connection) throws SQLException {
|
||||||
|
if (!CONNECTION.isBound() || CONNECTION.get() != connection) connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
ds.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ThrowingCallable<R, T extends Throwable> {
|
||||||
|
R call() throws T;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ThrowingRunnable<T extends Throwable> {
|
||||||
|
void run() throws T;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ThrowingConsumer<S, T extends Throwable> {
|
||||||
|
void accept(S object) throws T;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.Helper;
|
||||||
|
|
||||||
|
public interface Entity<T extends Record & Entity<T>> {
|
||||||
|
long id();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
default T withId(long id) {
|
||||||
|
return Helper.getWithId((Class<T>) getClass()).withId((T) this, id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database;
|
||||||
|
|
||||||
|
import lombok.experimental.StandardException;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
@StandardException
|
||||||
|
public class NonUniqueResultException extends SQLException {
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.RECORD_COMPONENT)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Column {
|
||||||
|
String value();
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.annotations;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Table {
|
||||||
|
String value();
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
abstract class AbstractFactory<F> extends ClassValue<F> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final @NotNull F computeValue(Class<?> type) {
|
||||||
|
if (!type.isRecord()) throw new IllegalArgumentException();
|
||||||
|
return bridge(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends Record> @NotNull F bridge(@NotNull Class<?> type) {
|
||||||
|
return computeValue0((Class<T>) type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract <T extends Record> @NotNull F computeValue0(@NotNull Class<T> type);
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public interface Binder<T extends Record> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T extends Record> @NotNull Binder<T> get(@NotNull Class<T> type) {
|
||||||
|
return (Binder<T>) BinderFactory.INSTANCE.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
default @NotNull T bind(@NotNull ResultSet set) throws SQLException {
|
||||||
|
return bind(set, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull T bind(@NotNull ResultSet set, @NotNull String prefix) throws SQLException;
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.BindException;
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.types.TypeAdapter;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
final class BinderFactory extends AbstractFactory<Binder<?>> {
|
||||||
|
public static final @NotNull BinderFactory INSTANCE = new BinderFactory();
|
||||||
|
|
||||||
|
private BinderFactory() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected @NotNull <T extends Record> Binder<T> computeValue0(@NotNull Class<T> type) {
|
||||||
|
var components = type.getRecordComponents();
|
||||||
|
|
||||||
|
var columns = new ArrayList<String>();
|
||||||
|
var adapters = new ArrayList<TypeAdapter<?>>();
|
||||||
|
var types = new ArrayList<Class<?>>();
|
||||||
|
|
||||||
|
for (var component : components) {
|
||||||
|
var componentType = component.getType();
|
||||||
|
types.add(componentType);
|
||||||
|
columns.add(Helper.getColumnName(component));
|
||||||
|
adapters.add(TypeAdapter.get(componentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctor = type.getDeclaredConstructor(types.toArray(Class[]::new));
|
||||||
|
return new BinderImpl<>(ctor, columns, adapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BinderImpl<T extends Record> implements Binder<T> {
|
||||||
|
private final @NotNull Constructor<T> constructor;
|
||||||
|
private final @NotNull List<@NotNull String> columns;
|
||||||
|
private final @NotNull List<@NotNull TypeAdapter<?>> adapters;
|
||||||
|
|
||||||
|
private BinderImpl(@NotNull Constructor<T> constructor, @NotNull List<@NotNull String> columns, @NotNull List<@NotNull TypeAdapter<?>> adapters) {
|
||||||
|
this.constructor = Objects.requireNonNull(constructor);
|
||||||
|
this.columns = List.copyOf(columns);
|
||||||
|
this.adapters = List.copyOf(adapters);
|
||||||
|
if (this.columns.size() != this.adapters.size()) throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull T bind(@NotNull ResultSet result, @NotNull String prefix) throws SQLException {
|
||||||
|
try {
|
||||||
|
var count = columns.size();
|
||||||
|
var arguments = new Object[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
var index = result.findColumn(prefix + columns.get(i));
|
||||||
|
arguments[i] = adapters.get(i).extract(result, index);
|
||||||
|
}
|
||||||
|
return constructor.newInstance(arguments);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BindException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.Entity;
|
||||||
|
import eu.jonahbauer.chat.bot.database.annotations.Column;
|
||||||
|
import eu.jonahbauer.chat.bot.database.annotations.Table;
|
||||||
|
import lombok.Lombok;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.reflect.RecordComponent;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.lang.invoke.MethodHandles.*;
|
||||||
|
import static java.lang.invoke.MethodType.methodType;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class Helper {
|
||||||
|
public static @NotNull String getQuotedColumnName(@NotNull RecordComponent component) {
|
||||||
|
return quote(getColumnName(component));
|
||||||
|
}
|
||||||
|
public static @NotNull String getColumnName(@NotNull RecordComponent component) {
|
||||||
|
return getColumnName(component.getDeclaringRecord(), component.getName());
|
||||||
|
}
|
||||||
|
public static @NotNull String getColumnName(@NotNull Class<?> type, @NotNull String component) {
|
||||||
|
var out = columnNameCache.get(type).get(component);
|
||||||
|
if (out == null) throw new IllegalArgumentException("invalid component name " + component);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
private static final @NotNull ClassValue<@NotNull Map<@NotNull String, @NotNull String>> columnNameCache = new ClassValue<>() {
|
||||||
|
@Override
|
||||||
|
protected @NotNull Map<@NotNull String, @NotNull String> computeValue(@NotNull Class<?> type) {
|
||||||
|
var out = new HashMap<String, String>();
|
||||||
|
|
||||||
|
for (var component : type.getRecordComponents()) {
|
||||||
|
var column = component.getAnnotation(Column.class);
|
||||||
|
if (column != null) {
|
||||||
|
out.put(component.getName(), column.value());
|
||||||
|
} else {
|
||||||
|
out.put(component.getName(), getDatabaseName(component.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Map.copyOf(out);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static @NotNull String getQuotedTableName(@NotNull Class<? extends Record> type) {
|
||||||
|
return tableNameCache.get(type);
|
||||||
|
}
|
||||||
|
private static final @NotNull ClassValue<@NotNull String> tableNameCache = new ClassValue<>() {
|
||||||
|
@Override
|
||||||
|
protected @NotNull String computeValue(@NotNull Class<?> type) {
|
||||||
|
var table = type.getAnnotation(Table.class);
|
||||||
|
if (table != null) {
|
||||||
|
return quote(table.value());
|
||||||
|
} else {
|
||||||
|
return quote(getDatabaseName(type.getSimpleName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static boolean isIdColumn(@NotNull RecordComponent component) {
|
||||||
|
return Entity.class.isAssignableFrom(component.getDeclaringRecord())
|
||||||
|
&& component.getType() == long.class
|
||||||
|
&& "id".equals(component.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T extends Record & Entity<T>> WithId<T> getWithId(@NotNull Class<T> type) {
|
||||||
|
return (WithId<T>) withIdCache.get(type);
|
||||||
|
}
|
||||||
|
private static final @NotNull ClassValue<@NotNull WithId<?>> withIdCache = new ClassValue<>() {
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected WithId<?> computeValue(Class<?> type) {
|
||||||
|
if (!type.isRecord()) throw new IllegalArgumentException();
|
||||||
|
if (!Entity.class.isAssignableFrom(type)) throw new IllegalArgumentException();
|
||||||
|
|
||||||
|
var lookup = MethodHandles.lookup();
|
||||||
|
|
||||||
|
var components = type.getRecordComponents();
|
||||||
|
var types = new Class<?>[components.length];
|
||||||
|
var accessors = new MethodHandle[components.length];
|
||||||
|
var reorder = new int[components.length];
|
||||||
|
|
||||||
|
for (int i = 0; i < components.length; i++) {
|
||||||
|
var component = components[i];
|
||||||
|
types[i] = component.getType();
|
||||||
|
if (isIdColumn(component)) {
|
||||||
|
accessors[i] = identity(long.class);
|
||||||
|
reorder[i] = 1;
|
||||||
|
} else {
|
||||||
|
accessors[i] = lookup.unreflect(component.getAccessor());
|
||||||
|
reorder[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctor = lookup.findConstructor(type, methodType(void.class, types));
|
||||||
|
var cloner = filterArguments(ctor, 0, accessors);
|
||||||
|
var wither = permuteArguments(cloner, methodType(type, type, long.class), reorder);
|
||||||
|
|
||||||
|
return create(wither.asType(methodType(Record.class, Record.class, long.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends Record & Entity<T>> WithId<T> create(@NotNull MethodHandle handle) {
|
||||||
|
return (entity, id) -> {
|
||||||
|
try {
|
||||||
|
return (T) handle.invokeExact(entity, id);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw Lombok.sneakyThrow(t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private static final @NotNull Logger log = LogManager.getLogger("eu.jonahbauer.chat.bot.database.SQL");
|
||||||
|
public static void logSqlStatement(@NotNull String sql) {
|
||||||
|
log.debug(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String getDatabaseName(@NotNull String name) {
|
||||||
|
if (name.length() <= 1) return name;
|
||||||
|
|
||||||
|
StringBuilder out = null;
|
||||||
|
for (int i = 1, length = name.length(); i < length; i++) {
|
||||||
|
var prev = name.charAt(i - 1);
|
||||||
|
var curr = name.charAt(i);
|
||||||
|
if (Character.isLowerCase(prev) && Character.isUpperCase(curr)) {
|
||||||
|
if (out == null) {
|
||||||
|
out = new StringBuilder(name.length() + 1);
|
||||||
|
out.append(name, 0, i);
|
||||||
|
out.setCharAt(0, Character.toLowerCase(out.charAt(0)));
|
||||||
|
}
|
||||||
|
out.append('_');
|
||||||
|
out.append(Character.toLowerCase(curr));
|
||||||
|
} else if (out != null) {
|
||||||
|
out.append(curr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out == null) {
|
||||||
|
if (Character.isUpperCase(name.charAt(0))) {
|
||||||
|
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
|
||||||
|
} else {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull String quote(@NotNull String name) {
|
||||||
|
var out = new StringBuilder(name.length() + 2);
|
||||||
|
out.append('"');
|
||||||
|
for (int i = 0, length = name.length(); i < length; i++) {
|
||||||
|
var chr = name.charAt(i);
|
||||||
|
if (chr == '"') out.append('"');
|
||||||
|
if (chr == '\n') out.append('\\');
|
||||||
|
|
||||||
|
out.append(chr);
|
||||||
|
}
|
||||||
|
out.append('"');
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public interface Inserter<T extends Record> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T extends Record> @NotNull Inserter<T> get(@NotNull Class<T> type) {
|
||||||
|
return (Inserter<T>) InserterFactory.INSTANCE.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
long insert(@NotNull Connection connection, @NotNull T entity) throws SQLException;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.Entity;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
final class InserterFactory extends AbstractFactory<@NotNull Inserter<?>> {
|
||||||
|
public static final @NotNull InserterFactory INSTANCE = new InserterFactory();
|
||||||
|
|
||||||
|
private InserterFactory() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <T extends Record> @NotNull Inserter<?> computeValue0(@NotNull Class<T> type) {
|
||||||
|
var isEntity = Entity.class.isAssignableFrom(type);
|
||||||
|
var table = Helper.getQuotedTableName(type);
|
||||||
|
var preparer = Preparer.get(type);
|
||||||
|
var columns = preparer.columns();
|
||||||
|
|
||||||
|
var query = STR."""
|
||||||
|
INSERT INTO \{table} (\{String.join(", ", columns)})
|
||||||
|
VALUES (\{String.join(", ", Collections.nCopies(columns.size(), "?"))})
|
||||||
|
\{isEntity ? "RETURNING id" : "RETURNING -1"}
|
||||||
|
""";
|
||||||
|
|
||||||
|
return (Connection connection, T entity) -> {
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var stmt = connection.prepareStatement(query)) {
|
||||||
|
preparer.prepare(stmt, entity);
|
||||||
|
try (var result = stmt.executeQuery()) {
|
||||||
|
if (!result.next()) throw new SQLException();
|
||||||
|
return result.getLong(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface Preparer<T extends Record> {
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T extends Record> @NotNull Preparer<T> get(@NotNull Class<T> type) {
|
||||||
|
return (Preparer<T>) PreparerFactory.INSTANCE.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void prepare(@NotNull PreparedStatement statement, @NotNull T entity) throws SQLException {
|
||||||
|
prepare(statement, entity, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepare(@NotNull PreparedStatement statement, @NotNull T entity, int index) throws SQLException;
|
||||||
|
|
||||||
|
@NotNull List<@NotNull String> columns();
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.impl.types.TypeAdapter;
|
||||||
|
import lombok.Lombok;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class PreparerFactory extends AbstractFactory<Preparer<?>> {
|
||||||
|
public static final @NotNull PreparerFactory INSTANCE = new PreparerFactory();
|
||||||
|
|
||||||
|
private PreparerFactory() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <T extends Record> @NotNull Preparer<T> computeValue0(@NotNull Class<T> type) {
|
||||||
|
var components = type.getRecordComponents();
|
||||||
|
|
||||||
|
var columns = new ArrayList<String>(components.length - 1);
|
||||||
|
var accessors = new ArrayList<Method>(components.length - 1);
|
||||||
|
var adapters = new ArrayList<TypeAdapter<?>>(components.length - 1);
|
||||||
|
|
||||||
|
for (var component : components) {
|
||||||
|
if (Helper.isIdColumn(component)) continue;
|
||||||
|
|
||||||
|
columns.add(Helper.getQuotedColumnName(component));
|
||||||
|
accessors.add(component.getAccessor());
|
||||||
|
adapters.add(TypeAdapter.get(component.getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PreparerImpl<>(columns, adapters, accessors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PreparerImpl<T extends Record> implements Preparer<T> {
|
||||||
|
private final @NotNull List<@NotNull String> columns;
|
||||||
|
private final @NotNull List<@NotNull TypeAdapter<?>> adapters;
|
||||||
|
private final @NotNull List<@NotNull Method> accessors;
|
||||||
|
|
||||||
|
public PreparerImpl(
|
||||||
|
@NotNull List<@NotNull String> columns,
|
||||||
|
@NotNull List<@NotNull TypeAdapter<?>> adapters,
|
||||||
|
@NotNull List<@NotNull Method> accessors
|
||||||
|
) {
|
||||||
|
this.columns = List.copyOf(columns);
|
||||||
|
this.adapters = List.copyOf(adapters);
|
||||||
|
this.accessors = List.copyOf(accessors);
|
||||||
|
if (this.columns.size() != this.adapters.size() || this.adapters.size() != this.accessors.size()) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare(@NotNull PreparedStatement stmt, @NotNull T entity, int offset) throws SQLException {
|
||||||
|
try {
|
||||||
|
for (int i = 0, length = columns.size(); i < length; i++) {
|
||||||
|
var value = accessors.get(i).invoke(entity);
|
||||||
|
adapters.get(i).prepareUnsafe(stmt, i + offset, value);
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw Lombok.sneakyThrow(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<@NotNull String> columns() {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.Entity;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public interface Updater<T extends Record & Entity> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T extends Record & Entity> @NotNull Updater<T> get(@NotNull Class<T> type) {
|
||||||
|
return (Updater<T>) UpdaterFactory.INSTANCE.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean update(@NotNull Connection connection, @NotNull T entity) throws SQLException;
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.Entity;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
final class UpdaterFactory extends AbstractFactory<@NotNull Updater<?>> {
|
||||||
|
public static final @NotNull UpdaterFactory INSTANCE = new UpdaterFactory();
|
||||||
|
|
||||||
|
private UpdaterFactory() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <T extends Record> @NotNull Updater<?> computeValue0(@NotNull Class<T> type) {
|
||||||
|
if (!Entity.class.isAssignableFrom(type)) throw new IllegalArgumentException();
|
||||||
|
return bridge(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T extends Record & Entity> @NotNull Updater<T> bridge(@NotNull Class<?> type) {
|
||||||
|
return computeValue1((Class<T>) type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends Record & Entity> @NotNull Updater<T> computeValue1(@NotNull Class<T> type) {
|
||||||
|
var table = Helper.getQuotedTableName(type);
|
||||||
|
var preparer = Preparer.get(type);
|
||||||
|
var columns = preparer.columns();
|
||||||
|
|
||||||
|
var query = STR."""
|
||||||
|
UPDATE \{table}
|
||||||
|
SET \{columns.stream().map(col -> col + " = ?").collect(Collectors.joining(", "))}
|
||||||
|
WHERE id = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
return (Connection connection, T entity) -> {
|
||||||
|
Helper.logSqlStatement(query);
|
||||||
|
try (var stmt = connection.prepareStatement(query)) {
|
||||||
|
preparer.prepare(stmt, entity);
|
||||||
|
stmt.setLong(columns.size() + 1, entity.id());
|
||||||
|
return stmt.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.Entity;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface WithId<T extends Record & Entity<T>> {
|
||||||
|
T withId(T entity, long id);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl.types;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EnumTypeAdapter<E extends Enum<E>> implements TypeAdapter<E> {
|
||||||
|
private final @NotNull Class<E> type;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare(@NotNull PreparedStatement stmt, int index, E value) throws SQLException {
|
||||||
|
if (value == null) {
|
||||||
|
stmt.setNull(index, Types.VARCHAR);
|
||||||
|
} else {
|
||||||
|
stmt.setString(index, value.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public E extract(@NotNull ResultSet result, int index) throws SQLException {
|
||||||
|
return Enum.valueOf(type, result.getString(index));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl.types;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public class InstantTypeAdapter implements TypeAdapter<Instant> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare(@NotNull PreparedStatement stmt, int index, Instant value) throws SQLException {
|
||||||
|
if (value == null) {
|
||||||
|
stmt.setNull(index, Types.INTEGER);
|
||||||
|
} else {
|
||||||
|
stmt.setLong(index, value.toEpochMilli());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant extract(@NotNull ResultSet result, int index) throws SQLException {
|
||||||
|
var millis = result.getLong(index);
|
||||||
|
if (result.wasNull()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return Instant.ofEpochMilli(millis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl.types;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
|
||||||
|
public class JdbcTypeAdapter<T> implements TypeAdapter<T> {
|
||||||
|
private final @NotNull Class<T> type;
|
||||||
|
|
||||||
|
JdbcTypeAdapter(@NotNull Class<T> type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare(@NotNull PreparedStatement stmt, int index, T value) throws SQLException {
|
||||||
|
if (type == byte.class || type == Byte.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.INTEGER);
|
||||||
|
else stmt.setByte(index, (byte) value);
|
||||||
|
} else if (type == char.class || type == Character.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.INTEGER);
|
||||||
|
else stmt.setShort(index, (short) (char) value);
|
||||||
|
} else if (type == short.class || type == Short.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.INTEGER);
|
||||||
|
else stmt.setShort(index, (short) value);
|
||||||
|
} else if (type == int.class || type == Integer.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.INTEGER);
|
||||||
|
else stmt.setInt(index, (int) value);
|
||||||
|
} else if (type == long.class || type == Long.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.INTEGER);
|
||||||
|
else stmt.setLong(index, (long) value);
|
||||||
|
} else if (type == float.class || type == Float.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.FLOAT);
|
||||||
|
else stmt.setFloat(index, (float) value);
|
||||||
|
} else if (type == double.class || type == Double.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.DOUBLE);
|
||||||
|
else stmt.setDouble(index, (double) value);
|
||||||
|
} else if (type == boolean.class || type == Boolean.class) {
|
||||||
|
if (value == null) stmt.setNull(index, Types.BOOLEAN);
|
||||||
|
else stmt.setBoolean(index, (boolean) value);
|
||||||
|
} else {
|
||||||
|
stmt.setObject(index, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public T extract(@NotNull ResultSet result, int index) throws SQLException {
|
||||||
|
if (type == byte.class || type == Byte.class) {
|
||||||
|
var out = (T) (Byte) result.getByte(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == char.class || type == Character.class) {
|
||||||
|
var out = (T) (Character) (char) result.getShort(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == short.class || type == Short.class) {
|
||||||
|
var out = (T) (Short) result.getShort(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == int.class || type == Integer.class) {
|
||||||
|
var out = (T) (Integer) result.getInt(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == long.class || type == Long.class) {
|
||||||
|
var out = (T) (Long) result.getLong(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == float.class || type == Float.class) {
|
||||||
|
var out = (T) (Float) result.getFloat(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == double.class || type == Double.class) {
|
||||||
|
var out = (T) (Double) result.getDouble(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else if (type == boolean.class || type == Boolean.class) {
|
||||||
|
var out = (T) (Boolean) result.getBoolean(index);
|
||||||
|
return result.wasNull() ? null : out;
|
||||||
|
} else {
|
||||||
|
return result.getObject(index, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database.impl.types;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public interface TypeAdapter<T> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T> @NotNull TypeAdapter<T> get(@NotNull Class<T> type) {
|
||||||
|
if (type == Instant.class) {
|
||||||
|
return (TypeAdapter<T>) new InstantTypeAdapter();
|
||||||
|
} else if (Enum.class.isAssignableFrom(type)) {
|
||||||
|
return (TypeAdapter<T>) newEnumTypeAdapter(type);
|
||||||
|
} else {
|
||||||
|
return new JdbcTypeAdapter<>(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <E extends Enum<E>> EnumTypeAdapter<?> newEnumTypeAdapter(Class<?> type) {
|
||||||
|
return new EnumTypeAdapter<>((Class<E>) type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
default void prepareUnsafe(@NotNull PreparedStatement stmt, int index, Object value) throws SQLException {
|
||||||
|
prepare(stmt, index, (T) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepare(@NotNull PreparedStatement stmt, int index, T value) throws SQLException;
|
||||||
|
|
||||||
|
T extract(@NotNull ResultSet result, int index) throws SQLException;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
module eu.jonahbauer.chat.bot.database {
|
||||||
|
exports eu.jonahbauer.chat.bot.database;
|
||||||
|
exports eu.jonahbauer.chat.bot.database.annotations;
|
||||||
|
|
||||||
|
requires transitive java.sql;
|
||||||
|
requires org.xerial.sqlitejdbc;
|
||||||
|
requires com.zaxxer.hikari;
|
||||||
|
requires org.apache.logging.log4j;
|
||||||
|
|
||||||
|
requires static lombok;
|
||||||
|
requires static transitive org.jetbrains.annotations;
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database;
|
||||||
|
|
||||||
|
import eu.jonahbauer.chat.bot.database.annotations.Column;
|
||||||
|
import eu.jonahbauer.chat.bot.database.annotations.Table;
|
||||||
|
import org.intellij.lang.annotations.Language;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class DatabaseTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
static Path temp;
|
||||||
|
|
||||||
|
Database database;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init() throws IOException, SQLException {
|
||||||
|
var file = Files.createTempFile(temp, "database", ".db");
|
||||||
|
var url = "jdbc:sqlite:" + file.toAbsolutePath();
|
||||||
|
database = new Database(url);
|
||||||
|
database.executeUpdate("""
|
||||||
|
CREATE TABLE "pizza"
|
||||||
|
(
|
||||||
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"restaurant_id" INTEGER NOT NULL,
|
||||||
|
"number" INTEGER NOT NULL,
|
||||||
|
"name" VARCHAR(255) NOT NULL,
|
||||||
|
"ingredients" VARCHAR(255) NOT NULL,
|
||||||
|
"price" DOUBLE NOT NULL,
|
||||||
|
"test" INTEGER
|
||||||
|
)
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void crud() throws SQLException {
|
||||||
|
crud(Pizza.class, Pizza::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void annotations() throws SQLException {
|
||||||
|
crud(AnnotatedPizza.class, AnnotatedPizza::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void transaction() throws SQLException {
|
||||||
|
assertThrowsExactly(RuntimeException.class, () -> {
|
||||||
|
database.transaction(() -> {
|
||||||
|
assertThrows(IllegalStateException.class, () -> database.transaction(() -> {}));
|
||||||
|
|
||||||
|
database.insert(new Pizza(-1, 1, 10, "Salami", "Salami, Tomaten, Brot", 12.5));
|
||||||
|
assertEquals(1, database.findAll(Pizza.class).size());
|
||||||
|
throw new RuntimeException();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assertEquals(List.of(), database.findAll(Pizza.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void find() throws SQLException {
|
||||||
|
var salami = new Pizza(1, 1, 10, "Salami", "Salami, Tomaten, Brot", 12.5);
|
||||||
|
var speziale = new Pizza(2, 1, 15, "Speziale", "Salami, Schinken, Pilze, Tomaten, Brot", 12.5);
|
||||||
|
|
||||||
|
assertEquals(1, database.insert(salami));
|
||||||
|
assertEquals(2, database.insert(speziale));
|
||||||
|
|
||||||
|
assertEquals(Optional.empty(), database.findWhere(Pizza.class, "name", "Hawaii"));
|
||||||
|
assertEquals(Optional.of(salami), database.findWhere(Pizza.class, "name", "Salami"));
|
||||||
|
assertEquals(Optional.of(speziale), database.findWhere(Pizza.class, "name", "Speziale"));
|
||||||
|
assertThrows(NonUniqueResultException.class, () -> database.findWhere(Pizza.class, "restaurantId", 1L));
|
||||||
|
assertEquals(List.of(salami, speziale), database.findAllWhere(Pizza.class, "restaurantId", 1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void join() throws SQLException {
|
||||||
|
var salami = new Pizza(1, 1, 10, "Salami", "Salami, Tomaten, Brot", 12.5);
|
||||||
|
var speziale = new Pizza(2, 1, 15, "Speziale", "Salami, Schinken, Pilze, Tomaten, Brot", 12.5);
|
||||||
|
|
||||||
|
assertEquals(1, database.insert(salami));
|
||||||
|
assertEquals(2, database.insert(speziale));
|
||||||
|
|
||||||
|
@Language("SQL")
|
||||||
|
var query = """
|
||||||
|
SELECT "pizza".*
|
||||||
|
FROM "pizza"
|
||||||
|
JOIN "pizza" "p2" ON "pizza"."restaurant_id" = "p2"."restaurant_id"
|
||||||
|
""";
|
||||||
|
assertEquals(List.of(salami, salami, speziale, speziale), database.executeQuery(Pizza.class, query));
|
||||||
|
|
||||||
|
@Language("SQL")
|
||||||
|
var query2 = """
|
||||||
|
SELECT "p2".*
|
||||||
|
FROM "pizza"
|
||||||
|
JOIN "pizza" "p2" ON "pizza"."restaurant_id" = "p2"."restaurant_id"
|
||||||
|
""";
|
||||||
|
assertEquals(List.of(salami, speziale, salami, speziale), database.executeQuery(Pizza.class, query2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends Record & Entity<T>> void crud(@NotNull Class<T> type, @NotNull PizzaBaker<T> baker) throws SQLException {
|
||||||
|
var salami = baker.bake(-1, 1, 10, "Salami", "Salami, Tomaten, Brot", 12.5);
|
||||||
|
var persistent = baker.bake(1, 1, 10, "Salami", "Salami, Tomaten, Brot", 12.5);
|
||||||
|
var speziale = baker.bake(1, 1, 15, "Speziale", "Salami, Schinken, Pilze, Tomaten, Brot", 12.5);
|
||||||
|
|
||||||
|
// create
|
||||||
|
assertEquals(1, database.insert(salami));
|
||||||
|
assertEquals(List.of(persistent), database.findAll(type));
|
||||||
|
assertEquals(Optional.of(persistent), database.findById(type, 1));
|
||||||
|
assertEquals(Optional.empty(), database.findById(type, 2));
|
||||||
|
|
||||||
|
// update
|
||||||
|
assertFalse(database.update(salami));
|
||||||
|
assertTrue(database.update(speziale));
|
||||||
|
assertEquals(List.of(speziale), database.findAll(type));
|
||||||
|
assertEquals(Optional.of(speziale), database.findById(type, 1));
|
||||||
|
assertEquals(Optional.empty(), database.findById(type, 2));
|
||||||
|
|
||||||
|
// delete
|
||||||
|
assertFalse(database.delete(type, 2));
|
||||||
|
assertTrue(database.delete(type, 1));
|
||||||
|
assertEquals(List.of(), database.findAll(type));
|
||||||
|
|
||||||
|
// truncate
|
||||||
|
database.insert(salami);
|
||||||
|
database.insert(speziale);
|
||||||
|
assertEquals(2, database.findAll(type).size());
|
||||||
|
assertEquals(2, database.truncate(type));
|
||||||
|
assertEquals(List.of(), database.findAll(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanup() {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface PizzaBaker<T extends Record & Entity<T>> {
|
||||||
|
@NotNull T bake(long id, long restaurantId, int number, @NotNull String name, @NotNull String ingredients, double price);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Pizza(long id, long restaurantId, int number, @NotNull String name, @NotNull String ingredients, double price) implements Entity<Pizza> {}
|
||||||
|
|
||||||
|
@Table("pizza")
|
||||||
|
public record AnnotatedPizza(long id, @Column("restaurant_id") long restaurant, int number, @NotNull String name, @NotNull String ingredients, double price) implements Entity<AnnotatedPizza> {}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.jonahbauer.chat.bot.database;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
public class EntityTest {
|
||||||
|
|
||||||
|
public record TestEntity(long id, String foo, String bar) implements Entity<TestEntity> {}
|
||||||
|
public record TestEntity2(String foo, String bar, long id) implements Entity<TestEntity2> {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withId() {
|
||||||
|
assertEquals(new TestEntity(1, "foo", "bar"), new TestEntity(0, "foo", "bar").withId(1));
|
||||||
|
assertEquals(new TestEntity2("foo", "bar", 1), new TestEntity2("foo", "bar", 0).withId(1));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue