Developing an example Protection plugin
#10
Storage startup
The next step is somewhat out-of-order, it was not exactly the most logical step to do, but since it posed an interesting challenge, I decided to tackle it first.
The SQLite DB that we're planning for the data storage needs to have its tables set up properly. We could either distribute an empty DB file with the plugin, or we could set the tables up in code. Since the tables structure may change in the future, which would necessitate this upgrade code anyway, I decided for the second option. So upon plugin startup, the storage needs to open the DB file and then check the structure. We want to ensure that the DB has at least the tables and columns we'll be using later, so the code first defines those tables and columns in variables, and then calls a function for each table that "massages" the current table, if any, into the needed structure. This piece of code is rather interesting and I'm quite proud of coding it, so I'll be commenting on the actual code. It is the cStorage:CreateTable() function inside Storage.lua.

1
2
3
4
5
6
7
8
9
function cStorage:CreateTable(a_TableName, a_Columns)
    local sql = "CREATE TABLE IF NOT EXISTS '" .. a_TableName .. "' (";
    sql = sql .. table.concat(a_Columns, ", ");
    sql = sql .. ")";
    local ErrCode = self.DB:exec(sql);
    if (ErrCode ~= sqlite3.OK) then
        LOGWARNING(PluginPrefix .. "Cannot create DB Table, error " .. ErrCode .. " (" .. self.DB:errmsg() .. ")");
        return false;
    end
First, the table is created, if it doesn't already exist. SQLite has a simple command for that, but it's impossible to know whether it created the table or it did exist before, so we need to continue in either case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
local RemoveExistingColumn = function(UserData, NumCols, Values, Names)
    -- Remove the received column from a_Columns. Search for column name in the Names[] / Values[] pairs
    for i = 1, NumCols do
        if (Names[i] == "name") then
            local ColumnName = Values[i]:lower();
            -- Search the a_Columns if they have that column:
            for j = 1, #a_Columns do
                -- Cut away all column specifiers (after the first space), if any:
                local SpaceIdx = string.find(a_Columns[j], " ");
                if (SpaceIdx ~= nil) then
                    SpaceIdx = SpaceIdx - 1;
                end
                local ColumnTemplate = string.lower(string.sub(a_Columns[j], 1, SpaceIdx));
                -- If it is a match, remove from a_Columns:
                if (ColumnTemplate == ColumnName) then
                    table.remove(a_Columns, j);
                    break-- for j
                end
            end  -- for j - a_Columns[]
        end
    end  -- for i - Names[] / Values[]
    return 0;
end
local ErrCode = self.DB:exec("PRAGMA table_info(" .. a_TableName .. ")", RemoveExistingColumn);
if (ErrCode ~= sqlite3.OK) then
    LOGWARNING(PluginPrefix .. "Cannot query DB table structure, error " .. ErrCode .. " (" .. self.DB:errmsg() ..")");
    return false;
end
As a second step, the database is queried for columns in the table, using a special PRAGMA SQL command. That command returns all of the columns' IDs, names, types and whatnot, we simply extract the column name (i-loop) and remove it from the list of columns that the function received (j-loop). Since we can receive columns that don't specify only a name, but also some type specifiers, we need to strip those away before comparing the column names, that's what the string.find() and string.sub() inside the j-loop are for. The code is not exactly top-performance, but since it's executed only once on plugin initialization and only for 2 tables, I think it's better to have less performant code that is easy to understand, rather than a jumping-through-hoops code that no-one understands.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    if (#a_Columns > 0) then
        LOGINFO(PluginPrefix .. "Database table \"" .. a_TableName .. "\" is missing " .. #a_Columns .. " columns, fixing now.");
        for idx, ColumnName in ipairs(a_Columns) do
            local ErrCode = self.DB:exec("ALTER TABLE '" .. a_TableName .. "' ADD COLUMN " .. ColumnName);
            if (ErrCode ~= sqlite3.OK) then
                LOGWARNING(PluginPrefix .. "Cannot add DB table \"" .. a_TableName .. "\" column \"" .. ColumnName .. "\", error " .. ErrCode .. " (" .. self.DB:errmsg() ..")");
                return false;
            end
        end
        LOGINFO(PluginPrefix .. "Database table \"" .. a_TableName .. "\" columns fixed.");
    end
     
    return true;
end
At this point, the a_Columns variable holds all the columns that we'll be needing and that aren't in the DB. So the last step is to alter the DB table, adding each column.

This is from rev 1559 in the MCServer SVN.
Reply
Thanks given by:


Messages In This Thread
Developing an example Protection plugin - by xoft - 05-18-2013, 01:29 AM
RE: Developing an example Protection plugin - by xoft - 06-07-2013, 03:29 AM



Users browsing this thread: 1 Guest(s)