1 module sqlbuilder.dataset; 2 3 import sqlbuilder.uda; 4 import sqlbuilder.types; 5 import sqlbuilder.traits; 6 7 TableDef buildTableDef(T, alias relation, mappings...)(Spec joinType, TableDef dependency) 8 { 9 assert(isJoin(joinType), "Error, invalid spec for join with dependencies: " ~ joinType); 10 auto tableid = makeSpec(dependency.as ~ "_" ~ joinType ~ "_" ~ relation.name, Spec.tableid); 11 auto deptable = dependency.as.makeSpec(Spec.tableid); 12 auto expr = ExprString(makeSpec(joinType), getTableName!(relation.foreign_table).makeSpec(Spec.id), 13 " AS ", tableid[2 .. $].makeSpec(Spec.id), " ON ("); 14 15 static string genKeyId(T, string fieldname)() 16 { 17 return getColumnName!(__traits(getMember, T, fieldname)).makeSpec(Spec.id); 18 } 19 20 if(mappings.length > 1) 21 expr ~= andSpec; 22 // static 23 foreach(i, m; mappings) 24 { 25 static if(i != 0) 26 expr ~= sepSpec; 27 static if(isKeyLiteral(m.foreign_key)) 28 { 29 // the foreign key is a value, which means we need to put it into 30 // the expression as-is. However, if the key is not a value, we 31 // need to swap the order. 32 static if(isKeyLiteral(m.key)) 33 { 34 expr ~= ExprString(m.foreign_key, " ", m.key); 35 } 36 else 37 { 38 expr ~= ExprString(deptable, genKeyId!(T, m.key), " ", m.foreign_key); 39 } 40 } 41 else static if(isKeyLiteral(m.key)) 42 expr ~= ExprString(tableid, genKeyId!(relation.foreign_table, m.foreign_key), " ", m.key); 43 else 44 expr ~= ExprString(tableid, genKeyId!(relation.foreign_table, m.foreign_key), " = ", deptable, genKeyId!(T, m.key)); 45 } 46 47 if(mappings.length > 1) 48 expr ~= endGroupSpec; 49 expr ~= ")"; 50 51 return TableDef(tableid[2 .. $], expr, [dependency]); 52 } 53 54 TableDef buildTableDef(T)(string rootName = null) 55 { 56 auto expr = ExprString([getTableName!T.makeSpec(Spec.id)]); 57 if(rootName == null) 58 rootName = getTableName!T; 59 if(rootName != getTableName!T) 60 { 61 expr ~= " AS "; 62 expr ~= rootName.makeSpec(Spec.id); 63 } 64 return TableDef(rootName, expr); 65 } 66 67 template staticTableDef(T, alias relation, Spec joinType, TableDef dependency, m...) 68 { 69 static const TableDef staticTableDef = buildTableDef!(T, relation, m)(joinType, dependency); 70 } 71 72 template staticTableDef(T) 73 { 74 static const TableDef staticTableDef = buildTableDef!(T)(); 75 } 76 77 /*auto quoteIdentifier2(string name) 78 { 79 import std.range : chain; 80 return chain(`"`, name, `"`); 81 }*/ 82 83 private ColumnDef!T makeColumnDef(T)(const TableDef table, string tablename, string colname) 84 { 85 return ColumnDef!T(table, ExprString([tablename.makeSpec(Spec.tableid), colname.makeSpec(Spec.id)])); 86 } 87 88 template DataSet(T) 89 { 90 alias DataSet = DataSet!(T, staticTableDef!(T), false); 91 } 92 93 struct DataSet(T, alias core, bool AN) 94 { 95 alias RowType = T; 96 enum tableDef = core; 97 enum anyNull = AN; 98 99 @property auto opDispatch(string item)() if (isField!(T, item)) 100 { 101 // This is a column of the row, so just build the correct column definition. 102 import std.typecons : Nullable; 103 static if(anyNull && !is(typeof(__traits(getMember, T, item)) : Nullable!U, U)) 104 alias X = Nullable!(typeof(__traits(getMember, T, item))); 105 else static if(is(getAllowNullType!(__traits(getMember, T, item)))) 106 { 107 // this tells the fetcher how to deal with nulls properly 108 alias X = getAllowNullType!(__traits(getMember, T, item)); 109 } 110 else 111 alias X = typeof(__traits(getMember, T, item)); 112 113 static auto result() { 114 return makeColumnDef!(X) (core, core.as, 115 getColumnName!(__traits(getMember, T, item))); 116 } 117 if(__ctfe) return result(); 118 static col = result(); 119 return col; 120 } 121 122 template opDispatch(string item) if (isRelation!(T, item)) 123 { 124 @property auto opDispatch(Spec joinType = Spec.none)() if (joinType == Spec.none || isJoin(joinType)) 125 { 126 // this is a relationship, use the UDAs assigned to the appropriate 127 // item to generate the new dataset. 128 enum field = getRelationField!(T, item); 129 static assert(field != null); 130 enum relation = getRelationFor!(__traits(getMember, T, field)); 131 alias m = getMappingsFor!(__traits(getMember, T, field)); 132 enum realJoinType = joinType == Spec.none ? (relation.joinType == Spec.none ? Spec.leftJoin : relation.joinType) : joinType; 133 return .DataSet!(relation.foreign_table, staticTableDef!(T, relation, realJoinType, core, m), anyNull || !relation.strong).init; 134 } 135 136 @property auto opDispatch(Strong strongOverride, Spec joinType = Spec.none)() if (joinType == Spec.none || isJoin(joinType)) 137 { 138 // this is a relationship, use the UDAs assigned to the appropriate 139 // item to generate the new dataset. 140 enum field = getRelationField!(T, item); 141 static assert(field != null); 142 enum relation = getRelationFor!(__traits(getMember, T, field)); 143 alias m = getMappingsFor!(__traits(getMember, T, field)); 144 enum realJoinType = joinType == Spec.none ? (relation.joinType == Spec.none ? Spec.leftJoin : relation.joinType) : joinType; 145 return .DataSet!(relation.foreign_table, staticTableDef!(T, relation, realJoinType, core, m), anyNull || !strongOverride).init; 146 } 147 } 148 149 // shortcut for all columns 150 @property auto allColumns() 151 { 152 import std.typecons : Nullable; 153 static if(anyNull) 154 alias X = Nullable!T; 155 else 156 alias X = T; 157 static auto result() { 158 return ColumnDef!(RowObj!X)(core, ExprString(core.as.makeSpec(Spec.tableid), "*", 159 objEndSpec)); 160 } 161 if(__ctfe) return result(); 162 static col = result(); 163 return col; 164 } 165 } 166 167 // gets a data set that is defined by the related table. Used when it's too 168 // cumbersome to define all the various relations in the primary table. 169 // 170 // if relationName is null, and there is only one relation between the tables, 171 // then that relation is used. If relationName is not null, then the relation 172 // must be that name. A relation name is required when 2 relations between the 173 // types exist. 174 // 175 // The join name is kind of convoluted, but hard to make a unique one that 176 // reads well in English. perhaps this can be improved (maybe reverse the table 177 // definitions up to that point). 178 auto related(T, string relationName = null, Spec joinType = Spec.none, Strong strongRelation = Strong.no, DS1)(DS1 dataset) if (isDataSet!DS1 && (joinType == Spec.none || isJoin(joinType))) 179 { 180 static if(relationName.length) 181 { 182 static assert(isRelation!(T, relationName), 183 "No relation named " ~ relationName ~ " for " ~ T.stringof 184 ~ " to " ~ DS1.RowType.stringof); 185 enum relatedField = getRelationField!(T, relationName); 186 } 187 else 188 { 189 enum relatedField = getRelationField!(T, dataset.RowType); 190 static assert(relatedField.length, 191 "No relation for " ~ T.stringof ~ " to " 192 ~ DS1.RowType.stringof); 193 } 194 195 // now have the related field, fetch the relation and the mapping, and do 196 // it in reverse. 197 static assert(relatedField != null); 198 import std.meta : staticMap; 199 template recipMapping(mapping m) 200 { 201 enum recipMapping = mapping(m.key, m.foreign_key); 202 } 203 alias mappings = staticMap!(recipMapping, getMappingsFor!(__traits(getMember, T, relatedField))); 204 enum revRelation = getRelationFor!(__traits(getMember, T, relatedField)); 205 enum relation = refersTo!T(getTableName!T ~ "_having_" ~ revRelation.name, revRelation.joinType, strongRelation); 206 enum realJoin = joinType == Spec.none ? (relation.joinType == Spec.none ? Spec.leftJoin : relation.joinType) : joinType; 207 return DataSet!(T, staticTableDef!(revRelation.foreign_table, relation, realJoin, dataset.tableDef, mappings), dataset.anyNull || !strongRelation).init; 208 } 209 210 auto alwaysRelated(T, string relationName = null, Spec joinType = Spec.none, DS1)(DS1 dataset) 211 { 212 return related!(T, relationName, joinType, Strong.yes)(dataset); 213 } 214 215 216 version(unittest) 217 { 218 struct MyBool 219 { 220 bool _val; 221 string dbValue() { return _val ? "Y" : "N"; } 222 alias _val this; 223 static MyBool fromDbValue(string item) { 224 return MyBool(item == "Y" || item == "y"); 225 } 226 } 227 228 @tableName("author") 229 static struct Author 230 { 231 string firstName; 232 string lastName; 233 @primaryKey @autoIncrement int id = -1; 234 @colType("VARCHAR(1)") MyBool ynAnswer; 235 236 // relations 237 static @mapping("author_id") @refersTo!book Relation books; 238 static @mapping("author_id") @mapping("book_type", "= 0") @refersTo!book Relation referenceBooks; 239 } 240 241 enum BookType { 242 Reference, 243 Fiction 244 } 245 static struct book 246 { 247 @unique @colName("name") @colType("VARCHAR(100)") string title; 248 @refersTo!Author("author", Spec.innerJoin) @colName("auth_id") int author_id; 249 BookType book_type; 250 @primaryKey @autoIncrement int id = -1; 251 } 252 static struct review 253 { 254 @refersTo!book("book") int book_id; 255 @allowNull string comment; 256 @allowNull(-1) int rating; 257 } 258 } 259 260 version(unittest) 261 import sqlbuilder.testing; 262 version(Have_mysql_native) unittest 263 { 264 import sqlbuilder.dialect.mysql; 265 import sqlbuilder.types; 266 import std.typecons : Nullable; 267 import std.stdio; 268 // create a dataset based on author 269 DataSet!(Author) ds; 270 with(ds) 271 { 272 auto q = select().where(lastName, " = ", "Alexandrescu".param).select(ds, books.title).orderBy(books.title); 273 //writeln(q); 274 writeln(q.sql); 275 writeln(q.params); 276 auto q2 = select(ds, referenceBooks.title).where(lastName, " = ", "Alexandrescu".param).orderBy(referenceBooks.title); 277 writeln(q2.sql); 278 writeln(q2.params); 279 } 280 281 DataSet!(book) ds2; 282 283 with(ds2) 284 { 285 import std.range; 286 static struct Optional(T) 287 { 288 Parameter!T value; 289 bool valid = false; 290 alias value this; 291 } 292 Optional!string s; 293 auto q = select(ds2, author.related!book.title.as("other_book_title")).where(author.lastName, " = ", s); 294 //pragma(msg, q.RowTypes); 295 writeln(q.sql); 296 writeln(q.RowTypes.stringof); 297 writeln(q.params); 298 299 s.params = only("Alexandrescu"); 300 s.valid = true; 301 auto q2 = select(allColumns, ds2.author.books!(Spec.innerJoin).title.as("other_book_title")).where(ds2.author.lastName, " = ", s); 302 writeln(q2.sql); 303 writeln(q2.RowTypes.stringof); 304 writeln(q2.params); 305 writeln(createTableSql!Author); 306 writeln(createTableSql!book); 307 writeln(createTableSql!review); 308 } 309 310 // try some inserts 311 { 312 auto i = insert(Author("Andrei", "Alexandrescu")); 313 writeln(i.sql); 314 writeln(i.params); 315 316 i = insert(ds.tableDef).set(ds.firstName, "Andrei".param); 317 writeln(i.sql); 318 writeln(i.params); 319 } 320 321 // updates 322 { 323 auto u = update(Author("Steven", "Schveighoffer", 1)); 324 writeln(u.sql); 325 writeln(u.params); 326 327 u = set(ds.firstName, "George".param).where(ds.id, " = ", 5.param); 328 writeln(u.sql); 329 writeln(u.params); 330 } 331 332 // deletes 333 { 334 auto d = remove(Author("Steven", "Schveighoffer", 1)); 335 writeln(d.sql); 336 writeln(d.params); 337 d = removeFrom(ds.tableDef).havingKey(Author("Steven", "Schveighoffer", 1)); 338 writeln(d.sql); 339 writeln(d.params); 340 d = removeFrom(ds.tableDef).havingKey(ds.books!(Spec.innerJoin), 1); 341 writeln(d.sql); 342 writeln(d.params); 343 d = removeFrom(ds.tableDef).havingKey!Author(1); 344 writeln(d.sql); 345 writeln(d.params); 346 } 347 348 // TODO: CTFE support 349 // Idea: using a custom dialect that uses strings instead of data values, 350 // store the FQN of the types needed to create the actual query. Then use a 351 // template which processes the FQN to provide a custom function that 352 // accepts the right parameter types/names. 353 //auto blah = genSQL(); 354 //enum ctsql = genSQL(); 355 /*pragma(msg, ctsql); 356 writeln(ctsql);*/ 357 }