diff --git a/Classes/DbScriptExtensions_ColumnType.ext b/Classes/DbScriptExtensions_ColumnType.ext new file mode 100644 index 0000000..d9de252 --- /dev/null +++ b/Classes/DbScriptExtensions_ColumnType.ext @@ -0,0 +1,13 @@ +ColumnType = {} +ColumnType.Bool = ElunaQuery.GetBool +ColumnType.Double = ElunaQuery.GetDouble +ColumnType.Float = ElunaQuery.GetFloat +ColumnType.Int8 = ElunaQuery.GetInt8 +ColumnType.Int16 = ElunaQuery.GetInt16 +ColumnType.Int32 = ElunaQuery.GetInt32 +ColumnType.Int64 = ElunaQuery.GetInt64 +ColumnType.String = ElunaQuery.GetString +ColumnType.UInt8 = ElunaQuery.GetUInt8 +ColumnType.UInt16 = ElunaQuery.GetUInt16 +ColumnType.UInt32 = ElunaQuery.GetUInt32 +ColumnType.UInt64 = ElunaQuery.GetUInt64 \ No newline at end of file diff --git a/Classes/DbScriptExtensions_Database.ext b/Classes/DbScriptExtensions_Database.ext new file mode 100644 index 0000000..a5c126d --- /dev/null +++ b/Classes/DbScriptExtensions_Database.ext @@ -0,0 +1,206 @@ +Database = {} + +setmetatable(Database, { + __call = function(cls, ...) + local self = setmetatable({}, cls) + return self + end +}) + +function Database:ContainsTable(value) + for _, v in ipairs(self.Tables) do + if (v == value) then + return true + end + end + + return false +end + +function Database:GetTableInformations() + -- taken from TC: https://github.com/TrinityCore/TrinityCore/blob/3.3.5/src/server/database/Database/Field.h#L62 + local columnTypeFunctions = { + ["bit"] = function() return ColumnType.Bool end, + ["tinyint"] = function() return ColumnType.Int8 end, + ["tinyint unsigned"] = function() return ColumnType.UInt8 end, + ["enum"] = function() return ColumnType.UInt8 end, + ["smallint"] = function() return ColumnType.Int16 end, + ["smallint unsigned"] = function() return ColumnType.UInt16 end, + ["mediumint"] = function() return ColumnType.Int32 end, + ["mediumint unsigned"] = function() return ColumnType.UInt32 end, + ["int"] = function() return ColumnType.Int32 end, + ["int unsigned"] = function() return ColumnType.UInt32 end, + ["integer"] = function() return ColumnType.Int32 end, + ["integer unsigned"] = function() return ColumnType.UInt32 end, + ["timestamp"] = function() return ColumnType.UInt32 end, + ["bigint"] = function() return ColumnType.Int64 end, + ["bigint unsigned"] = function() return ColumnType.UInt64 end, + ["float"] = function() return ColumnType.Float end, + ["double"] = function() return ColumnType.Double end, + ["dec"] = function() return ColumnType.Double end, + ["decimal"] = function() return ColumnType.Double end, + ["char"] = function() return ColumnType.String end, + ["character"] = function() return ColumnType.String end, + ["varchar"] = function() return ColumnType.String end, + ["tinytext"] = function() return ColumnType.String end, + ["mediumtext"] = function() return ColumnType.String end, + ["text"] = function() return ColumnType.String end, + ["longtext"] = function() return ColumnType.String end, + ["binary"] = function() return ColumnType.String end, + ["varbinary"] = function() return ColumnType.String end, + ["tinyblob"] = function() return ColumnType.String end, + ["mediumblob"] = function() return ColumnType.String end, + ["blob"] = function() return ColumnType.String end, + ["longblob"] = function() return ColumnType.String end, + } + local columnTypeNames = { + ["bit"] = function() return "ColumnType.Bool" end, + ["tinyint"] = function() return "ColumnType.Int8" end, + ["tinyint unsigned"] = function() return "ColumnType.UInt8" end, + ["enum"] = function() return "ColumnType.UInt8" end, + ["smallint"] = function() return "ColumnType.Int16" end, + ["smallint unsigned"] = function() return "ColumnType.UInt16" end, + ["mediumint"] = function() return "ColumnType.Int32" end, + ["mediumint unsigned"] = function() return "ColumnType.UInt32" end, + ["int"] = function() return "ColumnType.Int32" end, + ["int unsigned"] = function() return "ColumnType.UInt32" end, + ["integer"] = function() return "ColumnType.Int32" end, + ["integer unsigned"] = function() return "ColumnType.UInt32" end, + ["timestamp"] = function() return "ColumnType.UInt32" end, + ["bigint"] = function() return "ColumnType.Int64" end, + ["bigint unsigned"] = function() return "ColumnType.UInt64" end, + ["float"] = function() return "ColumnType.Float" end, + ["double"] = function() return "ColumnType.Double" end, + ["dec"] = function() return "ColumnType.Double" end, + ["decimal"] = function() return "ColumnType.Double" end, + ["char"] = function() return "ColumnType.String" end, + ["character"] = function() return "ColumnType.String" end, + ["varchar"] = function() return "ColumnType.String" end, + ["tinytext"] = function() return "ColumnType.String" end, + ["mediumtext"] = function() return "ColumnType.String" end, + ["text"] = function() return "ColumnType.String" end, + ["longtext"] = function() return "ColumnType.String" end, + ["binary"] = function() return "ColumnType.String" end, + ["varbinary"] = function() return "ColumnType.String" end, + ["tinyblob"] = function() return "ColumnType.String" end, + ["mediumblob"] = function() return "ColumnType.String" end, + ["blob"] = function() return "ColumnType.String" end, + ["longblob"] = function() return "ColumnType.String" end, + } + + local tableQuery = WorldDBQuery("SHOW TABLES") + if (not tableQuery) then + error("[DbScriptExtensions] Couldn't execute the query \"SHOW TABLES;\". Make sure your MySQL user has the rights to execute it.") + return + end + + local tableList = {} + repeat + local tableName = tableQuery:GetString(0) + local className = "Db"..tableName:gsub("^%l", tableName.upper):gsub("_(.)", tableName.upper) + table.insert(tableList, { + tableName = tableName, + className = className + }) + until not tableQuery:NextRow() + + local file = "" + local foundTables = {} + for i, v in ipairs(tableList) do + local tableName = tableList[i].tableName + local className = tableList[i].className + local query = WorldDBQuery("EXPLAIN "..tableName) + if (query) then + -- If we got a table description, we add the related class name to the found table list + table.insert(foundTables, tableList[i].className) + + local columns = {} + repeat + local obj = {} + obj.field = query:GetString(0) + local dataType = query:GetString(1) + dataType = dataType:gsub('%(.-%)', '') -- Cut out field length in brackets + local columnType = columnTypeNames[dataType] + if (columnType == nil) then + error("[DbScriptExtensions] Unreadable column type "..dataType.." found. File generation stopped.") + end + obj.columnType = columnType() + obj.isPrimaryKey = false + local key = query:GetString(3) + if (key == "PRI") then + obj.isPrimaryKey = true + end + obj.defaultValue = columnTypeFunctions[dataType]()(query, 4) + if (type(obj.defaultValue) == "string") then + obj.defaultValue = "\""..obj.defaultValue.."\"" + end + table.insert(columns, obj) + until not query:NextRow() + + file = file.."function "..className..":Default()\n " + file = file.."self:SetTableName(\""..tableName.."\")\n" + for _, v in ipairs(columns) do + file = file.." self:Add" + if (v.isPrimaryKey) then + file = file.."PrimaryKey" + end + file = file.."Column(\""..v.field.."\", "..tostring(v.defaultValue)..", "..v.columnType..")\n" + end + file = file.."end\n\n" + end + end + + local mappingHeader = "" + for i, v in ipairs(foundTables) do + mappingHeader = mappingHeader.."require(\"DbScriptExtensions_Mappings_"..foundTables[i].."\")\n" + end + + + mappingHeader = mappingHeader.."\nDatabase.Tables = {\n" + for i, v in ipairs(foundTables) do + mappingHeader = mappingHeader.." "..foundTables[i] + if (i ~= #foundTables) then + mappingHeader = mappingHeader.."," + end + mappingHeader = mappingHeader.."\n" + end + mappingHeader = mappingHeader.."}\n\n" + file = mappingHeader..file + + local mappingFilePath = "lua_scripts\\extensions\\DbScriptExtensions\\Mapping\\DbScriptExtensions_Mappings.ext" + local mappingFile = io.open(mappingFilePath, "w+") + mappingFile:write(file) + mappingFile:close() + + local classFileContentTemplate = [[ +require("DbScriptExtensions_Queryable") + +className = {} +for k, v in pairs(Queryable) do + className[k] = v +end +className.__newindex = Queryable.__newindex +className.__index = Queryable.__index + +setmetatable(className, { + __call = function (cls) + local self = setmetatable({}, cls) + self.__type = className + self:Default() + self:RefreshChangeTrackerValues() + return self + end +})]] + + for i, v in ipairs(foundTables) do + local classFileContent = string.gsub(classFileContentTemplate, "className", foundTables[i]) + local classFilePath = "lua_scripts\\extensions\\DbScriptExtensions\\Mapping\\DbScriptExtensions_Mappings_"..foundTables[i]..".ext" + local classFile = io.open(classFilePath, "w+") + classFile:write(classFileContent) + classFile:close() + end + + + + +end \ No newline at end of file diff --git a/DbScriptExtensions.ext b/DbScriptExtensions.ext new file mode 100644 index 0000000..b762671 --- /dev/null +++ b/DbScriptExtensions.ext @@ -0,0 +1,2 @@ +DbScriptExtensions_PrintQueries = false +DbScriptExtensions_GenerateTableFiles = true \ No newline at end of file diff --git a/DbScriptExtensions_Main.ext b/DbScriptExtensions_Main.ext new file mode 100644 index 0000000..4dcaf10 --- /dev/null +++ b/DbScriptExtensions_Main.ext @@ -0,0 +1,11 @@ +require("DbScriptExtensions_Database") + +-- Generate table classes if needed +if (DbScriptExtensions_GenerateTableFiles) then + print("[DbScriptExtensions] Generating table classes..") + Database:GetTableInformations(DbScriptExtensions_Tables) + print("[DbScriptExtensions] Table classes generated") + print("[DbScriptExtensions] It might be possible that an error occures when accessing a table class. Set DbScriptExtensions_GenerateTableFiles = false (in DbScriptExtensions.ext) and restart the world server. If the problem still occures, please report an issue on GitHub.") + require("DbScriptExtensions_Mappings") -- Make sure to load the mappings before loading lua scripts to prevent errors after generating mapping files +end + diff --git a/Mapping/.gitkeep b/Mapping/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Querying/DbScriptExtensions_Queryable.ext b/Querying/DbScriptExtensions_Queryable.ext new file mode 100644 index 0000000..11041bf --- /dev/null +++ b/Querying/DbScriptExtensions_Queryable.ext @@ -0,0 +1,245 @@ +Queryable = {} + +function Queryable:__newindex(index, value) + -- These both needs to be set before we do any column checking + if (index == "__type" or index == "__columns") then + rawset(self, index, value) + return + end + + -- If index is a known table column, set the value in the table column attribute + if (self.__columns[index] ~= nil) then + rawset(self.__columns, index, value) + return + end + + rawset(self, index, value) +end + +function Queryable:__index(index) + -- If index is a function, return the function + if (type(self.__type[index]) == "function") then + + -- We don't to write this over and over again in the mappings so we call it here instead + if (index == "Default") then + self:PrepareColumns() + end + + return self.__type[index] + end + + -- If index is in known table columns, return the value of table column attribute + if (self.__columns[index] ~= nil) then + return rawget(self.__columns, index) + end + + -- Return actual attribute value + return rawget(self, index) +end + +function Queryable:PrepareColumns() + self.__columns = {} + self.__columnIds = {} + self.__columnById = {} + self.__columnIdCounter = 0 + self.__columnFunctions = {} + self.__primaryKeys = {} + self.__changeTracker = {} +end + +function Queryable:SetTableName(tableName) + self.__tableName = tableName +end + +function Queryable:AddColumn(name, defaultValue, columnType) + self.__columns[name] = defaultValue + self.__columnIds[name] = self.__columnIdCounter + self.__columnById[self.__columnIdCounter] = name + self.__columnIdCounter = self.__columnIdCounter + 1 + self.__columnFunctions[name] = columnType +end + +function Queryable:AddPrimaryKeyColumn(name, defaultValue, columnType) + table.insert(self.__primaryKeys, name) + self:AddColumn(name, defaultValue, columnType) +end + +function Queryable:GetColumnsAsString() + local columns = "" + for i = 0, #self.__columnById do + columns = columns..self.__columnById[i] + if i ~= #self.__columnById then + columns = columns.."," + end + end + return columns +end + +function Queryable:GetColumnValuesAsString() + local values = "" + for i = 0, #self.__columnById do + local value = self[self.__columnById[i]] + if (type(value) == "string") then + value = "'"..value.."'" + end + values = values..value + if i ~= #self.__columnById then + values = values.."," + end + end + return values +end + +function Queryable:RefreshChangeTrackerValues() + for k, v in pairs(self.__columns) do + self.__changeTracker[k] = v + end +end + +function Queryable:ToObject(query) + for k, _ in pairs(self.__columns) do + self.__columns[k] = self.__columnFunctions[k](query, self.__columnIds[k]) + end +end + +function Queryable:GetPrimaryKeySelectorString(valueTable) + local primaryKeySelectorString = "" + for i = 1, #self.__primaryKeys do + primaryKeySelectorString = primaryKeySelectorString..self.__primaryKeys[i].."="..self.__columns[self.__primaryKeys[i]] + if (i ~= #self.__primaryKeys) then + primaryKeySelectorString = primaryKeySelectorString.." AND " + end + end + return primaryKeySelectorString +end + +function Queryable:GetPrimaryKeySelectorStringByValueTable(valueTable) + local primaryKeySelectorString = "" + for i = 1, #self.__primaryKeys do + primaryKeySelectorString = primaryKeySelectorString..self.__primaryKeys[i].."="..valueTable[i] + if (i ~= #self.__primaryKeys) then + primaryKeySelectorString = primaryKeySelectorString.." AND " + end + end + return primaryKeySelectorString +end + +function Queryable:GetSelectString(primaryKeySelectorString) + return "SELECT "..self:GetColumnsAsString().." FROM "..self.__tableName.." WHERE "..primaryKeySelectorString.." LIMIT 1" +end + +function Queryable:GetUpdateString(primaryKeySelectorString, changeString) + return "UPDATE "..self.__tableName.." SET "..changeString.." WHERE "..primaryKeySelectorString +end + +function Queryable:GetInsertString() + return "INSERT INTO "..self.__tableName.."("..self:GetColumnsAsString()..") VALUES ("..self:GetColumnValuesAsString()..")" +end + +function Queryable:GetDeleteString(primaryKeySelectorString) + return "DELETE FROM "..self.__tableName.." WHERE "..primaryKeySelectorString +end + +function Queryable:GetMaxPrimaryKeysString() + local query = "SELECT " + for i = 1, #self.__primaryKeys do + query = query.."MAX("..self.__primaryKeys[i]..")" + if (i ~= #self.__primaryKeys) then + query = query.."," + end + end + query = query.." FROM "..self.__tableName + return query +end + +function Queryable:GetChangeTrackerString() + local changeString = "" + for k, v in pairs(self.__columns) do + if (v ~= self.__changeTracker[k]) then + local value = self.__columns[k] + if (type(value) == "string") then + value = "'"..value.."'" + end + changeString = changeString..k.."="..value.."," + end + end + return changeString:sub(1, -2) +end + +function Queryable:Load(...) + local obj + if (Database:ContainsTable(self)) then -- Static like call, create a new object of table type and modify that + obj = self() + else -- Function like call, modify current object + obj = self + end + + local args = {...} + + if (#obj.__primaryKeys ~= #args) then + error("[DbScriptExtensions] You tried to load with "..#args.." IDs but the table has "..#obj.__primaryKeys.." primary keys.") + end + + local primaryKeySelectorString = obj:GetPrimaryKeySelectorStringByValueTable(args) + local queryString = obj:GetSelectString(primaryKeySelectorString) + if (DbScriptExtensions_PrintQueries) then + print("[DbScriptExtensions] "..queryString) + end + local query = WorldDBQuery(queryString) + if (not query) then + error("[DbScriptExtensions] Query didn't return anything with the following primary keys: "..primaryKeySelectorString) + end + + obj:ToObject(query) + obj:RefreshChangeTrackerValues() + return obj +end + +function Queryable:Save() + local primaryKeySelectorString = self:GetPrimaryKeySelectorString(self.__primaryKeys) + local selectQueryString = self:GetSelectString(primaryKeySelectorString) + local selectQuery = WorldDBQuery(selectQueryString) + local query + if (selectQuery) then -- Object already exists, use UPDATE + local changeTrackerString = self:GetChangeTrackerString() + if (changeTrackerString == "") then + return -- Don't do anything if there were no changes + end + query = self:GetUpdateString(primaryKeySelectorString, changeTrackerString) + else -- New object, use INSERT + query = self:GetInsertString() + end + if (DbScriptExtensions_PrintQueries) then + print("[DbScriptExtensions] "..query) + end + WorldDBExecute(query) + self:RefreshChangeTrackerValues() +end + +function Queryable:Delete() + local primaryKeySelectorString = self:GetPrimaryKeySelectorString(self.__primaryKeys) + local deleteQueryString = self:GetDeleteString(primaryKeySelectorString) + if (DbScriptExtensions_PrintQueries) then + print("[DbScriptExtensions] "..deleteQueryString) + end + WorldDBExecute(deleteQueryString) +end + +function Queryable:GetNewPrimaryKeys() + local queryString = self:GetMaxPrimaryKeysString() + if (DbScriptExtensions_PrintQueries) then + print("[DbScriptExtensions] "..queryString) + end + local query = WorldDBQuery(queryString) + if (query) then + local returnTable = {} + for i = 1, #self.__primaryKeys do + local value = self.__columnFunctions[self.__primaryKeys[i]](query, self.__columns[self.__primaryKeys[i]]) + + table.insert(returnTable, value + 1) + end + return table.unpack(returnTable) + end + error("[DbScriptExtensions] Couldn't get new primary keys. Please report as a GitHub issue with a code snippet and the executed query.") + return nil +end \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..73e2c5d --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# DbScriptExtensions + +## What is DbScriptExtensions? + +DbScriptExtensions is a Lua framework for the [Eluna Lua Engine ©](https://github.com/ElunaLuaEngine/Eluna) that allows you to create, load, modify and delete database entities via your own Lua code. + +## How to use DbScriptExtensions +* Copy the content of this repository to the folder `lua_scripts/extensions/DbScriptExtensions`. + + To check if you copied it correctly, make sure you find the README file under `lua_scripts/extensions/DbScriptExtensions/README.md` +* Run the world server of the emulator of your choice that supports [Eluna Lua Engine ©](https://github.com/ElunaLuaEngine/Eluna) +* After the server finished loading all Lua scripts, it will show you the message `"[DbScriptExtensions] Table classes generated"` +* Close the world server +* Open the file `lua_scripts/extensions/DbScriptExtensions/DbScriptExtensions.ext` and edit the line `DbScriptExtensions_GenerateTableFiles = true` to `DbScriptExtensions_GenerateTableFiles = false` +* Run the world server again and use DbScriptExtensions in your Lua scripts + +## Examples +### Creating a new database object + +```lua +local newCreature = DbCreature() +--[[ + Make sure that the primary key of a table (in this example "guid" of table "creature") isn't already existing, otherwise the object will not properly track changes to existing values in the database. + + All column values are automatically set to the default database values when you're creating a new object. +]] +newCreature.guid = 123123123 +newCreature:Save() +``` + +### Get unused primary keys +```lua +-- Use this while creating new entries without a fixed primary key +local race, class = DbPlayercreateinfo():GetNewPrimaryKeys() -- This will return race = 12, class = 12 because the highest race and class in the table playercreateinfo are ID 11. It will always return all primary key columns. +``` + +### Loading a database object +```lua +-- Way 1 +local existingCreature = DbCreature() -- Create new object of database table creature +existingCreature:Load(123) -- Load data of entry with guid = 123 (guid is the primary key of creature) + +-- Way 2 +local existingCreature = DbCreature():Load(123) + +-- Load entry with combined primary keys +local existingPlayerCreateInfo = DbPlayercreateinfo():Load(1, 4) -- Table playercreateinfo has a combined primary key based on column race and class. The order will always be the same as in the database which means we're loading the entry with race = 1 AND class = 4 +``` + +### Updating an existing database object +```lua +local existingCreature = DbCreature():Load(123) +existingCreature.map = 1337 -- Set map column value to 1337 +existingCreature:Save() -- Save changes to database, will insert a new entry with guid 123 or update existing entry with guid 123 +``` + +### Delete an existing database object +```lua +local existingCreature = DbCreature():Load(123) +existingCreature:Delete() -- Delete row with guid = 123 +``` + +## FAQ +### I see errors in my world server console. What can i do? +Set `DbScriptExtensions_PrintQueries = true` in your file `lua_scripts/extensions/DbScriptExtensions/DbScriptExtensions.ext`. Now restart the world server. It should show all DbScriptExtensions SQL queries that it tries to run. Now you can open up an issue on the GitHub repository with the query it tried to execute and a snippet of your code that leads to this error. Make sure to mention which emulator you're using. + +### I updated my emulator and database and now i see errors in my world server console. What can i do? +There were probably some changes in your database because of the update. Try to regenerate the table files by deleting all files under `lua_scripts/extensions/DbScriptExtensions/Mapping` (Do not delete the Mapping folder! Regenerating will fail if you do this!) and setting `DbScriptExtensions_GenerateTableFiles = true` in your file `lua_scripts/extensions/DbScriptExtensions/DbScriptExtensions.ext`. Now you can reload Eluna by typing `reload eluna` into your console or by typing `.reload eluna` ingame. Afterwards set the option back to false and restart your world server. If the error still persists, report the problem as explained in the first question. + +### Are all column names case sensitive? +Yes. + +### Does DbScriptExtensions support my custom tables? +Yes. After adding a new table to your world database, just regenerate the table files as explained in second question. Make sure to only use MySQL data types that are supported by your emulator. If i missed a type and you encounter an error, make sure to report it as a GitHub issue. + +### How do i figure out the class name of a specific table? +The mapping is done in the file `lua_scripts\extensions\DbScriptExtensions\Mapping\DbScriptExtensions_Mappings.ext`. You can find all class names in there. + +### Are there any attributes that could help me to create objects in a generic way? +There are several internal helper attributes that you can use. +```lua + local creature = DbCreature():Load(123) + local guid = creature.__columns["guid"] -- Access column directly by name, same value as creature.guid + local guidColumnId = creature.__columnIds["guid"] -- Will return 0 because guid is the first column in the table creature (it's 0 based because the ElunaQuery functions are 0 based. Lua is usually 1 based) + local guidColumnName = creature.__columnById[guidColumnId] -- Will return "guid" because column 1 is guid as seen in the line above + local amountOfColumns = creature.__columnIdCounter -- Will return 24 because the table creature has 24 columns + local elunaReadColumnFunction = self.__columnFunctions["guid"] -- Will return function ElunaQuery:GetUInt32 because this is the function to read the value out of an query object. Use like this: elunaReadColumnFunction(query) + local queryObject = WorldDBQuery("SELECT * FROM creature WHERE guid = 123") + if (queryObject) then + local guid = elunaReadColumnFunction(queryObject, guidColumnId) -- Will return 123 + end +``` + +### Where can i get support? +You can find me on the official [Eluna Discord server](https://discord.gg/Ed5HK3Dc). Ask your question in the support channel and tag me (Kaev#5208). + +### Can i support your work? +Sure. Contact me via Discord (Kaev#5208). \ No newline at end of file