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