first commit

This commit is contained in:
2026-01-15 22:38:46 +03:00
commit a70e9b7a79
58 changed files with 3980 additions and 0 deletions

View 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";
}
}

View 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();
}

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

View File

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

View File

@@ -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
);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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());
}
});
}
}

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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";
}
};
}
}

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

View 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);
}
}

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

View 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 "";
}

View 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 "";
}

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

View 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 {
}

View 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 {
}

View File

@@ -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);
}

View 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();
}

View 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();
}
}

View File

@@ -0,0 +1,7 @@
package com.andrewkydev.database.orm;
public interface TypeAdapter<T> {
Object toDatabase(T value);
T fromDatabase(Object value);
}

View File

@@ -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);
}

View File

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

View File

@@ -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();
}

View File

@@ -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);
}
}
}

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

View 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);
}

View File

@@ -0,0 +1,6 @@
package com.andrewkydev.database.schema;
public enum SqlDialect {
MYSQL,
POSTGRESQL
}

View 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);
}
}
}

View 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

View 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