diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java index 11d188e46..498796373 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/intermediateLang/interpreter/CompiletimeNatives.java @@ -16,8 +16,15 @@ import net.moonlightflower.wc3libs.misc.MetaFieldId; import net.moonlightflower.wc3libs.misc.ObjId; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; @SuppressWarnings("ucd") // ignore unused code detector warnings, because this class uses reflection public class CompiletimeNatives extends ReflectionBasedNativeProvider implements NativesProvider { @@ -185,4 +192,193 @@ public ILconstString getBuildDate() { public ILconstBool isProductionBuild() { return isProd ? ILconstBool.TRUE : ILconstBool.FALSE; } + + private int sqliteHandleCounter = 0; + private final Map sqliteConnections = new HashMap<>(); + private final Map sqliteStatements = new HashMap<>(); + private final Map sqliteResultSets = new HashMap<>(); + private final Map sqliteStatementConnections = new HashMap<>(); + + private Connection sqliteConnection(int handle) { + Connection connection = sqliteConnections.get(handle); + if (connection == null) { + throw new InterpreterException("Invalid SQLite connection handle: " + handle); + } + return connection; + } + + private PreparedStatement sqliteStatement(int handle) { + PreparedStatement statement = sqliteStatements.get(handle); + if (statement == null) { + throw new InterpreterException("Invalid SQLite statement handle: " + handle); + } + return statement; + } + + public ILconstInt sqlite_open(ILconstString path) { + try { + Connection conn = DriverManager.getConnection("jdbc:sqlite:" + path.getVal()); + int handle = ++sqliteHandleCounter; + sqliteConnections.put(handle, conn); + return new ILconstInt(handle); + } catch (SQLException e) { + throw new InterpreterException("Failed to open SQLite database " + path.getVal() + ": " + e.getMessage()); + } + } + + public ILconstInt sqlite_prepare(ILconstInt connection, ILconstString query) { + Connection conn = sqliteConnection(connection.getVal()); + try { + PreparedStatement stmt = conn.prepareStatement(query.getVal()); + int handle = ++sqliteHandleCounter; + sqliteStatements.put(handle, stmt); + sqliteStatementConnections.put(handle, connection.getVal()); + return new ILconstInt(handle); + } catch (SQLException e) { + throw new InterpreterException("Failed to prepare SQLite statement: " + e.getMessage()); + } + } + + public void sqlite_bind_int(ILconstInt statement, ILconstInt index, ILconstInt value) { + PreparedStatement stmt = sqliteStatement(statement.getVal()); + try { + stmt.setInt(index.getVal(), value.getVal()); + } catch (SQLException e) { + throw new InterpreterException("Failed to bind int: " + e.getMessage()); + } + } + + public void sqlite_bind_real(ILconstInt statement, ILconstInt index, ILconstReal value) { + PreparedStatement stmt = sqliteStatement(statement.getVal()); + try { + stmt.setDouble(index.getVal(), (double) value.getVal()); + } catch (SQLException e) { + throw new InterpreterException("Failed to bind real: " + e.getMessage()); + } + } + + public void sqlite_bind_string(ILconstInt statement, ILconstInt index, ILconstString value) { + PreparedStatement stmt = sqliteStatement(statement.getVal()); + try { + stmt.setString(index.getVal(), value.getVal()); + } catch (SQLException e) { + throw new InterpreterException("Failed to bind string: " + e.getMessage()); + } + } + + public ILconstBool sqlite_step(ILconstInt statement) { + PreparedStatement stmt = sqliteStatement(statement.getVal()); + try { + ResultSet rs = sqliteResultSets.get(statement.getVal()); + if (rs == null) { + boolean hasResultSet = stmt.execute(); + if (hasResultSet) { + rs = stmt.getResultSet(); + sqliteResultSets.put(statement.getVal(), rs); + boolean hasRow = rs.next(); + return hasRow ? ILconstBool.TRUE : ILconstBool.FALSE; + } else { + return ILconstBool.FALSE; + } + } else { + boolean hasRow = rs.next(); + return hasRow ? ILconstBool.TRUE : ILconstBool.FALSE; + } + } catch (SQLException e) { + throw new InterpreterException("Failed to step SQLite statement: " + e.getMessage()); + } + } + + public ILconstInt sqlite_column_count(ILconstInt statement) { + try { + ResultSet rs = sqliteResultSets.get(statement.getVal()); + if (rs != null) { + return new ILconstInt(rs.getMetaData().getColumnCount()); + } + PreparedStatement stmt = sqliteStatement(statement.getVal()); + java.sql.ResultSetMetaData meta = stmt.getMetaData(); + return new ILconstInt(meta == null ? 0 : meta.getColumnCount()); + } catch (SQLException e) { + throw new InterpreterException("Failed to get column count: " + e.getMessage()); + } + } + + public ILconstInt sqlite_column_int(ILconstInt statement, ILconstInt index) { + ResultSet rs = sqliteResultSets.get(statement.getVal()); + if (rs == null) throw new InterpreterException("No result set for statement handle: " + statement.getVal()); + try { + return new ILconstInt(rs.getInt(index.getVal() + 1)); + } catch (SQLException e) { + throw new InterpreterException("Failed to get column int: " + e.getMessage()); + } + } + + public ILconstReal sqlite_column_real(ILconstInt statement, ILconstInt index) { + ResultSet rs = sqliteResultSets.get(statement.getVal()); + if (rs == null) throw new InterpreterException("No result set for statement handle: " + statement.getVal()); + try { + return new ILconstReal((float) rs.getDouble(index.getVal() + 1)); + } catch (SQLException e) { + throw new InterpreterException("Failed to get column real: " + e.getMessage()); + } + } + + public ILconstString sqlite_column_string(ILconstInt statement, ILconstInt index) { + ResultSet rs = sqliteResultSets.get(statement.getVal()); + if (rs == null) throw new InterpreterException("No result set for statement handle: " + statement.getVal()); + try { + String val = rs.getString(index.getVal() + 1); + return new ILconstString(val == null ? "" : val); + } catch (SQLException e) { + throw new InterpreterException("Failed to get column string: " + e.getMessage()); + } + } + + public void sqlite_reset(ILconstInt statement) { + sqliteStatement(statement.getVal()); + try { + ResultSet rs = sqliteResultSets.remove(statement.getVal()); + if (rs != null) rs.close(); + } catch (SQLException e) { + throw new InterpreterException("Failed to reset SQLite statement: " + e.getMessage()); + } + } + + public void sqlite_finalize(ILconstInt statement) { + PreparedStatement stmt = sqliteStatements.remove(statement.getVal()); + if (stmt == null) throw new InterpreterException("Invalid SQLite statement handle: " + statement.getVal()); + sqliteStatementConnections.remove(statement.getVal()); + try { + ResultSet rs = sqliteResultSets.remove(statement.getVal()); + if (rs != null) rs.close(); + stmt.close(); + } catch (SQLException e) { + throw new InterpreterException("Failed to finalize SQLite statement: " + e.getMessage()); + } + } + + public void sqlite_close(ILconstInt connection) { + Connection conn = sqliteConnections.remove(connection.getVal()); + if (conn == null) throw new InterpreterException("Invalid SQLite connection handle: " + connection.getVal()); + for (int statement : sqliteStatementConnections.entrySet().stream() + .filter(entry -> entry.getValue() == connection.getVal()) + .map(Map.Entry::getKey) + .toList()) { + sqlite_finalize(new ILconstInt(statement)); + } + try { + conn.close(); + } catch (SQLException e) { + throw new InterpreterException("Failed to close SQLite connection: " + e.getMessage()); + } + } + + public void sqlite_exec(ILconstInt connection, ILconstString query) { + Connection conn = sqliteConnection(connection.getVal()); + try (java.sql.Statement stmt = conn.createStatement()) { + stmt.execute(query.getVal()); + } catch (SQLException e) { + throw new InterpreterException("Failed to exec SQLite query: " + e.getMessage()); + } + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java index 5f41d6a11..202cd44dc 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/CompiletimeTests.java @@ -422,4 +422,76 @@ public void nullBug() { } + @Test + public void testCompiletimeSQLite() { + test().withStdLib() + .executeProg(true) + .runCompiletimeFunctions(true) + .executeProgOnlyAfterTransforms() + .lines("package Test", + "import LinkedList", + "@extern native sqlite_open(string path) returns int", + "@extern native sqlite_prepare(int conn, string q) returns int", + "@extern native sqlite_step(int stmt) returns boolean", + "@extern native sqlite_column_string(int stmt, int idx) returns string", + "@extern native sqlite_column_count(int stmt) returns int", + "@extern native sqlite_exec(int conn, string q)", + "@extern native sqlite_bind_int(int stmt, int idx, int value)", + "@extern native sqlite_bind_string(int stmt, int idx, string value)", + "@extern native sqlite_reset(int stmt)", + "@extern native sqlite_finalize(int stmt)", + "@extern native sqlite_close(int conn)", + "", + "class SqlResult", + " string v1 = \"\"", + " string v2 = \"\"", + " string v3 = \"\"", + " string v4 = \"\"", + "", + "function sqlite_select(int db, string query) returns LinkedList", + " let list = new LinkedList()", + " let stmt = sqlite_prepare(db, query)", + " let cols = sqlite_column_count(stmt)", + " while sqlite_step(stmt)", + " let row = new SqlResult()", + " if cols > 0", + " row.v1 = sqlite_column_string(stmt, 0)", + " if cols > 1", + " row.v2 = sqlite_column_string(stmt, 1)", + " if cols > 2", + " row.v3 = sqlite_column_string(stmt, 2)", + " if cols > 3", + " row.v4 = sqlite_column_string(stmt, 3)", + " list.add(row)", + " sqlite_finalize(stmt)", + " return list", + "", + "function testSelect() returns int", + " let db = sqlite_open(\":memory:\")", + " sqlite_exec(db, \"CREATE TABLE Jobs (id INTEGER, name TEXT, desc TEXT, val TEXT)\")", + " let insert = sqlite_prepare(db, \"INSERT INTO Jobs VALUES (?, ?, 'Melee C', 'A')\")", + " sqlite_bind_int(insert, 1, 1)", + " sqlite_bind_string(insert, 2, \"Warrior\")", + " sqlite_step(insert)", + " sqlite_reset(insert)", + " sqlite_finalize(insert)", + " sqlite_exec(db, \"INSERT INTO Jobs VALUES (2, 'Mage', 'Ranged C', 'B')\")", + " let res = sqlite_select(db, \"SELECT * FROM Jobs ORDER BY id ASC\")", + " int count = 0", + " if res.size() == 2", + " let first = res.get(0)", + " if first.v1 == \"1\" and first.v2 == \"Warrior\"", + " count++", + " let second = res.get(1)", + " if second.v1 == \"2\" and second.v2 == \"Mage\"", + " count++", + " sqlite_close(db)", + " return count", + "", + "let c = compiletime(testSelect())", + "init", + " if c == 2", + " testSuccess()"); + } + }