arsd/database_generation.d

991 lines
26 KiB
D

/++
Helper functions for generating database stuff.
Note: this is heavily biased toward Postgres
+/
module arsd.database_generation;
/*
FIXME: support partial indexes and maybe "using"
FIXME: support views
Let's put indexes in there too and make index functions be the preferred way of doing a query
by making them convenient af.
*/
private enum UDA;
@UDA struct PrimaryKey {
string sql;
}
@UDA struct Default {
string sql;
}
@UDA struct Unique { }
@UDA struct ForeignKey(alias toWhat, string behavior) {
alias ReferencedTable = __traits(parent, toWhat);
}
enum CASCADE = "ON UPDATE CASCADE ON DELETE CASCADE";
enum NULLIFY = "ON UPDATE CASCADE ON DELETE SET NULL";
enum RESTRICT = "ON UPDATE CASCADE ON DELETE RESTRICT";
@UDA struct DBName { string name; }
struct Nullable(T) {
bool isNull = true;
T value;
void opAssign(typeof(null)) {
isNull = true;
}
void opAssign(T v) {
isNull = false;
value = v;
}
T toArsdJsvar() { return value; }
string toString() {
import std.conv;
if (isNull) {
return "Nullable.null";
}
else {
return to!string(this.value);
}
}
}
struct Timestamp {
string value;
string toArsdJsvar() { return value; }
// FIXME: timezone
static Timestamp fromStrings(string date, string time) {
if(time.length < 6)
time ~= ":00";
import std.datetime;
return Timestamp(SysTime.fromISOExtString(date ~ "T" ~ time).toISOExtString());
}
}
SysTime parseDbTimestamp(Timestamp s) {
return parseDbTimestamp(s.value);
}
SysTime parseDbTimestamp(string s) {
if(s.length == 0) return SysTime.init;
auto date = s[0 .. 10];
auto time = s[11 .. 20];
auto tz = s[20 .. $];
return SysTime.fromISOExtString(date ~ "T" ~ time ~ tz);
}
struct Constraint(string sql) {}
struct Index(Fields...) {}
struct UniqueIndex(Fields...) {}
struct Serial {
int value;
int toArsdJsvar() { return value; }
int getValue() { return value; }
alias getValue this;
}
string generateCreateTableFor(alias O)() {
enum tableName = tableNameFor!O();
string sql = "CREATE TABLE " ~ tableName ~ " (";
string postSql;
bool outputtedPostSql = false;
string afterTableSql;
void addAfterTableSql(string s) {
afterTableSql ~= s;
afterTableSql ~= "\n";
}
void addPostSql(string s) {
if(outputtedPostSql) {
postSql ~= ",";
}
postSql ~= "\n";
postSql ~= "\t" ~ s;
outputtedPostSql = true;
}
bool outputted = false;
static foreach(memberName; __traits(allMembers, O)) {{
alias member = __traits(getMember, O, memberName);
static if(is(typeof(member) == Constraint!constraintSql, string constraintSql)) {
version(dbgenerate_sqlite) {} else { // FIXME: make it work here too, it is the specifics of the constraint strings
if(outputted) {
sql ~= ",";
}
sql ~= "\n";
sql ~= "\tCONSTRAINT " ~ memberName;
sql ~= " ";
sql ~= constraintSql;
outputted = true;
}
} else static if(is(typeof(member) == Index!Fields, Fields...)) {
string fields = "";
static foreach(field; Fields) {
if(fields.length)
fields ~= ", ";
fields ~= __traits(identifier, field);
}
addAfterTableSql("CREATE INDEX " ~ tableName ~ "_" ~ memberName ~ " ON " ~ tableName ~ "("~fields~")");
} else static if(is(typeof(member) == UniqueIndex!Fields, Fields...)) {
string fields = "";
static foreach(field; Fields) {
if(fields.length)
fields ~= ", ";
fields ~= __traits(identifier, field);
}
addAfterTableSql("CREATE UNIQUE INDEX " ~ tableName ~ "_" ~ memberName ~ " ON " ~ tableName ~ "("~fields~")");
} else static if(is(typeof(member) T)) {
if(outputted) {
sql ~= ",";
}
sql ~= "\n";
sql ~= "\t" ~ memberName;
static if(is(T == Nullable!P, P)) {
static if(is(P == int))
sql ~= " INTEGER NULL";
else static if(is(P == string))
sql ~= " TEXT NULL";
else static if(is(P == double))
sql ~= " FLOAT NULL";
else static if(is(P == Timestamp))
sql ~= " TIMESTAMPTZ NULL";
else static assert(0, P.stringof);
} else static if(is(T == int))
sql ~= " INTEGER NOT NULL";
else static if(is(T == Serial)) {
version(dbgenerate_sqlite)
sql ~= " INTEGER PRIMARY KEY AUTOINCREMENT";
else
sql ~= " SERIAL"; // FIXME postgresism
} else static if(is(T == string))
sql ~= " TEXT NOT NULL";
else static if(is(T == double))
sql ~= " FLOAT NOT NULL";
else static if(is(T == bool))
sql ~= " BOOLEAN NOT NULL";
else static if(is(T == Timestamp)) {
version(dbgenerate_sqlite)
sql ~= " TEXT NOT NULL";
else
sql ~= " TIMESTAMPTZ NOT NULL"; // FIXME: postgresism
} else static if(is(T == enum))
sql ~= " INTEGER NOT NULL"; // potentially crap but meh
static foreach(attr; __traits(getAttributes, member)) {
static if(is(typeof(attr) == Default)) {
// FIXME: postgresism there, try current_timestamp in sqlite
version(dbgenerate_sqlite) {
import std.string;
sql ~= " DEFAULT " ~ std.string.replace(attr.sql, "now()", "current_timestamp");
} else
sql ~= " DEFAULT " ~ attr.sql;
} else static if(is(attr == Unique)) {
sql ~= " UNIQUE";
} else static if(is(attr == PrimaryKey)) {
version(dbgenerate_sqlite) {
static if(is(T == Serial)) {} // skip, it is done above
else
addPostSql("PRIMARY KEY(" ~ memberName ~ ")");
} else
addPostSql("PRIMARY KEY(" ~ memberName ~ ")");
} else static if(is(attr == ForeignKey!(to, sqlPolicy), alias to, string sqlPolicy)) {
string refTable = tableNameFor!(__traits(parent, to))();
string refField = to.stringof;
addPostSql("FOREIGN KEY(" ~ memberName ~ ") REFERENCES "~refTable~"("~refField~(sqlPolicy.length ? ") " : ")") ~ sqlPolicy);
}
}
outputted = true;
}
}}
if(postSql.length && outputted)
sql ~= ",\n";
sql ~= postSql;
sql ~= "\n);\n";
sql ~= afterTableSql;
return sql;
}
string tableNameFor(T)(string def = toTableName(T.stringof)) {
foreach(attr; __traits(getAttributes, T))
static if(is(typeof(attr) == DBName))
def = attr.name;
return def;
}
string toTableName(string t) {
return plural(50, beautify(t, '_', true));
}
// copy/pasted from english.d
private string plural(int count, string word, string pluralWord = null) {
if(count == 1 || word.length == 0)
return word; // it isn't actually plural
if(pluralWord !is null)
return pluralWord;
switch(word[$ - 1]) {
case 's':
return word ~ "es";
case 'f':
return word[0 .. $-1] ~ "ves";
case 'y':
return word[0 .. $-1] ~ "ies";
case 'a', 'e', 'i', 'o', 'u':
default:
return word ~ "s";
}
}
// copy/pasted from cgi
private string beautify(string name, char space = ' ', bool allLowerCase = false) {
if(name == "id")
return allLowerCase ? name : "ID";
char[160] buffer;
int bufferIndex = 0;
bool shouldCap = true;
bool shouldSpace;
bool lastWasCap;
foreach(idx, char ch; name) {
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
if((ch >= 'A' && ch <= 'Z') || ch == '_') {
if(lastWasCap) {
// two caps in a row, don't change. Prolly acronym.
} else {
if(idx)
shouldSpace = true; // new word, add space
}
lastWasCap = true;
} else {
lastWasCap = false;
}
if(shouldSpace) {
buffer[bufferIndex++] = space;
if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important
shouldSpace = false;
}
if(shouldCap) {
if(ch >= 'a' && ch <= 'z')
ch -= 32;
shouldCap = false;
}
if(allLowerCase && ch >= 'A' && ch <= 'Z')
ch += 32;
buffer[bufferIndex++] = ch;
}
return buffer[0 .. bufferIndex].idup;
}
import arsd.database;
/++
+/
void save(O)(ref O t, Database db) {
t.insert(db);
}
/++
+/
void insert(O)(ref O t, Database db) {
auto builder = new InsertBuilder;
builder.setTable(tableNameFor!O());
static foreach(memberName; __traits(allMembers, O)) {{
alias member = __traits(getMember, O, memberName);
static if(is(typeof(member) T)) {
static if(is(T == Nullable!P, P)) {
auto v = __traits(getMember, t, memberName);
if(v.isNull)
builder.addFieldWithSql(memberName, "NULL");
else
builder.addVariable(memberName, v.value);
} else static if(is(T == int))
builder.addVariable(memberName, __traits(getMember, t, memberName));
else static if(is(T == Serial)) {
auto v = __traits(getMember, t, memberName).value;
if(v) {
builder.addVariable(memberName, v);
} else {
// skip and let it auto-fill
}
} else static if(is(T == string)) {
builder.addVariable(memberName, __traits(getMember, t, memberName));
} else static if(is(T == double))
builder.addVariable(memberName, __traits(getMember, t, memberName));
else static if(is(T == bool))
builder.addVariable(memberName, __traits(getMember, t, memberName));
else static if(is(T == Timestamp)) {
auto v = __traits(getMember, t, memberName).value;
if(v.length)
builder.addVariable(memberName, v);
} else static if(is(T == enum))
builder.addVariable(memberName, cast(int) __traits(getMember, t, memberName));
}
}}
import std.conv;
version(dbgenerate_sqlite) {
builder.execute(db);
foreach(row; db.query("SELECT max(id) FROM " ~ tableNameFor!O()))
t.id.value = to!int(row[0]);
} else {
static if (__traits(hasMember, O, "id"))
{
foreach(row; builder.execute(db, "RETURNING id")) // FIXME: postgres-ism
t.id.value = to!int(row[0]);
}
else
{
builder.execute(db);
}
}
}
// Check that insert doesn't require an `id`
unittest
{
static struct NoPK
{
int a;
}
alias test = insert!NoPK;
}
///
class RecordNotFoundException : Exception {
this() { super("RecordNotFoundException"); }
}
/++
Returns a given struct populated from the database. Assumes types known to this module.
MyItem item = db.find!(MyItem.id)(3);
If you just give a type, it assumes the relevant index is "id".
+/
static auto find(alias T)(Database db, int id) {
// FIXME:
// if it is unique, return an individual item.
// if not, return the array
static if (!is(T)) {
static const string fieldName = T.stringof;
alias FType = typeof(T); // field type
alias TType = __traits(parent, T); // Table type
}
else {
static const string fieldName = "id";
alias FType = int;
alias TType = T;
}
static assert(is(FType : int),
TType.stringof ~ "." ~ fieldName ~ " should be an Integral field");
string q = "SELECT * FROM " ~ tableNameFor!TType() ~ " WHERE " ~ fieldName ~ " = ?";
foreach(record; db.query(q, id)) {
TType t;
populateFromDbRow(t, record);
return t;
// if there is ever a second record, that's a wtf, but meh.
}
throw new RecordNotFoundException();
}
private void populateFromDbRow(T)(ref T t, Row record) {
foreach(field, value; record) {
sw: switch(field) {
static foreach(const idx, alias mem; T.tupleof) {
case __traits(identifier, mem):
populateFromDbVal(t.tupleof[idx], value);
break sw;
}
default:
// intentionally blank
}
}
}
private void populateFromDbVal(V)(ref V val, string /*DatabaseDatum*/ value) {
import std.conv;
static if(is(V == Constraint!constraintSql, string constraintSql)) {
} else static if(is(V == Nullable!P, P)) {
// FIXME
if(value.length && value != "null" && value != "<null>") {
val.isNull = false;
import std.stdio; writeln(value);
val.value = to!P(value);
}
} else static if(is(V == bool)) {
val = value == "t" || value == "1" || value == "true";
} else static if(is(V == int) || is(V == string) || is(V == double)) {
val = to!V(value);
} else static if(is(V == enum)) {
val = cast(V) to!int(value);
} else static if(is(V == Timestamp)) {
val.value = value;
} else static if(is(V == Serial)) {
val.value = to!int(value);
}
}
unittest
{
static struct SomeStruct
{
int a;
void foo() {}
int b;
}
auto rs = new PredefinedResultSet(
[ "a", "b" ],
[ Row([ DatabaseDatum("1"), DatabaseDatum("2") ]) ]
);
SomeStruct s;
populateFromDbRow(s, rs.front);
assert(s.a == 1);
assert(s.b == 2);
}
/++
Gets all the children of that type. Specifically, it looks in T for a ForeignKey referencing B and queries on that.
To do a join through a many-to-many relationship, you could get the children of the join table, then get the children of that...
Or better yet, use real sql. This is more intended to get info where there is one parent row and then many child
rows, not for a combined thing.
+/
QueryBuilderHelper!(T[]) children(T, B)(B base) {
int countOfAssociations() {
int count = 0;
static foreach(memberName; __traits(allMembers, T))
static foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {{
static if(is(attr == ForeignKey!(K, policy), alias K, string policy)) {
static if(is(attr.ReferencedTable == B))
count++;
}
}}
return count;
}
static assert(countOfAssociations() == 1, T.stringof ~ " does not have exactly one foreign key of type " ~ B.stringof);
string keyName() {
static foreach(memberName; __traits(allMembers, T))
static foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) {{
static if(is(attr == ForeignKey!(K, policy), alias K, string policy)) {
static if(is(attr.ReferencedTable == B))
return memberName;
}
}}
}
// return QueryBuilderHelper!(T[])(toTableName(T.stringof)).where!(mixin(keyName ~ " => base.id"));
// changing mixin cuz of regression in dmd 2.088
mixin("return QueryBuilderHelper!(T[])(tableNameFor!T()).where!("~keyName ~ " => base.id);");
}
/++
Finds the single row associated with a foreign key in `base`.
`T` is used to find the key, unless ambiguous, in which case you must pass `key`.
To do a join through a many-to-many relationship, go to [children] or use real sql.
+/
T associated(B, T, string key = null)(B base, Database db) {
int countOfAssociations() {
int count = 0;
static foreach(memberName; __traits(allMembers, B))
static foreach(attr; __traits(getAttributes, __traits(getMember, B, memberName))) {
static if(is(attr == ForeignKey!(K, policy), alias K, string policy)) {
static if(is(attr.ReferencedTable == T))
static if(key is null || key == memberName)
count++;
}
}
return count;
}
static if(key is null) {
enum coa = countOfAssociations();
static assert(coa != 0, B.stringof ~ " has no association of type " ~ T);
static assert(coa == 1, B.stringof ~ " has multiple associations of type " ~ T ~ "; please specify the key you want");
static foreach(memberName; __traits(allMembers, B))
static foreach(attr; __traits(getAttributes, __traits(getMember, B, memberName))) {
static if(is(attr == ForeignKey!(K, policy), alias K, string policy)) {
static if(is(attr.ReferencedTable == T))
return db.find!T(__traits(getMember, base, memberName));
}
}
} else {
static assert(countOfAssociations() == 1, B.stringof ~ " does not have a key named " ~ key ~ " of type " ~ T);
static foreach(attr; __traits(getAttributes, __traits(getMember, B, memberName))) {
static if(is(attr == ForeignKey!(K, policy), alias K, string policy)) {
static if(is(attr.ReferencedTable == T)) {
return db.find!T(__traits(getMember, base, key));
}
}
}
assert(0);
}
}
/++
It will return an aggregate row with a member of type of each table in the join.
Could do an anonymous object for other things in the sql...
+/
auto join(TableA, TableB, ThroughTable = void)() {}
/++
+/
struct QueryBuilderHelper(T) {
static if(is(T == R[], R))
alias TType = R;
else
alias TType = T;
SelectBuilder selectBuilder;
this(string tableName) {
selectBuilder = new SelectBuilder();
selectBuilder.table = tableName;
selectBuilder.fields = ["*"];
}
T execute(Database db) {
selectBuilder.db = db;
static if(is(T == R[], R)) {
} else {
selectBuilder.limit = 1;
}
T ret;
bool first = true;
foreach(row; db.query(selectBuilder.toString())) {
TType t;
populateFromDbRow(t, row);
static if(is(T == R[], R))
ret ~= t;
else {
if(first) {
ret = t;
first = false;
} else {
assert(0);
}
}
}
return ret;
}
///
typeof(this) orderBy(string criterion)() {
string name() {
int idx = 0;
while(idx < criterion.length && criterion[idx] != ' ')
idx++;
return criterion[0 .. idx];
}
string direction() {
int idx = 0;
while(idx < criterion.length && criterion[idx] != ' ')
idx++;
import std.string;
return criterion[idx .. $].strip;
}
static assert(is(typeof(__traits(getMember, TType, name()))), TType.stringof ~ " has no field " ~ name());
static assert(direction().length == 0 || direction() == "ASC" || direction() == "DESC", "sort direction must be empty, ASC, or DESC");
selectBuilder.orderBys ~= criterion;
return this;
}
}
QueryBuilderHelper!(T[]) from(T)() {
return QueryBuilderHelper!(T[])(tableNameFor!T());
}
/// ditto
template where(conditions...) {
Qbh where(Qbh)(Qbh this_, string[] sqlCondition...) {
assert(this_.selectBuilder !is null);
static string extractName(string s) {
if(s.length == 0) assert(0);
auto i = s.length - 1;
while(i) {
if(s[i] == ')') {
// got to close paren, now backward to non-identifier char to get name
auto end = i;
while(i) {
if(s[i] == ' ')
return s[i + 1 .. end];
i--;
}
assert(0);
}
i--;
}
assert(0);
}
static foreach(idx, cond; conditions) {{
// I hate this but __parameters doesn't work here for some reason
// see my old thread: https://forum.dlang.org/post/awjuoemsnmxbfgzhgkgx@forum.dlang.org
enum name = extractName(typeof(cond!int).stringof);
auto value = cond(null);
// FIXME: convert the value as necessary
static if(is(typeof(value) == Serial))
auto dbvalue = value.value;
else static if(is(typeof(value) == enum))
auto dbvalue = cast(int) value;
else
auto dbvalue = value;
import std.conv;
static assert(is(typeof(__traits(getMember, Qbh.TType, name))), Qbh.TType.stringof ~ " has no member " ~ name);
static if(is(typeof(__traits(getMember, Qbh.TType, name)) == int)) {
static if(is(typeof(value) : const(int)[])) {
string s;
foreach(v; value) {
if(s.length) s ~= ", ";
s ~= to!string(v);
}
this_.selectBuilder.wheres ~= name ~ " IN (" ~ s ~ ")";
} else {
static assert(is(typeof(value) : const(int)) || is(typeof(value) == Serial), Qbh.TType.stringof ~ " is a integer key, but you passed an incompatible " ~ typeof(value).stringof);
auto placeholder = "?_internal" ~ to!string(idx);
this_.selectBuilder.wheres ~= name ~ " = " ~ placeholder;
this_.selectBuilder.setVariable(placeholder, dbvalue);
}
} else static if(is(typeof(__traits(getMember, Qbh.TType, name)) == Nullable!int)) {
static if(is(typeof(value) : const(int)[])) {
string s;
foreach(v; value) {
if(s.length) s ~= ", ";
s ~= to!string(v);
}
this_.selectBuilder.wheres ~= name ~ " IN (" ~ s ~ ")";
} else {
static assert(is(typeof(value) : const(int)) || is(typeof(value) == Serial), Qbh.TType.stringof ~ " is a integer key, but you passed an incompatible " ~ typeof(value).stringof);
auto placeholder = "?_internal" ~ to!string(idx);
this_.selectBuilder.wheres ~= name ~ " = " ~ placeholder;
this_.selectBuilder.setVariable(placeholder, dbvalue);
}
} else static if(is(typeof(__traits(getMember, Qbh.TType, name)) == Serial)) {
static if(is(typeof(value) : const(int)[])) {
string s;
foreach(v; value) {
if(s.length) s ~= ", ";
s ~= to!string(v);
}
this_.selectBuilder.wheres ~= name ~ " IN (" ~ s ~ ")";
} else {
static assert(is(typeof(value) : const(int)) || is(typeof(value) == Serial), Qbh.TType.stringof ~ " is a integer key, but you passed an incompatible " ~ typeof(value).stringof);
auto placeholder = "?_internal" ~ to!string(idx);
this_.selectBuilder.wheres ~= name ~ " = " ~ placeholder;
this_.selectBuilder.setVariable(placeholder, dbvalue);
}
} else {
static assert(is(typeof(__traits(getMember, Qbh.TType, name)) == typeof(value)), Qbh.TType.stringof ~ "." ~ name ~ " is not of type " ~ typeof(value).stringof);
auto placeholder = "?_internal" ~ to!string(idx);
this_.selectBuilder.wheres ~= name ~ " = " ~ placeholder;
this_.selectBuilder.setVariable(placeholder, dbvalue);
}
}}
this_.selectBuilder.wheres ~= sqlCondition;
return this_;
}
}
// Basically a wrapper for a ResultSet
struct TabResultSet(T)
{
this(ResultSet result)
{
this.result = result;
}
bool empty() @property
{
return this.result.empty;
}
T front() @property
{
T row;
row.populateFromDbRow(this.result.front);
return row;
}
void popFront()
{
this.result.popFront();
}
size_t length() @property
{
return this.result.length;
}
private ResultSet result;
}
// ditto
TabResultSet!T to_table_rows(T)(ResultSet res)
{
return TabResultSet!T(res);
}
private template FieldReference(alias field_)
{
alias Table = __traits(parent, field_);
alias field = field_;
}
private template isFieldRefInAttributes(Attributes...)
{
static if (Attributes.length == 0) {
static immutable bool isFieldRefInAttributes = false;
}
else {
alias attr = Attributes[0];
static if (is(attr == ForeignKey!(field, s), alias field, string s)) {
static immutable bool isFieldRefInAttributes = true;
}
else {
static immutable bool fieldRefInAttributes =
isFieldRefInAttributes!(Attributes[1..$]);
}
}
}
private template getFieldRefInAttributes(Attributes...)
{
alias attr = Attributes[0];
static if (is(attr == ForeignKey!(field, s), alias field, string s)) {
alias getFieldRefInAttributes = FieldReference!(field);
}
else {
alias fieldRefInAttributes =
getFieldRefInAttributes!(RT, Attributes[1..$]);
}
}
private alias getRefToField(alias fk_field) =
getFieldRefInAttributes!(__traits(getAttributes, fk_field));
unittest
{
struct Role { int id; }
struct User
{
int id;
@ForeignKey!(Role.id, "") int role_id;
}
alias FieldRef = getRefToField!(User.role_id);
assert(is(FieldRef.Table == Role));
assert(__traits(isSame, FieldRef.field, Role.id));
}
string toFieldName(T)(string s, bool isPlural = false)
{
int cnt = isPlural ? 2 : 1;
if (s is null)
return plural(cnt, beautify(tableNameFor!T(), '_', true));
return s;
}
/++
generates get functions for a one-to-many relationship with the form
`T2 get_<t2>(T1 row, Database db)` and
`TabResultSet!T1 get_<t1>(T2 row, Database db)`
[children] also works with a one-to-many relationship, but they are different in that [children] only gives you the many in the one-to-many relationship and only works with a single foreign key at a time.
Say you have a User and Role tables where each User has a role and a Role can be used by multiple users, with:
---
/*
This would give you all of the users with the Role `role`.
*/
auto res = role.children!(User, Role).execute(db);
---
However if you wanted to get the Role of a user there would be no way of doing so with children. It doesn't work the other way around.
Also the big thing that one_to_many can do and children can not do is handle multiple relationships(Multiple foreign keys pointing to the same Table for example:
---
import std.stdio;
import arsd.sqlite;
import arsd.database_generation;
alias FK(alias toWhat) = ForeignKey!(toWhat, null);
@DBName("Professor") struct Professor
{
int id;
string name;
}
@DBName("Course") struct Course
{
int id;
@FK!(Professor.id) int professor_id;
@FK!(Professor.id) int assistant_id;
}
mixin(one_to_many!(Course.professor_id, "prof", "courses_taught"));
mixin(one_to_many!(Course.assistant_id, "assistant", "courses_assisted"));
void main()
{
Database db = new Sqlite("test2.db");
Course course = db.find!Course(1);
Professor prof = course.get_prof(db);
writeln(prof.get_courses_taught(db));
writeln(prof.get_courses_assisted(db));
}
---
Here there are 2 relationships from Course to Professor here. One of them you can get from get_courses_taught and the other one with get_courses_assisted.
If you attempt to use children like so
---
writeln(prof.children!(Course, Professor).execute(db));
---
You would get:
$(CONSOLE
source/arsd/database_generation.d(489,2): Error: static assert: "Course does not have exactly one foreign key of type Professor"
)
In conclusion, children is nice in that its simple, doesn't require mixins to create extra symbols(functions). However it doesn't handle the one in one-to-many relationships at all, and it also doesn't work in tables with more than one relationship to a table. And finally, you might prefer the syntax of `prof.get_courses(db)` over `prof.children!(Course, Professor).execute(db)`.
Examples:
---
Struct Role { int id; }
struct User {
@ForeignKey!(Role.id, "") int role_id;
}
mixin(one_to_many!(User.role_id, "role", "users"));
void main()
{
Database db = ...
User user = db.find!User(1);
Role role = user.get_role(db);
auto users = role.get_users(db);
}
---
if t2 or t1 are set as "" the get function will not be generated
(the name will not be inferred), if set as null they will be inferred from
either the `DBName` attribute or from the name of the Table.
History:
Added November 5, 2022 (dub v10.10)
+/
template one_to_many(alias fk_field, string t2 = null, string t1 = null)
{
alias T1 = __traits(parent, fk_field);
static assert(
isFieldRefInAttributes!(__traits(getAttributes, fk_field)),
T1.stringof ~ "." ~ fk_field.stringof ~ " does't have a ForeignKey");
alias FieldRef = getRefToField!(fk_field);
alias T2 = FieldRef.Table;
alias ref_field = FieldRef.field;
immutable string t2_name = toFieldName!T2(t2);
immutable string t1_name = toFieldName!T1(t1, true);
static immutable string one = (t2 is "") ? "" :
T2.stringof~` get_`~t2_name~`(`~T1.stringof~` row, Database db)
{
import std.exception;
enforce(db !is null, "Database must not be null");
auto fk_id = row.`~fk_field.stringof~`;
auto res = db.query(
"select * from `~tableNameFor!T2()~`" ~
" where `~ref_field.stringof~` = ?", fk_id
).to_table_rows!`~T2.stringof~`;
return res.front();
}`;
static immutable string many = (t1 is "") ? "" : `
TabResultSet!`~T1.stringof~` get_`~t1_name~`(`~T2.stringof~` row, Database db)
{
import std.exception;
enforce(db !is null, "Database must not be null");
auto id = row.`~ref_field.stringof~`;
auto res = db.query(
"select * from `~tableNameFor!T1()~`"~
" where `~fk_field.stringof~` = ?", id
).to_table_rows!`~T1.stringof~`;
return res;
}`;
static immutable string one_to_many = one ~ many;
}