Description
tableDB is a lua module that adds an abstraction layer over sqlite database. With some minor effort it may be extended to other databases as well. Using this module you need no knowledge of sqlite or SQL at all. If you want to store data in Lua tables, this module helps map your lua data tables automatically to an sqlite database and manages the accesses to the database automatically. So why do it this way? What are the advantages over just using Lua Tables and storing them in text files directly? Here are some:
- This approach is more scalable since a relational database is much more scalable as opposed to storing Lua tables in files
- When you read the Lua table it is fully loaded in the memory. For this approach it only loads the accessed data into the tables so it can handle large amounts of data without loading the entire thing in memory
- It gives you the best of both worlds since whatever you want to store in Lua tables gets mapped to your own database without dealing with the complexities of designing the database tables yourself or learning the SQL to manipulate them. All table manipulations are automatically mapped to the database when it is saved.
It handles any Lua table that has a
number or a
string as a key and has a
table,
number,
string or
boolean as a value. This set can accommodate majority of data storage tables one can imagine in Lua and more by manipulating those to this set in lua itself. To begin with you create your storage database by giving the module a template of your table which is almost like giving it a sample of the table you want to store in the database. The module creates the database using the template table and returns you the empty lua table which you use as a normal Lua table to store or retrieve your values, while the module automatically syncs the data between that and the database.
To best understand it let us look at some examples below.
Note: Both examples require luasql sqlite3 module accessible by lua.
Example 1 - A simple recursive data structure
Suppose I have a program that is saving an Organization Hierarchy structure in a Lua table. Employee Data has the following information:
- Name
- Title
- Trial Period
- Phone Number
- Number of people Reporting
- People Reporting to this Employee
And suppose the company allows at most 4 people to report to any employee. So this is a tree hierarchy with every employee having at most 4 subordinate employee reporting to him. A template table for this would be:
- templateT = {
- Name = "name",
- Title = "title",
- TrialPeriod = true,
- NumOfReportees = 4
- }
- templateT[1] = templateT -- 1st reporting employee table
- templateT[2] = templateT -- 2nd reporting employee table
- templateT[3] = templateT -- 3rd reporting employee table
- templateT[4] = templateT -- 4th reporting employee table
Note the table is defined to cover the maximum data. The employee tables has the same structure so they are defined recursively back to the main table.
A sample program to use the above template (which is written to the file testTables2.lua) is presented below with the sample output:
- tdb = require("tableDB")
- dofile("testTables2.lua")
-
- print(templateT)
-
- emp = tdb.createDB("testdb2",templateT,true)
-
- emp.Name = "ABC"
- emp.Title = "CEO"
- emp.TrialPeriod = false
- emp.NumOfReportees = 2
-
- emp[1] = {
- Name = "DEF",
- Title = "VP",
- TrialPeriod = false,
- NumOfReportees = 1
- }
- emp[1][1] = {
- Name = "GHI",
- Title = "Engineer",
- TrialPeriod = false,
- NumOfReportees = 0
- }
- emp[2] = {
- Name = "JKL",
- Title = "VP",
- TrialPeriod = false,
- NumOfReportees = 1
- }
- emp[2][1] = {
- Name = "MNO",
- Title = "Engineer",
- TrialPeriod = true,
- NumOfReportees = 0
- }
-
- print(emp[2][1].Name) -- Would print MNO
- -- Lets add something not defined in the original template
- emp[2].Address = "123 Fortune Street"
-
- tdb.saveDB(emp) -- This saves all the data into the database
- tdb.closeDB(emp) -- close the file
-
- -- Open the file again and do more operations
- emp = tdb.openDB("testdb2")
- print(emp[2]) -- print the table reference
- print(emp[2].Title) -- prints VP
- print(emp[1][1].Title) -- prints Engineer
- print(emp[2].Address) -- prints 123 Fortune Street
And the output it gives is:
table: 007D1BE8
MNO
table: 00823E88
VP
Engineer
123 Fortune Street
A thing to note here is that even though the template table did not define the field address tableDB was still able to accommodate and store it. So why define template tables at all? Everything defined in the template table i.e. all the keys are mapped to different columns of a database. If some key that was not defined in the initial template database comes up then that is stored as Lua code in one combined column for the record. So retrieving that new data and putting it there is not as efficient as it would be for the structure defined in the template.
Example 2 - A recursive hierarchical structure with variable number of elements
This example covers the most diverse type of structure that we can have. In the previous example our database would only grow recursively but in many cases the database has sequential elements like a list of elements which grows. It is like an Array. Lua tables are used like Arrays by simply having numerical indices as keys. tableDB separates out numerical keys and Array types. This allows definition of database records which do not behave like arrays with numerical keys like we saw in Example 1 above.
The following template table define a database which has ARRAYs:
- templatet = {
- __ARRAY = {
- Tasks = {
- TaskID = "T1",
- Title = "This is root task 1",
- Estimate = 3,
- Private = true,
- SubTasks = {
- tasks = 0,
- __ARRAY = {
-
- }
- }
- } -- Tasks end
- } -- __ARRAY ends
- }
-
- templatet.__ARRAY.Tasks.SubTasks.__ARRAY.Tasks = templatet.__ARRAY.Tasks
This database is more complex but it is still a simple recursive Lua table. The only new thing here is the
__ARRAY keyword. This keyword when used as a key in the template table tells tableDB that we are defining an ARRAY for this table. So the main database represented by templatet table only has 1 type of data and that is an ARRAY. It has an Array of records which is defined by the table Tasks. The table Tasks has some elements and it has a table called SubTasks which again has an Array of records. This Array of records is recursively again the Tasks table as defined by the code line in the end.
So saving this in file testTables1.lua and a sample table like:
- sample = {
- [1] = {
- TaskID = "T1",
- Title = "This is root task 1",
- Estimate = 3,
- Private = nil,
- SubTasks = {
- tasks = 1
- },
- Hello=2,
- [4] = "This is it",
- [3] = "New data"
- }, -- [1] ends
- [2] = {
- TaskID = "T2",
- Title = "This is root task 2",
- Estimate = 4.4,
- Private = nil,
- SubTasks = {
- tasks = 2,
- [1] = {
- TaskID = "T2_1",
- Title = "This is Task 1 of T2",
- Estimate = 2.1,
- Private = nil,
- SubTasks = {
- tasks = 3
- }
- }, -- [1] ends
- [2] = {
- TaskID = "T2_2",
- Title = "This is Task 2 of T2",
- Estimate = 2.2,
- Private = nil,
- SubTasks = {
- tasks = 4,
- [1] = {
- TaskID = "T2_3",
- Title = "This is Task 3 of T3",
- Estimate = 2.3,
- Private = nil,
- SubTasks = {
- tasks = 5
- }
- } -- [] ends
- }
- } -- [2] ends
- }, -- Subtasks ends
- [3] = true
- }, -- [2] ends
- [3] = {
- TaskID = "T3",
- Title = "This is root task 3",
- Estimate = 1.2,
- Private = false,
- SubTasks = {
- tasks = 6,
- [1] = {
- TaskID = "T3_1",
- Title = "This is Task 1 of T3",
- Estimate = 3.1,
- Private = true,
- SubTasks = {
- tasks = 7
- }
- } -- [1] ends
- }, -- Subtasks ends
- }, -- [3] ends
- }
running the following test code on it gives the following output:
- tdb = require("tableDB")
- dofile("testTables2.lua")
-
-
- print(templatet)
-
- x = tdb.createDB("testdb2",templatet,true)
-
- for i = 1,#sample do
- x[i] = sample[i]
- end
- tdb.saveDB(x)
- tdb.closeDB(x)
-
- y = tdb.openDB("testdb2")
- print(tableToString(y)) -- Prints the whole table as a string
- print(y[1]) -- prints the table reference
- print(y[1].TaskID) -- prints T1
- print(y[1][3]) -- prints New Data
- print(y[1].Private) -- prints nil
- print(y[1][4]) -- prints This is it
- print(y[1].Hello) -- prints 2
- print(y[3].Private) -- prints true
- print(y[2].SubTasks[1].SubTasks.tasks) -- prints 3
-
- y[4] = {
- TaskID = "T4",
- Title = "This is root task 4",
- Private = true
- }
- y[3].SubTasks = nil
- y[2].SubTasks.tasks = 10
-
-
- print(tableToString(y)) -- prints the whole table (after modifications) as a string
-
- tdb.saveDB(y)
- tdb.closeDB(y)
For the definition of tableToString see the
Appendix Section. All the files for each example can also be downloaded in the
Appendix Section.
The output is:
table: 008DB818
{[1]={[3]="New data",[4]="This is it",Estimate=3,Hello=2,SubTasks={tasks=1},Task
ID="T1",Title="This is root task 1"},[2]={[3]="true",Estimate=4.4,SubTasks={[1]=
{Estimate=2.1,SubTasks={tasks=3},TaskID="T2_1",Title="This is Task 1 of T2"},[2]
={Estimate=2.2,SubTasks={[1]={Estimate=2.3,SubTasks={tasks=5},TaskID="T2_3",Titl
e="This is Task 3 of T3"},tasks=4},TaskID="T2_2",Title="This is Task 2 of T2"},t
asks=2},TaskID="T2",Title="This is root task 2"},[3]={Estimate=1.2,Private="true
",SubTasks={[1]={Estimate=3.1,Private="true",SubTasks={tasks=7},TaskID="T3_1",Ti
tle="This is Task 1 of T3"},tasks=6},TaskID="T3",Title="This is root task 3"}}
table: 0082BB20
T1
New data
nil
This is it
2
true
3
{[1]={[3]="New data",[4]="This is it",Estimate=3,Hello=2,SubTasks={tasks=1},Task
ID="T1",Title="This is root task 1"},[2]={[3]="true",Estimate=4.4,SubTasks={[1]=
{Estimate=2.1,SubTasks={tasks=3},TaskID="T2_1",Title="This is Task 1 of T2"},[2]
={Estimate=2.2,SubTasks={[1]={Estimate=2.3,SubTasks={tasks=5},TaskID="T2_3",Titl
e="This is Task 3 of T3"},tasks=4},TaskID="T2_2",Title="This is Task 2 of T2"},t
asks=10},TaskID="T2",Title="This is root task 2"},[3]={Estimate=1.2,Private="tru
e",TaskID="T3",Title="This is root task 3"},[4]={TaskID="T4",Title="This is root
task 4",Private="true"}}
As seen above the pairs and ipairs function also work on these database tables. For Lua 5.2 the # operator will also work on these tables to give the length of the table.
Word about Arrays
The ARRAY datatype has a single value with a string key which defines the data type that would be the elements of the array. This data type may itself be a table, string, number or boolean. If there is nothing in the ARRAY definition then it is a general array and can store any other record in the database at each position.
Backend database structure
In the backend the database contains the following tables to manage the database:
1. DBMANAGE - stores info about all the user tables in the database, about their column types etc.
2. ARRAYELEMENTS - table to store all links to ARRAY elements. Having an additional table to manage ARRAY element links makes it possible to have records with multiple parents and children.
3. DBROOT is the root table containing the template table definition. Sub tables in the template table generate other tables in the database.
For each key in the template table a column is created in the database table. Apart from that the database table has 2 additional columns:
1. ID - this is the record ID for each record in the table. Used by the database to identify records uniquely.
2. HOOKS - This is the column that holds any lua code associated with storing any keys that were not defined in the template but were stored in the table by the user
Links to other tables/records are in the form of 'ID:tableName' where ID is the corresponding record ID and the tableName is the name of the table where the record exists.
- Download the module here
- Download example files example1, example2
- tableToString function:
- function tableToString(t)
- local rL = {cL = 1} -- Table to track recursion into nested tables (cL = current recursion level)
- rL[rL.cL] = {}
- do
- rL[rL.cL]._f,rL[rL.cL]._s,rL[rL.cL]._var = pairs(t)
- rL[rL.cL].str = "{"
- rL[rL.cL].t = t
- while true do
- local k,v = rL[rL.cL]._f(rL[rL.cL]._s,rL[rL.cL]._var)
- rL[rL.cL]._var = k
- if not k and rL.cL == 1 then
- break
- elseif not k then
- -- go up in recursion level
- if string.sub(rL[rL.cL].str,-1,-1) == "," then
- rL[rL.cL].str = string.sub(rL[rL.cL].str,1,-2)
- end
- --print("GOING UP: "..rL[rL.cL].str.."}")
- rL[rL.cL-1].str = rL[rL.cL-1].str..rL[rL.cL].str.."}"
- rL.cL = rL.cL - 1
- rL[rL.cL+1] = nil
- rL[rL.cL].str = rL[rL.cL].str..","
- else
- -- Handle the key and value here
- if type(k) == "number" then
- rL[rL.cL].str = rL[rL.cL].str.."["..tostring(k).."]="
- else
- rL[rL.cL].str = rL[rL.cL].str..tostring(k).."="
- end
- if type(v) == "table" then
- -- Check if this is not a recursive table
- local goDown = true
- for i = 1, rL.cL do
- if v==rL[i].t then
- -- This is recursive do not go down
- goDown = false
- break
- end
- end
- if goDown then
- -- Go deeper in recursion
- rL.cL = rL.cL + 1
- rL[rL.cL] = {}
- rL[rL.cL]._f,rL[rL.cL]._s,rL[rL.cL]._var = pairs(v)
- rL[rL.cL].str = "{"
- rL[rL.cL].t = v
- --print("GOING DOWN:",k)
- else
- rL[rL.cL].str = rL[rL.cL].str.."\""..tostring(v).."\""
- rL[rL.cL].str = rL[rL.cL].str..","
- --print(k,"=",v)
- end
- elseif type(v) == "number" then
- rL[rL.cL].str = rL[rL.cL].str..tostring(v)
- rL[rL.cL].str = rL[rL.cL].str..","
- --print(k,"=",v)
- else
- rL[rL.cL].str = rL[rL.cL].str..string.format("%q",tostring(v))
- rL[rL.cL].str = rL[rL.cL].str..","
- --print(k,"=",v)
- end -- if type(v) == "table" then ends
- end -- if not rL[rL.cL]._var and rL.cL == 1 then ends
- end -- while true ends here
- end -- do ends
- if string.sub(rL[rL.cL].str,-1,-1) == "," then
- rL[rL.cL].str = string.sub(rL[rL.cL].str,1,-2)
- end
- rL[rL.cL].str = rL[rL.cL].str.."}"
- return rL[rL.cL].str
- end
References
|
|
|
|
|
|
|
|