diff --git a/src/main/java/com/btk5h/skriptdb/SkriptValueWrapper.java b/src/main/java/com/btk5h/skriptdb/SkriptValueWrapper.java new file mode 100644 index 0000000..14e18a2 --- /dev/null +++ b/src/main/java/com/btk5h/skriptdb/SkriptValueWrapper.java @@ -0,0 +1,202 @@ +package com.btk5h.skriptdb; + +import static java.sql.Types.*; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.IntStream; + +import ch.njol.skript.util.Date; +import ch.njol.skript.util.Time; + +public class SkriptValueWrapper { + + private static final Map, SkriptValueWrapper> wrappersBySkript = new HashMap<>(); + private static final Map wrappersBySql = new HashMap<>(); + + + public static final SkriptValueWrapper FALLBACK = register(Object.class, Function.identity(), ResultSet::getObject); + public static final SkriptValueWrapper TEXT_VALUE = register(String.class, SkriptValueWrapper::asString, SkriptValueWrapper::getString, CHAR, VARCHAR, LONGVARCHAR, CLOB); + public static final SkriptValueWrapper BOOLEAN_VALUE = register(Boolean.class, SkriptValueWrapper::asBoolean, ResultSet::getBoolean, BOOLEAN); + public static final SkriptValueWrapper LONG_VALUE = register(Long.class, x -> x, SkriptValueWrapper::getLong, BIT, TINYINT, SMALLINT, INTEGER, BIGINT); + public static final SkriptValueWrapper DOUBLE_VALUE = register(Double.class, x -> x, SkriptValueWrapper::getDouble, FLOAT, REAL, DOUBLE, NUMERIC, DECIMAL); + public static final SkriptValueWrapper DATE_VALUE = register(String.class, SkriptValueWrapper::noParser, SkriptValueWrapper::getString, DATE); + public static final SkriptValueWrapper TIME_VALUE = register(Time.class, SkriptValueWrapper::asTime, SkriptValueWrapper::getTime, TIME); + public static final SkriptValueWrapper TIMESTAMP_VALUE = register(Date.class, SkriptValueWrapper::asTimestamp, SkriptValueWrapper::getTimestamp, TIMESTAMP); + public static final SkriptValueWrapper BINARY_VALUE = register(String.class, SkriptValueWrapper::noParser, SkriptValueWrapper::getBinary, TIMESTAMP); + + + + private final Class skriptType; + private final Function skToSqlMapper; + private final ResultFetcher sqlToSkMapper; + private final int[] validSqlTypes; + + + @SuppressWarnings("unchecked") + private SkriptValueWrapper(Class skriptType, Function skToSql, ResultFetcher sqlToSk, int... validSqlTypes) { + this.skriptType = skriptType; + this.skToSqlMapper = (Function) skToSql; + this.sqlToSkMapper = (ResultFetcher) sqlToSk; + this.validSqlTypes = IntStream.of(validSqlTypes) + .sorted() + .toArray(); + } + + + public Object asSql(Object obj) { + return this.skToSqlMapper.apply(obj); + } + + + public boolean isSkApplicable(Class type) { + return this.skriptType.isAssignableFrom(type); + } + + + public boolean isSqlApplicable(int sqlType) { + return Arrays.binarySearch(this.validSqlTypes, sqlType) >= 0; + } + + + public Object asSkript(ResultSet rs, int column) throws SQLException { + return this.sqlToSkMapper.fetch(rs, column); + } + + + public static SkriptValueWrapper getBySQLType(int type) { + return wrappersBySql.getOrDefault(type, FALLBACK); + } + + + public static SkriptValueWrapper getBySkriptType(Class type) { + return wrappersBySkript.getOrDefault(type, FALLBACK); + } + + + public static SkriptValueWrapper getBySQLLType(int type) { + return switch (type) { + //case CHAR, VARCHAR, LONGVARCHAR, CLOB -> null; // string + //case BOOLEAN -> null; // boolean + //case BIT, TINYINT, SMALLINT, INTEGER, BIGINT -> null; // long + //case FLOAT, REAL, DOUBLE, NUMERIC, DECIMAL -> null; // double + //case DATE -> null; // string + //case TIME -> null; // time + //case TIMESTAMP -> null; // Date + //case BINARY, VARBINARY, LONGVARBINARY, BLOB -> null; // hex String + //case NULL, OTHER, JAVA_OBJECT, DISTINCT, STRUCT, ARRAY -> null; // just whatever it gives to me + default -> null; + }; + } + + + private static SkriptValueWrapper register(Class skriptType, Function skToSql, ResultFetcher sqlToSk, int... validSqlTypes) { + SkriptValueWrapper wrapper = new SkriptValueWrapper(skriptType, skToSql, sqlToSk, validSqlTypes); + wrappersBySkript.putIfAbsent(skriptType, wrapper); + for (int i : validSqlTypes) { + wrappersBySql.putIfAbsent(i, wrapper); + } + return wrapper; + } + + + private static String getString(ResultSet rs, int column) throws SQLException { + return rs.getString(column); + } + + private static String asString(Object obj) { + return obj != null ? String.valueOf(obj) : null; + } + + + private static Long getLong(ResultSet rs, int column) throws SQLException { + var val = rs.getLong(column); + if (!rs.wasNull()) { + return val; + } + return null; + } + + + private static Double getDouble(ResultSet rs, int column) throws SQLException { + var val = rs.getDouble(column); + if (!rs.wasNull()) { + return val; + } + return null; + } + + + private static final int TICKS_PER_HOUR = 1000; + private static final double TICKS_PER_MINUTE = 1000. / 60; + private static final int HOUR_ZERO = 6 * TICKS_PER_HOUR; + + private static Time getTime(ResultSet rs, int column) throws SQLException { + var time = rs.getTime(column); + if (time != null) { + var local = time.toLocalTime(); + var minutes = local.getMinute(); + var hours = local.getHour(); + return new Time((int) Math.round( + hours * TICKS_PER_HOUR + - HOUR_ZERO + + minutes * TICKS_PER_MINUTE)); + } + return null; + } + + private static Object asTime(Time time) { + if (time != null) { + return new java.sql.Time(time.getTime()); + } + return null; + } + + + private static Date getTimestamp(ResultSet rs, int column) throws SQLException { + return new Date(rs.getTimestamp(column).getTime()); + } + + private static Object asTimestamp(Date date) { + return Timestamp.from(Instant.ofEpochMilli(date.getTimestamp())); + } + + private static Object asBoolean(Object obj) { + if (obj instanceof Number n) { + return n.doubleValue() > 0; + } else if (obj instanceof Boolean b) { + return b; + } + return null; + } + + + private static String getBinary(ResultSet rs, int column) throws SQLException { + byte[] bytes = rs.getBytes(column); + if (!rs.wasNull()) { + StringBuilder sb = new StringBuilder(bytes.length); + for (int i = 0; i < bytes.length; i++) { + sb.append(String.format("%02X", bytes[i])); + } + return sb.toString(); + } + return null; + } + + + private static U noParser(T t) { + throw new IllegalStateException("This value shouldn't have a parser"); + } + + + private interface ResultFetcher { + public T fetch(ResultSet rs, int index) throws SQLException; + } + +} diff --git a/src/main/java/com/btk5h/skriptdb/skript/EffExecuteStatement.java b/src/main/java/com/btk5h/skriptdb/skript/EffExecuteStatement.java index ad6bfb5..4df340e 100644 --- a/src/main/java/com/btk5h/skriptdb/skript/EffExecuteStatement.java +++ b/src/main/java/com/btk5h/skriptdb/skript/EffExecuteStatement.java @@ -1,33 +1,51 @@ package com.btk5h.skriptdb.skript; -import ch.njol.skript.Skript; -import ch.njol.skript.effects.Delay; -import ch.njol.skript.lang.*; -import ch.njol.skript.variables.Variables; -import ch.njol.util.Kleenean; -import ch.njol.util.Pair; -import com.btk5h.skriptdb.SkriptDB; -import com.btk5h.skriptdb.SkriptUtil; -import com.zaxxer.hikari.HikariDataSource; -import org.bukkit.Bukkit; -import org.bukkit.event.Event; - -import javax.sql.DataSource; -import javax.sql.rowset.CachedRowSet; -import javax.sql.rowset.serial.SerialBlob; -import javax.sql.rowset.serial.SerialException; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Pattern; +import java.util.stream.Stream; + +import javax.sql.DataSource; +import javax.sql.rowset.serial.SerialBlob; +import javax.sql.rowset.serial.SerialException; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; + +import com.btk5h.skriptdb.SkriptDB; +import com.btk5h.skriptdb.SkriptUtil; +import com.btk5h.skriptdb.SkriptValueWrapper; +import com.zaxxer.hikari.HikariDataSource; + +import ch.njol.skript.Skript; +import ch.njol.skript.effects.Delay; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.lang.Variable; +import ch.njol.skript.lang.VariableString; +import ch.njol.skript.variables.Variables; +import ch.njol.util.Kleenean; +import ch.njol.util.Pair; /** * Executes a statement on a database and optionally stores the result in a variable. Expressions @@ -72,7 +90,7 @@ public class EffExecuteStatement extends Effect { DataSource ds = dataSource.getSingle(e); //if data source isn't set if (ds == null) { - return; + return; } Pair> parsedQuery = parseQuery(e); String baseVariable = resultVariableName != null ? resultVariableName.toString(e).toLowerCase(Locale.ENGLISH) : null; @@ -82,22 +100,22 @@ public class EffExecuteStatement extends Effect { //execute SQL statement if (Bukkit.isPrimaryThread()) { CompletableFuture.supplyAsync(() -> executeStatement(ds, baseVariable, parsedQuery), threadPool) - .whenComplete((resources, err) -> { - //handle last error syntax data - resetLastSQLError(); - if (err instanceof CompletionException && err.getCause() instanceof SkriptDBQueryException) { - setLastSQLError(err.getCause().getMessage()); - } - //if local variables are present - //bring back local variables - //populate SQL data into variables - if (!quickly) { - Bukkit.getScheduler().runTask(SkriptDB.getInstance(), - () -> postExecution(e, locals, resources)); - } else { - postExecution(e, locals, resources); - } - }); + .whenComplete((resources, err) -> { + //handle last error syntax data + resetLastSQLError(); + if (err instanceof CompletionException && err.getCause() instanceof SkriptDBQueryException) { + setLastSQLError(err.getCause().getMessage()); + } + //if local variables are present + //bring back local variables + //populate SQL data into variables + if (!quickly) { + Bukkit.getScheduler().runTask(SkriptDB.getInstance(), + () -> postExecution(e, locals, resources)); + } else { + postExecution(e, locals, resources); + } + }); // sync executed SQL query, same as above, just sync } else { isSync = true; @@ -105,23 +123,23 @@ public class EffExecuteStatement extends Effect { resetLastSQLError(); try { resources = executeStatement(ds, baseVariable, parsedQuery); - } catch (SkriptDBQueryException err) { - //handle last error syntax data - setLastSQLError(err.getMessage()); - } + } catch (SkriptDBQueryException err) { + //handle last error syntax data + setLastSQLError(err.getMessage()); + } //if local variables are present //bring back local variables //populate SQL data into variables - postExecution(e, locals, resources); + postExecution(e, locals, resources); } } private void postExecution(Event e, Object locals, Map resources) { - if (locals != null && getNext() != null) { + if (locals != null && getNext() != null) { Variables.setLocalVariables(e, locals); } if (resources != null) { - resources.forEach((name, value) -> setVariable(e, name, value)); + resources.forEach((name, value) -> setVariable(e, name, value)); } TriggerItem.walk(getNext(), e); //the line below is required to prevent memory leaks @@ -145,15 +163,18 @@ public class EffExecuteStatement extends Effect { int queryArgCount = (int) ARGUMENT_PLACEHOLDER.matcher(queryString).results().count(); if (queryArgCount != args.length) { Skript.warning(String.format("Your query has %d question marks, but you provided %d arguments. (%s) [%s]", - queryArgCount, - args.length, - queryArguments.toString(e, true), - Optional.ofNullable(getTrigger()) - .map(Trigger::getDebugLabel) - .orElse("unknown"))); + queryArgCount, + args.length, + queryArguments.toString(e, true), + Optional.ofNullable(getTrigger()) + .map(Trigger::getDebugLabel) + .orElse("unknown"))); args = Arrays.copyOf(args, queryArgCount); } - return new Pair<>(query.getSingle(e), Arrays.asList(args)); + List argsList = Stream.of(args) + .map(arg -> SkriptValueWrapper.getBySkriptType(arg.getClass()).asSql(arg)) + .toList(); + return new Pair<>(query.getSingle(e), argsList); } else if (query instanceof VariableString && !((VariableString) query).isSimple()) { return parseVariableQuery(e, (VariableString) query); } @@ -201,7 +222,7 @@ public class EffExecuteStatement extends Effect { private Map executeStatement(DataSource ds, String baseVariable, Pair> query) throws SkriptDBQueryException { if (ds == null) { - throw new SkriptDBQueryException("Data source is not set"); + throw new SkriptDBQueryException("Data source is not set"); } try (Connection conn = ds.getConnection()) { try (PreparedStatement stmt = createStatement(conn, query)) { @@ -223,14 +244,16 @@ public class EffExecuteStatement extends Effect { } if (hasResultSet) { - CachedRowSet crs = SkriptDB.getRowSetFactory().createCachedRowSet(); - crs.populate(generatedKeys ? stmt.getGeneratedKeys() : stmt.getResultSet()); + // TODO: Check if caching is even needed + //CachedRowSet crs = SkriptDB.getRowSetFactory().createCachedRowSet(); + ResultSet rs = generatedKeys ? stmt.getGeneratedKeys() : stmt.getResultSet(); + //crs.populate(rs); if (isList) { - return fetchQueryResultSet(crs, baseVariable); + return fetchQueryResultSet(rs, baseVariable); } else { - crs.last(); - return Map.of(baseVariable, crs.getRow()); + rs.last(); + return Map.of(baseVariable, rs.getRow()); } } else if (!isList) { //if no results are returned and the specified variable isn't a list variable, put the affected rows count in the variable @@ -239,21 +262,25 @@ public class EffExecuteStatement extends Effect { return Map.of(); } - private Map fetchQueryResultSet(CachedRowSet crs, String baseVariable) throws SQLException { - Map variableList = new HashMap<>(); + private Map fetchQueryResultSet(/*CachedRowSet*/ResultSet crs, String baseVariable) throws SQLException { + Map variableList = new HashMap<>(); ResultSetMetaData meta = crs.getMetaData(); int columnCount = meta.getColumnCount(); - + SkriptValueWrapper[] wrappers = new SkriptValueWrapper[columnCount]; + String[] columnNames = new String[columnCount]; for (int i = 1; i <= columnCount; i++) { String label = meta.getColumnLabel(i); variableList.put(baseVariable + label, label); + columnNames[i - 1] = label; + wrappers[i - 1] = SkriptValueWrapper.getBySQLType(meta.getColumnType(i)); } - + int rowNumber = 1; while (crs.next()) { for (int i = 1; i <= columnCount; i++) { - variableList.put(baseVariable + meta.getColumnLabel(i).toLowerCase(Locale.ENGLISH) - + Variable.SEPARATOR + rowNumber, crs.getObject(i)); + Object obj = wrappers[i - 1].asSkript(crs, i); + variableList.put(baseVariable + columnNames[i - 1].toLowerCase(Locale.ENGLISH) + + Variable.SEPARATOR + rowNumber, obj); } rowNumber++; } @@ -262,8 +289,8 @@ public class EffExecuteStatement extends Effect { private PreparedStatement createStatement(Connection conn, Pair> query) throws SQLException { PreparedStatement stmt = generatedKeys ? - conn.prepareStatement(query.getFirst(), Statement.RETURN_GENERATED_KEYS) - : conn.prepareStatement(query.getFirst(), Statement.NO_GENERATED_KEYS); + conn.prepareStatement(query.getFirst(), Statement.RETURN_GENERATED_KEYS) + : conn.prepareStatement(query.getFirst(), Statement.NO_GENERATED_KEYS); if (query.getSecond() != null) { Iterator iter = query.getSecond().iterator(); for (int i = 1; iter.hasNext(); i++) { @@ -307,11 +334,11 @@ public class EffExecuteStatement extends Effect { } private static void resetLastSQLError() { - lastError = null; + lastError = null; } private static void setLastSQLError(String error) { - lastError = error; + lastError = error; } @Override @@ -338,7 +365,6 @@ public class EffExecuteStatement extends Effect { } queryArguments = (Expression) exprs[2]; } - ; Expression resultHolder = exprs[3]; quickly = parseResult.hasTag("quickly"); if (resultHolder instanceof Variable) { @@ -356,12 +382,12 @@ public class EffExecuteStatement extends Effect { public static class SkriptDBQueryException extends RuntimeException { - private static final long serialVersionUID = -1869895286406538884L; - - public SkriptDBQueryException(String message) { - super(message); - } - + private static final long serialVersionUID = -1869895286406538884L; + + public SkriptDBQueryException(String message) { + super(message); + } + } } \ No newline at end of file