first commit
This commit is contained in:
62
src/main/java/com/andrewkydev/Loader.java
Normal file
62
src/main/java/com/andrewkydev/Loader.java
Normal file
@@ -0,0 +1,62 @@
|
||||
package com.andrewkydev;
|
||||
|
||||
import cn.nukkit.plugin.PluginBase;
|
||||
import com.andrewkydev.database.DatabaseApi;
|
||||
import com.andrewkydev.database.DatabaseProvider;
|
||||
import com.andrewkydev.database.config.DatabaseConfig;
|
||||
import com.andrewkydev.database.config.DatabaseConfigLoader;
|
||||
import com.andrewkydev.database.internal.DatabaseImpl;
|
||||
import com.andrewkydev.database.internal.JdbcUrlBuilder;
|
||||
import com.andrewkydev.database.schema.SqlDialect;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
|
||||
public class Loader extends PluginBase {
|
||||
private DatabaseApi databaseApi;
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
saveDefaultConfig();
|
||||
reloadConfig();
|
||||
|
||||
DatabaseConfig config = DatabaseConfigLoader.load(this);
|
||||
HikariConfig hikariConfig = new HikariConfig();
|
||||
hikariConfig.setJdbcUrl(JdbcUrlBuilder.build(config.dialect(), config.host(), config.port(), config.database()));
|
||||
hikariConfig.setUsername(config.username());
|
||||
hikariConfig.setPassword(config.password());
|
||||
hikariConfig.setDriverClassName(driverClassName(config.dialect()));
|
||||
hikariConfig.setMaximumPoolSize(config.pool().maxPoolSize());
|
||||
hikariConfig.setMinimumIdle(config.pool().minIdle());
|
||||
hikariConfig.setConnectionTimeout(config.pool().connectionTimeoutMs());
|
||||
hikariConfig.setIdleTimeout(config.pool().idleTimeoutMs());
|
||||
hikariConfig.setMaxLifetime(config.pool().maxLifetimeMs());
|
||||
|
||||
dataSource = new HikariDataSource(hikariConfig);
|
||||
databaseApi = new DatabaseImpl(dataSource, config);
|
||||
DatabaseProvider.set(databaseApi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
super.onDisable();
|
||||
if (databaseApi != null) {
|
||||
databaseApi.close();
|
||||
databaseApi = null;
|
||||
}
|
||||
dataSource = null;
|
||||
DatabaseProvider.set(null);
|
||||
}
|
||||
|
||||
private String driverClassName(SqlDialect dialect) {
|
||||
return dialect == SqlDialect.POSTGRESQL
|
||||
? "org.postgresql.Driver"
|
||||
: "com.mysql.cj.jdbc.Driver";
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/andrewkydev/database/DatabaseApi.java
Normal file
23
src/main/java/com/andrewkydev/database/DatabaseApi.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.andrewkydev.database;
|
||||
|
||||
import com.andrewkydev.database.query.QueryRunner;
|
||||
import com.andrewkydev.database.query.Transaction;
|
||||
import com.andrewkydev.database.schema.Schema;
|
||||
import com.andrewkydev.database.orm.EntityManager;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface DatabaseApi extends AutoCloseable {
|
||||
|
||||
Schema schema();
|
||||
|
||||
QueryRunner query();
|
||||
|
||||
EntityManager orm();
|
||||
|
||||
Transaction beginTransaction();
|
||||
|
||||
CompletableFuture<Transaction> beginTransactionAsync();
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
20
src/main/java/com/andrewkydev/database/DatabaseProvider.java
Normal file
20
src/main/java/com/andrewkydev/database/DatabaseProvider.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.andrewkydev.database;
|
||||
|
||||
public final class DatabaseProvider {
|
||||
private static volatile DatabaseApi api;
|
||||
|
||||
private DatabaseProvider() {
|
||||
}
|
||||
|
||||
public static void set(DatabaseApi api) {
|
||||
DatabaseProvider.api = api;
|
||||
}
|
||||
|
||||
public static DatabaseApi get() {
|
||||
DatabaseApi current = api;
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("DatabaseApi is not initialized");
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.andrewkydev.database.config;
|
||||
|
||||
import com.andrewkydev.database.schema.SqlDialect;
|
||||
|
||||
public final class DatabaseConfig {
|
||||
private final SqlDialect dialect;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String database;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String adminDatabase;
|
||||
private final PoolConfig pool;
|
||||
private final boolean autoTransactions;
|
||||
|
||||
public DatabaseConfig(
|
||||
SqlDialect dialect,
|
||||
String host,
|
||||
int port,
|
||||
String database,
|
||||
String username,
|
||||
String password,
|
||||
String adminDatabase,
|
||||
PoolConfig pool,
|
||||
boolean autoTransactions
|
||||
) {
|
||||
this.dialect = dialect;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.database = database;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.adminDatabase = adminDatabase;
|
||||
this.pool = pool;
|
||||
this.autoTransactions = autoTransactions;
|
||||
}
|
||||
|
||||
public SqlDialect dialect() {
|
||||
return dialect;
|
||||
}
|
||||
|
||||
public String host() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int port() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String database() {
|
||||
return database;
|
||||
}
|
||||
|
||||
public String username() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String password() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String adminDatabase() {
|
||||
return adminDatabase;
|
||||
}
|
||||
|
||||
public PoolConfig pool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public boolean autoTransactions() {
|
||||
return autoTransactions;
|
||||
}
|
||||
|
||||
public static final class PoolConfig {
|
||||
private final int maxPoolSize;
|
||||
private final int minIdle;
|
||||
private final long connectionTimeoutMs;
|
||||
private final long idleTimeoutMs;
|
||||
private final long maxLifetimeMs;
|
||||
|
||||
public PoolConfig(
|
||||
int maxPoolSize,
|
||||
int minIdle,
|
||||
long connectionTimeoutMs,
|
||||
long idleTimeoutMs,
|
||||
long maxLifetimeMs
|
||||
) {
|
||||
this.maxPoolSize = maxPoolSize;
|
||||
this.minIdle = minIdle;
|
||||
this.connectionTimeoutMs = connectionTimeoutMs;
|
||||
this.idleTimeoutMs = idleTimeoutMs;
|
||||
this.maxLifetimeMs = maxLifetimeMs;
|
||||
}
|
||||
|
||||
public int maxPoolSize() {
|
||||
return maxPoolSize;
|
||||
}
|
||||
|
||||
public int minIdle() {
|
||||
return minIdle;
|
||||
}
|
||||
|
||||
public long connectionTimeoutMs() {
|
||||
return connectionTimeoutMs;
|
||||
}
|
||||
|
||||
public long idleTimeoutMs() {
|
||||
return idleTimeoutMs;
|
||||
}
|
||||
|
||||
public long maxLifetimeMs() {
|
||||
return maxLifetimeMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.andrewkydev.database.config;
|
||||
|
||||
import cn.nukkit.plugin.PluginBase;
|
||||
import cn.nukkit.utils.Config;
|
||||
import cn.nukkit.utils.ConfigSection;
|
||||
import com.andrewkydev.database.schema.SqlDialect;
|
||||
|
||||
public final class DatabaseConfigLoader {
|
||||
|
||||
private DatabaseConfigLoader() {
|
||||
}
|
||||
|
||||
public static DatabaseConfig load(PluginBase plugin) {
|
||||
Config config = plugin.getConfig();
|
||||
|
||||
String dialectValue = config.getString("driver", "mysql");
|
||||
SqlDialect dialect = "postgres".equalsIgnoreCase(dialectValue)
|
||||
? SqlDialect.POSTGRESQL
|
||||
: SqlDialect.MYSQL;
|
||||
|
||||
String host = config.getString("host", "localhost");
|
||||
int port = config.getInt("port", dialect == SqlDialect.POSTGRESQL ? 5432 : 3306);
|
||||
String database = config.getString("database", "primalix");
|
||||
String username = config.getString("username", "root");
|
||||
String password = config.getString("password", "");
|
||||
String adminDatabase = config.getString("adminDatabase", "postgres");
|
||||
boolean autoTransactions = config.getBoolean("autoTransactions", true);
|
||||
|
||||
ConfigSection poolConfig = config.getSection("pool");
|
||||
int maxPoolSize = poolConfig.getInt("maxPoolSize", 10);
|
||||
int minIdle = poolConfig.getInt("minIdle", 2);
|
||||
long connectionTimeoutMs = poolConfig.getLong("connectionTimeoutMs", 30_000);
|
||||
long idleTimeoutMs = poolConfig.getLong("idleTimeoutMs", 600_000);
|
||||
long maxLifetimeMs = poolConfig.getLong("maxLifetimeMs", 1_800_000);
|
||||
|
||||
DatabaseConfig.PoolConfig pool = new DatabaseConfig.PoolConfig(
|
||||
maxPoolSize,
|
||||
minIdle,
|
||||
connectionTimeoutMs,
|
||||
idleTimeoutMs,
|
||||
maxLifetimeMs
|
||||
);
|
||||
|
||||
return new DatabaseConfig(
|
||||
dialect,
|
||||
host,
|
||||
port,
|
||||
database,
|
||||
username,
|
||||
password,
|
||||
adminDatabase,
|
||||
pool,
|
||||
autoTransactions
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.DatabaseApi;
|
||||
import com.andrewkydev.database.config.DatabaseConfig;
|
||||
import com.andrewkydev.database.query.QueryRunner;
|
||||
import com.andrewkydev.database.query.Transaction;
|
||||
import com.andrewkydev.database.schema.Schema;
|
||||
import com.andrewkydev.database.orm.EntityManager;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public final class DatabaseImpl implements DatabaseApi {
|
||||
private final DataSource dataSource;
|
||||
private final DatabaseConfig config;
|
||||
private final ExecutorService executor;
|
||||
private final Schema schema;
|
||||
private final QueryRunner queryRunner;
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public DatabaseImpl(DataSource dataSource, DatabaseConfig config) {
|
||||
this.dataSource = dataSource;
|
||||
this.config = config;
|
||||
this.executor = Executors.newFixedThreadPool(Math.max(2, config.pool().maxPoolSize() / 2));
|
||||
this.schema = new SchemaImpl(dataSource, config, executor);
|
||||
this.queryRunner = new JdbcQueryRunner(dataSource, executor);
|
||||
this.entityManager = new EntityManagerImpl(dataSource, executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Schema schema() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryRunner query() {
|
||||
return queryRunner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityManager orm() {
|
||||
return entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction beginTransaction() {
|
||||
try {
|
||||
Connection connection = dataSource.getConnection();
|
||||
connection.setAutoCommit(false);
|
||||
return new JdbcTransaction(connection, executor);
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to start transaction", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Transaction> beginTransactionAsync() {
|
||||
return CompletableFuture.supplyAsync(this::beginTransaction, executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
executor.shutdown();
|
||||
if (dataSource instanceof AutoCloseable) {
|
||||
try {
|
||||
((AutoCloseable) dataSource).close();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to close datasource", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.orm.EntityManager;
|
||||
import com.andrewkydev.database.orm.EntityQuery;
|
||||
import com.andrewkydev.database.orm.TypeAdapter;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.UUID;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public final class EntityManagerImpl implements EntityManager {
|
||||
private final DataSource dataSource;
|
||||
private final Executor executor;
|
||||
private final Map<Class<?>, EntityMetadata> metadataCache = new ConcurrentHashMap<>();
|
||||
private final Map<Class<?>, TypeAdapter<?>> adapters = new ConcurrentHashMap<>();
|
||||
private final Gson gson = new GsonBuilder().create();
|
||||
|
||||
public EntityManagerImpl(DataSource dataSource, Executor executor) {
|
||||
this.dataSource = dataSource;
|
||||
this.executor = executor;
|
||||
registerDefaults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void insert(T entity) {
|
||||
EntityMetadata metadata = metadata(entity.getClass());
|
||||
List<EntityMetadata.FieldMapping> columns = new ArrayList<>();
|
||||
List<Object> values = new ArrayList<>();
|
||||
EntityMetadata.FieldMapping idField = metadata.idFieldOrNull();
|
||||
boolean generateId = false;
|
||||
|
||||
for (EntityMetadata.FieldMapping mapping : metadata.fields()) {
|
||||
Object value = getValue(mapping, entity);
|
||||
if (mapping.isId() && mapping.autoIncrement() && !hasExplicitValue(value)) {
|
||||
generateId = true;
|
||||
continue;
|
||||
}
|
||||
columns.add(mapping);
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("INSERT INTO ").append(metadata.table()).append(" (");
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
if (i > 0) {
|
||||
sql.append(", ");
|
||||
}
|
||||
sql.append(columns.get(i).column());
|
||||
}
|
||||
sql.append(") VALUES (");
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
if (i > 0) {
|
||||
sql.append(", ");
|
||||
}
|
||||
sql.append("?");
|
||||
}
|
||||
sql.append(")");
|
||||
|
||||
if (generateId && idField != null) {
|
||||
Object generatedId = executeInsert(sql.toString(), values);
|
||||
if (generatedId != null) {
|
||||
setValue(idField, entity, generatedId);
|
||||
}
|
||||
} else {
|
||||
executeUpdate(sql.toString(), values);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void update(T entity) {
|
||||
EntityMetadata metadata = metadata(entity.getClass());
|
||||
EntityMetadata.FieldMapping idField = metadata.idField();
|
||||
Object idValue = getValue(idField, entity);
|
||||
if (!hasExplicitValue(idValue)) {
|
||||
throw new IllegalStateException("Entity id is required for update");
|
||||
}
|
||||
|
||||
List<Object> params = new ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("UPDATE ").append(metadata.table()).append(" SET ");
|
||||
|
||||
boolean first = true;
|
||||
for (EntityMetadata.FieldMapping mapping : metadata.fields()) {
|
||||
if (mapping.isId()) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
sql.append(", ");
|
||||
}
|
||||
first = false;
|
||||
sql.append(mapping.column()).append("=?");
|
||||
params.add(getValue(mapping, entity));
|
||||
}
|
||||
sql.append(" WHERE ").append(idField.column()).append("=?");
|
||||
params.add(idValue);
|
||||
|
||||
executeUpdate(sql.toString(), params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void delete(T entity) {
|
||||
EntityMetadata metadata = metadata(entity.getClass());
|
||||
EntityMetadata.FieldMapping idField = metadata.idField();
|
||||
Object idValue = getValue(idField, entity);
|
||||
if (!hasExplicitValue(idValue)) {
|
||||
throw new IllegalStateException("Entity id is required for delete");
|
||||
}
|
||||
String sql = "DELETE FROM " + metadata.table() + " WHERE " + idField.column() + "=?";
|
||||
executeUpdate(sql, Collections.singletonList(idValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findById(Class<T> type, Object id) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "SELECT * FROM " + metadata.table() + " WHERE " + metadata.idField().column() + "=?";
|
||||
List<T> result = query(sql, Collections.singletonList(id), type);
|
||||
return result.isEmpty() ? null : result.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findAll(Class<T> type) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "SELECT * FROM " + metadata.table();
|
||||
return query(sql, Collections.emptyList(), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findOneWhere(Class<T> type, String where, List<Object> params) {
|
||||
List<T> results = findWhere(type, where, params, null, 1, null);
|
||||
return results.isEmpty() ? null : results.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findWhere(Class<T> type, String where, List<Object> params) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "SELECT * FROM " + metadata.table() + whereClause(where);
|
||||
return query(sql, normalizeParams(params), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> long count(Class<T> type) {
|
||||
return count(type, "", Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> long count(Class<T> type, String where, List<Object> params) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "SELECT COUNT(*) FROM " + metadata.table() + whereClause(where);
|
||||
return queryCount(sql, normalizeParams(params));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> boolean exists(Class<T> type, String where, List<Object> params) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "SELECT 1 FROM " + metadata.table() + whereClause(where) + " LIMIT 1";
|
||||
List<Integer> result = queryScalar(sql, normalizeParams(params));
|
||||
return !result.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> int deleteWhere(Class<T> type, String where, List<Object> params) {
|
||||
if (where == null || where.trim().isEmpty()) {
|
||||
throw new IllegalStateException("deleteWhere requires a WHERE clause");
|
||||
}
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "DELETE FROM " + metadata.table() + whereClause(where);
|
||||
return executeUpdate(sql, normalizeParams(params));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> findWhere(
|
||||
Class<T> type,
|
||||
String where,
|
||||
List<Object> params,
|
||||
String orderBy,
|
||||
Integer limit,
|
||||
Integer offset
|
||||
) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
String sql = "SELECT * FROM " + metadata.table()
|
||||
+ whereClause(where)
|
||||
+ orderByClause(orderBy)
|
||||
+ limitClause(limit, offset);
|
||||
return query(sql, normalizeParams(params), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> findWhereAsync(
|
||||
Class<T> type,
|
||||
String where,
|
||||
List<Object> params,
|
||||
String orderBy,
|
||||
Integer limit,
|
||||
Integer offset
|
||||
) {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> findWhere(type, where, params, orderBy, limit, offset),
|
||||
executor
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void registerAdapter(Class<T> type, TypeAdapter<T> adapter) {
|
||||
adapters.put(type, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EntityQuery<T> query(Class<T> type) {
|
||||
return new EntityQueryImpl<>(this, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Void> insertAsync(T entity) {
|
||||
return CompletableFuture.runAsync(() -> insert(entity), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Void> updateAsync(T entity) {
|
||||
return CompletableFuture.runAsync(() -> update(entity), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Void> deleteAsync(T entity) {
|
||||
return CompletableFuture.runAsync(() -> delete(entity), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<T> findByIdAsync(Class<T> type, Object id) {
|
||||
return CompletableFuture.supplyAsync(() -> findById(type, id), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> findAllAsync(Class<T> type) {
|
||||
return CompletableFuture.supplyAsync(() -> findAll(type), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<T> findOneWhereAsync(Class<T> type, String where, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> findOneWhere(type, where, params), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> findWhereAsync(Class<T> type, String where, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> findWhere(type, where, params), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Long> countAsync(Class<T> type) {
|
||||
return CompletableFuture.supplyAsync(() -> count(type), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Long> countAsync(Class<T> type, String where, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> count(type, where, params), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Boolean> existsAsync(Class<T> type, String where, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> exists(type, where, params), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<Integer> deleteWhereAsync(Class<T> type, String where, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> deleteWhere(type, where, params), executor);
|
||||
}
|
||||
|
||||
private EntityMetadata metadata(Class<?> type) {
|
||||
return metadataCache.computeIfAbsent(type, EntityMetadata::resolve);
|
||||
}
|
||||
|
||||
private <T> List<T> query(String sql, List<Object> params, Class<T> type) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
Set<String> columns = resolveColumns(resultSet);
|
||||
List<T> results = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
results.add(mapRow(resultSet, type, columns));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to query SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
<T> List<T> queryCustom(String sql, List<Object> params, Class<T> type) {
|
||||
return query(sql, normalizeParams(params), type);
|
||||
}
|
||||
|
||||
long queryCountCustom(String sql, List<Object> params) {
|
||||
return queryCount(sql, normalizeParams(params));
|
||||
}
|
||||
|
||||
boolean queryExistsCustom(String sql, List<Object> params) {
|
||||
List<Integer> result = queryScalar(sql, normalizeParams(params));
|
||||
return !result.isEmpty();
|
||||
}
|
||||
|
||||
Executor executor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
String tableFor(Class<?> type) {
|
||||
return metadata(type).table();
|
||||
}
|
||||
|
||||
private long queryCount(String sql, List<Object> params) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
return resultSet.next() ? resultSet.getLong(1) : 0L;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to query SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> queryScalar(String sql, List<Object> params) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<Integer> results = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
results.add(resultSet.getInt(1));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to query SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private int executeUpdate(String sql, List<Object> params) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Object executeInsert(String sql, List<Object> params) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
statement.executeUpdate();
|
||||
try (ResultSet resultSet = statement.getGeneratedKeys()) {
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getObject(1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String whereClause(String where) {
|
||||
if (where == null || where.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return " WHERE " + where;
|
||||
}
|
||||
|
||||
private String orderByClause(String orderBy) {
|
||||
if (orderBy == null || orderBy.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return " ORDER BY " + orderBy;
|
||||
}
|
||||
|
||||
private String limitClause(Integer limit, Integer offset) {
|
||||
if (limit == null) {
|
||||
return "";
|
||||
}
|
||||
if (offset == null) {
|
||||
return " LIMIT " + limit;
|
||||
}
|
||||
return " LIMIT " + limit + " OFFSET " + offset;
|
||||
}
|
||||
|
||||
private List<Object> normalizeParams(List<Object> params) {
|
||||
if (params == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private boolean hasExplicitValue(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue() != 0L;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private <T> Object getValue(EntityMetadata.FieldMapping mapping, T entity) {
|
||||
try {
|
||||
Object value = mapping.field().get(entity);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (mapping.json()) {
|
||||
return gson.toJson(value, mapping.field().getGenericType());
|
||||
}
|
||||
TypeAdapter<Object> adapter = adapterFor(mapping.field().getType());
|
||||
if (adapter != null) {
|
||||
return adapter.toDatabase(value);
|
||||
}
|
||||
return value;
|
||||
} catch (IllegalAccessException ex) {
|
||||
throw new IllegalStateException("Failed to access field " + mapping.field().getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void setValue(EntityMetadata.FieldMapping mapping, T entity, Object value) {
|
||||
try {
|
||||
Class<?> targetType = mapping.field().getType();
|
||||
Object coerced;
|
||||
if (mapping.json()) {
|
||||
coerced = gson.fromJson(value == null ? "null" : value.toString(), mapping.field().getGenericType());
|
||||
} else {
|
||||
TypeAdapter<Object> adapter = adapterFor(targetType);
|
||||
coerced = adapter == null ? coerceValue(targetType, value) : adapter.fromDatabase(value);
|
||||
}
|
||||
mapping.field().set(entity, coerced);
|
||||
} catch (IllegalAccessException ex) {
|
||||
throw new IllegalStateException("Failed to set field " + mapping.field().getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Object coerceValue(Class<?> targetType, Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (targetType.isAssignableFrom(value.getClass())) {
|
||||
return value;
|
||||
}
|
||||
if (targetType == long.class || targetType == Long.class) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
if (targetType == int.class || targetType == Integer.class) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
if (targetType == short.class || targetType == Short.class) {
|
||||
return ((Number) value).shortValue();
|
||||
}
|
||||
if (targetType == String.class) {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private TypeAdapter<Object> adapterFor(Class<?> type) {
|
||||
return (TypeAdapter<Object>) adapters.get(type);
|
||||
}
|
||||
|
||||
private <T> T mapRow(ResultSet resultSet, Class<T> type, Set<String> columns) {
|
||||
EntityMetadata metadata = metadata(type);
|
||||
T instance = instantiate(type);
|
||||
for (EntityMetadata.FieldMapping mapping : metadata.fields()) {
|
||||
if (!columns.contains(mapping.column().toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Object value = resultSet.getObject(mapping.column());
|
||||
if (value == null && mapping.field().getType().isPrimitive()) {
|
||||
continue;
|
||||
}
|
||||
setValue(mapping, instance, value);
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to read column " + mapping.column(), ex);
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private Set<String> resolveColumns(ResultSet resultSet) throws SQLException {
|
||||
Set<String> columns = new HashSet<>();
|
||||
int count = resultSet.getMetaData().getColumnCount();
|
||||
for (int i = 1; i <= count; i++) {
|
||||
String label = resultSet.getMetaData().getColumnLabel(i);
|
||||
if (label != null) {
|
||||
columns.add(label.toLowerCase());
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private <T> T instantiate(Class<T> type) {
|
||||
try {
|
||||
Constructor<T> constructor = type.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Entity must have a no-arg constructor: " + type.getName(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerDefaults() {
|
||||
registerAdapter(UUID.class, new TypeAdapter<UUID>() {
|
||||
@Override
|
||||
public Object toDatabase(UUID value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID fromDatabase(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return UUID.fromString(value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.orm.DbColumn;
|
||||
import com.andrewkydev.database.orm.DbEntity;
|
||||
import com.andrewkydev.database.orm.DbId;
|
||||
import com.andrewkydev.database.orm.DbJson;
|
||||
import com.andrewkydev.database.orm.DbTransient;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class EntityMetadata {
|
||||
private final String table;
|
||||
private final List<FieldMapping> fields;
|
||||
private final FieldMapping idField;
|
||||
|
||||
private EntityMetadata(String table, List<FieldMapping> fields, FieldMapping idField) {
|
||||
this.table = table;
|
||||
this.fields = fields;
|
||||
this.idField = idField;
|
||||
}
|
||||
|
||||
static EntityMetadata resolve(Class<?> type) {
|
||||
String table = resolveTable(type);
|
||||
List<FieldMapping> fields = new ArrayList<>();
|
||||
FieldMapping idField = null;
|
||||
|
||||
Class<?> current = type;
|
||||
while (current != null && current != Object.class) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
if (field.isAnnotationPresent(DbTransient.class)) {
|
||||
continue;
|
||||
}
|
||||
String columnName = resolveColumnName(field);
|
||||
DbId id = field.getAnnotation(DbId.class);
|
||||
DbJson json = field.getAnnotation(DbJson.class);
|
||||
boolean isId = id != null;
|
||||
boolean autoIncrement = id != null && id.autoIncrement();
|
||||
boolean jsonField = json != null;
|
||||
FieldMapping mapping = new FieldMapping(field, columnName, isId, autoIncrement, jsonField);
|
||||
fields.add(mapping);
|
||||
if (isId) {
|
||||
if (idField != null) {
|
||||
throw new IllegalStateException("Multiple @DbId fields found for " + type.getName());
|
||||
}
|
||||
idField = mapping;
|
||||
}
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
if (fields.isEmpty()) {
|
||||
throw new IllegalStateException("No mappable fields found for " + type.getName());
|
||||
}
|
||||
return new EntityMetadata(table, fields, idField);
|
||||
}
|
||||
|
||||
String table() {
|
||||
return table;
|
||||
}
|
||||
|
||||
List<FieldMapping> fields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
FieldMapping idField() {
|
||||
if (idField == null) {
|
||||
throw new IllegalStateException("No @DbId field defined for entity table " + table);
|
||||
}
|
||||
return idField;
|
||||
}
|
||||
|
||||
FieldMapping idFieldOrNull() {
|
||||
return idField;
|
||||
}
|
||||
|
||||
private static String resolveTable(Class<?> type) {
|
||||
DbEntity entity = type.getAnnotation(DbEntity.class);
|
||||
if (entity != null && !entity.table().isEmpty()) {
|
||||
return entity.table();
|
||||
}
|
||||
return toSnakeCase(type.getSimpleName());
|
||||
}
|
||||
|
||||
private static String resolveColumnName(Field field) {
|
||||
DbColumn column = field.getAnnotation(DbColumn.class);
|
||||
if (column != null && !column.name().isEmpty()) {
|
||||
return column.name();
|
||||
}
|
||||
return toSnakeCase(field.getName());
|
||||
}
|
||||
|
||||
private static String toSnakeCase(String value) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) {
|
||||
builder.append('_');
|
||||
}
|
||||
builder.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
static final class FieldMapping {
|
||||
private final Field field;
|
||||
private final String column;
|
||||
private final boolean id;
|
||||
private final boolean autoIncrement;
|
||||
private final boolean json;
|
||||
|
||||
FieldMapping(Field field, String column, boolean id, boolean autoIncrement, boolean json) {
|
||||
this.field = field;
|
||||
this.column = column;
|
||||
this.id = id;
|
||||
this.autoIncrement = autoIncrement;
|
||||
this.json = json;
|
||||
this.field.setAccessible(true);
|
||||
}
|
||||
|
||||
Field field() {
|
||||
return field;
|
||||
}
|
||||
|
||||
String column() {
|
||||
return column;
|
||||
}
|
||||
|
||||
boolean isId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
boolean autoIncrement() {
|
||||
return autoIncrement;
|
||||
}
|
||||
|
||||
boolean json() {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.orm.EntityQuery;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import com.andrewkydev.database.orm.Condition;
|
||||
|
||||
final class EntityQueryImpl<T> implements EntityQuery<T> {
|
||||
private final EntityManagerImpl manager;
|
||||
private final Class<T> type;
|
||||
private List<String> selectColumns;
|
||||
private String joinSql;
|
||||
private String where;
|
||||
private List<Object> params;
|
||||
private String orderBy;
|
||||
private String groupBy;
|
||||
private String having;
|
||||
private List<Object> havingParams;
|
||||
private Integer limit;
|
||||
private Integer offset;
|
||||
|
||||
EntityQueryImpl(EntityManagerImpl manager, Class<T> type) {
|
||||
this.manager = manager;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> select(String... columns) {
|
||||
if (columns == null || columns.length == 0) {
|
||||
this.selectColumns = null;
|
||||
return this;
|
||||
}
|
||||
this.selectColumns = new ArrayList<>(Arrays.asList(columns));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> join(String joinSql) {
|
||||
this.joinSql = joinSql;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> where(String where, List<Object> params) {
|
||||
this.where = where;
|
||||
this.params = params == null ? null : new ArrayList<>(params);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> where(String where, Object... params) {
|
||||
this.where = where;
|
||||
if (params == null || params.length == 0) {
|
||||
this.params = null;
|
||||
} else {
|
||||
this.params = new ArrayList<>(Arrays.asList(params));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> where(Condition condition) {
|
||||
if (condition == null) {
|
||||
this.where = null;
|
||||
this.params = null;
|
||||
return this;
|
||||
}
|
||||
this.where = condition.sql();
|
||||
this.params = new ArrayList<>(condition.params());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> and(Condition condition) {
|
||||
if (condition == null) {
|
||||
return this;
|
||||
}
|
||||
if (this.where == null || this.where.trim().isEmpty()) {
|
||||
return where(condition);
|
||||
}
|
||||
this.where = "(" + this.where + " AND " + condition.sql() + ")";
|
||||
if (this.params == null) {
|
||||
this.params = new ArrayList<>();
|
||||
}
|
||||
this.params.addAll(condition.params());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> or(Condition condition) {
|
||||
if (condition == null) {
|
||||
return this;
|
||||
}
|
||||
if (this.where == null || this.where.trim().isEmpty()) {
|
||||
return where(condition);
|
||||
}
|
||||
this.where = "(" + this.where + " OR " + condition.sql() + ")";
|
||||
if (this.params == null) {
|
||||
this.params = new ArrayList<>();
|
||||
}
|
||||
this.params.addAll(condition.params());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> orderBy(String orderBy) {
|
||||
this.orderBy = orderBy;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> groupBy(String groupBy) {
|
||||
this.groupBy = groupBy;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> having(String having, List<Object> params) {
|
||||
this.having = having;
|
||||
this.havingParams = params == null ? null : new ArrayList<>(params);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> having(String having, Object... params) {
|
||||
this.having = having;
|
||||
if (params == null || params.length == 0) {
|
||||
this.havingParams = null;
|
||||
} else {
|
||||
this.havingParams = new ArrayList<>(Arrays.asList(params));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> having(Condition condition) {
|
||||
if (condition == null) {
|
||||
this.having = null;
|
||||
this.havingParams = null;
|
||||
return this;
|
||||
}
|
||||
this.having = condition.sql();
|
||||
this.havingParams = new ArrayList<>(condition.params());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> limit(int limit) {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> offset(int offset) {
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityQuery<T> limit(int limit, int offset) {
|
||||
this.limit = limit;
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> list() {
|
||||
return manager.queryCustom(
|
||||
buildSelectSql(),
|
||||
mergeParams(),
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T one() {
|
||||
List<T> results = manager.queryCustom(
|
||||
buildSelectSqlWithLimit(1),
|
||||
mergeParams(),
|
||||
type
|
||||
);
|
||||
return results.isEmpty() ? null : results.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return manager.queryCountCustom(buildCountSql(), mergeParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return manager.queryExistsCustom(buildExistsSql(), mergeParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete() {
|
||||
if (joinSql != null || groupBy != null || having != null) {
|
||||
throw new IllegalStateException("delete does not support join/group/having");
|
||||
}
|
||||
return manager.deleteWhere(type, where, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<T>> listAsync() {
|
||||
return CompletableFuture.supplyAsync(this::list, manager.executor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<T> oneAsync() {
|
||||
return CompletableFuture.supplyAsync(this::one, manager.executor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Long> countAsync() {
|
||||
return CompletableFuture.supplyAsync(this::count, manager.executor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> existsAsync() {
|
||||
return CompletableFuture.supplyAsync(this::exists, manager.executor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> deleteAsync() {
|
||||
return CompletableFuture.supplyAsync(this::delete, manager.executor());
|
||||
}
|
||||
|
||||
private String buildSelectSql() {
|
||||
return buildSelectSqlWithLimit(null);
|
||||
}
|
||||
|
||||
private String buildSelectSqlWithLimit(Integer overrideLimit) {
|
||||
String select = selectColumns == null || selectColumns.isEmpty()
|
||||
? "*"
|
||||
: String.join(", ", selectColumns);
|
||||
StringBuilder sql = new StringBuilder("SELECT ");
|
||||
sql.append(select).append(" FROM ").append(manager.tableFor(type));
|
||||
if (joinSql != null && !joinSql.trim().isEmpty()) {
|
||||
sql.append(" ").append(joinSql);
|
||||
}
|
||||
if (where != null && !where.trim().isEmpty()) {
|
||||
sql.append(" WHERE ").append(where);
|
||||
}
|
||||
if (groupBy != null && !groupBy.trim().isEmpty()) {
|
||||
sql.append(" GROUP BY ").append(groupBy);
|
||||
}
|
||||
if (having != null && !having.trim().isEmpty()) {
|
||||
sql.append(" HAVING ").append(having);
|
||||
}
|
||||
if (orderBy != null && !orderBy.trim().isEmpty()) {
|
||||
sql.append(" ORDER BY ").append(orderBy);
|
||||
}
|
||||
Integer effectiveLimit = overrideLimit == null ? limit : overrideLimit;
|
||||
if (effectiveLimit != null) {
|
||||
sql.append(" LIMIT ").append(effectiveLimit);
|
||||
}
|
||||
if (offset != null) {
|
||||
if (effectiveLimit == null) {
|
||||
sql.append(" LIMIT 2147483647");
|
||||
}
|
||||
sql.append(" OFFSET ").append(offset);
|
||||
}
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private String buildCountSql() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
if (groupBy == null || groupBy.trim().isEmpty()) {
|
||||
sql.append("SELECT COUNT(*) FROM ").append(manager.tableFor(type));
|
||||
if (joinSql != null && !joinSql.trim().isEmpty()) {
|
||||
sql.append(" ").append(joinSql);
|
||||
}
|
||||
if (where != null && !where.trim().isEmpty()) {
|
||||
sql.append(" WHERE ").append(where);
|
||||
}
|
||||
if (having != null && !having.trim().isEmpty()) {
|
||||
sql.append(" HAVING ").append(having);
|
||||
}
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
sql.append("SELECT COUNT(*) FROM (");
|
||||
sql.append("SELECT 1 FROM ").append(manager.tableFor(type));
|
||||
if (joinSql != null && !joinSql.trim().isEmpty()) {
|
||||
sql.append(" ").append(joinSql);
|
||||
}
|
||||
if (where != null && !where.trim().isEmpty()) {
|
||||
sql.append(" WHERE ").append(where);
|
||||
}
|
||||
sql.append(" GROUP BY ").append(groupBy);
|
||||
if (having != null && !having.trim().isEmpty()) {
|
||||
sql.append(" HAVING ").append(having);
|
||||
}
|
||||
sql.append(") t");
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private String buildExistsSql() {
|
||||
StringBuilder sql = new StringBuilder("SELECT 1 FROM ");
|
||||
sql.append(manager.tableFor(type));
|
||||
if (joinSql != null && !joinSql.trim().isEmpty()) {
|
||||
sql.append(" ").append(joinSql);
|
||||
}
|
||||
if (where != null && !where.trim().isEmpty()) {
|
||||
sql.append(" WHERE ").append(where);
|
||||
}
|
||||
if (groupBy != null && !groupBy.trim().isEmpty()) {
|
||||
sql.append(" GROUP BY ").append(groupBy);
|
||||
}
|
||||
if (having != null && !having.trim().isEmpty()) {
|
||||
sql.append(" HAVING ").append(having);
|
||||
}
|
||||
sql.append(" LIMIT 1");
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private List<Object> mergeParams() {
|
||||
List<Object> merged = new ArrayList<>();
|
||||
if (params != null) {
|
||||
merged.addAll(params);
|
||||
}
|
||||
if (havingParams != null) {
|
||||
merged.addAll(havingParams);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.query.QueryRunner;
|
||||
import com.andrewkydev.database.query.RowMapper;
|
||||
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.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public final class JdbcQueryRunner implements QueryRunner {
|
||||
private final DataSource dataSource;
|
||||
private final Executor executor;
|
||||
|
||||
public JdbcQueryRunner(DataSource dataSource, Executor executor) {
|
||||
this.dataSource = dataSource;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(String sql) {
|
||||
return execute(sql, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(String sql, List<Object> params) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> mapper) {
|
||||
return query(sql, Collections.emptyList(), mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, List<Object> params, RowMapper<T> mapper) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<T> results = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
results.add(mapper.map(resultSet));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to query SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> executeAsync(String sql) {
|
||||
return CompletableFuture.supplyAsync(() -> execute(sql), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> executeAsync(String sql, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> execute(sql, params), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> queryAsync(String sql, RowMapper<T> mapper) {
|
||||
return CompletableFuture.supplyAsync(() -> query(sql, mapper), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> queryAsync(String sql, List<Object> params, RowMapper<T> mapper) {
|
||||
return CompletableFuture.supplyAsync(() -> query(sql, params, mapper), executor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
final class JdbcSupport {
|
||||
private JdbcSupport() {
|
||||
}
|
||||
|
||||
static void bindParams(PreparedStatement statement, List<Object> params) throws SQLException {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
statement.setObject(i + 1, params.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.query.RowMapper;
|
||||
import com.andrewkydev.database.query.Transaction;
|
||||
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.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public final class JdbcTransaction implements Transaction {
|
||||
private final Connection connection;
|
||||
private final Executor executor;
|
||||
|
||||
public JdbcTransaction(Connection connection, Executor executor) {
|
||||
this.connection = connection;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(String sql) {
|
||||
return execute(sql, Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(String sql, List<Object> params) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> mapper) {
|
||||
return query(sql, Collections.emptyList(), mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, List<Object> params, RowMapper<T> mapper) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
JdbcSupport.bindParams(statement, params);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<T> results = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
results.add(mapper.map(resultSet));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to query SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> executeAsync(String sql) {
|
||||
return CompletableFuture.supplyAsync(() -> execute(sql), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> executeAsync(String sql, List<Object> params) {
|
||||
return CompletableFuture.supplyAsync(() -> execute(sql, params), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> queryAsync(String sql, RowMapper<T> mapper) {
|
||||
return CompletableFuture.supplyAsync(() -> query(sql, mapper), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<List<T>> queryAsync(String sql, List<Object> params, RowMapper<T> mapper) {
|
||||
return CompletableFuture.supplyAsync(() -> query(sql, params, mapper), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
try {
|
||||
connection.commit();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to commit transaction", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
try {
|
||||
connection.rollback();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to rollback transaction", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> commitAsync() {
|
||||
return CompletableFuture.runAsync(this::commit, executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> rollbackAsync() {
|
||||
return CompletableFuture.runAsync(this::rollback, executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to close transaction connection", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.schema.SqlDialect;
|
||||
|
||||
public final class JdbcUrlBuilder {
|
||||
private JdbcUrlBuilder() {
|
||||
}
|
||||
|
||||
public static String build(SqlDialect dialect, String host, int port, String database) {
|
||||
String dbSegment = database == null ? "" : database;
|
||||
return switch (dialect) {
|
||||
case POSTGRESQL -> {
|
||||
if (dbSegment.isEmpty()) {
|
||||
dbSegment = "postgres";
|
||||
}
|
||||
yield "jdbc:postgresql://" + host + ":" + port + "/" + dbSegment;
|
||||
}
|
||||
default -> {
|
||||
if (dbSegment.isEmpty()) {
|
||||
yield "jdbc:mysql://" + host + ":" + port + "/";
|
||||
}
|
||||
yield "jdbc:mysql://" + host + ":" + port + "/" + dbSegment
|
||||
+ "?useSSL=false&allowPublicKeyRetrieval=true";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
271
src/main/java/com/andrewkydev/database/internal/SchemaImpl.java
Normal file
271
src/main/java/com/andrewkydev/database/internal/SchemaImpl.java
Normal file
@@ -0,0 +1,271 @@
|
||||
package com.andrewkydev.database.internal;
|
||||
|
||||
import com.andrewkydev.database.config.DatabaseConfig;
|
||||
import com.andrewkydev.database.schema.ColumnSpec;
|
||||
import com.andrewkydev.database.schema.IndexSpec;
|
||||
import com.andrewkydev.database.schema.Schema;
|
||||
import com.andrewkydev.database.schema.SqlDialect;
|
||||
import com.andrewkydev.database.schema.TableSpec;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
public final class SchemaImpl implements Schema {
|
||||
private final DataSource dataSource;
|
||||
private final DatabaseConfig config;
|
||||
private final SqlDialect dialect;
|
||||
private final Executor executor;
|
||||
|
||||
public SchemaImpl(DataSource dataSource, DatabaseConfig config, Executor executor) {
|
||||
this.dataSource = dataSource;
|
||||
this.config = config;
|
||||
this.dialect = config.dialect();
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createDatabase(String name) {
|
||||
String sql = "CREATE DATABASE " + name;
|
||||
executeAdmin(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropDatabase(String name) {
|
||||
String sql = "DROP DATABASE " + name;
|
||||
executeAdmin(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createTable(TableSpec spec) {
|
||||
List<String> statements = new ArrayList<>();
|
||||
statements.add(buildCreateTable(spec));
|
||||
for (IndexSpec index : spec.indexes()) {
|
||||
statements.add(buildCreateIndex(spec.name(), index));
|
||||
}
|
||||
executeStatements(statements, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropTable(String table) {
|
||||
executeStatements(singletonList("DROP TABLE " + table), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addColumn(String table, ColumnSpec column) {
|
||||
executeStatements(singletonList("ALTER TABLE " + table + " ADD COLUMN " + columnDefinition(column)), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateColumn(String table, ColumnSpec column) {
|
||||
executeStatements(buildUpdateColumnStatements(table, column), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropColumn(String table, String column) {
|
||||
executeStatements(singletonList("ALTER TABLE " + table + " DROP COLUMN " + column), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIndex(String table, IndexSpec index) {
|
||||
executeStatements(singletonList(buildCreateIndex(table, index)), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropIndex(String table, String indexName) {
|
||||
String sql = dialect == SqlDialect.POSTGRESQL
|
||||
? "DROP INDEX " + indexName
|
||||
: "DROP INDEX " + indexName + " ON " + table;
|
||||
executeStatements(singletonList(sql), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> createDatabaseAsync(String name) {
|
||||
return CompletableFuture.runAsync(() -> createDatabase(name), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> dropDatabaseAsync(String name) {
|
||||
return CompletableFuture.runAsync(() -> dropDatabase(name), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> createTableAsync(TableSpec spec) {
|
||||
return CompletableFuture.runAsync(() -> createTable(spec), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> dropTableAsync(String table) {
|
||||
return CompletableFuture.runAsync(() -> dropTable(table), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> addColumnAsync(String table, ColumnSpec column) {
|
||||
return CompletableFuture.runAsync(() -> addColumn(table, column), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> updateColumnAsync(String table, ColumnSpec column) {
|
||||
return CompletableFuture.runAsync(() -> updateColumn(table, column), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> dropColumnAsync(String table, String column) {
|
||||
return CompletableFuture.runAsync(() -> dropColumn(table, column), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> addIndexAsync(String table, IndexSpec index) {
|
||||
return CompletableFuture.runAsync(() -> addIndex(table, index), executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> dropIndexAsync(String table, String indexName) {
|
||||
return CompletableFuture.runAsync(() -> dropIndex(table, indexName), executor);
|
||||
}
|
||||
|
||||
private void executeAdmin(String sql) {
|
||||
try (Connection connection = DriverManager.getConnection(
|
||||
JdbcUrlBuilder.build(dialect, config.host(), config.port(), adminDatabase()),
|
||||
config.username(),
|
||||
config.password()
|
||||
);
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute admin SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String adminDatabase() {
|
||||
if (dialect == SqlDialect.POSTGRESQL) {
|
||||
return config.adminDatabase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void executeStatements(List<String> statements, boolean allowTransactions) {
|
||||
boolean useTransaction = allowTransactions && config.autoTransactions();
|
||||
if (!useTransaction) {
|
||||
for (String statement : statements) {
|
||||
executeStatement(statement);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
try {
|
||||
for (String sql : statements) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException ex) {
|
||||
connection.rollback();
|
||||
throw ex;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute SQL statements", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> singletonList(String statement) {
|
||||
List<String> statements = new ArrayList<>(1);
|
||||
statements.add(statement);
|
||||
return statements;
|
||||
}
|
||||
|
||||
private void executeStatement(String sql) {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
throw new IllegalStateException("Failed to execute SQL: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildCreateTable(TableSpec spec) {
|
||||
StringJoiner joiner = new StringJoiner(", ");
|
||||
for (ColumnSpec column : spec.columns()) {
|
||||
joiner.add(columnDefinition(column));
|
||||
}
|
||||
if (!spec.primaryKey().isEmpty()) {
|
||||
joiner.add("PRIMARY KEY (" + String.join(", ", spec.primaryKey()) + ")");
|
||||
} else {
|
||||
List<String> inlineKeys = new ArrayList<>();
|
||||
for (ColumnSpec column : spec.columns()) {
|
||||
if (column.primaryKey()) {
|
||||
inlineKeys.add(column.name());
|
||||
}
|
||||
}
|
||||
if (!inlineKeys.isEmpty()) {
|
||||
joiner.add("PRIMARY KEY (" + String.join(", ", inlineKeys) + ")");
|
||||
}
|
||||
}
|
||||
return "CREATE TABLE " + spec.name() + " (" + joiner + ")";
|
||||
}
|
||||
|
||||
private String buildCreateIndex(String table, IndexSpec index) {
|
||||
String prefix = index.unique() ? "CREATE UNIQUE INDEX " : "CREATE INDEX ";
|
||||
return prefix + index.name() + " ON " + table + " (" + String.join(", ", index.columns()) + ")";
|
||||
}
|
||||
|
||||
private String columnDefinition(ColumnSpec column) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(column.name()).append(" ");
|
||||
builder.append(resolveColumnType(column));
|
||||
if (!column.nullable()) {
|
||||
builder.append(" NOT NULL");
|
||||
}
|
||||
if (column.defaultValue() != null && !column.defaultValue().isEmpty()) {
|
||||
builder.append(" DEFAULT ").append(column.defaultValue());
|
||||
}
|
||||
if (column.autoIncrement() && dialect == SqlDialect.MYSQL) {
|
||||
builder.append(" AUTO_INCREMENT");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String resolveColumnType(ColumnSpec column) {
|
||||
if (!column.autoIncrement() || dialect != SqlDialect.POSTGRESQL) {
|
||||
return column.type();
|
||||
}
|
||||
String type = column.type().toUpperCase();
|
||||
if (type.contains("BIG")) {
|
||||
return "BIGSERIAL";
|
||||
}
|
||||
if (type.contains("INT")) {
|
||||
return "SERIAL";
|
||||
}
|
||||
return column.type() + " GENERATED BY DEFAULT AS IDENTITY";
|
||||
}
|
||||
|
||||
private List<String> buildUpdateColumnStatements(String table, ColumnSpec column) {
|
||||
List<String> statements = new ArrayList<>();
|
||||
if (dialect == SqlDialect.MYSQL) {
|
||||
statements.add("ALTER TABLE " + table + " MODIFY COLUMN " + columnDefinition(column));
|
||||
return statements;
|
||||
}
|
||||
|
||||
statements.add("ALTER TABLE " + table + " ALTER COLUMN " + column.name() + " TYPE " + column.type());
|
||||
if (column.nullable()) {
|
||||
statements.add("ALTER TABLE " + table + " ALTER COLUMN " + column.name() + " DROP NOT NULL");
|
||||
} else {
|
||||
statements.add("ALTER TABLE " + table + " ALTER COLUMN " + column.name() + " SET NOT NULL");
|
||||
}
|
||||
if (column.defaultValue() == null || column.defaultValue().isEmpty()) {
|
||||
statements.add("ALTER TABLE " + table + " ALTER COLUMN " + column.name() + " DROP DEFAULT");
|
||||
} else {
|
||||
statements.add("ALTER TABLE " + table + " ALTER COLUMN " + column.name() + " SET DEFAULT " + column.defaultValue());
|
||||
}
|
||||
return statements;
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/andrewkydev/database/orm/Condition.java
Normal file
38
src/main/java/com/andrewkydev/database/orm/Condition.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class Condition {
|
||||
private final String sql;
|
||||
private final List<Object> params;
|
||||
|
||||
Condition(String sql, List<Object> params) {
|
||||
this.sql = sql;
|
||||
this.params = params == null ? Collections.emptyList() : params;
|
||||
}
|
||||
|
||||
public String sql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public List<Object> params() {
|
||||
return params;
|
||||
}
|
||||
|
||||
public Condition and(Condition other) {
|
||||
return combine("AND", other);
|
||||
}
|
||||
|
||||
public Condition or(Condition other) {
|
||||
return combine("OR", other);
|
||||
}
|
||||
|
||||
private Condition combine(String op, Condition other) {
|
||||
List<Object> combined = new ArrayList<>(params.size() + other.params.size());
|
||||
combined.addAll(params);
|
||||
combined.addAll(other.params);
|
||||
return new Condition("(" + sql + " " + op + " " + other.sql + ")", combined);
|
||||
}
|
||||
}
|
||||
85
src/main/java/com/andrewkydev/database/orm/Conditions.java
Normal file
85
src/main/java/com/andrewkydev/database/orm/Conditions.java
Normal file
@@ -0,0 +1,85 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class Conditions {
|
||||
private Conditions() {
|
||||
}
|
||||
|
||||
public static Condition raw(String sql, List<Object> params) {
|
||||
return new Condition(sql, params);
|
||||
}
|
||||
|
||||
public static Condition raw(String sql, Object... params) {
|
||||
return new Condition(sql, params == null ? Collections.emptyList() : asList(params));
|
||||
}
|
||||
|
||||
public static Condition eq(String column, Object value) {
|
||||
return new Condition(column + " = ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition ne(String column, Object value) {
|
||||
return new Condition(column + " <> ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition gt(String column, Object value) {
|
||||
return new Condition(column + " > ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition gte(String column, Object value) {
|
||||
return new Condition(column + " >= ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition lt(String column, Object value) {
|
||||
return new Condition(column + " < ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition lte(String column, Object value) {
|
||||
return new Condition(column + " <= ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition like(String column, Object value) {
|
||||
return new Condition(column + " LIKE ?", Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public static Condition in(String column, List<Object> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return new Condition("1=0", Collections.emptyList());
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(column).append(" IN (");
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append("?");
|
||||
}
|
||||
builder.append(")");
|
||||
return new Condition(builder.toString(), values);
|
||||
}
|
||||
|
||||
public static Condition in(String column, Object... values) {
|
||||
if (values == null || values.length == 0) {
|
||||
return new Condition("1=0", Collections.emptyList());
|
||||
}
|
||||
List<Object> list = new ArrayList<>(values.length);
|
||||
Collections.addAll(list, values);
|
||||
return in(column, list);
|
||||
}
|
||||
|
||||
public static Condition isNull(String column) {
|
||||
return new Condition(column + " IS NULL", Collections.emptyList());
|
||||
}
|
||||
|
||||
public static Condition isNotNull(String column) {
|
||||
return new Condition(column + " IS NOT NULL", Collections.emptyList());
|
||||
}
|
||||
|
||||
private static List<Object> asList(Object[] params) {
|
||||
List<Object> list = new ArrayList<>(params.length);
|
||||
Collections.addAll(list, params);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/andrewkydev/database/orm/DbColumn.java
Normal file
20
src/main/java/com/andrewkydev/database/orm/DbColumn.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface DbColumn {
|
||||
String name() default "";
|
||||
|
||||
boolean nullable() default true;
|
||||
|
||||
int length() default 255;
|
||||
|
||||
boolean unique() default false;
|
||||
|
||||
String type() default "";
|
||||
}
|
||||
12
src/main/java/com/andrewkydev/database/orm/DbEntity.java
Normal file
12
src/main/java/com/andrewkydev/database/orm/DbEntity.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface DbEntity {
|
||||
String table() default "";
|
||||
}
|
||||
12
src/main/java/com/andrewkydev/database/orm/DbId.java
Normal file
12
src/main/java/com/andrewkydev/database/orm/DbId.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface DbId {
|
||||
boolean autoIncrement() default true;
|
||||
}
|
||||
11
src/main/java/com/andrewkydev/database/orm/DbJson.java
Normal file
11
src/main/java/com/andrewkydev/database/orm/DbJson.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface DbJson {
|
||||
}
|
||||
11
src/main/java/com/andrewkydev/database/orm/DbTransient.java
Normal file
11
src/main/java/com/andrewkydev/database/orm/DbTransient.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface DbTransient {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface EntityManager {
|
||||
|
||||
<T> void insert(T entity);
|
||||
|
||||
<T> void update(T entity);
|
||||
|
||||
<T> void delete(T entity);
|
||||
|
||||
<T> T findById(Class<T> type, Object id);
|
||||
|
||||
<T> List<T> findAll(Class<T> type);
|
||||
|
||||
<T> T findOneWhere(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> List<T> findWhere(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> long count(Class<T> type);
|
||||
|
||||
<T> long count(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> boolean exists(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> int deleteWhere(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> List<T> findWhere(
|
||||
Class<T> type,
|
||||
String where,
|
||||
List<Object> params,
|
||||
String orderBy,
|
||||
Integer limit,
|
||||
Integer offset
|
||||
);
|
||||
|
||||
<T> CompletableFuture<List<T>> findWhereAsync(
|
||||
Class<T> type,
|
||||
String where,
|
||||
List<Object> params,
|
||||
String orderBy,
|
||||
Integer limit,
|
||||
Integer offset
|
||||
);
|
||||
|
||||
<T> void registerAdapter(Class<T> type, TypeAdapter<T> adapter);
|
||||
|
||||
<T> EntityQuery<T> query(Class<T> type);
|
||||
|
||||
<T> CompletableFuture<Void> insertAsync(T entity);
|
||||
|
||||
<T> CompletableFuture<Void> updateAsync(T entity);
|
||||
|
||||
<T> CompletableFuture<Void> deleteAsync(T entity);
|
||||
|
||||
<T> CompletableFuture<T> findByIdAsync(Class<T> type, Object id);
|
||||
|
||||
<T> CompletableFuture<List<T>> findAllAsync(Class<T> type);
|
||||
|
||||
<T> CompletableFuture<T> findOneWhereAsync(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> CompletableFuture<List<T>> findWhereAsync(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> CompletableFuture<Long> countAsync(Class<T> type);
|
||||
|
||||
<T> CompletableFuture<Long> countAsync(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> CompletableFuture<Boolean> existsAsync(Class<T> type, String where, List<Object> params);
|
||||
|
||||
<T> CompletableFuture<Integer> deleteWhereAsync(Class<T> type, String where, List<Object> params);
|
||||
}
|
||||
57
src/main/java/com/andrewkydev/database/orm/EntityQuery.java
Normal file
57
src/main/java/com/andrewkydev/database/orm/EntityQuery.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface EntityQuery<T> {
|
||||
|
||||
EntityQuery<T> select(String... columns);
|
||||
|
||||
EntityQuery<T> join(String joinSql);
|
||||
|
||||
EntityQuery<T> where(String where, List<Object> params);
|
||||
|
||||
EntityQuery<T> where(String where, Object... params);
|
||||
|
||||
EntityQuery<T> where(Condition condition);
|
||||
|
||||
EntityQuery<T> and(Condition condition);
|
||||
|
||||
EntityQuery<T> or(Condition condition);
|
||||
|
||||
EntityQuery<T> orderBy(String orderBy);
|
||||
|
||||
EntityQuery<T> groupBy(String groupBy);
|
||||
|
||||
EntityQuery<T> having(String having, List<Object> params);
|
||||
|
||||
EntityQuery<T> having(String having, Object... params);
|
||||
|
||||
EntityQuery<T> having(Condition condition);
|
||||
|
||||
EntityQuery<T> limit(int limit);
|
||||
|
||||
EntityQuery<T> offset(int offset);
|
||||
|
||||
EntityQuery<T> limit(int limit, int offset);
|
||||
|
||||
List<T> list();
|
||||
|
||||
T one();
|
||||
|
||||
long count();
|
||||
|
||||
boolean exists();
|
||||
|
||||
int delete();
|
||||
|
||||
CompletableFuture<List<T>> listAsync();
|
||||
|
||||
CompletableFuture<T> oneAsync();
|
||||
|
||||
CompletableFuture<Long> countAsync();
|
||||
|
||||
CompletableFuture<Boolean> existsAsync();
|
||||
|
||||
CompletableFuture<Integer> deleteAsync();
|
||||
}
|
||||
132
src/main/java/com/andrewkydev/database/orm/OrmSchema.java
Normal file
132
src/main/java/com/andrewkydev/database/orm/OrmSchema.java
Normal file
@@ -0,0 +1,132 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
import com.andrewkydev.database.schema.ColumnSpec;
|
||||
import com.andrewkydev.database.schema.IndexSpec;
|
||||
import com.andrewkydev.database.schema.SqlDialect;
|
||||
import com.andrewkydev.database.schema.TableSpec;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class OrmSchema {
|
||||
private OrmSchema() {
|
||||
}
|
||||
|
||||
public static TableSpec fromEntity(Class<?> type, SqlDialect dialect) {
|
||||
DbEntity entity = type.getAnnotation(DbEntity.class);
|
||||
String table = entity != null && !entity.table().isEmpty()
|
||||
? entity.table()
|
||||
: toSnakeCase(type.getSimpleName());
|
||||
|
||||
TableSpec.Builder builder = TableSpec.builder(table);
|
||||
List<String> primaryKeys = new ArrayList<>();
|
||||
List<IndexSpec> indexes = new ArrayList<>();
|
||||
|
||||
Class<?> current = type;
|
||||
while (current != null && current != Object.class) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
if (field.isAnnotationPresent(DbTransient.class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DbColumn column = field.getAnnotation(DbColumn.class);
|
||||
DbId id = field.getAnnotation(DbId.class);
|
||||
DbJson json = field.getAnnotation(DbJson.class);
|
||||
|
||||
String columnName = column != null && !column.name().isEmpty()
|
||||
? column.name()
|
||||
: toSnakeCase(field.getName());
|
||||
boolean nullable = column == null || column.nullable();
|
||||
boolean unique = column != null && column.unique();
|
||||
int length = column != null ? column.length() : 255;
|
||||
boolean autoIncrement = id != null && id.autoIncrement();
|
||||
boolean primaryKey = id != null;
|
||||
|
||||
String typeName = column != null ? column.type() : "";
|
||||
if (typeName == null || typeName.trim().isEmpty()) {
|
||||
typeName = resolveType(field.getType(), length, json != null, dialect);
|
||||
}
|
||||
|
||||
builder.column(ColumnSpec.builder(columnName, typeName)
|
||||
.nullable(nullable)
|
||||
.autoIncrement(autoIncrement)
|
||||
.primaryKey(primaryKey)
|
||||
.build());
|
||||
|
||||
if (primaryKey) {
|
||||
primaryKeys.add(columnName);
|
||||
}
|
||||
if (unique) {
|
||||
indexes.add(new IndexSpec(table + "_" + columnName + "_uk", listOf(columnName), true));
|
||||
}
|
||||
}
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
|
||||
if (!primaryKeys.isEmpty()) {
|
||||
builder.primaryKey(primaryKeys);
|
||||
}
|
||||
if (!indexes.isEmpty()) {
|
||||
builder.indexes(indexes);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static String resolveType(Class<?> fieldType, int length, boolean json, SqlDialect dialect) {
|
||||
if (json) {
|
||||
return dialect == SqlDialect.POSTGRESQL ? "JSONB" : "JSON";
|
||||
}
|
||||
if (fieldType == String.class) {
|
||||
return "VARCHAR(" + length + ")";
|
||||
}
|
||||
if (fieldType == int.class || fieldType == Integer.class) {
|
||||
return "INT";
|
||||
}
|
||||
if (fieldType == long.class || fieldType == Long.class) {
|
||||
return "BIGINT";
|
||||
}
|
||||
if (fieldType == short.class || fieldType == Short.class) {
|
||||
return "SMALLINT";
|
||||
}
|
||||
if (fieldType == boolean.class || fieldType == Boolean.class) {
|
||||
return dialect == SqlDialect.POSTGRESQL ? "BOOLEAN" : "TINYINT(1)";
|
||||
}
|
||||
if (fieldType == float.class || fieldType == Float.class) {
|
||||
return "FLOAT";
|
||||
}
|
||||
if (fieldType == double.class || fieldType == Double.class) {
|
||||
return "DOUBLE";
|
||||
}
|
||||
if (fieldType == java.util.UUID.class) {
|
||||
return dialect == SqlDialect.POSTGRESQL ? "UUID" : "CHAR(36)";
|
||||
}
|
||||
return "TEXT";
|
||||
}
|
||||
|
||||
private static List<String> listOf(String value) {
|
||||
List<String> list = new ArrayList<>(1);
|
||||
list.add(value);
|
||||
return list;
|
||||
}
|
||||
|
||||
private static String toSnakeCase(String value) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) {
|
||||
builder.append('_');
|
||||
}
|
||||
builder.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.andrewkydev.database.orm;
|
||||
|
||||
public interface TypeAdapter<T> {
|
||||
Object toDatabase(T value);
|
||||
|
||||
T fromDatabase(Object value);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.andrewkydev.database.query;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface QueryRunner {
|
||||
|
||||
int execute(String sql);
|
||||
|
||||
int execute(String sql, List<Object> params);
|
||||
|
||||
<T> List<T> query(String sql, RowMapper<T> mapper);
|
||||
|
||||
<T> List<T> query(String sql, List<Object> params, RowMapper<T> mapper);
|
||||
|
||||
CompletableFuture<Integer> executeAsync(String sql);
|
||||
|
||||
CompletableFuture<Integer> executeAsync(String sql, List<Object> params);
|
||||
|
||||
<T> CompletableFuture<List<T>> queryAsync(String sql, RowMapper<T> mapper);
|
||||
|
||||
<T> CompletableFuture<List<T>> queryAsync(String sql, List<Object> params, RowMapper<T> mapper);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.andrewkydev.database.query;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RowMapper<T> {
|
||||
T map(ResultSet resultSet) throws SQLException;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.andrewkydev.database.query;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Transaction extends AutoCloseable {
|
||||
|
||||
int execute(String sql);
|
||||
|
||||
int execute(String sql, List<Object> params);
|
||||
|
||||
<T> List<T> query(String sql, RowMapper<T> mapper);
|
||||
|
||||
<T> List<T> query(String sql, List<Object> params, RowMapper<T> mapper);
|
||||
|
||||
CompletableFuture<Integer> executeAsync(String sql);
|
||||
|
||||
CompletableFuture<Integer> executeAsync(String sql, List<Object> params);
|
||||
|
||||
<T> CompletableFuture<List<T>> queryAsync(String sql, RowMapper<T> mapper);
|
||||
|
||||
<T> CompletableFuture<List<T>> queryAsync(String sql, List<Object> params, RowMapper<T> mapper);
|
||||
|
||||
void commit();
|
||||
|
||||
void rollback();
|
||||
|
||||
CompletableFuture<Void> commitAsync();
|
||||
|
||||
CompletableFuture<Void> rollbackAsync();
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.andrewkydev.database.schema;
|
||||
|
||||
public final class ColumnSpec {
|
||||
private final String name;
|
||||
private final String type;
|
||||
private final boolean nullable;
|
||||
private final String defaultValue;
|
||||
private final boolean autoIncrement;
|
||||
private final boolean primaryKey;
|
||||
|
||||
private ColumnSpec(Builder builder) {
|
||||
this.name = builder.name;
|
||||
this.type = builder.type;
|
||||
this.nullable = builder.nullable;
|
||||
this.defaultValue = builder.defaultValue;
|
||||
this.autoIncrement = builder.autoIncrement;
|
||||
this.primaryKey = builder.primaryKey;
|
||||
}
|
||||
|
||||
public static Builder builder(String name, String type) {
|
||||
return new Builder(name, type);
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean nullable() {
|
||||
return nullable;
|
||||
}
|
||||
|
||||
public String defaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public boolean autoIncrement() {
|
||||
return autoIncrement;
|
||||
}
|
||||
|
||||
public boolean primaryKey() {
|
||||
return primaryKey;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final String name;
|
||||
private final String type;
|
||||
private boolean nullable = true;
|
||||
private String defaultValue;
|
||||
private boolean autoIncrement;
|
||||
private boolean primaryKey;
|
||||
|
||||
private Builder(String name, String type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Builder nullable(boolean nullable) {
|
||||
this.nullable = nullable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder defaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder autoIncrement(boolean autoIncrement) {
|
||||
this.autoIncrement = autoIncrement;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder primaryKey(boolean primaryKey) {
|
||||
this.primaryKey = primaryKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ColumnSpec build() {
|
||||
return new ColumnSpec(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/andrewkydev/database/schema/IndexSpec.java
Normal file
29
src/main/java/com/andrewkydev/database/schema/IndexSpec.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.andrewkydev.database.schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class IndexSpec {
|
||||
private final String name;
|
||||
private final List<String> columns;
|
||||
private final boolean unique;
|
||||
|
||||
public IndexSpec(String name, List<String> columns, boolean unique) {
|
||||
this.name = name;
|
||||
this.columns = Collections.unmodifiableList(new ArrayList<>(columns));
|
||||
this.unique = unique;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> columns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public boolean unique() {
|
||||
return unique;
|
||||
}
|
||||
}
|
||||
42
src/main/java/com/andrewkydev/database/schema/Schema.java
Normal file
42
src/main/java/com/andrewkydev/database/schema/Schema.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.andrewkydev.database.schema;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Schema {
|
||||
|
||||
void createDatabase(String name);
|
||||
|
||||
void dropDatabase(String name);
|
||||
|
||||
void createTable(TableSpec spec);
|
||||
|
||||
void dropTable(String table);
|
||||
|
||||
void addColumn(String table, ColumnSpec column);
|
||||
|
||||
void updateColumn(String table, ColumnSpec column);
|
||||
|
||||
void dropColumn(String table, String column);
|
||||
|
||||
void addIndex(String table, IndexSpec index);
|
||||
|
||||
void dropIndex(String table, String indexName);
|
||||
|
||||
CompletableFuture<Void> createDatabaseAsync(String name);
|
||||
|
||||
CompletableFuture<Void> dropDatabaseAsync(String name);
|
||||
|
||||
CompletableFuture<Void> createTableAsync(TableSpec spec);
|
||||
|
||||
CompletableFuture<Void> dropTableAsync(String table);
|
||||
|
||||
CompletableFuture<Void> addColumnAsync(String table, ColumnSpec column);
|
||||
|
||||
CompletableFuture<Void> updateColumnAsync(String table, ColumnSpec column);
|
||||
|
||||
CompletableFuture<Void> dropColumnAsync(String table, String column);
|
||||
|
||||
CompletableFuture<Void> addIndexAsync(String table, IndexSpec index);
|
||||
|
||||
CompletableFuture<Void> dropIndexAsync(String table, String indexName);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.andrewkydev.database.schema;
|
||||
|
||||
public enum SqlDialect {
|
||||
MYSQL,
|
||||
POSTGRESQL
|
||||
}
|
||||
80
src/main/java/com/andrewkydev/database/schema/TableSpec.java
Normal file
80
src/main/java/com/andrewkydev/database/schema/TableSpec.java
Normal file
@@ -0,0 +1,80 @@
|
||||
package com.andrewkydev.database.schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class TableSpec {
|
||||
private final String name;
|
||||
private final List<ColumnSpec> columns;
|
||||
private final List<String> primaryKey;
|
||||
private final List<IndexSpec> indexes;
|
||||
|
||||
private TableSpec(Builder builder) {
|
||||
this.name = builder.name;
|
||||
this.columns = Collections.unmodifiableList(new ArrayList<>(builder.columns));
|
||||
this.primaryKey = Collections.unmodifiableList(new ArrayList<>(builder.primaryKey));
|
||||
this.indexes = Collections.unmodifiableList(new ArrayList<>(builder.indexes));
|
||||
}
|
||||
|
||||
public static Builder builder(String name) {
|
||||
return new Builder(name);
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<ColumnSpec> columns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public List<String> primaryKey() {
|
||||
return primaryKey;
|
||||
}
|
||||
|
||||
public List<IndexSpec> indexes() {
|
||||
return indexes;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final String name;
|
||||
private final List<ColumnSpec> columns = new ArrayList<>();
|
||||
private final List<String> primaryKey = new ArrayList<>();
|
||||
private final List<IndexSpec> indexes = new ArrayList<>();
|
||||
|
||||
private Builder(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Builder column(ColumnSpec column) {
|
||||
this.columns.add(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder columns(List<ColumnSpec> columns) {
|
||||
this.columns.addAll(columns);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder primaryKey(List<String> columns) {
|
||||
this.primaryKey.clear();
|
||||
this.primaryKey.addAll(columns);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder index(IndexSpec index) {
|
||||
this.indexes.add(index);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder indexes(List<IndexSpec> indexes) {
|
||||
this.indexes.addAll(indexes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableSpec build() {
|
||||
return new TableSpec(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/main/resources/config.yml
Normal file
16
src/main/resources/config.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
#support mysql | postgres
|
||||
driver: "mysql"
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
database: "primalix"
|
||||
username: "root"
|
||||
password: ""
|
||||
adminDatabase: "postgres"
|
||||
autoTransactions: true
|
||||
|
||||
pool:
|
||||
maxPoolSize: 10
|
||||
minIdle: 2
|
||||
connectionTimeoutMs: 30000
|
||||
idleTimeoutMs: 600000
|
||||
maxLifetimeMs: 1800000
|
||||
8
src/main/resources/plugin.yml
Normal file
8
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
name: Database
|
||||
description: "Database plugin for Primalix"
|
||||
main: org.andrewkydev.Loader
|
||||
version: "0.0.1"
|
||||
api: [ 1.1.0 ]
|
||||
|
||||
load: POSTWORLD
|
||||
|
||||
Reference in New Issue
Block a user