diff --git a/README.md b/README.md index 300f134..fd2b030 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,285 @@ -# dlang-sqlite +# 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](#how-it-works) +- [Requirements](#requirements) +- [Building](#building) +- [Installing as a plugin](#installing-as-a-plugin) +- [Quick start](#quick-start) +- [API reference](#api-reference) +- [Result format](#result-format) +- [Asynchronous usage](#asynchronous-usage) +- [Limitations and known issues](#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: + +```bash +# 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//dlang.sqlite/`: + +- Windows: `dlang.sqlite.dll` +- Linux: `dlang.sqlite.so` + +> Note: the project only builds the module (a `SHARED` library). 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/.dll` (Windows) or +`./plugins/.so` (Linux), relative to the interpreter's working directory. + +1. Create a `plugins` folder next to the Dlang interpreter. +2. Copy the built library there and **rename** it to the name you use in `require(...)`: + + ```text + my-dlang-app/ + ├── dlang.exe # or dlang + └── plugins/ + └── sqlite.dll # Windows (built as dlang.sqlite.dll) + sqlite.so # Linux (built as dlang.sqlite.so) + ``` + +3. In your Dlang script: + + ```dlang + require("sqlite"); + ``` + + The `require` call returns the string `"plugin loaded!"` or `"Failed to load plugin!"`. + +--- + +## Quick start + +```dlang +// 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 = WAL` and `PRAGMA synchronous = NORMAL` for better performance. +- Returns `true` on success, `false` on 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](#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. + +- `sql` must be a string and `callback` must 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: + +```dlang +var res = sqlite.query("SELECT id, name FROM users;"); +foreach (row in res.rows) do + print(row.id, row.name); +end +``` + +--- + +## Asynchronous usage + +```dlang +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: + +1. **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. +2. **Single global connection.** `g_db` is a single `sqlite3*`; concurrent queries or multiple + databases are not supported. +3. **Thread safety.** `sqlite3_open`, `sqlite3_close` and `query` run without a lock alongside + the background threads of `query_async`. A `close()` during an in-flight async query can + cause crashes or undefined behaviour. SQLite is also not opened in `serialized` mode by + default. +4. **64-bit integers.** `SQLITE_INTEGER` is converted to `double`; values above 2^53 may lose + precision. +5. **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. +6. **No transactions or busy timeout.** There is no API for `BEGIN`/`COMMIT` or for + `sqlite3_busy_timeout`; on a locked database a query fails immediately with `SQLITE_BUSY`. +7. **Windows and Linux only.** The plugin loader supports no other platforms. +8. **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.