-
Notifications
You must be signed in to change notification settings - Fork 53
feat: Add SQLite compiletime native bindings #450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Donach
wants to merge
2
commits into
wurstscript:master
Choose a base branch
from
Donach:feature/sqlite-externs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+665
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| package SQLite | ||
| import ArrayList | ||
|
|
||
| // ============================================================================ | ||
| // COMPILETIME ONLY — SQLite JDBC bindings exposed by the WurstScript compiler. | ||
| // These run during compilation (@compiletime functions) and unit tests (@test). | ||
| // They do NOT exist inside Warcraft III at runtime. Do not call any sqlite_* | ||
| // native or use SqlResult / SqliteDb / SQL from code that runs in-game. | ||
| // | ||
| // UPSTREAM DEPENDENCY: Requires wurstscript/WurstScript PR #1180 which adds | ||
| // compiletime SQLite native support to the interpreter. These bindings will | ||
| // not function without that compiler-side change. | ||
| // | ||
| // ERROR BEHAVIOUR: The compiler-side natives throw InterpreterExceptions on | ||
| // failure (invalid handles, SQL errors, JDBC failures). These exceptions are | ||
| // fatal and will abort the compiletime run with an error message. There is no | ||
| // recoverable error path — if sqlite_open, sqlite_prepare, or sqlite_exec | ||
| // fails, compilation stops. | ||
| // | ||
| // SQL INJECTION WARNING: The convenience methods (exec, select, selectFirst, | ||
| // exists, count) accept raw query strings with NO parameterisation. For any | ||
| // query involving dynamic values (user input, computed strings), use the | ||
| // parameterised variants (execPrepared, selectPrepared, selectFirstPrepared) | ||
| // or call the sqlite_prepare/sqlite_bind_* natives directly. | ||
| // ============================================================================ | ||
|
|
||
| @extern public native sqlite_open(string path) returns int | ||
| @extern public native sqlite_prepare(int conn, string q) returns int | ||
| @extern public native sqlite_bind_int(int stmt, int idx, int val) | ||
| @extern public native sqlite_bind_real(int stmt, int idx, real val) | ||
| @extern public native sqlite_bind_string(int stmt, int idx, string val) | ||
| @extern public native sqlite_step(int stmt) returns boolean | ||
| @extern public native sqlite_column_count(int stmt) returns int | ||
| @extern public native sqlite_column_int(int stmt, int idx) returns int | ||
| @extern public native sqlite_column_real(int stmt, int idx) returns real | ||
| @extern public native sqlite_column_string(int stmt, int idx) returns string | ||
| @extern public native sqlite_reset(int stmt) | ||
| @extern public native sqlite_finalize(int stmt) | ||
| @extern public native sqlite_close(int conn) | ||
| @extern public native sqlite_exec(int conn, string q) | ||
|
|
||
| // The compiletime interpreter runs on JVM, so columns are dynamically | ||
| // allocated in ArrayList without hitting JASS array limits. | ||
|
|
||
| // ============================================================================ | ||
| // SqlResult — a single row from a SELECT query | ||
| // ============================================================================ | ||
|
|
||
| /** A row returned from a SELECT query. Access columns by index via col(). */ | ||
| public class SqlResult | ||
| ArrayList<string> cols = new ArrayList<string>() | ||
|
|
||
| ondestroy | ||
| destroy this.cols | ||
|
|
||
| /** Raw string value at column index. */ | ||
| function col(int index) returns string | ||
| return this.cols.get(index) | ||
|
|
||
| /** Column value parsed as int. */ | ||
| function colInt(int index) returns int | ||
| return this.cols.get(index).toInt() | ||
|
|
||
| /** Column value parsed as real. */ | ||
| function colReal(int index) returns real | ||
| return this.cols.get(index).toReal() | ||
|
|
||
| /** Column value as boolean ("1" or "true", case-insensitive → true). */ | ||
| function colBool(int index) returns boolean | ||
| let v = this.cols.get(index) | ||
| return v == "1" or v == "true" or v == "TRUE" or v == "True" | ||
|
|
||
| /** Number of columns in this row. */ | ||
| function size() returns int | ||
| return this.cols.size() | ||
|
|
||
| // ============================================================================ | ||
| // SqliteDb — OOP wrapper around a database connection | ||
| // ============================================================================ | ||
|
|
||
| /** Wraps an SQLite database connection with convenience methods. */ | ||
| public class SqliteDb | ||
| private int conn | ||
|
|
||
| /** Opens (or creates) the database at the given path. Use ":memory:" for temporary databases. */ | ||
| construct(string path) | ||
| this.conn = sqlite_open(path) | ||
|
|
||
| /** Returns the raw connection handle for direct native calls. */ | ||
| function getHandle() returns int | ||
| return this.conn | ||
|
|
||
| // -- Raw query methods (no parameterisation) -------------------------------- | ||
| // WARNING: These accept raw SQL strings. Do not interpolate untrusted values | ||
| // into the query string. Use the parameterised variants or the native | ||
| // sqlite_prepare/sqlite_bind_* functions for dynamic values. | ||
|
|
||
| /** Executes a statement that returns no rows (DDL, INSERT, UPDATE, DELETE). */ | ||
| function exec(string query) | ||
| sqlite_exec(this.conn, query) | ||
|
|
||
| private function readRow(int stmt, int colCount) returns SqlResult | ||
| let row = new SqlResult() | ||
| for i = 0 to colCount - 1 | ||
| row.cols.add(sqlite_column_string(stmt, i)) | ||
| return row | ||
|
|
||
| /** Executes a SELECT and returns all result rows. Caller owns the list | ||
| and its SqlResult entries — destroy both when done. | ||
|
|
||
| Column count is determined after the first sqlite_step to ensure the | ||
| JDBC ResultSet metadata is available. */ | ||
| function select(string query) returns ArrayList<SqlResult> | ||
| let list = new ArrayList<SqlResult>() | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
|
|
||
| if sqlite_step(stmt) | ||
| let numCols = sqlite_column_count(stmt) | ||
| list.add(readRow(stmt, numCols)) | ||
|
|
||
| while sqlite_step(stmt) | ||
| list.add(readRow(stmt, numCols)) | ||
|
|
||
| sqlite_finalize(stmt) | ||
| return list | ||
|
|
||
| /** Executes a SELECT and returns only the first row, or null if no rows match. | ||
| Caller owns the returned SqlResult — destroy it when done. */ | ||
| function selectFirst(string query) returns SqlResult | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
| SqlResult row = null | ||
|
|
||
| if sqlite_step(stmt) | ||
| let numCols = sqlite_column_count(stmt) | ||
| row = readRow(stmt, numCols) | ||
|
|
||
| sqlite_finalize(stmt) | ||
| return row | ||
|
|
||
| /** Returns true if the query produces at least one row. */ | ||
| function exists(string query) returns boolean | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
| let found = sqlite_step(stmt) | ||
| sqlite_finalize(stmt) | ||
| return found | ||
|
|
||
| /** Runs a "SELECT count(…)" query and returns the first column of the first row as int. */ | ||
| function count(string query) returns int | ||
| let row = this.selectFirst(query) | ||
| if row != null | ||
| let c = row.colInt(0) | ||
| destroy row | ||
| return c | ||
| return 0 | ||
|
|
||
| // -- Parameterised query methods ------------------------------------------- | ||
| // These use sqlite_prepare + sqlite_bind_* under the hood. Pass parameter | ||
| // values as a ArrayList<string>. Each "?" placeholder in the query is bound | ||
| // to the corresponding list entry (index 1-based for JDBC). All values are | ||
| // bound as strings; SQLite's type affinity handles coercion. | ||
|
|
||
| /** Binds a list of string parameters to a prepared statement, 1-indexed. */ | ||
| private function bindParams(int stmt, ArrayList<string> params) | ||
| var idx = 1 | ||
| for p in params | ||
| sqlite_bind_string(stmt, idx, p) | ||
| idx++ | ||
|
|
||
| /** Executes a parameterised statement that returns no rows. | ||
| Example: db.execPrepared("INSERT INTO t VALUES (?, ?)", asList("foo", "42")) */ | ||
| function execPrepared(string query, ArrayList<string> params) | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
| bindParams(stmt, params) | ||
| sqlite_step(stmt) | ||
| sqlite_finalize(stmt) | ||
|
|
||
| /** Executes a parameterised SELECT and returns all result rows. | ||
| Caller owns the list and its SqlResult entries — destroy both when done. */ | ||
| function selectPrepared(string query, ArrayList<string> params) returns ArrayList<SqlResult> | ||
| let list = new ArrayList<SqlResult>() | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
| bindParams(stmt, params) | ||
|
|
||
| if sqlite_step(stmt) | ||
| let numCols = sqlite_column_count(stmt) | ||
| list.add(readRow(stmt, numCols)) | ||
|
|
||
| while sqlite_step(stmt) | ||
| list.add(readRow(stmt, numCols)) | ||
|
|
||
| sqlite_finalize(stmt) | ||
| return list | ||
|
|
||
| /** Executes a parameterised SELECT and returns only the first row, or null. */ | ||
| function selectFirstPrepared(string query, ArrayList<string> params) returns SqlResult | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
| bindParams(stmt, params) | ||
| SqlResult row = null | ||
|
|
||
| if sqlite_step(stmt) | ||
| let numCols = sqlite_column_count(stmt) | ||
| row = readRow(stmt, numCols) | ||
|
|
||
| sqlite_finalize(stmt) | ||
| return row | ||
|
|
||
| /** Returns true if the parameterised query produces at least one row. */ | ||
| function existsPrepared(string query, ArrayList<string> params) returns boolean | ||
| let stmt = sqlite_prepare(this.conn, query) | ||
| bindParams(stmt, params) | ||
| let found = sqlite_step(stmt) | ||
| sqlite_finalize(stmt) | ||
| return found | ||
|
|
||
| ondestroy | ||
| sqlite_close(this.conn) | ||
|
|
||
| // ============================================================================ | ||
| // SQL — configurable singleton for the common single-database workflow | ||
| // ============================================================================ | ||
|
|
||
| /** Override this in your package to point at your project database. */ | ||
| @configurable public constant SQL_DATABASE_PATH = ":memory:" | ||
|
|
||
| /** Singleton database access. Configure the path via SQL_DATABASE_PATH. */ | ||
| public class SQL | ||
| private static SqliteDb db = null | ||
|
|
||
| private static function connection() returns SqliteDb | ||
| if db == null | ||
| db = new SqliteDb(SQL_DATABASE_PATH) | ||
| return db | ||
|
|
||
| static function exec(string query) | ||
| connection().exec(query) | ||
|
|
||
| static function select(string query) returns ArrayList<SqlResult> | ||
| return connection().select(query) | ||
|
|
||
| static function selectFirst(string query) returns SqlResult | ||
| return connection().selectFirst(query) | ||
|
|
||
| static function exists(string query) returns boolean | ||
| return connection().exists(query) | ||
|
|
||
| static function count(string query) returns int | ||
| return connection().count(query) | ||
|
|
||
| // -- Parameterised convenience methods -- | ||
|
|
||
| static function execPrepared(string query, ArrayList<string> params) | ||
| connection().execPrepared(query, params) | ||
|
|
||
| static function selectPrepared(string query, ArrayList<string> params) returns ArrayList<SqlResult> | ||
| return connection().selectPrepared(query, params) | ||
|
|
||
| static function selectFirstPrepared(string query, ArrayList<string> params) returns SqlResult | ||
| return connection().selectFirstPrepared(query, params) | ||
|
|
||
| static function existsPrepared(string query, ArrayList<string> params) returns boolean | ||
| return connection().existsPrepared(query, params) | ||
|
|
||
| /** Returns the underlying SqliteDb instance. */ | ||
| static function getDb() returns SqliteDb | ||
| return connection() | ||
|
|
||
| /** Returns the raw connection handle for direct native calls. */ | ||
| static function getHandle() returns int | ||
| return connection().getHandle() | ||
|
|
||
| /** Closes the singleton connection. Next call reopens it. */ | ||
| static function close() | ||
| if db != null | ||
| destroy db | ||
| db = null | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When prepared queries are used, this iterates an
ArrayList<string>withfor p in params, butArrayListin this repo does not expose an iterator and is documented/implemented for index-based traversal only. That makes the new SQLite package fail to typecheck as soon as this method is compiled; the same pattern is repeated in the new tests overrows, so these should be changed tofor i = 0 to params.size() - 1/params.get(i)style loops.Useful? React with 👍 / 👎.