253 lines
7.2 KiB
D
253 lines
7.2 KiB
D
module util.sqlite;
|
|
|
|
import std.datetime;
|
|
|
|
import slf4d;
|
|
import d2sqlite3;
|
|
import handy_http_primitives : Optional;
|
|
|
|
/**
|
|
* Tries to find a single row from a database.
|
|
* Params:
|
|
* db = The database to use.
|
|
* query = The query to execute.
|
|
* resultMapper = A function to map rows to the desired result type.
|
|
* args = Arguments for the query.
|
|
* Returns: An optional result.
|
|
*/
|
|
Optional!T findOne(T, Args...)(Database db, string query, T delegate(Row) resultMapper, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
ResultRange result = stmt.execute();
|
|
if (result.empty) return Optional!T.empty;
|
|
return Optional!T.of(resultMapper(result.front));
|
|
}
|
|
/// Overload that accepts a function.
|
|
Optional!T findOne(T, Args...)(Database db, string query, T function(Row) resultMapper, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
ResultRange result = stmt.execute();
|
|
if (result.empty) return Optional!T.empty;
|
|
return Optional!T.of(resultMapper(result.front));
|
|
}
|
|
|
|
/**
|
|
* Tries to find a single entity by its id, selecting all properties.
|
|
* Params:
|
|
* db = The database to use.
|
|
* table = The table to select from.
|
|
* resultMapper = A function to map rows to the desired result type.
|
|
* id = The entity's id.
|
|
* Returns: An optional result.
|
|
*/
|
|
Optional!T findById(T)(Database db, string table, T function(Row) resultMapper, ulong id) {
|
|
Statement stmt = db.prepare("SELECT * FROM " ~ table ~ " WHERE id = ?");
|
|
stmt.bind(1, id);
|
|
ResultRange result = stmt.execute();
|
|
if (result.empty) return Optional!T.empty;
|
|
return Optional!T.of(resultMapper(result.front));
|
|
}
|
|
|
|
/**
|
|
* Finds a list of records from a database.
|
|
* Params:
|
|
* db = The database to use.
|
|
* query = The query to execute.
|
|
* resultMapper = A function to map rows to the desired result type.
|
|
* args = Arguments for the query.
|
|
* Returns: A list of results.
|
|
*/
|
|
T[] findAll(T, Args...)(Database db, string query, T function(Row) resultMapper, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
import std.algorithm : map;
|
|
import std.array : array;
|
|
return stmt.execute().map!(r => resultMapper(r)).array;
|
|
}
|
|
|
|
/**
|
|
* Finds a list of records from a database, using a single function to parse
|
|
* the entire result set at once, useful for cases where records may be spread
|
|
* over multiple rows due to joined properties.
|
|
* Params:
|
|
* db = The database to use.
|
|
* query = The query to execute.
|
|
* resultMapper = A function to map the result range to the list of results.
|
|
* args = Arguments for the query.
|
|
* Returns: A list of results.
|
|
*/
|
|
T[] findAllDirect(T, Args...)(Database db, string query, T[] function(ResultRange) resultMapper, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
return resultMapper(stmt.execute());
|
|
}
|
|
|
|
/**
|
|
* Determines if at least one record exists.
|
|
* Params:
|
|
* db = The database to use.
|
|
* query = The query to execute.
|
|
* args = The arguments for the query.
|
|
* Returns: True if at least one record is returned, or false if not.
|
|
*/
|
|
bool exists(Args...)(Database db, string query, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
return !stmt.execute().empty();
|
|
}
|
|
|
|
/**
|
|
* Performs an update (UPDATE/INSERT/DELETE).
|
|
* Params:
|
|
* db = The database to use.
|
|
* query = The query to execute.
|
|
* args = The arguments for the query.
|
|
* Returns: The number of rows that were affected.
|
|
*/
|
|
int update(Args...)(Database db, string query, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
stmt.execute();
|
|
return db.changes();
|
|
}
|
|
|
|
/**
|
|
* Deletes an entity from a table.
|
|
* Params:
|
|
* db = The database to use.
|
|
* table = The table to delete from.
|
|
* id = The id of the entity to delete.
|
|
*/
|
|
void deleteById(Database db, string table, ulong id) {
|
|
Statement stmt = db.prepare("DELETE FROM " ~ table ~ " WHERE id = ?");
|
|
stmt.bind(1, id);
|
|
stmt.execute();
|
|
}
|
|
|
|
/**
|
|
* Wraps a given delegate block of code in an SQL transaction, so that all
|
|
* operations will be committed at once when done. If an exception is thrown,
|
|
* then the changes will be rolled back.
|
|
* Params:
|
|
* db = The database to use.
|
|
* dg = The delegate block of code to run in the transaction.
|
|
* Returns: The return value of the delegate, if the delegate does indeed
|
|
* return something.
|
|
*/
|
|
T doTransaction(T)(Database db, T delegate() dg) {
|
|
try {
|
|
db.begin();
|
|
static if (is(T : void)) {
|
|
dg();
|
|
} else {
|
|
T result = dg();
|
|
}
|
|
db.commit();
|
|
static if (!is(T : void)) return result;
|
|
} catch (Exception e) {
|
|
error("Rolling back transaction due to exception.", e);
|
|
db.rollback();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a "SELECT COUNT..." query on a database.
|
|
* Params:
|
|
* db = The database to use.
|
|
* query = The query to use, which must return an integer as its sole result.
|
|
* args = Arguments to provide to the query.
|
|
* Returns: The count returned by the query.
|
|
*/
|
|
ulong count(Args...)(Database db, string query, Args args) {
|
|
Statement stmt = db.prepare(query);
|
|
stmt.bindAll(args);
|
|
ResultRange result = stmt.execute();
|
|
if (result.empty) return 0;
|
|
return result.front.peek!ulong(0);
|
|
}
|
|
|
|
/**
|
|
* Reads an ISO-8601 UTC timestamp from a result row.
|
|
* Params:
|
|
* row = The row to read from.
|
|
* idx = The column index in the row to read.
|
|
* Returns: The timestamp that was read.
|
|
*/
|
|
SysTime parseISOTimestamp(Row row, size_t idx) {
|
|
return SysTime.fromISOExtString(
|
|
row.peek!(string, PeekMode.slice)(idx),
|
|
UTC()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reads a set of bytes from a result row.
|
|
* Params:
|
|
* row = The row to read from.
|
|
* idx = The column index in the row to read.
|
|
* Returns: The blob data that was read.
|
|
*/
|
|
immutable(ubyte[]) parseBlob(Row row, size_t idx) {
|
|
return row.peek!(ubyte[], PeekMode.slice)(idx).idup;
|
|
}
|
|
|
|
struct QueryBuilder {
|
|
string fromTable;
|
|
string[] selections;
|
|
string[] joins;
|
|
string[] conditions;
|
|
void delegate(ref Statement, ref int)[] argBinders;
|
|
|
|
this(string fromTable) {
|
|
this.fromTable = fromTable;
|
|
}
|
|
|
|
ref select(string expr) {
|
|
selections ~= expr;
|
|
return this;
|
|
}
|
|
|
|
ref join(string expr) {
|
|
joins ~= expr;
|
|
return this;
|
|
}
|
|
|
|
ref where(string expr) {
|
|
conditions ~= expr;
|
|
return this;
|
|
}
|
|
|
|
ref withArgBinding(void delegate(ref Statement, ref int) dg) {
|
|
argBinders ~= dg;
|
|
return this;
|
|
}
|
|
|
|
string build() const {
|
|
import std.algorithm : map;
|
|
import std.string : join;
|
|
import std.array : appender;
|
|
auto app = appender!string;
|
|
app ~= "SELECT\n";
|
|
if (selections.length > 0) {
|
|
app ~= selections.map!(s => " " ~ s).join(",\n");
|
|
} else {
|
|
app ~= " *";
|
|
}
|
|
app ~= "\nFROM " ~ fromTable ~ "\n";
|
|
app ~= joins.join("\n");
|
|
if (conditions.length > 0) {
|
|
app ~= "\nWHERE\n";
|
|
app ~= conditions.map!(s => " " ~ s).join(" AND\n");
|
|
}
|
|
return app[];
|
|
}
|
|
|
|
void applyArgBindings(ref Statement stmt) const {
|
|
int idx = 1;
|
|
foreach (binding; argBinders) {
|
|
binding(stmt, idx);
|
|
}
|
|
}
|
|
}
|