Add. Support multiple database backends.

```Lua
local Database = require 'resources.functions.database'

-- Default backend configured via xml_handler.db_backend = 'native'
dbh = Database.new("system")

-- To use other backends you can use
dbh = Database.backend.luasql("system")
```
This commit is contained in:
Alexey Melnichuk 2016-02-19 17:03:45 +03:00
parent 80758e8826
commit 5afec68fb1
7 changed files with 493 additions and 156 deletions

View File

@ -0,0 +1,67 @@
-- Start background service to support Lua-ODBC-Pool database backend
require "resources.functions.config"
require "resources.functions.file_exists"
local log = require "resources.functions.log".dbpool
local odbc = require "odbc"
local odbcpool = require "odbc.pool"
-- Configuration
local POLL_TIMEOUT = 5
local run_file = scripts_dir .. "/run/dbpool.tmp";
-- Pool ctor
local function run_odbc_pool(name, n)
local connection_string = assert(database[name])
local typ, dsn, user, password = connection_string:match("^(.-)://(.-):(.-):(.-)$")
assert(typ == 'odbc', "unsupported connection string:" .. connection_string)
local cli = odbcpool.client(name)
log.noticef("Starting reconnect thread[%s] ...", name)
local rthread = odbcpool.reconnect_thread(cli, dsn, user, password)
rthread:start()
log.noticef("Reconnect thread[%s] started", name)
local env = odbc.environment()
local connections = {}
for i = 1, (n or 10) do
local cnn = odbc.assert(env:connection())
connections[#connections+1] = cnn
cli:reconnect(cnn)
end
return {
name = name;
cli = cli;
cnn = connections;
thr = rthread;
}
end
local function stop_odbc_pool(ctx)
log.noticef("Stopping reconnect thread[%s] ...", ctx.name)
ctx.thr:stop()
log.noticef("Reconnect thread[%s] stopped", ctx.name)
end
local function main()
local system_pool = run_odbc_pool("system", 10)
local switch_pool = run_odbc_pool("switch", 10)
local file = assert(io.open(run_file, "w"));
file:write("remove this file to stop the script");
file:close()
while file_exists(run_file) do
freeswitch.msleep(POLL_TIMEOUT*1000)
end
stop_odbc_pool(system_pool)
stop_odbc_pool(switch_pool)
end
main()

View File

@ -1,129 +1,39 @@
---
-- @usage
-- -- Use default backend
-- dbh = Database.new("system")
-- .....
--
-- @usage
-- -- Use LuaSQL backend
-- dbh = Database.backend.luasql("system")
-- .....
require 'resources.functions.config' require 'resources.functions.config'
----------------------------------------------------------- local log = require "resources.functions.log".database
local OdbcDatabase = {} if not freeswitch then
OdbcDatabase.__index = OdbcDatabase
local odbc = require "odbc.dba" local BACKEND = xml_handler and xml_handler.db_backend or 'native'
function OdbcDatabase.new(name)
local self = setmetatable({}, OdbcDatabase)
local connection_string = assert(database[name])
local typ, dsn, user, password = connection_string:match("^(.-)://(.-):(.-):(.-)$")
assert(typ == 'odbc', "unsupported connection string:" .. connection_string)
self._dbh = odbc.Connect(dsn, user, password)
return self
end
function OdbcDatabase:query(sql, fn)
self._rows_affected = nil
if fn then
return self._dbh:neach(sql, function(row)
local o = {}
for k, v in pairs(row) do
if v == odbc.NULL then
o[k] = nil
else
o[k] = tostring(v)
end
end
return fn(o)
end)
end
local ok, err = self._dbh:exec(sql)
if not ok then return nil, err end
self._rows_affected = ok
return self._rows_affected
end
function OdbcDatabase:affected_rows()
return self._rows_affected;
end
function OdbcDatabase:release()
if self._dbh then
self._dbh:destroy()
self._dbh = nil
end
end
function OdbcDatabase:connected()
return self._dbh and self._dbh:connected()
end
end
-----------------------------------------------------------
----------------------------------------------------------- -----------------------------------------------------------
local FsDatabase = {} if freeswitch then local installed_classes = {}
local default_backend = FsDatabase
local function new_database(backend)
local class = installed_classes[backend]
if class then return class end
require "resources.functions.file_exists" local Database = {} do
require "resources.functions.database_handle" Database.__index = Database
Database.__base = backend or default_backend
Database = setmetatable(Database, Database.__base)
FsDatabase.__index = FsDatabase function Database.new(...)
function FsDatabase.new(name)
local dbh = assert(name)
if type(name) == 'string' then
if name == 'switch' and file_exists(database_dir.."/core.db") then
dbh = freeswitch.Dbh("sqlite://"..database_dir.."/core.db")
else
dbh = database_handle(name)
end
end
assert(dbh:connected())
local self = setmetatable({
_dbh = dbh;
}, FsDatabase)
return self
end
function FsDatabase:query(sql, fn)
if fn then
return self._dbh:query(sql, fn)
end
return self._dbh:query(sql)
end
function FsDatabase:affected_rows()
if self._dbh then
return self._dbh:affected_rows()
end
end
function FsDatabase:release()
if self._dbh then
self._dbh:release()
self._dbh = nil
end
end
function FsDatabase:connected()
return self._dbh and self._dbh:connected()
end
end
-----------------------------------------------------------
-----------------------------------------------------------
local Database = {} do
Database.__index = Database
Database.__base = freeswitch and FsDatabase or OdbcDatabase
Database = setmetatable(Database, Database.__base)
function Database.new(...)
local self = Database.__base.new(...) local self = Database.__base.new(...)
setmetatable(self, Database) setmetatable(self, Database)
return self return self
end end
function Database:first_row(sql) function Database:first_row(sql)
local result local result
local ok, err = self:query(sql, function(row) local ok, err = self:query(sql, function(row)
result = row result = row
@ -131,16 +41,16 @@ function Database:first_row(sql)
end) end)
if not ok then return nil, err end if not ok then return nil, err end
return result return result
end end
function Database:first_value(sql) function Database:first_value(sql)
local result, err = self:first_row(sql) local result, err = self:first_row(sql)
if not result then return nil, err end if not result then return nil, err end
local k, v = next(result) local k, v = next(result)
return v return v
end end
function Database:first(sql, ...) function Database:first(sql, ...)
local result, err = self:first_row(sql) local result, err = self:first_row(sql)
if not result then return nil, err end if not result then return nil, err end
local t, n = {}, select('#', ...) local t, n = {}, select('#', ...)
@ -148,19 +58,21 @@ function Database:first(sql, ...)
t[i] = result[(select(i, ...))] t[i] = result[(select(i, ...))]
end end
return unpack(t, 1, n) return unpack(t, 1, n)
end end
function Database:fetch_all(sql) function Database:fetch_all(sql)
local result = {} local result = {}
local ok, err = self:query(sql, function(row) local ok, err = self:query(sql, function(row)
result[#result + 1] = row result[#result + 1] = row
end) end)
if (not ok) and err then return nil, err end if (not ok) and err then return nil, err end
return result return result
end end
function Database.__self_test__(...) function Database.__self_test__(...)
log.info('self_test Database - ' .. Database._backend_name)
local db = Database.new(...) local db = Database.new(...)
assert(db:connected()) assert(db:connected())
assert("1" == db:first_value("select 1 as v union all select 2 as v")) assert("1" == db:first_value("select 1 as v union all select 2 as v"))
@ -179,10 +91,44 @@ function Database.__self_test__(...)
-- assert(nil == db:first_value("some non sql query")) -- assert(nil == db:first_value("some non sql query"))
-- select NULL
local a = assert(db:first_value("select NULL as a"))
assert(a == "")
db:release() db:release()
assert(not db:connected()) assert(not db:connected())
print(" * databse - OK!") log.info('self_test Database - pass')
end
end
installed_classes[backend] = Database
return Database
end end
-----------------------------------------------------------
-----------------------------------------------------------
local Database = {} do
local backend_loader = setmetatable({}, {__index = function(self, backend)
local class = require("resources.functions.database." .. backend)
local database = new_database(class)
self[backend] = function(...)
return database.new(...)
end
return self[backend]
end})
Database.backend = backend_loader
Database.new = Database.backend[BACKEND]
Database.__self_test__ = function(backends, ...)
for _, backend in ipairs(backends) do
local t = Database.backend[backend]
t(...).__self_test__(...)
end
end;
end end
----------------------------------------------------------- -----------------------------------------------------------

View File

@ -0,0 +1,102 @@
--
-- LuaSQL backend to FusionPBX database class
--
require "resources.functions.split"
local log = require "resources.functions.log".database
local LuaSQLDatabase = {} do
LuaSQLDatabase.__index = LuaSQLDatabase
LuaSQLDatabase._backend_name = 'LuaSQL'
local map = {
pgsql = 'postgres';
}
local function apply_names(row, colnames, null_value)
for _, name in pairs(colnames) do
if row[name] == nil then
row[name] = null_value
else
row[name] = tostring(row[name])
end
end
return row
end
function LuaSQLDatabase.new(name)
local self = setmetatable({}, OdbcDatabase)
local connection_string = assert(database[name])
local typ, args = split_first(database[name], "://", true);
typ = map[typ] or typ
local luasql = require ("luasql." .. typ)
local env = assert (luasql[typ]())
local dbh = assert (env:connect( usplit(args, ':', true) ))
self._env, self._dbh = env, dbh
return self
end
function LuaSQLDatabase:query(sql, fn)
self._rows_affected = nil
if fn then
local cur, err = self._dbh:execute(sql)
if err and not cur then
log.errf("Can not execute sql: %s\n%s", tostring(err), sql)
end
local colnames = cur:getcolnames()
while true do
local row, err = cur:fetch({}, "a")
if not row then break end
local ok, ret = pcall(fn, apply_names(row, colnames, ""))
if (not ok) or (type(ret) == 'number' and ret > 0) then
break
end
end
cur:close()
return true
end
local ok, err = self._dbh:execute(sql)
if err and not ok then
log.errf("Can not execute sql: %s\n%s", tostring(err), sql)
end
if not ok then return nil, err end
if type(ok) ~= 'number' then
ok:close()
log.warning('SQL return recordset')
else
self._rows_affected = ok
end
self._rows_affected = ok
return self._rows_affected
end
function LuaSQLDatabase:affected_rows()
return self._rows_affected;
end
function LuaSQLDatabase:release()
if self._dbh then
self._dbh:close()
self._env:close()
self._env, self._dbh = nil
end
end
function LuaSQLDatabase:connected()
return self._dbh and self._dbh:connected()
end
end
return LuaSQLDatabase

View File

@ -0,0 +1,61 @@
--
-- Native backend to FusionPBX database class
--
local log = require "resources.functions.log".database
-----------------------------------------------------------
local FsDatabase = {} if freeswitch then
require "resources.functions.file_exists"
require "resources.functions.database_handle"
FsDatabase.__index = FsDatabase
FsDatabase._backend_name = 'native'
function FsDatabase.new(name)
local dbh = assert(name)
if type(name) == 'string' then
if name == 'switch' and file_exists(database_dir.."/core.db") then
dbh = freeswitch.Dbh("sqlite://"..database_dir.."/core.db")
else
dbh = database_handle(name)
end
end
assert(dbh:connected())
local self = setmetatable({
_dbh = dbh;
}, FsDatabase)
return self
end
function FsDatabase:query(sql, fn)
if fn then
return self._dbh:query(sql, fn)
end
return self._dbh:query(sql)
end
function FsDatabase:affected_rows()
if self._dbh then
return self._dbh:affected_rows()
end
end
function FsDatabase:release()
if self._dbh then
self._dbh:release()
self._dbh = nil
end
end
function FsDatabase:connected()
return self._dbh and self._dbh:connected()
end
end
-----------------------------------------------------------
return FsDatabase

View File

@ -0,0 +1,67 @@
--
-- Lua-ODBC backend to FusionPBX database class
--
local log = require "resources.functions.log".database
local odbc = require "odbc.dba"
local function remove_null(row, null, null_value)
local o = {}
for k, v in pairs(row) do
if v == null then
o[k] = null_value
else
o[k] = tostring(v)
end
end
return o
end
local OdbcDatabase = {} do
OdbcDatabase.__index = OdbcDatabase
OdbcDatabase._backend_name = 'ODBC'
function OdbcDatabase.new(name)
local self = setmetatable({}, OdbcDatabase)
local connection_string = assert(database[name])
local typ, dsn, user, password = connection_string:match("^(.-)://(.-):(.-):(.-)$")
assert(typ == 'odbc', "unsupported connection string:" .. connection_string)
self._dbh = odbc.Connect(dsn, user, password)
return self
end
function OdbcDatabase:query(sql, fn)
self._rows_affected = nil
if fn then
return self._dbh:neach(sql, function(row)
return fn(remove_null(row, odbc.NULL, ""))
end)
end
local ok, err = self._dbh:exec(sql)
if not ok then return nil, err end
self._rows_affected = ok
return self._rows_affected
end
function OdbcDatabase:affected_rows()
return self._rows_affected;
end
function OdbcDatabase:release()
if self._dbh then
self._dbh:destroy()
self._dbh = nil
end
end
function OdbcDatabase:connected()
return self._dbh and self._dbh:connected()
end
end
return OdbcDatabase

View File

@ -0,0 +1,82 @@
--
-- Lua-ODBC-Pool backend to FusionPBX database class
--
local log = require "resources.functions.log".database
local odbc = require "odbc.dba"
local odbcpool = require "odbc.dba.pool"
local function remove_null(row, null, null_value)
local o = {}
for k, v in pairs(row) do
if v == null then
o[k] = null_value
else
o[k] = tostring(v)
end
end
return o
end
-----------------------------------------------------------
local OdbcPoolDatabase = {} do
OdbcPoolDatabase.__index = OdbcPoolDatabase
OdbcPoolDatabase._backend_name = 'ODBC Pool'
function OdbcPoolDatabase.new(name)
local self = setmetatable({}, OdbcPoolDatabase)
self._cli = odbcpool.client(name)
self._timeout = 1000
self._rows_affected = nil
return self
end
function OdbcPoolDatabase:query(sql, fn)
self._rows_affected = nil
local cli = self._cli
local ok, err
if fn then
ok, err = cli:acquire(self._timeout, function(dbh)
local ok, err = dbh:neach(sql, function(row)
return fn(remove_null(row, odbc.NULL, ""))
end)
if err and not ok then
log.errf("Can not execute sql: %s\n%s", tostring(err), sql)
end
return not not dbh:connected(), true
end)
else
ok, err = cli:acquire(self._timeout, function(dbh)
local ok, err = dbh:exec(sql)
if err and not ok then
log.errf("Can not execute sql: %s\n%s", tostring(err), sql)
end
self._rows_affected = ok
return not not dbh:connected(), ok
end)
end
if err and not ok then
log.errf("Can not get database handle: %s", tostring(err))
end
return ok
end
function OdbcPoolDatabase:affected_rows()
return self._rows_affected;
end
function OdbcPoolDatabase:release()
self._cli = nil
end
function OdbcPoolDatabase:connected()
return not not self._cli
end
end
-----------------------------------------------------------
return OdbcPoolDatabase

View File

@ -0,0 +1,12 @@
local Cache = require 'resources.functions.cache'
local Database = require 'resources.functions.database'
Database.__self_test__({
"native",
"luasql",
"odbc",
"odbcpool",
},
"system")
Cache._self_test()