Körperbewegung
This commit is contained in:
parent
b16b24e4f7
commit
95945c0306
78 changed files with 12503 additions and 0 deletions
318
mods/modlib/persistence/sqlite3.lua
Normal file
318
mods/modlib/persistence/sqlite3.lua
Normal file
|
@ -0,0 +1,318 @@
|
|||
local assert, error, math_huge, modlib, minetest, setmetatable, type, table_insert, table_sort, pairs, ipairs
|
||||
= assert, error, math.huge, modlib, minetest, setmetatable, type, table.insert, table.sort, pairs, ipairs
|
||||
|
||||
local sqlite3 = ...
|
||||
|
||||
--[[
|
||||
Currently uses reference counting to immediately delete tables which aren't reachable from the root table anymore, which has two issues:
|
||||
1. Deletion might trigger a large deletion chain
|
||||
TODO defer deletion, clean up unused tables on startup, delete & iterate tables partially
|
||||
2. Reference counting is unable to handle cycles. `:collectgarbage()` implements a tracing "stop-the-world" garbage collector which handles cycles.
|
||||
TODO take advantage of Lua's garbage collection by keeping a bunch of "twin" objects in a weak structure using proxies (Lua 5.1) or the __gc metamethod (Lua 5.2)
|
||||
See https://wiki.c2.com/?ReferenceCountingCanHandleCycles, https://www.memorymanagement.org/mmref/recycle.html#mmref-recycle and https://wiki.c2.com/?GenerationalGarbageCollectio
|
||||
Weak tables are of no use here, as we need to be notified when a reference is dropped
|
||||
]]
|
||||
|
||||
local ptab = {} -- SQLite3-backed implementation for a persistent Lua table ("ptab")
|
||||
local metatable = {__index = ptab}
|
||||
ptab.metatable = metatable
|
||||
|
||||
-- Note: keys may not be marked as weak references: wouldn't close the database: see persistence/lua_log_file.lua
|
||||
local databases = {}
|
||||
|
||||
local types = {
|
||||
boolean = 1,
|
||||
number = 2,
|
||||
string = 3,
|
||||
table = 4
|
||||
}
|
||||
|
||||
local function increment_highest_table_id(self)
|
||||
self.highest_table_id = self.highest_table_id + 1
|
||||
if self.highest_table_id > 2^50 then
|
||||
-- IDs are approaching double precision limit (52 bits mantissa), defragment them
|
||||
self:defragment_ids()
|
||||
end
|
||||
return self.highest_table_id
|
||||
end
|
||||
|
||||
function ptab.new(file_path, root)
|
||||
return setmetatable({
|
||||
database = sqlite3.open(file_path),
|
||||
root = root
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
function ptab.setmetatable(self)
|
||||
assert(self.database and self.root)
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
local set
|
||||
|
||||
local function add_table(self, table)
|
||||
if type(table) ~= "table" then return end
|
||||
if self.counts[table] then
|
||||
self.counts[table] = self.counts[table] + 1
|
||||
return
|
||||
end
|
||||
self.table_ids[table] = increment_highest_table_id(self)
|
||||
self.counts[table] = 1
|
||||
for k, v in pairs(table) do
|
||||
set(self, table, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
local decrement_reference_count
|
||||
|
||||
local function delete_table(self, table)
|
||||
local id = assert(self.table_ids[table])
|
||||
self.table_ids[table] = nil
|
||||
self.counts[table] = nil
|
||||
for k, v in pairs(table) do
|
||||
decrement_reference_count(self, k)
|
||||
decrement_reference_count(self, v)
|
||||
end
|
||||
local statement = self._prepared.delete_table
|
||||
statement:bind(1, id)
|
||||
statement:step()
|
||||
statement:reset()
|
||||
end
|
||||
|
||||
function decrement_reference_count(self, table)
|
||||
if type(table) ~= "table" then return end
|
||||
local count = self.counts[table]
|
||||
if not count then return end
|
||||
count = count - 1
|
||||
if count == 0 then return delete_table(self, table) end
|
||||
self.counts[table] = count
|
||||
end
|
||||
|
||||
function set(self, table, key, value)
|
||||
local deletion = value == nil
|
||||
if not deletion then
|
||||
add_table(self, key)
|
||||
add_table(self, value)
|
||||
end
|
||||
local previous_value = table[key]
|
||||
if type(previous_value) == "table" then
|
||||
decrement_reference_count(self, previous_value)
|
||||
end
|
||||
if deletion and type(key) == "table" then
|
||||
decrement_reference_count(self, key)
|
||||
end
|
||||
local statement = self._prepared[deletion and "delete" or "insert"]
|
||||
local function bind_type_and_content(n, value)
|
||||
local type_ = type(value)
|
||||
statement:bind(n, assert(types[type_]))
|
||||
if type_ == "boolean" then
|
||||
statement:bind(n + 1, value and 1 or 0)
|
||||
elseif type_ == "number" then
|
||||
if value ~= value then
|
||||
statement:bind(n + 1, "nan")
|
||||
elseif value == math_huge then
|
||||
statement:bind(n + 1, "inf")
|
||||
elseif value == -math_huge then
|
||||
statement:bind(n + 1, "-inf")
|
||||
else
|
||||
statement:bind(n + 1, value)
|
||||
end
|
||||
elseif type_ == "string" then
|
||||
-- Use bind_blob instead of bind as Lua strings are effectively byte strings
|
||||
statement:bind_blob(n + 1, value)
|
||||
elseif type_ == "table" then
|
||||
statement:bind(n + 1, self.table_ids[value])
|
||||
end
|
||||
end
|
||||
statement:bind(1, assert(self.table_ids[table]))
|
||||
bind_type_and_content(2, key)
|
||||
if not deletion then
|
||||
bind_type_and_content(4, value)
|
||||
end
|
||||
statement:step()
|
||||
statement:reset()
|
||||
end
|
||||
|
||||
local function exec(self, sql)
|
||||
if self.database:exec(sql) ~= sqlite3.OK then
|
||||
error(self.database:errmsg())
|
||||
end
|
||||
end
|
||||
|
||||
function ptab:init()
|
||||
local database = self.database
|
||||
local function prepare(sql)
|
||||
local stmt = database:prepare(sql)
|
||||
if not stmt then error(database:errmsg()) end
|
||||
return stmt
|
||||
end
|
||||
exec(self, [[
|
||||
CREATE TABLE IF NOT EXISTS table_entries (
|
||||
table_id INTEGER NOT NULL,
|
||||
key_type INTEGER NOT NULL,
|
||||
key BLOB NOT NULL,
|
||||
value_type INTEGER NOT NULL,
|
||||
value BLOB NOT NULL,
|
||||
PRIMARY KEY (table_id, key_type, key)
|
||||
)]])
|
||||
self._prepared = {
|
||||
insert = prepare"INSERT OR REPLACE INTO table_entries(table_id, key_type, key, value_type, value) VALUES (?, ?, ?, ?, ?)",
|
||||
delete = prepare"DELETE FROM table_entries WHERE table_id = ? AND key_type = ? AND key = ?",
|
||||
delete_table = prepare"DELETE FROM table_entries WHERE table_id = ?",
|
||||
update = {
|
||||
id = prepare"UPDATE table_entries SET table_id = ? WHERE table_id = ?",
|
||||
keys = prepare("UPDATE table_entries SET key = ? WHERE key_type = " .. types.table .. " AND key = ?"),
|
||||
values = prepare("UPDATE table_entries SET value = ? WHERE value_type = " .. types.table .. " AND value = ?")
|
||||
}
|
||||
}
|
||||
-- Default value
|
||||
self.highest_table_id = 0
|
||||
for id in self.database:urows"SELECT MAX(table_id) FROM table_entries" do
|
||||
-- Gets a single value
|
||||
self.highest_table_id = id
|
||||
end
|
||||
increment_highest_table_id(self)
|
||||
local tables = {}
|
||||
local counts = {}
|
||||
self.counts = counts
|
||||
local function get_value(type_, content)
|
||||
if type_ == types.boolean then
|
||||
if content == 0 then return false end
|
||||
if content == 1 then return true end
|
||||
error("invalid boolean value: " .. content)
|
||||
end
|
||||
if type_ == types.number then
|
||||
if content == "nan" then
|
||||
return 0/0
|
||||
end
|
||||
if content == "inf" then
|
||||
return math_huge
|
||||
end
|
||||
if content == "-inf" then
|
||||
return -math_huge
|
||||
end
|
||||
assert(type(content) == "number")
|
||||
return content
|
||||
end
|
||||
if type_ == types.string then
|
||||
assert(type(content) == "string")
|
||||
return content
|
||||
end
|
||||
if type_ == types.table then
|
||||
-- Table reference
|
||||
tables[content] = tables[content] or {}
|
||||
counts[content] = counts[content] or 1
|
||||
return tables[content]
|
||||
end
|
||||
-- Null is unused
|
||||
error("unsupported type: " .. type_)
|
||||
end
|
||||
-- Order by key_content to retrieve list parts in the correct order, making it easier for Lua
|
||||
for table_id, key_type, key, value_type, value in self.database:urows"SELECT * FROM table_entries ORDER BY table_id, key_type, key" do
|
||||
local table = tables[table_id] or {}
|
||||
counts[table] = counts[table] or 1
|
||||
table[get_value(key_type, key)] = get_value(value_type, value)
|
||||
tables[table_id] = table
|
||||
end
|
||||
if tables[1] then
|
||||
self.root = tables[1]
|
||||
counts[self.root] = counts[self.root] + 1
|
||||
self.table_ids = modlib.table.flip(tables)
|
||||
self:collectgarbage()
|
||||
else
|
||||
self.highest_table_id = 0
|
||||
self.table_ids = {}
|
||||
add_table(self, self.root)
|
||||
end
|
||||
databases[self] = true
|
||||
end
|
||||
|
||||
function ptab:rewrite()
|
||||
exec(self, "BEGIN EXCLUSIVE TRANSACTION")
|
||||
exec(self, "DELETE FROM table_entries")
|
||||
self.highest_table_id = 0
|
||||
self.table_ids = {}
|
||||
self.counts = {}
|
||||
add_table(self, self.root)
|
||||
exec(self, "COMMIT TRANSACTION")
|
||||
end
|
||||
|
||||
function ptab:set(table, key, value)
|
||||
exec(self, "BEGIN EXCLUSIVE TRANSACTION")
|
||||
local previous_value = table[key]
|
||||
if previous_value == value then
|
||||
-- no change
|
||||
return
|
||||
end
|
||||
set(self, table, key, value)
|
||||
table[key] = value
|
||||
exec(self, "COMMIT TRANSACTION")
|
||||
end
|
||||
|
||||
function ptab:set_root(key, value)
|
||||
return self:set(self.root, key, value)
|
||||
end
|
||||
|
||||
function ptab:collectgarbage()
|
||||
local marked = {}
|
||||
local function mark(table)
|
||||
if type(table) ~= "table" or marked[table] then return end
|
||||
marked[table] = true
|
||||
for k, v in pairs(table) do
|
||||
mark(k)
|
||||
mark(v)
|
||||
end
|
||||
end
|
||||
mark(self.root)
|
||||
for table in pairs(self.table_ids) do
|
||||
if not marked[table] then
|
||||
delete_table(self, table)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ptab:defragment_ids()
|
||||
local ids = {}
|
||||
for _, id in pairs(self.table_ids) do
|
||||
table_insert(ids, id)
|
||||
end
|
||||
table_sort(ids)
|
||||
local update = self._prepared.update
|
||||
local tables = modlib.table.flip(self.table_ids)
|
||||
for new_id, old_id in ipairs(ids) do
|
||||
for _, stmt in pairs(update) do
|
||||
stmt:bind_values(new_id, old_id)
|
||||
stmt:step()
|
||||
stmt:reset()
|
||||
end
|
||||
self.table_ids[tables[old_id]] = new_id
|
||||
end
|
||||
self.highest_table_id = #ids
|
||||
end
|
||||
|
||||
local function finalize_statements(table)
|
||||
for _, stmt in pairs(table) do
|
||||
if type(stmt) == "table" then
|
||||
finalize_statements(stmt)
|
||||
else
|
||||
local errcode = stmt:finalize()
|
||||
assert(errcode == sqlite3.OK, errcode)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ptab:close()
|
||||
finalize_statements(self._prepared)
|
||||
self.database:close()
|
||||
databases[self] = nil
|
||||
end
|
||||
|
||||
if minetest then
|
||||
minetest.register_on_shutdown(function()
|
||||
for self in pairs(databases) do
|
||||
self:close()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return ptab
|
Loading…
Add table
Add a link
Reference in a new issue