util-sql-sessions.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['util-sql-sessions'] = {
    version   = 1.001,
    comment   = "companion to lmx-*",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

-- This is experimental code and currently part of the base installation simply
-- because it's easier to dirtribute this way. Eventually it will be documented
-- and the related scripts will show up as well.

-- maybe store threshold in session (in seconds)

local tonumber = tonumber
local format = string.format
local ostime, uuid, osfulltime = os.time, os.uuid, os.fulltime
local random = math.random

-- In older frameworks we kept a session table in memory. This time we
-- follow a route where we store session data in a sql table. Each session
-- has a token (similar to what we do on q2p and pod services), a data
-- blob which is just a serialized lua table (we could consider a dump instead)
-- and two times: the creation and last accessed time. The first one is handy
-- for statistics and the second one for cleanup. Both are just numbers so that
-- we don't have to waste code on conversions. Anyhow, we provide variants so that
-- we can always choose what is best.

local sql         = utilities.sql
local sessions    = { }
sql.sessions      = sessions

local trace_sql   = false  trackers.register("sql.sessions.trace", function(v) trace_sql = v end)
local report      = logs.reporter("sql","sessions")

sessions.newtoken = sql.tokens.new

local function checkeddb(presets,datatable)
    return sql.usedatabase(presets,datatable or presets.datatable or "sessions")
end

sessions.usedb = checkeddb

local template =[[
    CREATE TABLE IF NOT EXISTS %basename% (
        `token`    varchar(50)       NOT NULL,
        `data`     longtext          NOT NULL,
        `created`  int(11)           NOT NULL,
        `accessed` int(11)           NOT NULL,
        UNIQUE KEY `token_unique_key` (`token`)
    ) DEFAULT CHARSET = utf8 ;
]]

local sqlite_template =[[
    CREATE TABLE IF NOT EXISTS %basename% (
        `token`    TEXT NOT NULL,
        `data`     TEXT NOT NULL,
        `created`  INTEGER DEFAULT '0',
        `accessed` INTEGER DEFAULT '0'
    ) ;
]]

function sessions.createdb(presets,datatable)

    local db = checkeddb(presets,datatable)

    db.execute {
        template  = db.usedmethod == "sqlite" and sqlite_template or template,
        variables = {
            basename = db.basename,
        },
    }

    report("datatable %a created in %a",db.name,db.base)

    return db

end

local template =[[
    DROP TABLE IF EXISTS %basename% ;
]]

function sessions.deletedb(presets,datatable)

    local db = checkeddb(presets,datatable)

    db.execute {
        template  = template,
        variables = {
            basename = db.basename,
        },
    }

    report("datatable %a removed in %a",db.name,db.base)

end

local template =[[
    INSERT INTO %basename% (
        `token`,
        `created`,
        `accessed`,
        `data`
    ) VALUES (
        '%token%',
        %time%,
        %time%,
        '%[data]%'
    ) ;
]]

function sessions.create(db,data)

    local token  = sessions.newtoken()
    local time   = ostime()

    db.execute {
        template  = template,
        variables = {
            basename = db.basename,
            token    = token,
            time     = time,
            data     = db.serialize(data or { },"return")
        },
    }

    if trace_sql then
        report("created: %s at %s",token,osfulltime(time))
    end

    return {
        token    = token,
        created  = time,
        accessed = time,
        data     = data,
    }
end

local template =[[
    UPDATE
        %basename%
    SET
        `data` = '%[data]%',
        `accessed` = %time%
    WHERE
        `token` = '%token%' ;
]]

function sessions.save(db,session)

    local time  = ostime()
    local data  = db.serialize(session.data or { },"return")
    local token = session.token

    session.accessed = time

    db.execute {
        template  = template,
        variables = {
            basename = db.basename,
            token    = token,
            time     = ostime(),
            data     = data,
        },
    }

    if trace_sql then
        report("saved: %s at %s",token,osfulltime(time))
    end

    return session
end

local template = [[
    UPDATE
        %basename%
    SET
        `accessed` = %time%
    WHERE
        `token` = '%token%' ;
]]

function sessions.touch(db,token)

    db.execute {
        template  = template,
        variables = {
            basename = db.basename,
            token    = token,
            time     = ostime(),
        },
    }

end

local template = [[
    UPDATE
        %basename%
    SET
        `accessed` = %time%
    WHERE
        `token` = '%token%' ;
    SELECT
        *
    FROM
        %basename%
    WHERE
        `token` = '%token%' ;
]]

function sessions.restore(db,token)

    local records, keys = db.execute {
        template  = template,
        variables = {
            basename = db.basename,
            token    = token,
            time     = ostime(),
        },
    }

    local record = records and records[1]

    if record then
        if trace_sql then
            report("restored: %s",token)
        end
        record.data = db.deserialize(record.data or "")
        return record, keys
    elseif trace_sql then
        report("unknown: %s",token)
    end

end

local template =[[
    DELETE FROM
        %basename%
    WHERE
        `token` = '%token%' ;
]]

function sessions.remove(db,token)

    db.execute {
        template  = template,
        variables = {
            basename = db.basename,
            token    = token,
        },
    }

    if trace_sql then
        report("removed: %s",token)
    end

end

local template_collect_yes =[[
    SELECT
        *
    FROM
        %basename%
    ORDER BY
        `created` ;
]]

local template_collect_nop =[[
    SELECT
        `accessed`,
        `created`,
        `accessed`,
        `token`
    FROM
        %basename%
    ORDER BY
        `created` ;
]]

function sessions.collect(db,nodata)

    local records, keys = db.execute {
        template  = nodata and template_collect_nop or template_collect_yes,
        variables = {
            basename = db.basename,
        },
    }

    if not nodata then
        db.unpackdata(records)
    end

    if trace_sql then
        report("collected: %s sessions",#records)
    end

    return records, keys

end

local template_cleanup_yes =[[
    SELECT
        *
    FROM
        %basename%
    WHERE
        `accessed` < %time%
    ORDER BY
        `created` ;
    DELETE FROM
        %basename%
    WHERE
        `accessed` < %time% ;
]]

local template_cleanup_nop =[[
    SELECT
        `accessed`,
        `created`,
        `accessed`,
        `token`
    FROM
        %basename%
    WHERE
        `accessed` < %time%
    ORDER BY
        `created` ;
    DELETE FROM
        %basename%
    WHERE
        `accessed` < %time% ;
]]

function sessions.cleanupdb(db,delta,nodata)

    local time = ostime()

    local records, keys = db.execute {
        template  = nodata and template_cleanup_nop or template_cleanup_yes,
        variables = {
            basename = db.basename,
            time     = time - delta
        },
    }

    if not nodata then
        db.unpackdata(records)
    end

    if trace_sql then
        report("cleaned: %s seconds before %s",delta,osfulltime(time))
    end

    return records, keys

end