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