1 /** 2 * Copyright © DiamondMVC 2018 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.data.mapping.engines.mysql; 7 8 import std.variant : Variant; 9 import std.traits : hasMember; 10 import std.algorithm : map; 11 import std.conv : to; 12 import std..string : format; 13 import std.typecons : Nullable; 14 import std.array : array; 15 16 import vibe.data.serialization : optional; 17 18 import mysql; 19 20 public 21 { 22 import diamond.data.mapping.engines.mysql.generators; 23 import diamond.data.mapping.engines.mysql.model; 24 } 25 26 /// A variant db parameter type. 27 alias DbParam = Variant; 28 29 /// The connection string format. 30 private enum connectionStringFormat = "host=%s;port=%s;user=%s;pwd=%s;db=%s"; 31 32 /// The db connection string. 33 private static __gshared immutable string _dbConnectionString; 34 35 /// Static shared constructor for the module. 36 shared static this() 37 { 38 import diamond.core.webconfig; 39 40 if (!webConfig) 41 { 42 loadWebConfig(); 43 } 44 45 if (!webConfig.dbConnections) 46 { 47 return; 48 } 49 50 auto dbConfig = webConfig.dbConnections.mysql.get("default", null); 51 52 if (!dbConfig) 53 { 54 return; 55 } 56 57 _dbConnectionString = connectionStringFormat.format( 58 dbConfig.host, dbConfig.port ? dbConfig.port : 3306, 59 dbConfig.user, dbConfig.password, 60 dbConfig.database 61 ); 62 63 _pools[_dbConnectionString] = new MySQLPool(_dbConnectionString); 64 } 65 66 @property 67 { 68 /// Gets the connection string. 69 auto dbConnectionString() 70 { 71 return _dbConnectionString; 72 } 73 } 74 75 /// Collection of connection pools. 76 private static __gshared MySQLPool[string] _pools; 77 78 /// Global pool lock to ensure we don't attempt to create a connection pool twice on same connection string. 79 private static shared globalPoolLock = new Object; 80 81 /** 82 * Gets or creates a mysql pool from a connection string. 83 * Params: 84 * connectionString = The connection string for the pool. 85 * Returns: 86 * The mysql pool. 87 */ 88 private MySQLPool getPool(string connectionString) 89 { 90 auto pool = _pools.get(connectionString, null); 91 92 if (!pool) 93 { 94 synchronized (globalPoolLock) 95 { 96 pool = new MySQLPool(connectionString); 97 98 _pools[connectionString] = pool; 99 } 100 101 return getPool(connectionString); 102 } 103 104 return pool; 105 } 106 107 /** 108 * Prepares a specialized parameter sql. 109 * Params: 110 * sql = The sql. 111 * params = The params. 112 * transformedSql = The newly transformed sql. 113 * Returns: 114 * The raw db parameters. 115 */ 116 private DbParam[] prepareSql(string sql, DbParam[string] params, out string transformedSql) 117 { 118 transformedSql = ""; 119 string paramName = ""; 120 bool selectParam = false; 121 DbParam[] sqlParams; 122 123 foreach (i; 0 .. sql.length) 124 { 125 auto c = sql[i]; 126 127 if (c == 13) continue; 128 129 bool isEnd = i == (sql.length - 1); 130 131 if (c == '@') 132 { 133 paramName = ""; 134 selectParam = true; 135 } 136 else if (selectParam && ( 137 c == ';' || c == '=' || 138 c == '+' || c == '-' || 139 c == 9 || c == 13 || 140 c == 10 || c == ' ' || 141 c == 0 || c == '|' || 142 c == '.' || c == '/' || 143 c == '*' || c == '(' || 144 c == ')' || c == '[' || 145 c == ']' || c == ',' || 146 c == '`' || c == 39 147 )) 148 { 149 if (paramName == "table") 150 { 151 transformedSql ~= params[paramName].get!string ~ to!string(c); 152 selectParam = false; 153 paramName = ""; 154 } 155 else 156 { 157 sqlParams ~= params[paramName]; 158 transformedSql ~= "?" ~ c; 159 160 selectParam = false; 161 paramName = ""; 162 } 163 } 164 else if (selectParam) 165 { 166 paramName ~= c; 167 168 if (isEnd) 169 { 170 if (paramName == "table") 171 { 172 transformedSql ~= params[paramName].get!string ~ to!string(c); 173 } 174 else 175 { 176 sqlParams ~= params[paramName]; 177 transformedSql ~= "?"; 178 } 179 } 180 } 181 else 182 { 183 transformedSql ~= c; 184 } 185 } 186 187 return sqlParams; 188 } 189 190 /// CTFE string for mixin MySql connection setup with specialized parameters. 191 private enum MySqlConnectionNamedParametersSetup = q{ 192 auto useDbConnectionString = connectionString ? connectionString : _dbConnectionString; 193 194 // Prepare statement 195 string newSql; 196 DbParam[] newParams = null; 197 if (params) 198 { 199 newParams = prepareSql(sql, params, newSql); 200 } 201 else 202 { 203 newSql = sql; 204 } 205 206 auto pool = getPool(useDbConnectionString); 207 auto connection = pool.lockConnection(); 208 auto prepared = connection.prepare(newSql); 209 210 prepared.setArgs(newParams); 211 }; 212 213 /// CTFE string for mixin MySql connection setup. 214 private enum MySqlConnectionSetup = q{ 215 auto useDbConnectionString = connectionString ? connectionString : _dbConnectionString; 216 217 auto pool = getPool(useDbConnectionString); 218 auto connection = pool.lockConnection(); 219 auto prepared = connection.prepare(sql); 220 221 prepared.setArgs(params); 222 }; 223 224 /** 225 * Executes an sql statement. 226 * Params: 227 * sql = The sql query. 228 * params = The parameters. 229 * connectionString = The connection string. (If null, it will select the default) 230 * Returns: 231 * The amount of rows affected. 232 */ 233 ulong execute(string sql, DbParam[string] params, string connectionString = null) 234 { 235 mixin(MySqlConnectionNamedParametersSetup); 236 237 return connection.exec(prepared); 238 } 239 240 /** 241 * Executes a raw sql statement. 242 * Params: 243 * sql = The sql query. 244 * params = The parameters. 245 * connectionString = The connection string. (If null, it will select the default) 246 * Returns: 247 * The amount of rows affected. 248 */ 249 ulong executeRaw(string sql, DbParam[] params, string connectionString = null) 250 { 251 mixin(MySqlConnectionSetup); 252 253 return connection.exec(prepared); 254 } 255 256 /** 257 * Executes a scalar sql statement. 258 * Params: 259 * sql = The sql query. 260 * params = The parameters. 261 * connectionString = The connection string. (If null, it will select the default) 262 * Returns: 263 * The value of the statement. 264 */ 265 T scalar(T)(string sql, DbParam[string] params, string connectionString = null) 266 { 267 mixin(MySqlConnectionNamedParametersSetup); 268 269 auto value = connection.queryValue(prepared); 270 271 if (value.isNull) 272 { 273 return T.init; 274 } 275 276 return value.get.get!T; 277 } 278 279 /** 280 * Executes a raw scalar sql statement. 281 * Params: 282 * sql = The sql query. 283 * params = The parameters. 284 * connectionString = The connection string. (If null, it will select the default) 285 * Returns: 286 * The value of the statement. 287 */ 288 T scalarRaw(T)(string sql, DbParam[] params, string connectionString = null) 289 { 290 mixin(MySqlConnectionSetup); 291 292 auto value = connection.queryValue(prepared); 293 294 if (value.isNull) 295 { 296 return T.init; 297 } 298 299 return value.get.get!T; 300 } 301 302 /** 303 * Executes a scalar insert sql statement. 304 * Params: 305 * sql = The sql query. 306 * params = The parameters. 307 * connectionString = The connection string. (If null, it will select the default) 308 * Returns: 309 * The id of inserted row. 310 */ 311 T scalarInsert(T)(string sql, DbParam[string] params, string connectionString = null) 312 { 313 auto rows = execute(sql, params, connectionString); 314 315 if (!rows) 316 { 317 return T.init; 318 } 319 320 static const idSql = "SELECT last_insert_id()"; 321 322 return scalar!T(sql, null, connectionString); 323 } 324 325 /** 326 * Executes a raw scalar insert sql statement. 327 * Params: 328 * sql = The sql query. 329 * params = The parameters. 330 * connectionString = The connection string. (If null, it will select the default) 331 * Returns: 332 * The id of the inserted row. 333 */ 334 T scalarInsertRaw(T)(string sql, DbParam[] params, string connectionString = null) 335 { 336 auto rows = executeRaw(sql, params, connectionString); 337 338 if (!rows) 339 { 340 return T.init; 341 } 342 343 static const idSql = "SELECT last_insert_id()"; 344 345 return scalarRaw!T(sql, null, connectionString); 346 } 347 348 /** 349 * Validates whether a row is selected from the query or not. 350 * Params: 351 * sql = The sql query. 352 * params = The parameters. 353 * connectionString = The connection string. (If null, it will select the default) 354 * Returns: 355 * True if the row exists, false otherwise. 356 */ 357 bool exists(string sql, DbParam[string] params, string connectionString = null) 358 { 359 auto rows = execute(sql, params, connectionString); 360 361 return cast(bool)rows; 362 } 363 364 /** 365 * Validates whether a row is selected from the raw query or not. 366 * Params: 367 * sql = The sql query. 368 * params = The parameters. 369 * connectionString = The connection string. (If null, it will select the default) 370 * Returns: 371 * True if the row exists, false otherwise. 372 */ 373 bool existsRaw(string sql, DbParam[] params, string connectionString = null) 374 { 375 auto rows = executeRaw(sql, params, connectionString); 376 377 return cast(bool)rows; 378 } 379 380 /** 381 * Executes a single sql read. 382 * Params: 383 * sql = The sql query. 384 * params = The parameters. 385 * connectionString = The connection string. (If null, it will select the default) 386 * Returns: 387 * The model of the first row read. 388 */ 389 TModel readSingle(TModel : IMySqlModel)(string sql, DbParam[string] params, string connectionString = null) 390 { 391 params["table"] = TModel.table; 392 393 mixin(MySqlConnectionNamedParametersSetup); 394 395 auto row = connection.queryRow(prepared); 396 397 if (row.isNull) 398 { 399 return TModel.init; 400 } 401 402 auto model = new TModel; 403 model.row = row.get; 404 model.readModel(); 405 return model; 406 } 407 408 /** 409 * Executes a single raw sql read. 410 * Params: 411 * sql = The sql query. 412 * params = The parameters. 413 * connectionString = The connection string. (If null, it will select the default) 414 * Returns: 415 * The model of the first row read. 416 */ 417 TModel readSingleRaw(TModel : IMySqlModel)(string sql, DbParam[] params, string connectionString = null) 418 { 419 mixin(MySqlConnectionSetup); 420 421 auto row = connection.queryRow(prepared); 422 423 if (row.isNull) 424 { 425 return TModel.init; 426 } 427 428 auto model = new TModel; 429 model.row = row.get; 430 model.readModel(); 431 return model; 432 } 433 434 /** 435 * Executes a multi sql read. 436 * Params: 437 * sql = The sql query. 438 * params = The parameters. 439 * connectionString = The connection string. (If null, it will select the default) 440 * Returns: 441 * A range filled models with the rows returned by the sql read. 442 */ 443 auto readMany(TModel : IMySqlModel)(string sql, DbParam[string] params, string connectionString = null) 444 { 445 params["table"] = TModel.table; 446 447 mixin(MySqlConnectionNamedParametersSetup); 448 449 return connection.query(prepared).map!((row) 450 { 451 auto model = new TModel; 452 model.row = row; 453 model.readModel(); 454 return model; 455 }).array; 456 } 457 458 /** 459 * Executes a raw multi sql read. 460 * Params: 461 * sql = The sql query. 462 * params = The parameters. 463 * connectionString = The connection string. (If null, it will select the default) 464 * Returns: 465 * A range filled models with the rows returned by the sql read. 466 */ 467 auto readManyRaw(TModel : IMySqlModel)(string sql, DbParam[] params, string connectionString = null) 468 { 469 mixin(MySqlConnectionSetup); 470 471 return connection.query(prepared).map!((row) 472 { 473 auto model = new TModel; 474 model.row = row; 475 model.readModel(); 476 return model; 477 }).array; 478 }