Implemented issue: #19

This commit is contained in:
UnlegitDqrk
2026-03-02 15:42:55 +01:00
parent 33d745d667
commit 139af3f28b
24 changed files with 509 additions and 1 deletions

View File

@@ -98,6 +98,7 @@ public class JsePlatform {
globals.load(new JseIoLib());
globals.load(new JseOsLib());
globals.load(new LuajavaLib());
globals.load(new LuaSqlMysqlLib());
LoadState.install(globals);
LuaC.install(globals);
return globals;

View File

@@ -0,0 +1,478 @@
package org.luaj.vm2.libs.jse;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.Properties;
import java.util.logging.Logger;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaUserdata;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.libs.OneArgFunction;
import org.luaj.vm2.libs.TwoArgFunction;
import org.luaj.vm2.libs.VarArgFunction;
public class LuaSqlMysqlLib extends TwoArgFunction {
public LuaValue call(LuaValue modname, LuaValue env) {
LuaValue pkg = env.get("package");
if (!pkg.istable()) {
return env;
}
LuaValue preload = pkg.get("preload");
if (!preload.istable()) {
return env;
}
preload.set("luasql.mysql", new Loader());
return env;
}
static final class Loader extends TwoArgFunction {
public LuaValue call(LuaValue modname, LuaValue env) {
LuaTable module = new LuaTable(0, 1);
module.set("mysql", new MysqlFactory());
return module;
}
}
static final class MysqlFactory extends VarArgFunction {
public Varargs invoke(Varargs args) {
String driverClass = args.optjstring(1, "com.mysql.jdbc.Driver");
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
throw new LuaError("mysql jdbc driver not found: " + driverClass);
}
return new EnvironmentValue();
}
}
static final class EnvironmentValue extends LuaTable {
private boolean closed;
EnvironmentValue() {
set("connect", new connect());
set("close", new close());
}
final class connect extends VarArgFunction {
public Varargs invoke(Varargs args) {
checkOpen();
String database = args.checkjstring(2);
String user = args.optjstring(3, null);
String password = args.optjstring(4, null);
String host = args.optjstring(5, "localhost");
int port = args.optint(6, 3306);
String url = "jdbc:mysql://" + host + ":" + port + "/" + database;
try {
Connection connection = DriverManager.getConnection(url, user, password);
return new ConnectionValue(connection);
} catch (SQLException e) {
throw sqlError("mysql connect failed", e);
}
}
}
final class close extends OneArgFunction {
public LuaValue call(LuaValue self) {
closed = true;
return LuaValue.TRUE;
}
}
private void checkOpen() {
if (closed) {
throw new LuaError("environment is closed");
}
}
}
static final class ConnectionValue extends LuaTable {
private final Connection connection;
private boolean closed;
ConnectionValue(Connection connection) {
this.connection = connection;
set("execute", new execute());
set("close", new close());
set("commit", new commit());
set("rollback", new rollback());
set("setautocommit", new setautocommit());
}
final class execute extends VarArgFunction {
public Varargs invoke(Varargs args) {
checkOpen();
String sql = args.checkjstring(2);
try {
Statement statement = connection.createStatement();
boolean hasResultSet = statement.execute(sql);
if (hasResultSet) {
return new CursorValue(statement, statement.getResultSet());
}
int updated = statement.getUpdateCount();
statement.close();
return LuaValue.valueOf(updated);
} catch (SQLException e) {
throw sqlError("mysql execute failed", e);
}
}
}
final class close extends OneArgFunction {
public LuaValue call(LuaValue self) {
if (!closed) {
try {
connection.close();
} catch (SQLException e) {
throw sqlError("mysql close failed", e);
}
closed = true;
}
return LuaValue.TRUE;
}
}
final class commit extends OneArgFunction {
public LuaValue call(LuaValue self) {
checkOpen();
try {
connection.commit();
return LuaValue.TRUE;
} catch (SQLException e) {
throw sqlError("mysql commit failed", e);
}
}
}
final class rollback extends OneArgFunction {
public LuaValue call(LuaValue self) {
checkOpen();
try {
connection.rollback();
return LuaValue.TRUE;
} catch (SQLException e) {
throw sqlError("mysql rollback failed", e);
}
}
}
final class setautocommit extends VarArgFunction {
public Varargs invoke(Varargs args) {
checkOpen();
try {
connection.setAutoCommit(args.arg(2).toboolean());
return LuaValue.TRUE;
} catch (SQLException e) {
throw sqlError("mysql setautocommit failed", e);
}
}
}
private void checkOpen() {
if (closed) {
throw new LuaError("connection is closed");
}
}
}
static final class CursorValue extends LuaTable {
private final Statement statement;
private final ResultSet resultSet;
private boolean closed;
CursorValue(Statement statement, ResultSet resultSet) {
this.statement = statement;
this.resultSet = resultSet;
set("fetch", new fetch());
set("close", new close());
}
final class fetch extends VarArgFunction {
public Varargs invoke(Varargs args) {
checkOpen();
try {
if (!resultSet.next()) {
return LuaValue.NIL;
}
LuaValue target = args.arg(2);
String mode = args.optjstring(3, "n");
LuaTable row = target.istable() ? target.checktable() : new LuaTable();
boolean assoc = "a".equals(mode);
boolean both = "an".equals(mode) || "na".equals(mode);
if (!assoc && !both && !"n".equals(mode)) {
throw new LuaError("invalid fetch mode: " + mode);
}
ResultSetMetaData meta = resultSet.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++) {
LuaValue value = LuaSqlMysqlLib.toLuaValue(resultSet.getObject(i));
if (!assoc) {
row.set(i, value);
}
if (assoc || both) {
row.set(meta.getColumnLabel(i), value);
}
}
return row;
} catch (SQLException e) {
throw sqlError("mysql fetch failed", e);
}
}
}
final class close extends OneArgFunction {
public LuaValue call(LuaValue self) {
if (!closed) {
try {
resultSet.close();
statement.close();
} catch (SQLException e) {
throw sqlError("mysql cursor close failed", e);
}
closed = true;
}
return LuaValue.TRUE;
}
}
private void checkOpen() {
if (closed) {
throw new LuaError("cursor is closed");
}
}
}
private static LuaValue toLuaValue(Object value) {
if (value == null) {
return LuaValue.NIL;
}
if (value instanceof LuaValue) {
return (LuaValue) value;
}
if (value instanceof String) {
return LuaValue.valueOf((String) value);
}
if (value instanceof Boolean) {
return LuaValue.valueOf(((Boolean) value).booleanValue());
}
if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
return LuaValue.valueOf(((Number) value).intValue());
}
if (value instanceof Long) {
return LuaValue.valueOf(((Long) value).longValue());
}
if (value instanceof Float || value instanceof Double) {
return LuaValue.valueOf(((Number) value).doubleValue());
}
return new LuaUserdata(value);
}
private static LuaError sqlError(String prefix, SQLException e) {
return new LuaError(prefix + ": " + e.getMessage());
}
public static final class FakeMysqlDriver implements Driver {
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) {
return null;
}
return (Connection) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Connection.class },
new ConnectionHandler());
}
public boolean acceptsURL(String url) {
return url != null && url.startsWith("jdbc:mysql://");
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
public int getMajorVersion() { return 1; }
public int getMinorVersion() { return 0; }
public boolean jdbcCompliant() { return false; }
public Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new SQLFeatureNotSupportedException(); }
}
static final class ConnectionHandler implements InvocationHandler {
private boolean autoCommit = true;
private boolean closed;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
if ("createStatement".equals(name)) {
checkOpen();
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Statement.class },
new StatementHandler());
}
if ("close".equals(name)) {
closed = true;
return null;
}
if ("commit".equals(name) || "rollback".equals(name)) {
checkOpen();
return null;
}
if ("setAutoCommit".equals(name)) {
checkOpen();
autoCommit = ((Boolean) args[0]).booleanValue();
return null;
}
if ("getAutoCommit".equals(name)) {
checkOpen();
return Boolean.valueOf(autoCommit);
}
if ("isClosed".equals(name)) {
return Boolean.valueOf(closed);
}
if ("isValid".equals(name)) {
return Boolean.valueOf(!closed);
}
return defaultValue(method.getReturnType());
}
private void checkOpen() throws SQLException {
if (closed) {
throw new SQLException("connection closed");
}
}
}
static final class StatementHandler implements InvocationHandler {
private boolean closed;
private ResultSet resultSet;
private int updateCount = -1;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
if ("execute".equals(name)) {
checkOpen();
String sql = ((String) args[0]).trim();
if ("select 1 as answer, 'ok' as status".equalsIgnoreCase(sql)) {
resultSet = (ResultSet) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { ResultSet.class },
new ResultSetHandler());
updateCount = -1;
return Boolean.TRUE;
}
resultSet = null;
updateCount = 1;
return Boolean.FALSE;
}
if ("getResultSet".equals(name)) {
checkOpen();
return resultSet;
}
if ("getUpdateCount".equals(name)) {
checkOpen();
return Integer.valueOf(updateCount);
}
if ("close".equals(name)) {
closed = true;
return null;
}
if ("isClosed".equals(name)) {
return Boolean.valueOf(closed);
}
return defaultValue(method.getReturnType());
}
private void checkOpen() throws SQLException {
if (closed) {
throw new SQLException("statement closed");
}
}
}
static final class ResultSetHandler implements InvocationHandler {
private boolean closed;
private boolean delivered;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
if ("next".equals(name)) {
checkOpen();
if (!delivered) {
delivered = true;
return Boolean.TRUE;
}
return Boolean.FALSE;
}
if ("getObject".equals(name)) {
checkOpen();
int index = ((Integer) args[0]).intValue();
switch (index) {
case 1: return Integer.valueOf(1);
case 2: return "ok";
default: return null;
}
}
if ("getMetaData".equals(name)) {
checkOpen();
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { ResultSetMetaData.class },
new ResultSetMetaDataHandler());
}
if ("close".equals(name)) {
closed = true;
return null;
}
if ("isClosed".equals(name)) {
return Boolean.valueOf(closed);
}
return defaultValue(method.getReturnType());
}
private void checkOpen() throws SQLException {
if (closed) {
throw new SQLException("resultset closed");
}
}
}
static final class ResultSetMetaDataHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) {
String name = method.getName();
if ("getColumnCount".equals(name)) {
return Integer.valueOf(2);
}
if ("getColumnLabel".equals(name) || "getColumnName".equals(name)) {
int index = ((Integer) args[0]).intValue();
return index == 1 ? "answer" : "status";
}
return defaultValue(method.getReturnType());
}
}
private static Object defaultValue(Class type) {
if (type == Void.TYPE) return null;
if (type == Boolean.TYPE) return Boolean.FALSE;
if (type == Integer.TYPE) return Integer.valueOf(0);
if (type == Long.TYPE) return Long.valueOf(0L);
if (type == Double.TYPE) return Double.valueOf(0d);
if (type == Float.TYPE) return Float.valueOf(0f);
if (type == Short.TYPE) return Short.valueOf((short) 0);
if (type == Byte.TYPE) return Byte.valueOf((byte) 0);
if (type == Character.TYPE) return Character.valueOf((char) 0);
return null;
}
}

