1 /** 2 Copyright: Copyright (c) 2017, Oleg Butko. All rights reserved. 3 Copyright: Copyright (c) 2018-2019, Joakim Brännström. All rights reserved. 4 License: MIT 5 Author: Oleg Butko (deviator) 6 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 7 */ 8 module miniorm.schema; 9 10 version (unittest) { 11 import std.algorithm : map; 12 import unit_threaded.assertions; 13 } 14 15 /// UDA controlling the name of the table. 16 struct TableName { 17 string value; 18 } 19 20 /// UDA controlling constraints of a table. 21 struct TableConstraint { 22 string value; 23 } 24 25 /// UDA setting a field other than id to being the primary key. 26 struct TablePrimaryKey { 27 string value; 28 } 29 30 /// UDA for foreign keys on a table. 31 struct TableForeignKey { 32 string foreignKey; 33 KeyRef r; 34 KeyParam p; 35 } 36 37 struct KeyRef { 38 string value; 39 } 40 41 struct KeyParam { 42 string value; 43 } 44 45 /// UDA controlling extra attributes for a field. 46 struct ColumnParam { 47 string value; 48 } 49 50 /// UDA to control the column name that a field end up as. 51 struct ColumnName { 52 string value; 53 } 54 55 /** Create SQL for creating tables if not exists 56 * 57 * Params: 58 * Types = types of structs which will be a tables 59 * name of struct -> name of table 60 * name of field -> name of column 61 * prefix = prefix to use for the tables that are created. 62 * 63 * To change the name of the table: 64 * --- 65 * @TableName("my_name") 66 * struct Foo {} 67 * --- 68 */ 69 auto buildSchema(Types...)(string prefix = null) { 70 import std.algorithm : joiner, map; 71 import std.array : appender, array; 72 import std.range : only; 73 74 auto ret = appender!string; 75 foreach (T; Types) { 76 static if (is(T == struct)) { 77 ret.put("CREATE TABLE IF NOT EXISTS "); 78 ret.put(prefix); 79 ret.put(tableName!T); 80 ret.put(" (\n"); 81 ret.put(only(fieldToCol!("", T)().map!"a.toColumn".array, 82 tableConstraints!T(), tableForeinKeys!T()).joiner.joiner(",\n")); 83 ret.put(");\n"); 84 } else 85 static assert(0, "not supported non-struct type"); 86 } 87 return ret.data; 88 } 89 90 unittest { 91 static struct Foo { 92 ulong id; 93 float value; 94 ulong ts; 95 } 96 97 static struct Bar { 98 ulong id; 99 string text; 100 ulong ts; 101 } 102 103 buildSchema!(Foo, Bar).shouldEqual(`CREATE TABLE IF NOT EXISTS Foo ( 104 'id' INTEGER PRIMARY KEY, 105 'value' REAL NOT NULL, 106 'ts' INTEGER NOT NULL); 107 CREATE TABLE IF NOT EXISTS Bar ( 108 'id' INTEGER PRIMARY KEY, 109 'text' TEXT NOT NULL, 110 'ts' INTEGER NOT NULL); 111 `); 112 } 113 114 @("shall create a schema with a table name derived from the UDA") 115 unittest { 116 @TableName("my_table") 117 static struct Foo { 118 ulong id; 119 } 120 121 assert(buildSchema!(Foo) == `CREATE TABLE IF NOT EXISTS my_table ( 122 'id' INTEGER PRIMARY KEY); 123 `); 124 } 125 126 @("shall create a schema with an integer column that may be NULL") 127 unittest { 128 static struct Foo { 129 ulong id; 130 @ColumnParam("") 131 ulong int_; 132 } 133 134 buildSchema!(Foo).shouldEqual(`CREATE TABLE IF NOT EXISTS Foo ( 135 'id' INTEGER PRIMARY KEY, 136 'int_' INTEGER); 137 `); 138 } 139 140 @("shall create a schema with constraints from UDAs") 141 unittest { 142 @TableConstraint("u UNIQUE p") 143 static struct Foo { 144 ulong id; 145 ulong p; 146 } 147 148 assert(buildSchema!(Foo) == `CREATE TABLE IF NOT EXISTS Foo ( 149 'id' INTEGER PRIMARY KEY, 150 'p' INTEGER NOT NULL, 151 CONSTRAINT u UNIQUE p); 152 `, buildSchema!(Foo)); 153 } 154 155 @("shall create a schema with a foregin key from UDAs") 156 unittest { 157 @TableForeignKey("p", KeyRef("bar(id)"), KeyParam("ON DELETE CASCADE")) 158 static struct Foo { 159 ulong id; 160 ulong p; 161 } 162 163 assert(buildSchema!(Foo) == `CREATE TABLE IF NOT EXISTS Foo ( 164 'id' INTEGER PRIMARY KEY, 165 'p' INTEGER NOT NULL, 166 FOREIGN KEY(p) REFERENCES bar(id) ON DELETE CASCADE); 167 `, buildSchema!(Foo)); 168 } 169 170 @("shall create a schema with a name from UDA") 171 unittest { 172 static struct Foo { 173 ulong id; 174 @ColumnName("version") 175 ulong version_; 176 } 177 178 assert(buildSchema!(Foo) == `CREATE TABLE IF NOT EXISTS Foo ( 179 'id' INTEGER PRIMARY KEY, 180 'version' INTEGER NOT NULL); 181 `, buildSchema!(Foo)); 182 } 183 184 @("shall create a schema with a table name derived from the UDA with specified prefix") 185 unittest { 186 @TableName("my_table") 187 static struct Foo { 188 ulong id; 189 } 190 191 buildSchema!Foo("new_").shouldEqual(`CREATE TABLE IF NOT EXISTS new_my_table ( 192 'id' INTEGER PRIMARY KEY); 193 `); 194 } 195 196 @("shall create a schema with a column of type DATETIME") 197 unittest { 198 import std.datetime : SysTime; 199 200 static struct Foo { 201 ulong id; 202 SysTime timestamp; 203 } 204 205 buildSchema!Foo.shouldEqual(`CREATE TABLE IF NOT EXISTS Foo ( 206 'id' INTEGER PRIMARY KEY, 207 'timestamp' DATETIME NOT NULL); 208 `); 209 } 210 211 @("shall create a schema with a column where the second column is a string and primary key") 212 unittest { 213 import std.datetime : SysTime; 214 215 @TablePrimaryKey("key") 216 static struct Foo { 217 ulong id; 218 string key; 219 } 220 221 buildSchema!Foo.shouldEqual(`CREATE TABLE IF NOT EXISTS Foo ( 222 'id' INTEGER NOT NULL, 223 'key' TEXT PRIMARY KEY); 224 `); 225 } 226 227 import std.format : format, formattedWrite; 228 import std.traits; 229 import std.meta : Filter; 230 231 package: 232 233 enum SEPARATOR = "."; 234 235 string tableName(T)() { 236 enum nameAttrs = getUDAs!(T, TableName); 237 static assert(nameAttrs.length == 0 || nameAttrs.length == 1, 238 "Found multiple TableName UDAs on " ~ T.stringof); 239 enum hasName = nameAttrs.length; 240 static if (hasName) { 241 return nameAttrs[0].value; 242 } else 243 return T.stringof; 244 } 245 246 string[] tableConstraints(T)() { 247 enum constraintAttrs = getUDAs!(T, TableConstraint); 248 enum hasConstraints = constraintAttrs.length; 249 250 string[] rval; 251 static if (hasConstraints) { 252 static foreach (const c; constraintAttrs) 253 rval ~= "CONSTRAINT " ~ c.value; 254 } 255 return rval; 256 } 257 258 string tablePrimaryKey(T)() { 259 enum keyAttr = getUDAs!(T, TablePrimaryKey); 260 static if (keyAttr.length != 0) { 261 return keyAttr[0].value; 262 } else 263 return "id"; 264 } 265 266 string[] tableForeinKeys(T)() { 267 enum foreignKeyAttrs = getUDAs!(T, TableForeignKey); 268 enum hasForeignKeys = foreignKeyAttrs.length; 269 270 string[] rval; 271 static if (hasForeignKeys) { 272 static foreach (a; foreignKeyAttrs) 273 rval ~= "FOREIGN KEY(" ~ a.foreignKey ~ ") REFERENCES " ~ a.r.value ~ ( 274 (a.p.value.length == 0) ? null : " " ~ a.p.value); 275 } 276 277 return rval; 278 } 279 280 string[] fieldNames(string name, T)(string prefix = "") { 281 static if (is(T == struct)) { 282 T t; 283 string[] ret; 284 foreach (i, f; t.tupleof) { 285 enum fname = __traits(identifier, t.tupleof[i]); 286 alias F = typeof(f); 287 auto np = prefix ~ (name.length ? name ~ SEPARATOR : ""); 288 ret ~= fieldNames!(fname, F)(np); 289 } 290 return ret; 291 } else 292 return ["'" ~ prefix ~ name ~ "'"]; 293 } 294 295 @("shall derive the field names from the inspected struct") 296 unittest { 297 struct Foo { 298 ulong id; 299 float xx; 300 string yy; 301 } 302 303 struct Bar { 304 ulong id; 305 float abc; 306 Foo foo; 307 string baz; 308 } 309 310 import std.algorithm; 311 import std.utf; 312 313 assert(fieldNames!("", Bar) == [ 314 "'id'", "'abc'", "'foo.id'", "'foo.xx'", "'foo.yy'", "'baz'" 315 ]); 316 fieldToCol!("", Bar).map!"a.quoteIdentifier".shouldEqual([ 317 "'id'", "'abc'", "'foo.id'", "'foo.xx'", "'foo.yy'", "'baz'" 318 ]); 319 } 320 321 struct FieldColumn { 322 /// Identifier in the struct. 323 string identifier; 324 /// Name of the column in the table. 325 string columnName; 326 /// The type is user defined 327 string columnType; 328 /// Parameters for the column when creating the table. 329 string columnParam; 330 /// If the field is a primary key. 331 bool isPrimaryKey; 332 333 string quoteIdentifier() @safe pure nothrow const { 334 return "'" ~ identifier ~ "'"; 335 } 336 337 string quoteColumnName() @safe pure nothrow const { 338 return "'" ~ columnName ~ "'"; 339 } 340 341 string toColumn() @safe pure nothrow const { 342 return quoteColumnName ~ " " ~ columnType ~ columnParam; 343 } 344 } 345 346 FieldColumn[] fieldToCol(string name, T)(string prefix = "") { 347 return fieldToColRecurse!(name, T, 0)(prefix); 348 } 349 350 private: 351 352 FieldColumn[] fieldToColRecurse(string name, T, ulong depth)(string prefix) { 353 import std.datetime : SysTime; 354 import std.meta : AliasSeq; 355 356 static if (!is(T == struct)) 357 static assert( 358 "Building a schema from a type is only supported for struct's. This type is not supported: " 359 ~ T.stringof); 360 361 static if (tablePrimaryKey!T.length != 0) 362 enum primaryKey = tablePrimaryKey!T; 363 else 364 enum primaryKey = "id"; 365 366 T t; 367 FieldColumn[] ret; 368 foreach (i, f; t.tupleof) { 369 enum fname = __traits(identifier, t.tupleof[i]); 370 alias F = typeof(f); 371 auto np = prefix ~ (name.length ? name ~ SEPARATOR : ""); 372 373 enum udas = AliasSeq!(getUDAs!(t.tupleof[i], ColumnParam), 374 getUDAs!(t.tupleof[i], ColumnName)); 375 376 static if (is(F == SysTime)) 377 ret ~= fieldToColInternal!(fname, primaryKey, F, depth, udas)(np); 378 else static if (is(F == struct)) 379 ret ~= fieldToColRecurse!(fname, F, depth + 1)(np); 380 else 381 ret ~= fieldToColInternal!(fname, primaryKey, F, depth, udas)(np); 382 } 383 return ret; 384 } 385 386 /** 387 * Params: 388 * depth = A primary key can only be at the outer most struct. Any other "id" fields are normal integers. 389 */ 390 FieldColumn[] fieldToColInternal(string name, string primaryKey, T, ulong depth, FieldUDAs...)( 391 string prefix) { 392 import std.datetime : SysTime; 393 import std.traits : OriginalType; 394 395 enum bool isFieldParam(alias T) = is(typeof(T) == ColumnParam); 396 enum bool isFieldName(alias T) = is(typeof(T) == ColumnName); 397 398 string type, param; 399 400 enum paramAttrs = Filter!(isFieldParam, FieldUDAs); 401 static assert(paramAttrs.length == 0 || paramAttrs.length == 1, 402 "Found multiple ColumnParam UDAs on " ~ T.stringof); 403 enum hasParam = paramAttrs.length; 404 static if (hasParam) { 405 static if (paramAttrs[0].value.length == 0) 406 param = ""; 407 else 408 param = " " ~ paramAttrs[0].value; 409 } else 410 param = " NOT NULL"; 411 412 static if (is(T == enum)) 413 alias originalT = OriginalType!T; 414 else 415 alias originalT = T; 416 417 static if (isFloatingPoint!originalT) 418 type = "REAL"; 419 else static if (isNumeric!originalT || is(originalT == bool)) { 420 type = "INTEGER"; 421 } else static if (isSomeString!originalT) 422 type = "TEXT"; 423 else static if (isArray!originalT) 424 type = "BLOB"; 425 else static if (is(originalT == SysTime)) { 426 type = "DATETIME"; 427 } else 428 static assert(0, "unsupported type: " ~ T.stringof); 429 430 enum nameAttr = Filter!(isFieldName, FieldUDAs); 431 static assert(nameAttr.length == 0 || nameAttr.length == 1, 432 "Found multiple ColumnName UDAs on " ~ T.stringof); 433 static if (nameAttr.length) 434 enum columnName = nameAttr[0].value; 435 else 436 enum columnName = name; 437 438 static if (columnName == primaryKey && depth == 0) { 439 return [ 440 FieldColumn(prefix ~ name, prefix ~ columnName, type, " PRIMARY KEY", true) 441 ]; 442 } else { 443 return [FieldColumn(prefix ~ name, prefix ~ columnName, type, param)]; 444 } 445 } 446 447 unittest { 448 struct Baz { 449 string a, b; 450 } 451 452 struct Foo { 453 float xx; 454 string yy; 455 Baz baz; 456 int zz; 457 } 458 459 struct Bar { 460 ulong id; 461 float abc; 462 Foo foo; 463 string baz; 464 ubyte[] data; 465 } 466 467 enum shouldWorkAtCompileTime = fieldToCol!("", Bar); 468 469 fieldToCol!("", Bar)().map!"a.toColumn".shouldEqual([ 470 "'id' INTEGER PRIMARY KEY", "'abc' REAL NOT NULL", 471 "'foo.xx' REAL NOT NULL", "'foo.yy' TEXT NOT NULL", 472 "'foo.baz.a' TEXT NOT NULL", "'foo.baz.b' TEXT NOT NULL", 473 "'foo.zz' INTEGER NOT NULL", "'baz' TEXT NOT NULL", 474 "'data' BLOB NOT NULL" 475 ]); 476 }