2024-12-16 22:20:15 +00:00
|
|
|
module db;
|
|
|
|
|
|
|
|
import std.algorithm;
|
|
|
|
import std.array;
|
|
|
|
import std.typecons;
|
|
|
|
import std.conv;
|
|
|
|
|
|
|
|
import d2sqlite3;
|
|
|
|
import slf4d;
|
|
|
|
import handy_httpd.components.optional;
|
|
|
|
|
|
|
|
struct Column {
|
|
|
|
const string name;
|
|
|
|
}
|
|
|
|
|
|
|
|
Database getDb() {
|
|
|
|
import std.file;
|
|
|
|
bool shouldInitDb = !exists("teacher-tools.db");
|
|
|
|
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
|
|
|
|
if (d2sqlite3.threadSafe()) {
|
|
|
|
flags |= SQLITE_OPEN_NOMUTEX;
|
|
|
|
}
|
|
|
|
Database db = Database("teacher-tools.db", flags);
|
|
|
|
db.execute("PRAGMA foreign_keys=ON");
|
|
|
|
if (shouldInitDb) {
|
2024-12-17 03:22:56 +00:00
|
|
|
const string authSchema = import("schema/auth.sql");
|
|
|
|
const string classroomComplianceSchema = import("schema/classroom_compliance.sql");
|
|
|
|
db.run(authSchema);
|
|
|
|
db.run(classroomComplianceSchema);
|
|
|
|
|
|
|
|
const string sampleData = import("schema/sample_data.sql");
|
|
|
|
db.run(sampleData);
|
|
|
|
|
2024-12-16 22:20:15 +00:00
|
|
|
info("Initialized database schema.");
|
|
|
|
}
|
|
|
|
return db;
|
|
|
|
}
|
|
|
|
|
2024-12-17 03:22:56 +00:00
|
|
|
T[] findAll(T, Args...)(Database db, string query, Args args) {
|
|
|
|
Statement stmt = db.prepare(query);
|
|
|
|
stmt.bindAll(args);
|
|
|
|
ResultRange result = stmt.execute();
|
|
|
|
return result.map!(row => parseRow!T(row)).array;
|
|
|
|
}
|
|
|
|
|
|
|
|
Optional!T findOne(T, Args...)(Database db, string query, 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(parseRow!T(result.front));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool canFind(Args...)(Database db, string query, Args args) {
|
|
|
|
Statement stmt = db.prepare(query);
|
|
|
|
stmt.bindAll(args);
|
|
|
|
return !stmt.execute().empty;
|
|
|
|
}
|
|
|
|
|
|
|
|
private string toSnakeCase(string camelCase) {
|
|
|
|
import std.uni;
|
|
|
|
if (camelCase.length == 0) return camelCase;
|
|
|
|
auto app = appender!string;
|
|
|
|
app ~= toLower(camelCase[0]);
|
|
|
|
for (int i = 1; i < camelCase.length; i++) {
|
|
|
|
if (isUpper(camelCase[i])) {
|
|
|
|
app ~= '_';
|
|
|
|
app ~= toLower(camelCase[i]);
|
|
|
|
} else {
|
|
|
|
app ~= camelCase[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return app[];
|
|
|
|
}
|
|
|
|
|
|
|
|
unittest {
|
|
|
|
assert(toSnakeCase("testValue") == "test_value");
|
|
|
|
}
|
|
|
|
|
2024-12-16 22:20:15 +00:00
|
|
|
private string[] getColumnNames(T)() {
|
|
|
|
import std.string : toLower;
|
|
|
|
alias members = __traits(allMembers, T);
|
|
|
|
string[members.length] columnNames;
|
|
|
|
static foreach (i; 0 .. members.length) {
|
|
|
|
static if (__traits(getAttributes, __traits(getMember, T, members[i])).length > 0) {
|
|
|
|
columnNames[i] = toLower(__traits(getAttributes, __traits(getMember, T, members[i]))[0].name);
|
|
|
|
} else {
|
2024-12-17 03:22:56 +00:00
|
|
|
columnNames[i] = toLower(toSnakeCase(members[i]));
|
2024-12-16 22:20:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return columnNames.dup;
|
|
|
|
}
|
|
|
|
|
|
|
|
private string getArgsStr(T)() {
|
|
|
|
import std.traits : RepresentationTypeTuple;
|
|
|
|
alias types = RepresentationTypeTuple!T;
|
|
|
|
string argsStr = "";
|
|
|
|
static foreach (i, type; types) {
|
|
|
|
argsStr ~= "row.peek!(" ~ type.stringof ~ ")(" ~ i.to!string ~ ")";
|
|
|
|
static if (i + 1 < types.length) {
|
|
|
|
argsStr ~= ", ";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return argsStr;
|
|
|
|
}
|
|
|
|
|
|
|
|
T parseRow(T)(Row row) {
|
|
|
|
mixin("T t = T(" ~ getArgsStr!T ~ ");");
|
|
|
|
return t;
|
|
|
|
}
|