custom_merge.lua
Verified Featured
Added by iNTERFACEWARE
A customizable database merge method for Iguana 5.5.1 and up.
Source Code
-- $Revision: 1.5 $
-- $Date: 2013-10-23 20:30:15 $
--
-- The custom_merge module
-- Copyright (c) 2012-2013 iNTERFACEWARE Inc. ALL RIGHTS RESERVED
-- iNTERFACEWARE permits you to use, modify, and distribute this file in accordance
-- with the terms of the iNTERFACEWARE license agreement accompanying the software
-- in which it is used.
--
if not db then
error('db module must be present.', 2)
end
if not db.connect then
error('db.customMerge requires Iguana 5.5.1 or newer.', 2)
end
----------------------
local Conn, Params --The database handle and parameters for the method call.
local SQLmethods --Database specific methods, defined below.
----------------------
-- Returns a table with object-oriented methods to collect non-nil strings and to return them concatenated.
-- Inspired by: http://snippets.luacode.org/?p=snippets/String_Writer_108 (MIT/X11 License)
-- Due to technical limitations, the variable arguments (...) to write() may not display properly in annotations.
local function getStringWriter()
return setmetatable({}, {
__index = {
write = function(self, ...)
for _,s in ipairs({...}) do --Nil arguments will cause subsequent ones to be ignored.
self[#self + 1] = tostring(s)
end
return self
end,
getValue = function(self)
return table.concat(self)
end
},
_display = function(self)
--Used in annotations - short and long view.
return 'StringWriter', self:getValue()
end
}
)
end
----------------------
local function OracleDateTime(Node, Buffer)
Buffer:write('TO_DATE(\'', Node, '\', \'YYYY-MM-DD HH24:MI:SS\')')
end
local function addSQLValue(Node, Buffer)
if Node:isNull() then
Buffer:write('NULL')
return
end
local curType = Node:nodeType()
if SQLmethods[Params.api].AddValue and SQLmethods[Params.api].AddValue[curType] then
SQLmethods[Params.api].AddValue[curType](Node, Buffer)
elseif curType == 'string' then
if tostring(Node):match('0x') then
Buffer:write(Node)
else
Buffer:write(Conn:quote(tostring(Node)))
end
elseif curType == 'datetime' then
Buffer:write(Conn:quote(tostring(Node)))
else --Double or integer assumed.
Buffer:write(Node)
end
end
local function addKeyConditions(Row, Buffer, Keys)
Buffer:write(' WHERE ')
for i = 1,#Keys do
if i > 1 then
Buffer:write(' AND ')
end
Buffer:write(Keys[i], ' = ')
addSQLValue(Row[Keys[i]], Buffer)
end
end
local function addRowInsert(TableName, Row, Buffer)
Buffer:write('INSERT INTO ', TableName, '(')
local NotFirstVal = false
for i=1, #Row do
if Row[i]:nodeValue() ~= '' or
(Row[i]:nodeValue() == '' and Params.merge_null) then
if i ~= 1 and NotFirstVal then
Buffer:write(', ')
end
Buffer:write(Row[i]:nodeName())
NotFirstVal = true
end
end
Buffer:write(') VALUES(')
NotFirstVal = false
for i=1, #Row do
if Row[i]:nodeValue() ~= '' or
(Row[i]:nodeValue() == '' and Params.merge_null) then
if NotFirstVal then
Buffer:write(', ')
end
addSQLValue(Row[i], Buffer)
NotFirstVal = true
end
end
Buffer:write(')')
end
local function addRowUpdate(TableName, Row, Buffer, Keys)
Buffer:write('UPDATE ', TableName, ' SET ')
local NotFirstVal = false
for i=1, #Row do
if Row[i]:nodeValue() ~= '' or
(Row[i]:nodeValue() == '' and Params.merge_null) then
if NotFirstVal then
Buffer:write(', ')
end
Buffer:write(Row[i]:nodeName(),' = ')
addSQLValue(Row[i], Buffer)
NotFirstVal = true
end
end
addKeyConditions(Row, Buffer, Keys)
end
local function genericMergeSQL(Table, Statements, Keys)
for i=1, #Table do
local Buffer = getStringWriter()
if i > 1 then
Buffer:write('\n')
end
local count = 0
if #Keys > 0 then
local CountBuffer = getStringWriter()
CountBuffer:write('SELECT COUNT(', Keys[1],
') FROM ', Table:nodeName())
addKeyConditions(Table[i], CountBuffer, Keys)
local results = Conn:query(CountBuffer:getValue())
count = tonumber(results[1][1]:nodeValue())
end
if count == 0 then
addRowInsert(Table:nodeName(), Table[i], Buffer)
elseif count == 1 then
addRowUpdate(Table:nodeName(), Table[i], Buffer, Keys)
else
error('Multiple rows found with same primary keys in table '
.. Table:nodeName() .. '.')
end
Statements[#Statements+1] = Buffer:getValue()
end
end
local function batchBeginEnd(Statements)
return 'BEGIN\n' .. table.concat(Statements, ';\n') .. ';\nEND;'
end
SQLmethods = { --Declared as local at the top.
[db.MY_SQL] = {Merge = genericMergeSQL},
[db.ORACLE_OCI] = {Merge = genericMergeSQL, Batch = batchBeginEnd, AddValue = {datetime = OracleDateTime}},
[db.ORACLE_ODBC] = {Merge = genericMergeSQL, Batch = batchBeginEnd, AddValue = {datetime = OracleDateTime}},
[db.SQL_SERVER] = {Merge = genericMergeSQL, Batch = batchBeginEnd},
[db.SQLITE] = {Merge = genericMergeSQL},
}
local function getMergeStatements()
local Tables = Params.data
local SQLstatements = {}
for i=1, #Tables do
--Need non-zero number of rows to merge and to determine keys.
if #Tables[i] > 0 then
local Keys = {}
for j=1,#Tables[i][1] do
if Tables[i][1][j]:isKey() then
Keys[#Keys + 1] = Tables[i][1][j]:nodeName()
end
end
SQLmethods[Params.api].Merge(Tables[i], SQLstatements, Keys)
end
end
return SQLstatements
end
local function checkTable(T, CallDepth)
if type(T) ~= 'table' then
error('Expected a table of parameters.', CallDepth)
end
end
local function checkParam(ParamTable, List, CallDepth)
for i=1, #List do
if not ParamTable[List[i]] then
error('Missing parameter "'..List[i]..'".', CallDepth)
end
end
end
local function getParams(arg1, arg2, CallDepth)
if getmetatable(arg1) == getmetatable(db)._submeta.db_connection then
--First argument is a db.connection, so second one is the method parameters.
Conn, Params = arg1, arg2
Params.api = Conn:info().api
Params.name = Conn:info().name
if Conn:info().live == false --Connection itself isn't live.
then Params.live = false
end
else --Assume no db.connection was passed in.
Params = arg1
checkTable(Params, CallDepth + 1)
checkParam(Params, {'api','name','user','password'}, CallDepth + 1)
Conn = db.connect{
api=Params.api,
name=Params.name,
user =Params.user,
password = Params.password,
use_unicode = Params.use_unicode,
timeout = Params.timeout,
live = Params.live
}
Params.IsTempConnection = true
end
checkParam(Params, {'data'}, CallDepth + 1)
if not SQLmethods[Params.api] then
error('Merge using this database API is not currently supported.', CallDepth)
end
if type(Params.data) ~= 'userdata' or not Params.data:nodeType() or Params.data:nodeType() ~= 'table_collection' then
error('The data parameter must be a table collection as produced from db.tables{}.', CallDepth)
end
-- merge_null defaults to true to match db.merge() behaviour
if Params.merge_null == nil then Params.merge_null = true end
-- transaction defaults to true to match db.merge() behaviour
if Params.transaction == nil then Params.transaction = true end
end
function db.customMerge(arg1, arg2, CallDepth)
if not CallDepth then
--Assume this function is being called directly.
CallDepth = 2
end
getParams(arg1,arg2,CallDepth + 1)
local MergeSQL = getMergeStatements()
if Params.transaction then
Conn:begin{live = Params.live}
end
local success, result = pcall(
--Enclosing the merge in a pcall to be able to roll back.
function(Conn, Params, MergeSQL)
if SQLmethods[Params.api].Batch then --Batch the operations if we can.
Conn:execute{sql = SQLmethods[Params.api].Batch(MergeSQL), live = Params.live}
else
for i=1,#MergeSQL do
Conn:execute{sql = MergeSQL[i], live = Params.live}
end
end
end,
Conn, Params, MergeSQL
)
--Commit or rollback the transaction, if we began one.
if success then
if Params.transaction then Conn:commit{live = Params.live} end
else
if Params.transaction then Conn:rollback{live = Params.live} end
error(result, CallDepth)
end
if Params.IsTempConnection then Conn:close() end
if Params.live ~= true then
return 'Operation not live.'
end
end
--A wrapper for db.customMerge for object-oriented use with db.connections.
--We do it this way to attach different help data to the wrapper.
getmetatable(db)._submeta.db_connection.__index.customMerge = function(arg1, arg2)
local CallDepth = 2 --Used for bubbling up errors to where the method is being called.
checkTable(arg2, CallDepth + 1) --Expecting a db.connection and a table of parameters.
local res = db.customMerge(arg1, arg2, CallDepth + 1)
--Avoiding a tail call (which changes the call stack in Lua) so errors can be bubbled up properly.
return res
end
if help then
--Help data for use as db.customMerge().
help.set{input_function=db.customMerge,
help_data= {
SummaryLine = 'Merges records into a database (customizable Lua method).',
Desc = 'Merges records into a database. This is a special, customizable method written in Lua. '..
'It currently supports MySQL, SQL Server, Oracle ODBC/OCI, and SQLite.',
Parameters = {
{api = {Desc = 'Database API, should be in the form: db.SQL_SERVER, db.MY_SQL, etc.'}},
{name = {Desc = 'Database name/address.'}},
{user = {Desc = 'User name.'}},
{password = {Desc = 'Password.'}},
{data = {Desc = 'The data to merge, in the form of a node tree created using db.tables().'}},
{live = {Desc = 'Whether operation should be executed live in the editor (default = false).', Opt = true}},
{use_unicode = {Desc = 'Whether Unicode should be used when communicating with the database.', Opt = true}},
{timeout = {Desc = 'Maximum time in seconds allowed for the connection (0 for infinite).', Opt = true}},
{transaction = {Desc = 'Whether merge should be performed as a transaction (default = true).', Opt = true}},
{merge_null = {Desc = 'Whether NULL values should be updated/inserted (default = true).', Opt = true}},
},
Title = 'db.customMerge',
Usage = "db.customMerge{api=<value>, name=<value>, ...}",
SeeAlso = {{Title = 'More on customMerge', Link = 'http://wiki.interfaceware.com/888.html'}},
ParameterTable = true
}
}
--Help data for use as conn:customMerge().
help.set{input_function=getmetatable(db)._submeta.db_connection.__index.customMerge,
help_data={
SummaryLine = 'Merges records into a database (customizable Lua method).',
Desc = 'Merges records into a database. This is a special, customizable method written in Lua. '..
'It currently supports MySQL, SQL Server, Oracle ODBC/OCI, and SQLite.',
Parameters = {
{data = {Desc = 'The data to merge, in the form of a node tree created using db.tables().'}},
{live = {Desc = 'Whether operation should be executed live in the editor (default = false).', Opt = true}},
{transaction = {Desc = 'Whether merge should be performed as a transaction (default = true).', Opt = true}},
{merge_null = {Desc = 'Whether NULL values should be updated/inserted (default = true).', Opt = true}},
},
Title = 'customMerge',
Usage = "Conn:customMerge{data=<value> [, merge_null=<value>] [, ...]}",
SeeAlso = {{Title = 'More on customMerge', Link = 'http://wiki.interfaceware.com/888.html'}},
ParameterTable = true
}
}
end
Description
A customizable database merge method for Iguana 5.5.1 and up.
Usage Details
At its core customMerge() does a SELECT call on the target table using its Primary Key(s). If a matching row is found it is updated, if no row is found a new row is inserted. This algorithm uses the same logic as the built-in conn:merge{} function, with one important difference: The addition of the merge_null parameter, which controls whether NULLs are written to the database.
How to use custom_merge.lua:
- Add it to your shared modules in any Translator project.
- Make sure to
require 'custom_merge'at the top of your script. - Use the Translator auto-completion to understand the parameters.
- Call the
customMerge()method anywhere you would call the regular merge, either on database connection objects asconn:customMerge()or by itself asdb.customMerge(). - Dive into the
customMerge()code using the Translator annotations to help you make the changes you need.