├── .gitignore ├── MIT-LICENSE.txt ├── README.md └── dbmanager.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | meta.xml 3 | database 4 | test.lua -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DB Manager Resource for MTA:SA 2 | 3 | ## About 4 | 5 | This is library for easy manipulation of database in MTA:SA, with a clean syntax and optimized. You don't need to worry about using **Queries** several times, this library manipulates a table in **location** to include all data in cache, this allows you not to look for the data directly in the database, but in the cache. 6 | 7 | ## Documentation 8 | This is a link to the documentation of the library, where you can find all the functions and how to use them. 9 | 10 | - [Click here to see the Documentation](https://github.com/lodsdev/db-manager/wiki) 11 | 12 | ## Example (using MySQL) 13 | 14 | Example of how to use the library, in this example we will create a table for users, and we will perform the following actions: 15 | 16 | ```lua 17 | -- connect to database (MySQL) 18 | -- "DBManager" is the variable that contains the library 19 | -- [check the documentation for more information] 20 | local conn = DBManager:new({ 21 | dialect = "mysql", 22 | host = "localhost", 23 | port = 3306, 24 | username = "root", 25 | password = "123456", 26 | database = "test_db_manager", 27 | }) 28 | 29 | -- check if connection is successful 30 | -- "getConnection" is a function that returns a boolean value indicating if the connection was successful 31 | -- [check the documentation for more information] 32 | if (not conn:getConnection()) then 33 | error("DBManager: Connection failed", 2) 34 | end 35 | 36 | -- create a table for users 37 | -- "define" is a function that creates a table in the database 38 | -- [check the documentation for more information] 39 | local Users = conn:define("Users", { 40 | id = { 41 | type = DBManager.INTEGER(), 42 | allowNull = false, 43 | autoIncrement = true, 44 | primaryKey = true, 45 | }, 46 | name = { 47 | type = DBManager.TEXT(), 48 | allowNull = false, 49 | } 50 | }) 51 | 52 | -- It's important to sync the local table with the database 53 | -- "sync" is a function that syncs the local table with the database 54 | -- [check the documentation for more information] 55 | Users:sync() 56 | 57 | addCommandHandler("insertUser", function(player, cmd, name) 58 | -- insert a new user 59 | Users:create({ name = name }) 60 | end) 61 | 62 | addCommandHandler("getUsers", function(player, cmd) 63 | -- get all users 64 | local users = Users:findAll() 65 | 66 | iprint(users) --[[ 67 | { 68 | [1] = { 69 | id = 1, 70 | name = 'John' 71 | }, 72 | [2] = { 73 | id = 2, 74 | name = 'Jane' 75 | } 76 | } 77 | ]] 78 | end) 79 | 80 | addCommandHandler("getUser", function(player, cmd, id) 81 | -- get a user by id 82 | local user = Users:findByPk(id) 83 | 84 | iprint(user) --[[ 85 | { 86 | id = 1, 87 | name = 'John' 88 | } 89 | ]] 90 | end) 91 | 92 | addCommandHandler("getUserByName", function(player, cmd, name) 93 | -- get a user by name 94 | local user = Users:findOne({ 95 | where = { 96 | name = name 97 | } 98 | }) 99 | 100 | iprint(user) --[[ 101 | { 102 | id = 1, 103 | name = 'John' 104 | } 105 | ]] 106 | end) 107 | 108 | addCommandHandler("updateUser", function(player, cmd, id, name) 109 | -- update a user by id 110 | Users:update({ 111 | name = name 112 | }, { 113 | where = { 114 | id = id 115 | } 116 | }) 117 | end) 118 | 119 | addCommandHandler("deleteUser", function(player, cmd, id) 120 | -- delete a user by id 121 | Users:destroy({ 122 | where = { 123 | id = id 124 | } 125 | }) 126 | end) 127 | ``` 128 | 129 | ## Example (using SQLite) 130 | 131 | ```lua 132 | -- connect to database (MySQL) 133 | local conn = DBManager:new({ 134 | dialect = "sqlite", 135 | storage = "database/db.sqlite" 136 | }) 137 | ``` 138 | 139 | ## Supporting the project 140 | Did you like DB Manager and would you like to contribute to its growth? 141 | 142 | Feel free to create your fork on your profile and contribute PR's to improve the project. 143 | 144 | ## License 145 | 146 | [Click here to see the license.](https://github.com/lodsdev/database-management/blob/main/MIT-LICENSE.txt) 147 | 148 | -------------------------------------------------------------------------------- /dbmanager.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Library: DB Manager 3 | Author: https://github.com/lodsdev 4 | Version: 1.1.2 5 | 6 | MIT License 7 | Copyright (c) 2012-2022 Scott Chacon and others 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | ]] 26 | 27 | local VERSION = 112 28 | 29 | addEventHandler("onResourceStart", resourceRoot, function() 30 | if (not (hasObjectPermissionTo(getThisResource(), 'function.fetchRemote', false))) then 31 | error("[DEBUG - DBManager]: You need to add 'function.fetchRemote' permission to check for new releases!") 32 | end 33 | 34 | fetchRemote("https://api.github.com/repos/lodsdev/db-manager/releases/latest", function(data, status) 35 | assert(status == 0 and data, "[DBManager] Can't fetch 'api.github.com' for new releases! (Status code: " .. tostring(status) .. ")") 36 | 37 | local responseData = fromJSON(data) 38 | if (responseData) then 39 | local tag_name = responseData.tag_name:gsub("%.", "") 40 | local latestVersion = tonumber(tag_name) 41 | if (latestVersion > VERSION) then 42 | outputDebugString("[DEBUG - DBManager]: New version available! (v" .. responseData.tag_name .. ")") 43 | outputDebugString("[DEBUG - DBManager]: Download: " .. responseData.html_url) 44 | end 45 | end 46 | end) 47 | end) 48 | 49 | local cacheUUID = {} 50 | local function generateUUID() 51 | local function hex(num) 52 | local hexstr = '0123456789abcdef' 53 | local s = '' 54 | while (num > 0) do 55 | local mod = math.fmod(num, 16) 56 | s = string.sub(hexstr, mod+1, mod+1) .. s 57 | num = math.floor(num / 16) 58 | end 59 | if (s == '') then s = '0' end 60 | return s 61 | end 62 | 63 | local function create() 64 | math.randomseed(os.time()) 65 | math.random() 66 | math.random() 67 | math.random() 68 | local id0 = hex(math.random(0, 0xffff) + 0x10000) 69 | local id1 = hex(math.random(0, 0xffff) + 0x10000) 70 | local id2 = hex(math.random(0, 0xffff) + 0x10000) 71 | local id3 = hex(math.random(0, 0xffff) + 0x10000) 72 | local id4 = hex(math.random(0, 0xffff) + 0x10000) 73 | local id5 = hex(math.random(0, 0xffff) + 0x10000) 74 | local id6 = hex(math.random(0, 0xffff) + 0x10000) 75 | local id7 = hex(math.random(0, 0xffff) + 0x10000) 76 | return id0 .. id1 .. '-' .. id2 .. '-' .. id3 .. '-' .. id4 .. '-' .. id5 .. id6 .. id7 77 | end 78 | 79 | local uuid = create() 80 | while (cacheUUID[uuid]) do 81 | uuid = create() 82 | end 83 | 84 | cacheUUID[uuid] = true 85 | return uuid 86 | end 87 | 88 | local function isTable(tbl) 89 | return type(tbl) == 'table' 90 | end 91 | 92 | local function isString(str) 93 | return type(str) == 'string' 94 | end 95 | 96 | local function isBoolean(bool) 97 | return type(bool) == 'boolean' 98 | end 99 | 100 | local function isNil(nilValue) 101 | return type(nilValue) == 'nil' 102 | end 103 | 104 | local function isNumber(number) 105 | return type(number) == 'number' 106 | end 107 | 108 | local function isJSON(value) 109 | return ((isString(value) and value:gmatch('%[%[.+%]%]')) and fromJSON(value) or value) 110 | end 111 | 112 | local function async(f, callback, ...) 113 | local asyncCoroutine = coroutine.create(f) 114 | local function step(...) 115 | if (coroutine.status(asyncCoroutine) == 'dead') then 116 | if (callback) then 117 | callback(...) 118 | end 119 | else 120 | local success, result = coroutine.resume(asyncCoroutine, ...) 121 | if (success) then 122 | step(result) 123 | else 124 | error(result, 2) 125 | end 126 | end 127 | end 128 | step(...) 129 | end 130 | 131 | local function tblFind(tbl, value) 132 | if (not tbl or not value) then return nil end 133 | for k, v in pairs(tbl) do 134 | if (v == value) then 135 | return k, v 136 | end 137 | end 138 | end 139 | 140 | local function prepareAndExecQuery(db, query, ...) 141 | local params = { ... } 142 | local queryString = dbPrepareString(db, query, unpack(params)) 143 | if (not queryString) then 144 | return false 145 | end 146 | local queryExec = dbExec(db, queryString) 147 | if (not queryExec) then 148 | return false 149 | end 150 | return true 151 | end 152 | 153 | local function addZeroForLessThan10(number) 154 | if(number < 10) then 155 | return 0 .. number 156 | else 157 | return number 158 | end 159 | end 160 | 161 | local function generateDateTime() 162 | local dateTimeTable = os.date('*t') 163 | local dateTime = dateTimeTable.year .. addZeroForLessThan10(dateTimeTable.month) .. 164 | addZeroForLessThan10(dateTimeTable.day) .. addZeroForLessThan10(dateTimeTable.hour) .. addZeroForLessThan10(dateTimeTable.min) .. addZeroForLessThan10(dateTimeTable.sec) 165 | return dateTime 166 | end 167 | 168 | local function toSQLValue(value) 169 | if (isNumber(value)) then 170 | return tostring(value) 171 | elseif (isBoolean(value)) then 172 | return value and 1 or 0 173 | elseif (isTable(value)) then 174 | return toJSON(value) 175 | elseif (isNil(value)) then 176 | return 'NULL' 177 | else 178 | return value 179 | end 180 | end 181 | 182 | local function formatTblFromDB(tbl) 183 | local newTbl = {} 184 | for k, v in ipairs(tbl) do 185 | if (not newTbl[k]) then 186 | newTbl[k] = {} 187 | end 188 | for key, value in pairs(v) do 189 | newTbl[k][key] = isJSON(value) 190 | end 191 | end 192 | return newTbl 193 | end 194 | 195 | local CONSTRAINTS_DATA = { 196 | allowNull = 'NOT NULL', 197 | unique = 'UNIQUE', 198 | defaultValue = 'DEFAULT', 199 | check = 'CHECK', 200 | primaryKey = 'PRIMARY KEY', 201 | foreingKey = 'FOREIGN KEY', 202 | autoIncrement = 'AUTO_INCREMENT', 203 | references = 'REFERENCES', 204 | onDelete = 'ON DELETE', 205 | onUpdate = 'ON UPDATE', 206 | comment = 'COMMENT', 207 | } 208 | 209 | local CONSTRAINT_ORDER = { 210 | "type", 211 | "autoIncrement", 212 | "allowNull", 213 | "unique", 214 | "defaultValue", 215 | "check", 216 | "references", 217 | "onDelete", 218 | "onUpdate", 219 | "comment", 220 | "primaryKey", 221 | "foreignKey" 222 | } 223 | 224 | local function addConstraintToQuery(query, key, value) 225 | local constraintsToAdd = {} 226 | for i = 1, #CONSTRAINT_ORDER do 227 | local constraint = CONSTRAINT_ORDER[i] 228 | if (constraint ~= 'type' and value[constraint] ~= nil) then 229 | if (constraint == 'allowNull' and not value[constraint]) then 230 | constraintsToAdd[#constraintsToAdd+1] = CONSTRAINTS_DATA[constraint] 231 | elseif (constraint ~= 'primaryKey' and constraint ~= 'allowNull') then 232 | local constraintValue = isBoolean(value[constraint]) and '' or ' ' .. value[constraint] 233 | constraintsToAdd[#constraintsToAdd+1] = CONSTRAINTS_DATA[constraint] .. constraintValue 234 | end 235 | end 236 | end 237 | return query .. ' ' .. table.concat(constraintsToAdd, ' ') 238 | end 239 | 240 | local DEBUG_RUNNING_DEFAULT = 'DBManager RUNNING (Default): ' 241 | 242 | local crud = { 243 | sync = function(self) 244 | return async(function() 245 | local exec = prepareAndExecQuery(self.db:getConnection(), self.queryDefine) 246 | if (not exec) then 247 | error('DBManager: Error in query', 2) 248 | end 249 | return exec 250 | end, function() 251 | local valuesInDB = self.valuesInDB 252 | local strValues = table.concat(valuesInDB, ', ') 253 | 254 | local querySelect = 'SELECT ' .. strValues .. ' FROM `' .. self.tableName .. '` AS `' .. self.tableName .. '`' 255 | local prepareQuery = dbPrepareString(self.db:getConnection(), querySelect) 256 | if (not prepareQuery) then 257 | return false 258 | end 259 | 260 | local result, numAffectedRows, lastInsertId = dbPoll(dbQuery(self.db:getConnection(), prepareQuery), -1) 261 | self.datas = (#result > 0) and formatTblFromDB(result) or {} 262 | outputDebugString(DEBUG_RUNNING_DEFAULT .. querySelect) 263 | 264 | return result, numAffectedRows, lastInsertId 265 | end) 266 | end, 267 | 268 | create = function(self, data) 269 | local values = {} 270 | local queryParts = { 'INSERT INTO `', self.tableName, '` (' } 271 | local dataValues = {} 272 | 273 | if (self.primaryKey) then 274 | queryParts[#queryParts+1] = '`' .. self.primaryKey .. '`' 275 | values[#values+1] = '?' 276 | dataValues[#dataValues+1] = #self.datas + 1 277 | end 278 | 279 | local i = 0 280 | for key, value in pairs(data) do 281 | if (#values > 0) then 282 | queryParts[#queryParts+1] = ', ' 283 | end 284 | 285 | queryParts[#queryParts+1] = '`' 286 | queryParts[#queryParts+1] = key 287 | queryParts[#queryParts+1] = '`' 288 | values[#values+1] = '?' 289 | dataValues[#dataValues+1] = toSQLValue(value) 290 | 291 | i = i + 1 292 | end 293 | 294 | queryParts[#queryParts+1] = ') VALUES (' .. table.concat(values, ', ') .. ')' 295 | 296 | local query = table.concat(queryParts) 297 | local exec = prepareAndExecQuery(self.db:getConnection(), query, unpack(dataValues)) 298 | if (not exec) then 299 | error('DBManager: Error in query, can\'t create data', 2) 300 | end 301 | 302 | self.datas[#self.datas + 1] = data 303 | 304 | outputDebugString('DB Manager RUNNING: ' .. query) 305 | return data 306 | end, 307 | 308 | select = function(self, whereClauses, attributes, justOne) 309 | if (not whereClauses or not isTable(whereClauses)) then 310 | error('DBManager: Invalid whereClauses (select)', 2) 311 | end 312 | 313 | local rows = self.datas 314 | local valuesInDB = self.valuesInDB 315 | local results = {} 316 | local resultsWithAttributes = {} 317 | 318 | for _, attribute in ipairs(attributes or {}) do 319 | if (not tblFind(valuesInDB, attribute)) then 320 | error('DBManager: Invalid attribute (select)', 2) 321 | end 322 | end 323 | 324 | for _, row in pairs(rows) do 325 | local valid = true 326 | 327 | for key, value in pairs(whereClauses) do 328 | if (not tblFind(valuesInDB, key)) then 329 | error('DBManager: Invalid attribute (select)', 2) 330 | end 331 | if (row[key] ~= value) then 332 | valid = false 333 | break 334 | end 335 | end 336 | 337 | if (valid) then 338 | results[#results+1] = row 339 | if (justOne) then 340 | break 341 | end 342 | end 343 | end 344 | 345 | if (attributes and (#attributes > 0)) then 346 | for _, row in ipairs(results) do 347 | local result = {} 348 | for _, attribute in ipairs(attributes) do 349 | result[attribute] = row[attribute] 350 | end 351 | resultsWithAttributes[#resultsWithAttributes+1] = result 352 | end 353 | results = resultsWithAttributes 354 | end 355 | 356 | return results 357 | end, 358 | 359 | findAll = function(self, options) 360 | local whereClauses = options and options.where or {} 361 | local attributes = options and options.attributes or {} 362 | local orderBy = options and options.orderBy or nil 363 | local limit = options and options.limit or nil 364 | local offset = options and options.offset or 0 365 | local order = options and options.order or 'ASC' 366 | 367 | local results = self:select(whereClauses, attributes) 368 | 369 | if (not results) then 370 | error('DBManager: Invalid results (findAll), please open the issue in GitHub', 2) 371 | end 372 | 373 | if (orderBy) then 374 | local orderType = (order == 'ASC') and 1 or -1 375 | table.sort(results, function(a, b) 376 | return orderType * (a[orderBy] < b[orderBy] and -1 or 1) < 0 377 | end) 378 | end 379 | 380 | if (limit) then 381 | for i = #results, 1, -1 do 382 | if (i <= offset or i > (offset + limit)) then 383 | table.remove(results, i) 384 | end 385 | end 386 | end 387 | return results 388 | end, 389 | 390 | findOne = function(self, options) 391 | if (not options) then 392 | error('DBManager: Invalid options (findOne)', 2) 393 | end 394 | 395 | local whereClauses = options.where or {} 396 | local attributes = options.attributes or {} 397 | 398 | local results = self:select(whereClauses, attributes, true) 399 | 400 | if (not results) then 401 | error('DBManager: Invalid results (findOne), please open the issue in GitHub', 2) 402 | end 403 | return results[1] 404 | end, 405 | 406 | findByPk = function(self, pk, options) 407 | if (not self.primaryKey) then 408 | error('DBManager: Invalid primaryKey (findByPk)', 2) 409 | end 410 | 411 | if (not pk) then 412 | error('DBManager: Invalid pk (findByPk)', 2) 413 | end 414 | 415 | local whereClauses = options and options.where or {} 416 | local attributes = options and options.attributes or {} 417 | 418 | whereClauses[self.primaryKey] = pk 419 | 420 | local results = self:select(whereClauses, attributes, true) 421 | 422 | if (not results) then 423 | error('DBManager: Invalid results (findByPk), please open the issue in GitHub', 2) 424 | end 425 | return results 426 | end, 427 | 428 | update = function(self, data, options) 429 | if (not options or not isTable(options)) then 430 | error('DBManager: Invalid options (update)', 2) 431 | end 432 | 433 | local whereClauses = options.where or {} 434 | local queryParts = { 'UPDATE `', self.tableName, '` SET ' } 435 | local values = {} 436 | 437 | for key, value in pairs(data) do 438 | if (#values > 0) then 439 | queryParts[#queryParts+1] = ', ' 440 | end 441 | 442 | queryParts[#queryParts+1] = '`' 443 | queryParts[#queryParts+1] = key 444 | queryParts[#queryParts+1] = '` = ?' 445 | 446 | values[#values+1] = toSQLValue(value) 447 | end 448 | 449 | if (self.db.data.dialect == 'sqlite') then 450 | queryParts[#queryParts+1] = ', `updated_at` = CURRENT_TIMESTAMP' 451 | end 452 | 453 | queryParts[#queryParts+1] = ' WHERE ' 454 | 455 | local i = 0 456 | for key, value in pairs(whereClauses) do 457 | if (i > 0) then 458 | queryParts[#queryParts+1] = ' AND ' 459 | end 460 | 461 | queryParts[#queryParts+1] = '`' 462 | queryParts[#queryParts+1] = key 463 | queryParts[#queryParts+1] = '` = ?' 464 | 465 | values[#values+1] = toSQLValue(value) 466 | 467 | i = i + 1 468 | end 469 | 470 | local query = table.concat(queryParts) 471 | local exec = prepareAndExecQuery(self.db:getConnection(), query, unpack(values)) 472 | if (not exec) then 473 | error('DBManager: ERROR when updating data, please open the issue in GitHub', 2) 474 | end 475 | 476 | local rows = self.datas 477 | local valuesInDB = self.valuesInDB 478 | 479 | for _, row in ipairs(rows) do 480 | local valid = true 481 | for key, value in pairs(whereClauses) do 482 | if (not tblFind(valuesInDB, key)) then 483 | error('DBManager: Invalid attribute (update)', 2) 484 | end 485 | if (row[key] ~= value) then 486 | valid = false 487 | break 488 | end 489 | end 490 | if (valid) then 491 | for key, value in pairs(data) do 492 | if (not tblFind(valuesInDB, key)) then 493 | error('DBManager: Invalid attribute (update)', 2) 494 | end 495 | row[key] = value 496 | end 497 | end 498 | end 499 | 500 | return true 501 | end, 502 | 503 | updateAll = function(self, data) 504 | if (not data or not isTable(data)) then 505 | error('DBManager: Invalid data (updateAll)', 2) 506 | end 507 | 508 | local queryParts = { 'UPDATE `', self.tableName, '` SET ' } 509 | local values = {} 510 | 511 | for key, value in pairs(data) do 512 | if (#values > 0) then 513 | queryParts[#queryParts+1] = ', ' 514 | end 515 | 516 | queryParts[#queryParts+1] = '`' 517 | queryParts[#queryParts+1] = key 518 | queryParts[#queryParts+1] = '` = ?' 519 | 520 | values[#values+1] = toSQLValue(value) 521 | end 522 | 523 | if (self.db.data.dialect == 'sqlite') then 524 | queryParts[#queryParts+1] = ', `updated_at` = CURRENT_TIMESTAMP' 525 | end 526 | 527 | local query = table.concat(queryParts) 528 | local exec = prepareAndExecQuery(self.db:getConnection(), query, unpack(values)) 529 | if (not exec) then 530 | error('DBManager: ERROR when updating data, please open the issue in GitHub', 2) 531 | end 532 | 533 | local rows = self.datas 534 | for _, row in ipairs(rows) do 535 | for key, value in pairs(data) do 536 | if (not tblFind(self.valuesInDB, key)) then 537 | error('DBManager: Invalid attribute (updateAll)', 2) 538 | end 539 | row[key] = value 540 | end 541 | end 542 | 543 | return true 544 | end, 545 | 546 | destroy = function(self, options) 547 | if (not options or not isTable(options)) then 548 | error('DBManager: Invalid options (destroy)', 2) 549 | end 550 | 551 | local whereClauses = options.where or {} 552 | local truncate = options.truncate or false 553 | local queryParts = { 'DELETE FROM `', self.tableName, '` WHERE '} 554 | local values = {} 555 | 556 | if (truncate) then 557 | queryParts = { 'TRUNCATE TABLE `', self.tableName, '`' } 558 | else 559 | local i = 0 560 | for key, value in pairs(whereClauses) do 561 | if (i > 0) then 562 | queryParts[#queryParts+1] = ' AND ' 563 | end 564 | 565 | queryParts[#queryParts+1] = '`' 566 | queryParts[#queryParts+1] = key 567 | queryParts[#queryParts+1] = '` = ?' 568 | values[#values+1] = toSQLValue(value) 569 | 570 | i = i + 1 571 | end 572 | end 573 | 574 | local query = table.concat(queryParts) 575 | local exec = prepareAndExecQuery(self.db:getConnection(), query, unpack(values)) 576 | if (not exec) then 577 | error('DBManager: ERROR when destroying data, please open the issue in GitHub', 2) 578 | end 579 | 580 | if (not truncate) then 581 | local valuesInDB = self.valuesInDB 582 | for i = #self.datas, 1, -1 do 583 | local row = self.datas[i] 584 | local valid = true 585 | for key, value in ipairs(whereClauses) do 586 | if (not tblFind(valuesInDB, key)) then 587 | error('DBManager: Invalid attribute (destroy)', 2) 588 | end 589 | if (row[key] ~= value) then 590 | valid = false 591 | break 592 | end 593 | end 594 | if (valid) then 595 | table.remove(self.datas, i) 596 | end 597 | end 598 | else 599 | self.datas = {} 600 | end 601 | 602 | return true 603 | end, 604 | 605 | drop = function(self) 606 | local query = 'DROP TABLE `' .. self.tableName .. '`' 607 | local exec = prepareAndExecQuery(self.db:getConnection(), query) 608 | if (not exec) then 609 | error('DBManager: ERROR when dropping table, please open the issue in GitHub', 2) 610 | end 611 | 612 | self.datas = {} 613 | self.db:removeTable(self.tableName) 614 | 615 | return true 616 | end, 617 | 618 | every = function(self, callback) 619 | for i, data in ipairs(self.datas) do 620 | callback(data, i) 621 | end 622 | end, 623 | } 624 | 625 | local private = {} 626 | setmetatable(private, {__mode = 'k'}) 627 | 628 | DBManager = { 629 | STRING = function(length) 630 | return (length) and 'VARCHAR(' .. length .. ')' or 'VARCHAR(255)' 631 | end, 632 | BINARY = function(length) 633 | return (length) and 'BINARY(' .. length .. ')' or 'BINARY' 634 | end, 635 | TEXT = function(t) 636 | return (t == 'tiny') and 'TINYTEXT' or 'TEXT' 637 | end, 638 | BOOLEAN = function() 639 | return 'BOOLEAN' 640 | end, 641 | INTEGER = function() 642 | return 'INTEGER' 643 | end, 644 | INT = function(length) 645 | return (length) and 'INT(' .. length .. ')' or 'INT' 646 | end, 647 | BIGINT = function(length) 648 | return (length) and 'BIGINT(' .. length .. ')' or 'BIGINT' 649 | end, 650 | FLOAT = function(length, decimals) 651 | if (length) then 652 | return 'FLOAT(' .. length .. ')' 653 | elseif (length and decimals) then 654 | return 'FLOAT(' .. length .. ', ' .. decimals .. ')' 655 | else 656 | return 'FLOAT' 657 | end 658 | end, 659 | DOUBLE = function(length, decimals) 660 | if (length) then 661 | return 'DOUBLE(' .. length .. ')' 662 | elseif (length and decimals) then 663 | return 'DOUBLE(' .. length .. ', ' .. decimals .. ')' 664 | else 665 | return 'DOUBLE' 666 | end 667 | end, 668 | DECIMAL = function(length, decimals) 669 | if (length and decimals) then 670 | return 'DECIMAL(' .. length .. ', ' .. decimals .. ')' 671 | else 672 | return 'DECIMAL' 673 | end 674 | end, 675 | BLOB = function(t) 676 | return (t == 'tiny') and 'TINYBLOB' or 'BLOB' 677 | end, 678 | DATE = function() 679 | return 'DATE' 680 | end, 681 | TIME = function() 682 | return 'TIME' 683 | end, 684 | DATETIME = function() 685 | return 'DATETIME' 686 | end, 687 | DATEONLY = function() 688 | return 'DATE' 689 | end, 690 | NOW = function () 691 | return generateDateTime() 692 | end, 693 | UUID = generateUUID, 694 | 695 | models = {} 696 | } 697 | 698 | function DBManager:new(data) 699 | local instance = {} 700 | 701 | if (not data) then 702 | error('DBManager: Data is required', 2) 703 | end 704 | 705 | instance.data = data 706 | 707 | if (instance.data.dialect == 'mysql') then 708 | instance.data.charset = instance.data.charset or 'utf8' 709 | instance.data.options = instance.data.options or '' 710 | instance.CONNECTION = dbConnect( 711 | instance.data.dialect, 712 | 'host=' .. instance.data.host .. ';port=' .. instance.data.port .. ';dbname=' .. instance.data.database .. ';charset=' .. instance.data.charset, 713 | instance.data.username, 714 | instance.data.password, 715 | instance.data.options 716 | ) 717 | elseif (data.dialect == 'sqlite') then 718 | instance.CONNECTION = dbConnect(instance.data.dialect, instance.data.storage or 'database.db') 719 | end 720 | 721 | setmetatable(instance, { __index = self }) 722 | return instance 723 | end 724 | 725 | function DBManager:getConnection() 726 | return self.CONNECTION 727 | end 728 | 729 | function DBManager:close() 730 | local connection = self:getConnection() 731 | if (isElement(connection)) then 732 | destroyElement(connection) 733 | return true 734 | end 735 | return false 736 | end 737 | 738 | function DBManager:query(queryString) 739 | local preparatedString = dbPrepareString(self:getConnection(), queryString) 740 | if (not preparatedString) then 741 | return false 742 | end 743 | 744 | local query = dbQuery(self:getConnection(), preparatedString) 745 | if (not query) then 746 | return false 747 | end 748 | 749 | local result, numAffectedRows, lastInsertId = dbPoll(query, -1) 750 | if (not result) then 751 | return false 752 | end 753 | 754 | return result, numAffectedRows, lastInsertId 755 | end 756 | 757 | function DBManager:define(tableName, modelDefinition) 758 | local instance = {} 759 | 760 | instance.modelDefinition = modelDefinition 761 | instance.tableName = tableName 762 | instance.db = self 763 | instance.dataTypes = {} 764 | instance.primaryKey = "" 765 | instance.valuesInDB = {} 766 | instance.datas = {} 767 | 768 | local queryDefine = "CREATE TABLE IF NOT EXISTS `" .. instance.tableName .. "` (" 769 | 770 | local i = 0 771 | for key, value in pairs(modelDefinition) do 772 | if (i > 0) then 773 | queryDefine = queryDefine .. ", " 774 | end 775 | 776 | queryDefine = queryDefine .. "`" .. key .. "`" 777 | 778 | if (isTable(value)) then 779 | if (value.type) then 780 | instance.dataTypes[key] = value.type 781 | queryDefine = queryDefine .. " " .. value.type 782 | end 783 | 784 | if (instance.primaryKey == "" and value.primaryKey) then 785 | instance.primaryKey = key 786 | elseif (instance.primaryKey ~= "" and value.primaryKey) then 787 | error('DBManager: Only one primary key is allowed', 2) 788 | end 789 | 790 | queryDefine = addConstraintToQuery(queryDefine, key, value) 791 | else 792 | instance.dataTypes[key] = value 793 | queryDefine = queryDefine .. " " .. value 794 | end 795 | 796 | instance.valuesInDB[#instance.valuesInDB+1] = key 797 | i = i + 1 798 | end 799 | 800 | if (instance.primaryKey == "") then 801 | error('DBManager: No primary key defined', 2) 802 | end 803 | 804 | if (instance.db.dialect == 'mysql') then 805 | queryDefine = queryDefine .. ", `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP" 806 | queryDefine = queryDefine .. ", `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" 807 | else 808 | queryDefine = queryDefine .. ", `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP" 809 | queryDefine = queryDefine .. ", `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP" 810 | end 811 | 812 | instance.valuesInDB[#instance.valuesInDB+1] = 'created_at' 813 | instance.valuesInDB[#instance.valuesInDB+1] = 'updated_at' 814 | 815 | instance.queryDefine = queryDefine .. ", PRIMARY KEY (`" .. instance.primaryKey .. "`))" 816 | 817 | self.models[tableName] = instance 818 | setmetatable(instance, { __index = crud }) 819 | return instance 820 | end 821 | 822 | function DBManager:removeTable(tableName) 823 | for i, model in ipairs(allModels) do 824 | if (model.tableName == tableName) then 825 | table.remove(allModels, i) 826 | end 827 | end 828 | end 829 | 830 | function DBManager:sync() 831 | for _, model in pairs(self.models) do 832 | model:sync() 833 | end 834 | end 835 | 836 | function DBManager:drop() 837 | for _, model in pairs(self.models) do 838 | model:drop() 839 | end 840 | end --------------------------------------------------------------------------------