dlang.sqlite
🤖 AI-generated
This project, its source code and this documentation were (largely) generated by AI (GitHub Copilot). The code has not been fully reviewed by a human for correctness, security or production readiness.
Use at your own risk. Review the code before putting it into production, and be extra careful about the limitations listed below (e.g. no parameter binding).
SQLite module (plugin) for the Dlang interpreter. The module lets a Dlang program open a SQLite database, run queries, and run queries asynchronously without blocking the VM.
Contents
- How it works
- Requirements
- Building
- Installing as a plugin
- Quick start
- API reference
- Result format
- Asynchronous usage
- Limitations and known issues
How it works
The plugin builds a native shared library (dlang.sqlite.dll on Windows, dlang.sqlite.so
on Linux). The Dlang interpreter loads it dynamically via require("sqlite"), which pulls the
file from the ./plugins/ folder next to the interpreter.
The plugin exports a single function, initialize, which creates a global sqlite table in
the VM containing the functions open, query, query_async, poll and close.
Queries are executed with sqlite3_prepare_v2 and the results are pushed onto the VM stack as
Dlang tables and lists.
Requirements
| Component | Version / note |
|---|---|
| CMake | 3.10 or newer |
| C++ compiler | C++20 (MSVC, GCC or Clang) |
| Git | required for the SQLite FetchContent |
| Ninja | used by the CMake presets |
| SQLite | 3.49.1, fetched automatically via FetchContent (sjinks/sqlite3-cmake) |
A working Dlang interpreter is needed to actually load the plugin.
Building
The project uses CMakePresets.json. Use the CMake extension in VS Code, or build manually
from the command line:
# Configure (pick the preset that matches your platform)
cmake --preset x64-release
# Build
cmake --build --preset x64-release
Available presets:
| Preset | Platform |
|---|---|
x64-debug |
Windows x64, Debug |
x64-release |
Windows x64, Release |
x86-debug |
Windows x86, Debug |
x86-release |
Windows x86, Release |
linux-debug |
Linux, Debug |
macos-debug |
macOS, Debug |
The build output lands in out/build/<preset>/dlang.sqlite/:
- Windows:
dlang.sqlite.dll - Linux:
dlang.sqlite.so
Note: the project only builds the module (a
SHAREDlibrary). There is no executable; the module is meant to be loaded by the Dlang interpreter.
Installing as a plugin
The plugin loader (plugins.hpp) looks for the file as ./plugins/<name>.dll (Windows) or
./plugins/<name>.so (Linux), relative to the interpreter's working directory.
-
Create a
pluginsfolder next to the Dlang interpreter. -
Copy the built library there and rename it to the name you use in
require(...):my-dlang-app/ ├── dlang.exe # or dlang └── plugins/ └── sqlite.dll # Windows (built as dlang.sqlite.dll) sqlite.so # Linux (built as dlang.sqlite.so) -
In your Dlang script:
require("sqlite");The
requirecall returns the string"plugin loaded!"or"Failed to load plugin!".
Quick start
// main.dl
require("sqlite");
// 1. Open the database (creates the file if it does not exist)
if (sqlite.open("test.db")) then
print("Database opened");
else
print("Could not open the database");
end
// 2. Create and populate a table
sqlite.query("CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT);");
var ins = sqlite.query("INSERT INTO users (id, name) VALUES (1, 'Ada');");
print("Rows changed:", ins.rowsAffected);
// 3. A query that returns rows
var res = sqlite.query("SELECT id, name FROM users;");
if (res.success) then
foreach (row in res.rows) do
print(row.name);
end
else
print("Error:", res.error);
end
// 4. Shut down
sqlite.close();
The syntax above follows the Dlang language (
func,if ... then ... end,foreach (x in list) do ... end, comments with//or--).
API reference
The module is available as the global sqlite table (after require("sqlite")).
sqlite.open(path) -> bool
Opens (or creates) a database file at path.
- If a connection is already open, it is closed first.
- Sets
PRAGMA journal_mode = WALandPRAGMA synchronous = NORMALfor better performance. - Returns
trueon success,falseon failure or if the argument is not a string. - The path is taken from the argument; there is a single global connection (
g_db).
sqlite.query(sql) -> table
Runs the SQL synchronously and returns a result table (see Result format).
Works for SELECT, INSERT, UPDATE, DELETE, DDL and multiple statements in one string.
sqlite.query_async(sql, callback)
Runs the SQL on a background thread and calls callback later with the result. The call
does not block the VM.
sqlmust be a string andcallbackmust be a function; otherwise nothing happens.- The result is pushed onto a queue; the callback is only invoked during a call to
sqlite.poll(). - Returns nothing.
sqlite.poll()
Drains all completed asynchronous tasks and invokes their callbacks (each with one argument: the result table). Call this regularly in your main loop.
sqlite.close() -> bool
Closes the global database connection and returns true.
Result format
query and the query_async callback return a table with these fields:
| Field | Type | Meaning |
|---|---|---|
success |
bool | true if the query ran without error |
error |
string | SQLite error message; only present when success is false |
rowsAffected |
number | Number of changed rows (sqlite3_changes) |
rows |
list | List of rows; each row is a table of column name → value |
Values inside a row are converted as follows:
| SQLite type | Dlang type |
|---|---|
INTEGER |
number (via double, so 64-bit precision may be lost) |
FLOAT |
number |
TEXT |
string |
NULL / other |
nil |
Example of reading a row:
var res = sqlite.query("SELECT id, name FROM users;");
foreach (row in res.rows) do
print(row.id, row.name);
end
Asynchronous usage
require("sqlite");
sqlite.open("test.db");
func onResult(res)
if (res.success) then
print("Query finished, rows:", res.rowsAffected);
else
print("Query failed:", res.error);
end
end
// Start the query on a background thread
sqlite.query_async("SELECT * FROM users;", onResult);
// Process completed tasks in the main loop
while (true) do
sqlite.poll();
end
Important:
- Without regular
sqlite.poll()calls, callbacks are never invoked. - The callback always runs on the main thread (during
poll), never on the background thread.
Limitations and known issues
These are deliberate simplifications or real risks in the current, AI-generated code:
- No parameter binding. SQL is passed to SQLite as a plain string. Never put user input directly into a query → SQL injection. This is the most important limitation to fix.
- Single global connection.
g_dbis a singlesqlite3*; concurrent queries or multiple databases are not supported. - Thread safety.
sqlite3_open,sqlite3_closeandqueryrun without a lock alongside the background threads ofquery_async. Aclose()during an in-flight async query can cause crashes or undefined behaviour. SQLite is also not opened inserializedmode by default. - 64-bit integers.
SQLITE_INTEGERis converted todouble; values above 2^53 may lose precision. - Column names as keys. For columns with the same name (e.g. from a
JOIN) and for unnamed expressions, the last value wins; the column name can be empty. - No transactions or busy timeout. There is no API for
BEGIN/COMMITor forsqlite3_busy_timeout; on a locked database a query fails immediately withSQLITE_BUSY. - Windows and Linux only. The plugin loader supports no other platforms.
- AI code. This module is AI-generated and not fully tested. The project contains no tests.
License
There is no license file in this repository yet. Add a license before reusing or distributing this project.