Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -185,4 +192,215 @@ public ILconstString getBuildDate() {
public ILconstBool isProductionBuild() {
return isProd ? ILconstBool.TRUE : ILconstBool.FALSE;
}

private int sqliteHandleCounter = 0;
private final Map<Integer, Connection> sqliteConnections = new HashMap<>();
private final Map<Integer, PreparedStatement> sqliteStatements = new HashMap<>();
private final Map<Integer, ResultSet> sqliteResultSets = new HashMap<>();
private final Map<Integer, Integer> 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track non-query statements after stepping

When sqlite_step is called more than once on an INSERT/UPDATE/DDL statement before sqlite_reset, sqliteResultSets is still empty, so this path calls stmt.execute() again and repeats the write instead of returning the already-reached DONE state. This can duplicate compiletime mutations in helper code that probes/steps a statement again before reset/finalize; store per-statement completion state and only execute again after sqlite_reset.

Useful? React with 👍 / 👎.

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) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
ResultSet rs = sqliteResultSets.remove(statement.getVal());
if (rs != null) rs.close();
stmt.clearParameters();
} 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().intValue() == 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());
}
}

/**
* Closes all open SQLite resources (result sets, statements, connections).
* Called by the interpreter on shutdown to prevent resource leaks.
*/
public void closeAllSqliteResources() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close SQLite resources from the runner

This cleanup hook is never invoked by CompiletimeFunctionRunner or ILInterpreter (the provider is registered and then only used through NativesProvider.invoke), so if compiletime code opens a database and then throws or forgets sqlite_close, the long-lived compiler/LSP process keeps JDBC statements/connections open and can leave file-backed SQLite databases locked across subsequent builds. Please wire this into a finally/owned provider lifecycle instead of relying on an unreachable public method.

Useful? React with 👍 / 👎.

for (ResultSet rs : sqliteResultSets.values()) {
try { rs.close(); } catch (SQLException ignored) {}
}
sqliteResultSets.clear();
for (PreparedStatement stmt : sqliteStatements.values()) {
try { stmt.close(); } catch (SQLException ignored) {}
}
sqliteStatements.clear();
sqliteStatementConnections.clear();
for (Connection conn : sqliteConnections.values()) {
try { conn.close(); } catch (SQLException ignored) {}
}
sqliteConnections.clear();
sqliteHandleCounter = 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,59 @@ 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_int(int stmt, int idx) returns int",
"@extern native sqlite_column_real(int stmt, int idx) returns real",
"@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_real(int stmt, int idx, real 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)",
"",
"function testFullSQLiteApi() returns int",
" let db = sqlite_open(\":memory:\")",
" sqlite_exec(db, \"CREATE TABLE Items (id INTEGER, name TEXT, price REAL)\")",
" let insert = sqlite_prepare(db, \"INSERT INTO Items VALUES (?, ?, ?)\")",
" sqlite_bind_int(insert, 1, 101)",
" sqlite_bind_string(insert, 2, \"Sword\")",
" sqlite_bind_real(insert, 3, 15.5)",
" sqlite_step(insert)",
" sqlite_reset(insert)",
" sqlite_bind_int(insert, 1, 102)",
" sqlite_bind_string(insert, 2, \"Shield\")",
" sqlite_bind_real(insert, 3, 25.0)",
" sqlite_step(insert)",
" sqlite_finalize(insert)",
" let query = sqlite_prepare(db, \"SELECT id, name, price FROM Items ORDER BY id ASC\")",
" let cols = sqlite_column_count(query)",
" int count = 0",
" if cols == 3 and sqlite_step(query)",
" if sqlite_column_int(query, 0) == 101 and sqlite_column_string(query, 1) == \"Sword\" and sqlite_column_real(query, 2) == 15.5",
" count++",
" if sqlite_step(query)",
" if sqlite_column_int(query, 0) == 102 and sqlite_column_string(query, 1) == \"Shield\" and sqlite_column_real(query, 2) == 25.0",
" count++",
" sqlite_finalize(query)",
" sqlite_close(db)",
" return count",
"",
"let c = compiletime(testFullSQLiteApi())",
"init",
" if c == 2",
" testSuccess()");
}
}
Loading