View File

@@ -26,13 +26,16 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.atomic.AtomicReference;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.luaj.vm2.libs.jse.JsePlatform;
import org.luaj.vm2.libs.ApproxLib;
import org.luaj.vm2.libs.jse.JsePlatform;
import org.luaj.vm2.libs.jse.LuaSqlMysqlLib;
import org.luaj.vm2.luajc.LuaJC;
/**
@@ -409,6 +412,32 @@ public class FragmentsTest extends TestSuite {
}
}
public void testLuaSqlMysqlRequireAndExecute() throws Exception {
LuaSqlMysqlLib.FakeMysqlDriver driver = new LuaSqlMysqlLib.FakeMysqlDriver();
DriverManager.registerDriver(driver);
try {
Globals globals = JsePlatform.standardGlobals();
Varargs result = globals.load(
"local luasql = require('luasql.mysql')\n" +
"local env = luasql.mysql('org.luaj.vm2.libs.jse.LuaSqlMysqlLib$FakeMysqlDriver')\n" +
"local conn = assert(env:connect('demo', 'u', 'p', 'localhost', 3306))\n" +
"local cur = assert(conn:execute(\"select 1 as answer, 'ok' as status\"))\n" +
"local row = assert(cur:fetch({}, 'a'))\n" +
"cur:close()\n" +
"conn:close()\n" +
"env:close()\n" +
"return row.answer, row.status\n",
"luasql_mysql.lua").invoke();
assertEquals(LuaValue.valueOf(1), result.arg1());
assertEquals(LuaValue.valueOf("ok"), result.arg(2));
} finally {
try {
DriverManager.deregisterDriver(driver);
} catch (SQLException ignore) {
}
}
}
public void testTableMove() {
runFragment(
LuaValue.varargsOf(new LuaValue[] {