Module:Cargo/config: Difference between revisions
Jump to navigation
Jump to search
(Created page with "------------------------------------------------------------------------------- -- -- Configuration for Module:Cargo -- ------------------------------...") |
No edit summary |
||
Line 1: | Line 1: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- | -- | ||
-- | -- Module:Cargo | ||
-- | -- | ||
-- Common tasks for the cargo extension are generalized into handy functions | |||
-- in this meta module | |||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local cfg = | local getArgs = require('Module:Arguments').getArgs | ||
local m_util = require('Module:Util') | |||
local cargo = mw.ext.cargo | |||
-- The cfg table contains all localisable strings and configuration, to make it | |||
-- easier to port this module to another wiki. | |||
local cfg = mw.loadData('Module:Cargo/config') | |||
local i18n = cfg.i18n | |||
-- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
-- | -- Exported functions | ||
-- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
local m_cargo = {} | |||
-- | |||
-- Cargo function wrappers | |||
-- | |||
function m_cargo.declare(frame, args) | |||
return frame:callParserFunction('#cargo_declare:', args) | |||
end | |||
cfg. | function m_cargo.attach(frame, args) | ||
return frame:callParserFunction('#cargo_attach:', args) | |||
end | |||
function m_cargo.store(frame, values, args) | |||
-- Calls the cargo_store parser function and ensures the values passed are casted properly | |||
-- | |||
-- Value handling: | |||
-- tables - automatically concat | |||
-- booleans - automatically casted to 1 or 0 to ensure they're stored properly | |||
-- | |||
-- Arguments: | |||
-- frame - frame object | |||
-- values - table of field/value pairs to store | |||
-- args - any additional arguments | |||
-- sep - separator to use for concat | |||
-- store_empty - if specified, allow storing empty rows | |||
-- debug - send the converted values to the lua debug log | |||
args = args or {} | |||
args.sep = args.sep or {} | |||
local i = 0 | |||
for k, v in pairs(values) do | |||
i = i + 1 | |||
if type(v) == 'table' then | |||
if #v == 0 then | |||
i = i - 1 | |||
values[k] = nil | |||
else | |||
values[k] = table.concat(v, args.sep[k] or ',') | |||
end | |||
elseif type(v) == 'boolean' then | |||
if v == true then | |||
v = '1' | |||
elseif v == false then | |||
v = '0' | |||
end | |||
values[k] = v | |||
end | |||
end | |||
-- i must be greater then 1 since we at least expect the _table argument to be set, even if no values are set | |||
if i > 1 or args.store_empty then | |||
if args.debug ~= nil then | |||
mw.logObject(values) | |||
end | |||
return frame:callParserFunction('#cargo_store:', values) | |||
end | |||
end | |||
function m_cargo.query(tables, fields, query, args) | |||
-- Wrapper for mw.ext.cargo.query that helps to work around some bugs | |||
-- | |||
-- Current workarounds: | |||
-- field names will be "aliased" to themselves | |||
-- | |||
-- Takes 3 arguments: | |||
-- tables - array containing tables | |||
-- fields - array containing fields; these will automatically be renamed to the way they are specified to work around bugs when results are returned | |||
-- query - array containing cargo sql clauses | |||
-- args | |||
-- args.keep_empty | |||
-- Cargo bug workaround | |||
args = args or {} | |||
for i, field in ipairs(fields) do | |||
-- already has some alternate name set, so do not do this. | |||
if string.find(field, '=') == nil then | |||
fields[i] = string.format('%s=%s', field, field) | |||
end | |||
end | |||
query.limit = query.limit or cfg.limit*100 | |||
query.offset = query.offset or 0 | |||
local results = {} | |||
repeat | |||
local result = cargo.query(table.concat(tables, ','), table.concat(fields, ','), query) | |||
query.offset = query.offset + #result | |||
for _,v in ipairs(result) do | |||
results[#results + 1] = v | |||
end | |||
until (#result < cfg.limit) or (#results >= query.limit) | |||
if args.keep_empty == nil then | |||
for _, row in ipairs(results) do | |||
for k, v in pairs(row) do | |||
if v == "" then | |||
row[k] = nil | |||
end | |||
end | |||
end | |||
end | |||
return results | |||
end | |||
-- | |||
-- Extended cargo functions | |||
-- | |||
function m_cargo.store_from_lua(args) | |||
-- Factory for function that stores data from lua data into a cargo table from a template call | |||
-- | |||
-- Arguments: | |||
-- module: Name of the module where the data is located, without the module prefix | |||
-- tables: Mapping of the table data | |||
-- | |||
-- Return: | |||
-- function that takes frame argument | |||
-- | |||
-- | |||
-- The function created takes the following arguments: | |||
-- REQURIED: | |||
-- tbl: table to store | |||
-- src: source wiki path after the module if it differs from the table name | |||
-- index_start: Starting index (default: 1) | |||
-- index_end: Ending index (default: data length, i.e. all data) | |||
args = args or {} | |||
if args.module == nil or args.tables == nil then | |||
error(i18n.errors.store_from_lua_missing_arguments) | |||
end | |||
return function (frame) | |||
-- Get args | |||
tpl_args = getArgs(frame, { | |||
parentFirst = true | |||
}) | |||
frame = m_util.misc.get_frame(frame) | |||
if args.tables[tpl_args.tbl] == nil then | |||
error(string.format(i18n.errors.store_from_lua_invalid_table, tostring(tpl_args.tbl))) | |||
end | |||
-- mw.loadData has some problems... | |||
local data = require(string.format('%s:%s/%s', i18n.module, args.module, tpl_args.src or tpl_args.tbl)) | |||
tpl_args.index_start = math.max(tonumber(tpl_args.index_start) or 1, 1) | |||
tpl_args.index_end = math.min(tonumber(tpl_args.index_end) or #data, #data) | |||
for i=tpl_args.index_start, tpl_args.index_end do | |||
local row = data[i] | |||
if row == nil then | |||
break | |||
end | |||
-- get full table name | |||
row._table = args.tables[tpl_args.tbl].table | |||
m_cargo.store(frame, row) | |||
end | |||
return string.format(i18n.tooltips.store_rows, tpl_args.index_start, tpl_args.index_end, tpl_args.index_end-tpl_args.index_start+1, tpl_args.tbl) | |||
end | |||
end | |||
--[[test = { | |||
tables = { | |||
}, | |||
{ | |||
arg = {'argument1', 'argument2'}, | |||
header = 'Table header', | |||
fields = {'mods.granted_skill'}, | |||
display = function(tpl_args, frame, tr, data) | |||
tr | |||
:tag('td') | |||
:wikitext(data['mods.granted_skill']) | |||
end, | |||
order = 1000, | |||
sort_type = 'text', | |||
options = '', | |||
}, | |||
} | } | ||
=p.table_query{ | |||
tpl_args={ | |||
test=true, | |||
q_where='mods.generation_type = 5 AND mods.domain = 1', | |||
q_tables='spawn_weights', | |||
q_join='mods._pageID=spawn_weights._pageID', | |||
}, | |||
frame=nil, | |||
main_table='mods', | |||
--unique_row_fields={}, | |||
--empty_cell | |||
data = { | |||
tables = { | |||
mod_stats = {join = 'mods._pageID=mod_stats._pageID'}, | |||
}, | |||
{ | |||
args = {'test'}, | |||
header = 'Id', | |||
fields = {'mods.id'}, | |||
display = function(tpl_args, frame, tr, rows, rowinfo) | |||
tr | |||
:tag('td') | |||
:wikitext(rows[1]['mods.id']) | |||
end, | |||
--order = 0, | |||
sort_type = 'text', | |||
--options = {}, | |||
}, | |||
{ | |||
args = {'test'}, | |||
header = 'Stats', | |||
fields = {'mod_stats.id', 'mod_stats.min', 'mod_stats.max'}, | |||
display = function(tpl_args, frame, tr, rows, rowinfo) | |||
local stats = {} | |||
for _, row in ipairs(rows) do | |||
stats[#stats+1] = string.format('%s: %s to %s', row['mod_stats.id'], row['mod_stats.min'], row['mod_stats.max']) | |||
end | |||
tr | |||
:tag('td') | |||
:wikitext(table.concat(stats, '<br>')) | |||
end, | |||
--order = 0, | |||
sort_type = 'text', | |||
--options = {}, | |||
}, | |||
}, | |||
} | |||
]] | |||
function m_cargo.table_query(args) | |||
-- REQUIRED | |||
-- tpl_args | |||
-- frame | |||
-- main_table | |||
-- data | |||
-- tables | |||
-- [...] | |||
-- args | |||
-- header | |||
-- fields | |||
-- display | |||
-- order | |||
-- sort_type | |||
-- options | |||
-- [...] | |||
-- OPTIONAL | |||
-- row_unique_fields | |||
-- empty_cell | |||
-- table_css | |||
-- TPL_ARGS: | |||
-- q_*** | |||
-- default | |||
-- before | |||
-- after | |||
-- *** - as defined in data | |||
local tpl_args = args.tpl_args | |||
local frame = m_util.misc.get_frame(args.frame) | |||
args.data.tables = args.data.tables or {} | |||
args.row_unique_fields = args.row_unique_fields or {string.format('%s._pageID', args.main_table)} | |||
args.empty_cell = args.empty_cell or '<td></td>' | |||
local row_infos = {} | |||
for _, row_info in ipairs(args.data) do | |||
local enabled = false | |||
if row_info.args == nil then | |||
enabled = true | |||
elseif type(row_info.args) == 'string' and m_util.cast.boolean(tpl_args[row_info.args]) then | |||
enabled = true | |||
elseif type(row_info.args) == 'table' then | |||
for _, argument in ipairs(row_info.args) do | |||
if m_util.cast.boolean(tpl_args[argument]) then | |||
enabled = true | |||
break | |||
end | |||
end | |||
end | |||
if enabled then | |||
row_info.options = row_info.options or {} | |||
row_infos[#row_infos+1] = row_info | |||
end | |||
end | |||
-- sort the rows | |||
table.sort(row_infos, function (a, b) | |||
return (a.order or 0) < (b.order or 0) | |||
end) | |||
-- Set tables | |||
local tables_assoc = { | |||
[args.main_table] = true, | |||
} | |||
if tpl_args.q_tables then | |||
for _, tbl_name in ipairs(m_util.string.split(tpl_args.q_tables, ',%s*')) do | |||
tables_assoc[tbl_name] = true | |||
end | |||
end | |||
-- Set required fields | |||
local fields_assoc = { | |||
[string.format('%s._pageID', args.main_table)] = true, | |||
} | |||
for _, rowinfo in ipairs(row_infos) do | |||
if type(rowinfo.fields) == 'function' then | |||
rowinfo.fields = rowinfo.fields() | |||
end | |||
for index, field in ipairs(rowinfo.fields) do | |||
rowinfo.options[index] = rowinfo.options[index] or {} | |||
-- Support using functions such as CONCAT() in fields: | |||
local f = string.match( | |||
field, m_util.string.pattern.valid_var_name() .. '%.' | |||
) | |||
if f ~= nil then | |||
tables_assoc[f] = true | |||
end | |||
fields_assoc[field] = true | |||
-- The results from the cargo query will use the aliased field: | |||
field = m_util.string.split(field, '%s*=%s*') | |||
rowinfo.fields[index] = field[2] or field[1] | |||
end | |||
end | |||
for _, field in ipairs(args.row_unique_fields) do | |||
fields_assoc[field] = true | |||
end | |||
-- Parse query arguments | |||
local query = { | |||
} | |||
for key, value in pairs(tpl_args) do | |||
if string.sub(key, 0, 2) == 'q_' then | |||
query[string.sub(key, 3)] = value | |||
end | |||
end | |||
if tpl_args.q_fields then | |||
local _extra_fields = m_util.string.split_outer( | |||
tpl_args.q_fields, | |||
',%s*', | |||
{'%(', '%)'} | |||
) | |||
for _, field in ipairs(_extra_fields) do | |||
fields_assoc[field] = true | |||
end | |||
end | |||
-- | |||
-- Query | |||
-- | |||
local tables = {args.main_table} | |||
local joins = {} | |||
for tbl_name, _ in pairs(tables_assoc) do | |||
args.data.tables[tbl_name] = args.data.tables[tbl_name] or {} | |||
if args.data.tables[tbl_name].join then | |||
joins[#joins+1] = args.data.tables[tbl_name].join | |||
tables[#tables+1] = tbl_name | |||
elseif string.match(tpl_args.q_join or '', '.*' .. tbl_name .. '%..*') ~= nil then | |||
tables[#tables+1] = tbl_name | |||
elseif tbl_name ~= args.main_table then | |||
error(string.format(i18n.errors.no_join, tbl_name)) | |||
end | |||
end | |||
local fields = {} | |||
for field, _ in pairs(fields_assoc) do | |||
fields[#fields+1] = field | |||
end | |||
if #joins > 0 then | |||
if query.join then | |||
query.join = query.join .. ',' .. table.concat(joins, ',') | |||
else | |||
query.join = table.concat(joins, ',') | |||
end | |||
end | |||
local results = {} | |||
local results_order = {} | |||
local cur_results = m_cargo.query( | |||
tables, | |||
fields, | |||
query | |||
) | |||
for _, row in ipairs(cur_results) do | |||
local unique_key = {} | |||
for _, field_name in ipairs(args.row_unique_fields) do | |||
if row[field_name] == nil then | |||
error(string.format(i18n.errors.missing_unique_field_in_result_row, field_name, string.gsub(mw.dumpObject(row), '\n', '<br>'))) | |||
end | |||
unique_key[#unique_key+1] = row[field_name] | |||
end | |||
unique_key = table.concat(unique_key, '__') | |||
if results[unique_key] then | |||
table.insert(results[unique_key], row) | |||
else | |||
results[unique_key] = {row} | |||
results_order[#results_order+1] = unique_key | |||
end | |||
end | |||
if #results_order == 0 then | |||
if tpl_args.default ~= nil then | |||
return tpl_args.default | |||
else | |||
return i18n.errors.no_results | |||
end | |||
end | |||
-- | |||
-- Display | |||
-- | |||
-- Preformance optimization | |||
if tpl_args.q_fields then | |||
tpl_args._extra_fields = m_util.string.split_outer( | |||
tpl_args.q_fields, | |||
',%s*', | |||
{'%(', '%)'} | |||
) | |||
for index, field in ipairs(tpl_args._extra_fields) do | |||
field = m_util.string.split(field, '%s*=%s*') | |||
-- field[2] will be nil if there is no alias | |||
tpl_args._extra_fields[index] = field[2] or field[1] | |||
end | |||
else | |||
tpl_args._extra_fields = {} | |||
end | |||
local tbl = mw.html.create('table') | |||
tbl:attr('class', 'wikitable sortable ' .. (args.table_css or '')) | |||
-- Header | |||
local tr = tbl:tag('tr') | |||
for _, row_info in ipairs(row_infos) do | |||
tr | |||
:tag('th') | |||
:attr('data-sort-type', row_info.sort_type or 'number') | |||
:wikitext(row_info.header) | |||
:done() | |||
end | |||
for _, field in ipairs(tpl_args._extra_fields) do | |||
tr | |||
:tag('th') | |||
:wikitext(field) | |||
end | |||
-- Body | |||
for _, unique_key in ipairs(results_order) do | |||
local rows = results[unique_key] | |||
tr = tbl:tag('tr') | |||
for _, rowinfo in ipairs(row_infos) do | |||
local display_fields = {} | |||
for index, field in ipairs(rowinfo.fields) do | |||
if rowinfo.options[index].optional ~= true then | |||
display_fields[field] = false | |||
for _, row in ipairs(rows) do | |||
if row[field] ~= nil then | |||
display_fields[field] = true | |||
break | |||
end | |||
end | |||
end | |||
end | |||
local display = true | |||
for key, value in pairs(display_fields) do | |||
if not value then | |||
display = false | |||
break | |||
end | |||
end | |||
if display then | |||
rowinfo.display(tpl_args, frame, tr, rows, rowinfo) | |||
else | |||
tr:wikitext(args.empty_cell) | |||
end | |||
end | |||
-- Add extra columns specified by tpl_args.q_fields: | |||
for _, field in ipairs(tpl_args._extra_fields) do | |||
local extra_col = {} | |||
for _, row in ipairs(rows) do | |||
if row[field] then | |||
extra_col[#extra_col+1] = row[field] | |||
end | |||
end | |||
if #extra_col > 0 then | |||
tr | |||
:tag('td') | |||
:wikitext(table.concat(extra_col, '<br>')) | |||
else | |||
tr:wikitext(args.empty_cell) | |||
end | |||
end | |||
end | |||
return (tpl_args.before or '') .. tostring(tbl) .. (tpl_args.after or '') | |||
end | |||
function m_cargo.parse_field_arguments(args) | |||
-- Maps the arguments from a cargo argument table (i.e. the ones used in m_cargo.declare_factory) | |||
-- | |||
-- It will expect/handle the following fields: | |||
-- map.order - REQUIRED - Array table for the order in which the arguments in map.fields will be parsed | |||
-- map.table - REQUIRED - Table name (for storage) | |||
-- map.fields[id].field - REQUIRED - Name of the field in cargo table | |||
-- map.fields[id].type - REQUIRED - Type of the field in cargo table | |||
-- map.fields[id].func - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame. | |||
-- The function should return the parsed value. | |||
-- | |||
-- If no function is specified, default handling depending on the cargo field type will be used | |||
-- map.fields[id].default - OPTIONAL - Default value if the value is not set or returned as nil | |||
-- If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field. | |||
-- map.fields[id].name - OPTIONAL - Name of the field in tpl_args if it differs from the id in map.fields. Used for i18n for example | |||
-- map.fields[id].required - OPTIONAL - Whether a value for the field is required or whether it can be left empty | |||
-- Note: With a default value the field will never be empty | |||
-- map.fields[id].skip - OPTIONAL - Skip field if missing from order | |||
-- | |||
-- | |||
-- Expects argument table. | |||
-- REQUIRED: | |||
-- tpl_args - arguments passed to template after preprecessing | |||
-- frame - frame object | |||
-- table_map - table mapping object | |||
-- rtr - if set return cargo props instead of storing them | |||
local tpl_args = args.tpl_args | |||
local frame = args.frame | |||
local map = args.table_map | |||
local cargo_values = {_table = map.table} | |||
-- for checking missing keys in order | |||
local available_fields = {} | |||
for key, field in pairs(map.fields) do | |||
if field.skip == nil then | |||
available_fields[key] = true | |||
end | |||
end | |||
-- main loop | |||
for _, key in ipairs(map.order) do | |||
local field = map.fields[key] | |||
if field == nil then | |||
error(string.format(i18n.errors.missing_key_in_fields, key, map.table)) | |||
else | |||
available_fields[key] = nil | |||
end | |||
-- key in argument mapping | |||
local args_key | |||
if field.name then | |||
args_key = field.name | |||
else | |||
args_key = key | |||
end | |||
-- Retrieve value | |||
local value | |||
-- automatic handling only works if the field type is set | |||
if field.type ~= nil then | |||
value = tpl_args[args_key] | |||
local cfield = m_cargo.parse_field{field=field.type} | |||
local handler | |||
if cfield.type == 'Integer' or cfield.type == 'Float' then | |||
handler = tonumber | |||
elseif cfield.type == 'Boolean' then | |||
handler = function (value) | |||
return m_util.cast.boolean(value, {cast_nil=false}) | |||
end | |||
end | |||
if cfield.list and value ~= nil then | |||
-- ingore whitespace between separator and values | |||
value = m_util.string.split(value, cfield.list .. '%s*') | |||
if handler then | |||
for index, v in ipairs(value) do | |||
value[index] = handler(v) | |||
if value[index] == nil then | |||
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, v, field.type)) | |||
end | |||
end | |||
end | |||
elseif handler and value ~= nil then | |||
value = handler(value) | |||
if value == nil then | |||
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, tpl_args[args_key], field.type)) | |||
end | |||
end | |||
-- Don't need special handling: String, Text, Wikitext, Searchtext | |||
-- Consider: Page, Date, Datetime, Coordinates, File, URL, Email | |||
end | |||
if field.func ~= nil then | |||
value = field.func(tpl_args, frame, value) | |||
end | |||
-- Check defaults | |||
if value == nil and field.default ~= nil then | |||
if type(field.default) == 'function' then | |||
value = field.default(tpl_args, frame) | |||
elseif type(field.default) == 'table' then | |||
mw.logObject(string.format(i18n.errors.table_object_as_default, key, map.table)) | |||
value = mw.clone(field.default) | |||
else | |||
value = field.default | |||
end | |||
end | |||
-- Add value to arguments and cargo data | |||
if value ~= nil then | |||
-- key will be used here since the value will be used internally from here on in english | |||
tpl_args[key] = value | |||
if field.field ~= nil then | |||
cargo_values[field.field] = value | |||
end | |||
elseif field.required == true then | |||
error(string.format(i18n.errors.argument_required, args_key)) | |||
end | |||
end | |||
-- check for missing keys and return error if any are missing | |||
local missing = {} | |||
for key, _ in pairs(available_fields) do | |||
missing[#missing+1] = key | |||
end | |||
if #missing > 0 then | |||
error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat(missing, '\n'))) | |||
end | |||
-- finally store data in DB | |||
if args.rtr ~= nil then | |||
return cargo_values | |||
else | |||
m_cargo.store(frame, cargo_values) | |||
end | |||
end | |||
function m_cargo.declare_factory(args) | |||
-- Returns a function that can be called by templates to declare cargo tables | |||
-- | |||
-- args | |||
-- data: data table | |||
-- table: name of cargo table | |||
-- fields: associative table with: | |||
-- field: name of the field to declare | |||
-- type: type of the field | |||
return function (frame) | |||
frame = m_util.misc.get_frame(frame) | |||
local dcl_args = {} | |||
dcl_args._table = args.data.table | |||
for k, field_data in pairs(args.data.fields) do | |||
if field_data.field then | |||
dcl_args[field_data.field] = field_data.type | |||
end | |||
end | |||
return m_cargo.declare(frame, dcl_args) | |||
end | |||
end | |||
function m_cargo.attach_factory(args) | |||
-- Returns a function that can be called by templates to attach cargo tables | |||
-- | |||
-- args | |||
-- data: data table | |||
-- table: name of cargo table | |||
-- fields: associative table with: | |||
-- field: name of the field to declare | |||
-- type: type of the field | |||
return function (frame) | |||
frame = m_util.misc.get_frame(frame) | |||
local attach_args = {} | |||
attach_args._table = args.data.table | |||
return m_cargo.attach(frame, attach_args) | |||
end | |||
end | |||
-- mw.logObject(m_cargo.map_results_to_id{results=mw.ext.cargo.query('mods,spawn_weights', 'mods._pageID, spawn_weights.tag', {where='mods.id="Strength1"', join='mods._pageID=spawn_weights._pageID'}), field='mods._pageID'}) | |||
function m_cargo.map_results_to_id(args) | |||
-- Maps the results passed to a table containing the specified field as key and a table of rows for the particular page as values. | |||
-- | |||
-- args | |||
-- results : Table of results returned from mw.ext.cargo.query to map to the specified id field. | |||
-- field : Name of the id field to map results to | |||
-- the field has to be in the fields list of the original query or it will cause errors. | |||
-- keep_id_field : If set then don't delete _pageID. | |||
-- append_id_field : If set then append the id to the table sequentially as well which allows preserving | |||
-- the id order they were found in. | |||
-- | |||
-- return | |||
-- table | |||
-- key : The specified id field | |||
-- value : Array containing the found rows (in the order that they were found) | |||
local out = {} | |||
for _, row in ipairs(args.results) do | |||
local key = row[args.field] | |||
if out[key] then | |||
out[key][#out[key]+1] = row | |||
else | |||
out[key] = {row} | |||
-- Append the ids sequentially, this allows preserving the order | |||
-- the ids were found: | |||
if args.append_id_field ~= nil then | |||
out[#out+1] = key | |||
end | |||
end | |||
--Discard the pageID, don't need this any longer in most cases: | |||
if args.keep_id_field == nil then | |||
row[args.field] = nil | |||
end | |||
end | |||
return out | |||
end | |||
function m_cargo.array_query(args) | |||
-- Performs a long "OR" query from the given array and field validating that there is only exactly one match returned | |||
-- | |||
-- args: | |||
-- REQUIRED: | |||
-- tables - array of tables (see m_cargo.query) | |||
-- fields - array of fields (see m_cargo.query) | |||
-- id_array - list of ids to query for | |||
-- id_field - name of the id field, will be automatically added to fields | |||
-- OPTIONAL: | |||
-- query - array containing cargo sql clauses [optional] (see m_cargo.query) | |||
-- ingore_missing - skip the check for missing fields entirely | |||
-- warning_on_missing - issue warning instead of error if missing values | |||
-- | |||
-- RETURN: | |||
-- table - results as given by mw.ext.cargo.query | |||
-- msg - any error messages if it was used as warning | |||
args.query = args.query or {} | |||
args.fields[#args.fields+1] = args.id_field | |||
if #args.id_array == 0 then | |||
return {} | |||
end | |||
-- | -- remove blanks | ||
local id_array = {} | |||
for _, value in ipairs(args.id_array) do | |||
if value ~= '' then | |||
id_array[#id_array+1] = value | |||
end | |||
end | |||
-- | -- for error returning | ||
local msg = {} | |||
local where = string.format('%s IN ("%s")', args.id_field, table.concat(id_array, '","')) | |||
} | if args.query.where then | ||
args.query.where = string.format('(%s) AND (%s)', args.query.where, where) | |||
else | |||
args.query.where = where | |||
end | |||
-- | |||
-- Prepare query | |||
-- | |||
local results = m_cargo.query( | |||
args.tables, | |||
args.fields, | |||
args.query | |||
) | |||
-- | |||
-- Check missing results | |||
-- | |||
if #results ~= #id_array then | |||
-- | |||
-- Check for duplicates | |||
-- | |||
-- The usage of distinct should elimate duplicates here from cargo being bugged while still showing actual data duplicates. | |||
local dupes = m_cargo.query( | |||
args.tables, | |||
{ | |||
string.format('COUNT(DISTINCT %s._pageID)=count', args.tables[1]), | |||
args.id_field, | |||
}, | |||
{ | |||
join=args.query.join, | |||
where=args.query.where, | |||
groupBy=args.id_field, | |||
having=string.format('COUNT(DISTINCT %s._pageID) > 1', args.tables[1]), | |||
} | |||
) | |||
if #dupes > 0 then | |||
out = {} | |||
for _, row in ipairs(dupes) do | |||
out[#out+1] = string.format('%s (%s pages found)', row[args.id_field], row['count']) | |||
end | |||
error(string.format(i18n.errors.duplicate_ids, args.id_field, table.concat(out, '\n'))) | |||
end | |||
local dupes = m_cargo.query( | |||
args.tables, | |||
{ | |||
string.format('COUNT(%s)=count', args.id_field), | |||
string.format('%s._pageName=page', args.tables[1]), | |||
}, | |||
{ | |||
join=args.query.join, | |||
where=args.query.where, | |||
groupBy=string.format('%s._pageName', args.tables[1]), | |||
having=string.format('COUNT(%s) > 1', args.id_field), | |||
} | |||
) | |||
if #dupes > 0 then | |||
out = {} | |||
for _, row in ipairs(dupes) do | |||
out[#out+1] = string.format('"%s" (%s entries found)', row['page'], row['count']) | |||
end | |||
error(string.format(i18n.errors.duplicate_ids, args.id_field, table.concat(out, '\n'))) | |||
end | |||
if not args.ignore_missing then | |||
local missing = {} | |||
for _, id in ipairs(id_array) do | |||
missing[id] = true | |||
end | |||
for _, row in ipairs(results) do | |||
missing[row[args.id_field]] = nil | |||
end | |||
local missing_ids = {} | |||
for k, _ in pairs(missing) do | |||
missing_ids[#missing_ids+1] = k | |||
end | |||
msg[#msg+1] = string.format(i18n.errors.missing_ids, args.id_field, table.concat(missing_ids, '\n')) | |||
if args.warning_on_missing == nil then | |||
error(msg[#msg]) | |||
else | |||
mw.logObject(msg[#msg]) | |||
end | |||
end | |||
end | |||
return results, msg | |||
end | |||
function m_cargo.replace_holds(args) | |||
-- Replaces a holds query with a like or regexp equivalent. | |||
-- | |||
-- required args: | |||
-- string: string to replace | |||
-- | |||
-- optional args: | |||
-- mode : Either "like" or "regex"; default "regex" | |||
-- like: Replaces the holds query with a LIKE equivalent | |||
-- regex: Replaces the holds query with a REGEXP equivalent | |||
-- field : Field pattern to use. Can be used to restrict the hold replacement to specific fields. | |||
-- Default: all fields are matched. | |||
-- separator: Separator for field entries to use in the REGEXP mode. | |||
-- Default: , | |||
-- | |||
-- Returns the replaced query | |||
local args = args or {} | |||
-- if the field is not specified, replace any holds query | |||
args.field = args.field or '[%w_\.]+' | |||
if args.mode == 'like' or args.mode == nil then | |||
return string.gsub( | |||
args.string, | |||
string.format('(%s) HOLDS ([NOT ]*)([LIKE ]*)"([^"]+)"', args.field), | |||
'%1__full %2LIKE "%%%4%%"' | |||
) | |||
elseif args.mode == 'regex' then | |||
args.separator = args.separator or ',' | |||
return string.gsub( | |||
args.string, | |||
string.format('(%s) HOLDS ([NOT ]*)"([^"]+)"', args.field), | |||
string.format('%%1__full %%2REGEXP "(%s|^)%%3(%s|$)"', args.separator, args.separator) | |||
) | |||
else | |||
error('Invalid mode specified. Acceptable values are like or regex.') | |||
end | |||
end | |||
function m_cargo.parse_field(args) | |||
-- Parse a cargo field declaration and returns a table containing the results | |||
-- | |||
-- required args: | |||
-- field: field to parse | |||
-- | |||
-- Return | |||
-- type - Type of the field | |||
-- list - Separator of the list if it is a list type field | |||
-- parameters - any parameters to the field itself | |||
local field = args.field | |||
local results = {} | |||
local match | |||
match = { string.match(field, 'List %(([^%(%)]+)%) of (.*)') } | |||
if #match > 0 then | |||
results.list = match[1] | |||
field = match[2] | |||
end | |||
-- - | match = { string.match(field, '%s*(%a+)%s*%(([^%(%)]+)%)') } | ||
if #match > 0 then | |||
results.type = match[1] | |||
field = match[2] | |||
results.parameters = {} | |||
for _, param_string in ipairs(m_util.string.split(field, ';')) do | |||
local index = { string.find(param_string, '=') } | |||
local key | |||
local value | |||
if #index > 0 then | |||
key = string.sub(param_string, 0, index[1]-1) | |||
value = m_util.string.strip(string.sub(param_string, index[1]+1)) | |||
else | |||
key = param_string | |||
value = true | |||
end | |||
results.parameters[m_util.string.strip(key)] = value | |||
end | |||
else | |||
-- the reminder must be the type since there is no extra declarations | |||
results.type = string.match(field, '%s*(%a+)%s*') | |||
end | |||
return results | |||
end | |||
return | return m_cargo |
Revision as of 05:18, 12 May 2021
This is the configuration file for Module:Cargo. This file can be edited to allow easy translation/porting of the module to other wikis.
The above documentation is transcluded from Module:Cargo/config/doc.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
--
-- Module:Cargo
--
-- Common tasks for the cargo extension are generalized into handy functions
-- in this meta module
-------------------------------------------------------------------------------
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local cargo = mw.ext.cargo
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Cargo/config')
local i18n = cfg.i18n
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local m_cargo = {}
--
-- Cargo function wrappers
--
function m_cargo.declare(frame, args)
return frame:callParserFunction('#cargo_declare:', args)
end
function m_cargo.attach(frame, args)
return frame:callParserFunction('#cargo_attach:', args)
end
function m_cargo.store(frame, values, args)
-- Calls the cargo_store parser function and ensures the values passed are casted properly
--
-- Value handling:
-- tables - automatically concat
-- booleans - automatically casted to 1 or 0 to ensure they're stored properly
--
-- Arguments:
-- frame - frame object
-- values - table of field/value pairs to store
-- args - any additional arguments
-- sep - separator to use for concat
-- store_empty - if specified, allow storing empty rows
-- debug - send the converted values to the lua debug log
args = args or {}
args.sep = args.sep or {}
local i = 0
for k, v in pairs(values) do
i = i + 1
if type(v) == 'table' then
if #v == 0 then
i = i - 1
values[k] = nil
else
values[k] = table.concat(v, args.sep[k] or ',')
end
elseif type(v) == 'boolean' then
if v == true then
v = '1'
elseif v == false then
v = '0'
end
values[k] = v
end
end
-- i must be greater then 1 since we at least expect the _table argument to be set, even if no values are set
if i > 1 or args.store_empty then
if args.debug ~= nil then
mw.logObject(values)
end
return frame:callParserFunction('#cargo_store:', values)
end
end
function m_cargo.query(tables, fields, query, args)
-- Wrapper for mw.ext.cargo.query that helps to work around some bugs
--
-- Current workarounds:
-- field names will be "aliased" to themselves
--
-- Takes 3 arguments:
-- tables - array containing tables
-- fields - array containing fields; these will automatically be renamed to the way they are specified to work around bugs when results are returned
-- query - array containing cargo sql clauses
-- args
-- args.keep_empty
-- Cargo bug workaround
args = args or {}
for i, field in ipairs(fields) do
-- already has some alternate name set, so do not do this.
if string.find(field, '=') == nil then
fields[i] = string.format('%s=%s', field, field)
end
end
query.limit = query.limit or cfg.limit*100
query.offset = query.offset or 0
local results = {}
repeat
local result = cargo.query(table.concat(tables, ','), table.concat(fields, ','), query)
query.offset = query.offset + #result
for _,v in ipairs(result) do
results[#results + 1] = v
end
until (#result < cfg.limit) or (#results >= query.limit)
if args.keep_empty == nil then
for _, row in ipairs(results) do
for k, v in pairs(row) do
if v == "" then
row[k] = nil
end
end
end
end
return results
end
--
-- Extended cargo functions
--
function m_cargo.store_from_lua(args)
-- Factory for function that stores data from lua data into a cargo table from a template call
--
-- Arguments:
-- module: Name of the module where the data is located, without the module prefix
-- tables: Mapping of the table data
--
-- Return:
-- function that takes frame argument
--
--
-- The function created takes the following arguments:
-- REQURIED:
-- tbl: table to store
-- src: source wiki path after the module if it differs from the table name
-- index_start: Starting index (default: 1)
-- index_end: Ending index (default: data length, i.e. all data)
args = args or {}
if args.module == nil or args.tables == nil then
error(i18n.errors.store_from_lua_missing_arguments)
end
return function (frame)
-- Get args
tpl_args = getArgs(frame, {
parentFirst = true
})
frame = m_util.misc.get_frame(frame)
if args.tables[tpl_args.tbl] == nil then
error(string.format(i18n.errors.store_from_lua_invalid_table, tostring(tpl_args.tbl)))
end
-- mw.loadData has some problems...
local data = require(string.format('%s:%s/%s', i18n.module, args.module, tpl_args.src or tpl_args.tbl))
tpl_args.index_start = math.max(tonumber(tpl_args.index_start) or 1, 1)
tpl_args.index_end = math.min(tonumber(tpl_args.index_end) or #data, #data)
for i=tpl_args.index_start, tpl_args.index_end do
local row = data[i]
if row == nil then
break
end
-- get full table name
row._table = args.tables[tpl_args.tbl].table
m_cargo.store(frame, row)
end
return string.format(i18n.tooltips.store_rows, tpl_args.index_start, tpl_args.index_end, tpl_args.index_end-tpl_args.index_start+1, tpl_args.tbl)
end
end
--[[test = {
tables = {
},
{
arg = {'argument1', 'argument2'},
header = 'Table header',
fields = {'mods.granted_skill'},
display = function(tpl_args, frame, tr, data)
tr
:tag('td')
:wikitext(data['mods.granted_skill'])
end,
order = 1000,
sort_type = 'text',
options = '',
},
}
=p.table_query{
tpl_args={
test=true,
q_where='mods.generation_type = 5 AND mods.domain = 1',
q_tables='spawn_weights',
q_join='mods._pageID=spawn_weights._pageID',
},
frame=nil,
main_table='mods',
--unique_row_fields={},
--empty_cell
data = {
tables = {
mod_stats = {join = 'mods._pageID=mod_stats._pageID'},
},
{
args = {'test'},
header = 'Id',
fields = {'mods.id'},
display = function(tpl_args, frame, tr, rows, rowinfo)
tr
:tag('td')
:wikitext(rows[1]['mods.id'])
end,
--order = 0,
sort_type = 'text',
--options = {},
},
{
args = {'test'},
header = 'Stats',
fields = {'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
display = function(tpl_args, frame, tr, rows, rowinfo)
local stats = {}
for _, row in ipairs(rows) do
stats[#stats+1] = string.format('%s: %s to %s', row['mod_stats.id'], row['mod_stats.min'], row['mod_stats.max'])
end
tr
:tag('td')
:wikitext(table.concat(stats, '<br>'))
end,
--order = 0,
sort_type = 'text',
--options = {},
},
},
}
]]
function m_cargo.table_query(args)
-- REQUIRED
-- tpl_args
-- frame
-- main_table
-- data
-- tables
-- [...]
-- args
-- header
-- fields
-- display
-- order
-- sort_type
-- options
-- [...]
-- OPTIONAL
-- row_unique_fields
-- empty_cell
-- table_css
-- TPL_ARGS:
-- q_***
-- default
-- before
-- after
-- *** - as defined in data
local tpl_args = args.tpl_args
local frame = m_util.misc.get_frame(args.frame)
args.data.tables = args.data.tables or {}
args.row_unique_fields = args.row_unique_fields or {string.format('%s._pageID', args.main_table)}
args.empty_cell = args.empty_cell or '<td></td>'
local row_infos = {}
for _, row_info in ipairs(args.data) do
local enabled = false
if row_info.args == nil then
enabled = true
elseif type(row_info.args) == 'string' and m_util.cast.boolean(tpl_args[row_info.args]) then
enabled = true
elseif type(row_info.args) == 'table' then
for _, argument in ipairs(row_info.args) do
if m_util.cast.boolean(tpl_args[argument]) then
enabled = true
break
end
end
end
if enabled then
row_info.options = row_info.options or {}
row_infos[#row_infos+1] = row_info
end
end
-- sort the rows
table.sort(row_infos, function (a, b)
return (a.order or 0) < (b.order or 0)
end)
-- Set tables
local tables_assoc = {
[args.main_table] = true,
}
if tpl_args.q_tables then
for _, tbl_name in ipairs(m_util.string.split(tpl_args.q_tables, ',%s*')) do
tables_assoc[tbl_name] = true
end
end
-- Set required fields
local fields_assoc = {
[string.format('%s._pageID', args.main_table)] = true,
}
for _, rowinfo in ipairs(row_infos) do
if type(rowinfo.fields) == 'function' then
rowinfo.fields = rowinfo.fields()
end
for index, field in ipairs(rowinfo.fields) do
rowinfo.options[index] = rowinfo.options[index] or {}
-- Support using functions such as CONCAT() in fields:
local f = string.match(
field, m_util.string.pattern.valid_var_name() .. '%.'
)
if f ~= nil then
tables_assoc[f] = true
end
fields_assoc[field] = true
-- The results from the cargo query will use the aliased field:
field = m_util.string.split(field, '%s*=%s*')
rowinfo.fields[index] = field[2] or field[1]
end
end
for _, field in ipairs(args.row_unique_fields) do
fields_assoc[field] = true
end
-- Parse query arguments
local query = {
}
for key, value in pairs(tpl_args) do
if string.sub(key, 0, 2) == 'q_' then
query[string.sub(key, 3)] = value
end
end
if tpl_args.q_fields then
local _extra_fields = m_util.string.split_outer(
tpl_args.q_fields,
',%s*',
{'%(', '%)'}
)
for _, field in ipairs(_extra_fields) do
fields_assoc[field] = true
end
end
--
-- Query
--
local tables = {args.main_table}
local joins = {}
for tbl_name, _ in pairs(tables_assoc) do
args.data.tables[tbl_name] = args.data.tables[tbl_name] or {}
if args.data.tables[tbl_name].join then
joins[#joins+1] = args.data.tables[tbl_name].join
tables[#tables+1] = tbl_name
elseif string.match(tpl_args.q_join or '', '.*' .. tbl_name .. '%..*') ~= nil then
tables[#tables+1] = tbl_name
elseif tbl_name ~= args.main_table then
error(string.format(i18n.errors.no_join, tbl_name))
end
end
local fields = {}
for field, _ in pairs(fields_assoc) do
fields[#fields+1] = field
end
if #joins > 0 then
if query.join then
query.join = query.join .. ',' .. table.concat(joins, ',')
else
query.join = table.concat(joins, ',')
end
end
local results = {}
local results_order = {}
local cur_results = m_cargo.query(
tables,
fields,
query
)
for _, row in ipairs(cur_results) do
local unique_key = {}
for _, field_name in ipairs(args.row_unique_fields) do
if row[field_name] == nil then
error(string.format(i18n.errors.missing_unique_field_in_result_row, field_name, string.gsub(mw.dumpObject(row), '\n', '<br>')))
end
unique_key[#unique_key+1] = row[field_name]
end
unique_key = table.concat(unique_key, '__')
if results[unique_key] then
table.insert(results[unique_key], row)
else
results[unique_key] = {row}
results_order[#results_order+1] = unique_key
end
end
if #results_order == 0 then
if tpl_args.default ~= nil then
return tpl_args.default
else
return i18n.errors.no_results
end
end
--
-- Display
--
-- Preformance optimization
if tpl_args.q_fields then
tpl_args._extra_fields = m_util.string.split_outer(
tpl_args.q_fields,
',%s*',
{'%(', '%)'}
)
for index, field in ipairs(tpl_args._extra_fields) do
field = m_util.string.split(field, '%s*=%s*')
-- field[2] will be nil if there is no alias
tpl_args._extra_fields[index] = field[2] or field[1]
end
else
tpl_args._extra_fields = {}
end
local tbl = mw.html.create('table')
tbl:attr('class', 'wikitable sortable ' .. (args.table_css or ''))
-- Header
local tr = tbl:tag('tr')
for _, row_info in ipairs(row_infos) do
tr
:tag('th')
:attr('data-sort-type', row_info.sort_type or 'number')
:wikitext(row_info.header)
:done()
end
for _, field in ipairs(tpl_args._extra_fields) do
tr
:tag('th')
:wikitext(field)
end
-- Body
for _, unique_key in ipairs(results_order) do
local rows = results[unique_key]
tr = tbl:tag('tr')
for _, rowinfo in ipairs(row_infos) do
local display_fields = {}
for index, field in ipairs(rowinfo.fields) do
if rowinfo.options[index].optional ~= true then
display_fields[field] = false
for _, row in ipairs(rows) do
if row[field] ~= nil then
display_fields[field] = true
break
end
end
end
end
local display = true
for key, value in pairs(display_fields) do
if not value then
display = false
break
end
end
if display then
rowinfo.display(tpl_args, frame, tr, rows, rowinfo)
else
tr:wikitext(args.empty_cell)
end
end
-- Add extra columns specified by tpl_args.q_fields:
for _, field in ipairs(tpl_args._extra_fields) do
local extra_col = {}
for _, row in ipairs(rows) do
if row[field] then
extra_col[#extra_col+1] = row[field]
end
end
if #extra_col > 0 then
tr
:tag('td')
:wikitext(table.concat(extra_col, '<br>'))
else
tr:wikitext(args.empty_cell)
end
end
end
return (tpl_args.before or '') .. tostring(tbl) .. (tpl_args.after or '')
end
function m_cargo.parse_field_arguments(args)
-- Maps the arguments from a cargo argument table (i.e. the ones used in m_cargo.declare_factory)
--
-- It will expect/handle the following fields:
-- map.order - REQUIRED - Array table for the order in which the arguments in map.fields will be parsed
-- map.table - REQUIRED - Table name (for storage)
-- map.fields[id].field - REQUIRED - Name of the field in cargo table
-- map.fields[id].type - REQUIRED - Type of the field in cargo table
-- map.fields[id].func - OPTIONAL - Function to handle the arguments. It will be passed tpl_args and frame.
-- The function should return the parsed value.
--
-- If no function is specified, default handling depending on the cargo field type will be used
-- map.fields[id].default - OPTIONAL - Default value if the value is not set or returned as nil
-- If default is a function, the function will be called with (tpl_args, frame) and expected to return a default value for the field.
-- map.fields[id].name - OPTIONAL - Name of the field in tpl_args if it differs from the id in map.fields. Used for i18n for example
-- map.fields[id].required - OPTIONAL - Whether a value for the field is required or whether it can be left empty
-- Note: With a default value the field will never be empty
-- map.fields[id].skip - OPTIONAL - Skip field if missing from order
--
--
-- Expects argument table.
-- REQUIRED:
-- tpl_args - arguments passed to template after preprecessing
-- frame - frame object
-- table_map - table mapping object
-- rtr - if set return cargo props instead of storing them
local tpl_args = args.tpl_args
local frame = args.frame
local map = args.table_map
local cargo_values = {_table = map.table}
-- for checking missing keys in order
local available_fields = {}
for key, field in pairs(map.fields) do
if field.skip == nil then
available_fields[key] = true
end
end
-- main loop
for _, key in ipairs(map.order) do
local field = map.fields[key]
if field == nil then
error(string.format(i18n.errors.missing_key_in_fields, key, map.table))
else
available_fields[key] = nil
end
-- key in argument mapping
local args_key
if field.name then
args_key = field.name
else
args_key = key
end
-- Retrieve value
local value
-- automatic handling only works if the field type is set
if field.type ~= nil then
value = tpl_args[args_key]
local cfield = m_cargo.parse_field{field=field.type}
local handler
if cfield.type == 'Integer' or cfield.type == 'Float' then
handler = tonumber
elseif cfield.type == 'Boolean' then
handler = function (value)
return m_util.cast.boolean(value, {cast_nil=false})
end
end
if cfield.list and value ~= nil then
-- ingore whitespace between separator and values
value = m_util.string.split(value, cfield.list .. '%s*')
if handler then
for index, v in ipairs(value) do
value[index] = handler(v)
if value[index] == nil then
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, v, field.type))
end
end
end
elseif handler and value ~= nil then
value = handler(value)
if value == nil then
error(string.format(i18n.errors.handler_returned_nil, map.table, args_key, tpl_args[args_key], field.type))
end
end
-- Don't need special handling: String, Text, Wikitext, Searchtext
-- Consider: Page, Date, Datetime, Coordinates, File, URL, Email
end
if field.func ~= nil then
value = field.func(tpl_args, frame, value)
end
-- Check defaults
if value == nil and field.default ~= nil then
if type(field.default) == 'function' then
value = field.default(tpl_args, frame)
elseif type(field.default) == 'table' then
mw.logObject(string.format(i18n.errors.table_object_as_default, key, map.table))
value = mw.clone(field.default)
else
value = field.default
end
end
-- Add value to arguments and cargo data
if value ~= nil then
-- key will be used here since the value will be used internally from here on in english
tpl_args[key] = value
if field.field ~= nil then
cargo_values[field.field] = value
end
elseif field.required == true then
error(string.format(i18n.errors.argument_required, args_key))
end
end
-- check for missing keys and return error if any are missing
local missing = {}
for key, _ in pairs(available_fields) do
missing[#missing+1] = key
end
if #missing > 0 then
error(string.format(i18n.errors.missing_key_in_order, map.table, table.concat(missing, '\n')))
end
-- finally store data in DB
if args.rtr ~= nil then
return cargo_values
else
m_cargo.store(frame, cargo_values)
end
end
function m_cargo.declare_factory(args)
-- Returns a function that can be called by templates to declare cargo tables
--
-- args
-- data: data table
-- table: name of cargo table
-- fields: associative table with:
-- field: name of the field to declare
-- type: type of the field
return function (frame)
frame = m_util.misc.get_frame(frame)
local dcl_args = {}
dcl_args._table = args.data.table
for k, field_data in pairs(args.data.fields) do
if field_data.field then
dcl_args[field_data.field] = field_data.type
end
end
return m_cargo.declare(frame, dcl_args)
end
end
function m_cargo.attach_factory(args)
-- Returns a function that can be called by templates to attach cargo tables
--
-- args
-- data: data table
-- table: name of cargo table
-- fields: associative table with:
-- field: name of the field to declare
-- type: type of the field
return function (frame)
frame = m_util.misc.get_frame(frame)
local attach_args = {}
attach_args._table = args.data.table
return m_cargo.attach(frame, attach_args)
end
end
-- mw.logObject(m_cargo.map_results_to_id{results=mw.ext.cargo.query('mods,spawn_weights', 'mods._pageID, spawn_weights.tag', {where='mods.id="Strength1"', join='mods._pageID=spawn_weights._pageID'}), field='mods._pageID'})
function m_cargo.map_results_to_id(args)
-- Maps the results passed to a table containing the specified field as key and a table of rows for the particular page as values.
--
-- args
-- results : Table of results returned from mw.ext.cargo.query to map to the specified id field.
-- field : Name of the id field to map results to
-- the field has to be in the fields list of the original query or it will cause errors.
-- keep_id_field : If set then don't delete _pageID.
-- append_id_field : If set then append the id to the table sequentially as well which allows preserving
-- the id order they were found in.
--
-- return
-- table
-- key : The specified id field
-- value : Array containing the found rows (in the order that they were found)
local out = {}
for _, row in ipairs(args.results) do
local key = row[args.field]
if out[key] then
out[key][#out[key]+1] = row
else
out[key] = {row}
-- Append the ids sequentially, this allows preserving the order
-- the ids were found:
if args.append_id_field ~= nil then
out[#out+1] = key
end
end
--Discard the pageID, don't need this any longer in most cases:
if args.keep_id_field == nil then
row[args.field] = nil
end
end
return out
end
function m_cargo.array_query(args)
-- Performs a long "OR" query from the given array and field validating that there is only exactly one match returned
--
-- args:
-- REQUIRED:
-- tables - array of tables (see m_cargo.query)
-- fields - array of fields (see m_cargo.query)
-- id_array - list of ids to query for
-- id_field - name of the id field, will be automatically added to fields
-- OPTIONAL:
-- query - array containing cargo sql clauses [optional] (see m_cargo.query)
-- ingore_missing - skip the check for missing fields entirely
-- warning_on_missing - issue warning instead of error if missing values
--
-- RETURN:
-- table - results as given by mw.ext.cargo.query
-- msg - any error messages if it was used as warning
args.query = args.query or {}
args.fields[#args.fields+1] = args.id_field
if #args.id_array == 0 then
return {}
end
-- remove blanks
local id_array = {}
for _, value in ipairs(args.id_array) do
if value ~= '' then
id_array[#id_array+1] = value
end
end
-- for error returning
local msg = {}
local where = string.format('%s IN ("%s")', args.id_field, table.concat(id_array, '","'))
if args.query.where then
args.query.where = string.format('(%s) AND (%s)', args.query.where, where)
else
args.query.where = where
end
--
-- Prepare query
--
local results = m_cargo.query(
args.tables,
args.fields,
args.query
)
--
-- Check missing results
--
if #results ~= #id_array then
--
-- Check for duplicates
--
-- The usage of distinct should elimate duplicates here from cargo being bugged while still showing actual data duplicates.
local dupes = m_cargo.query(
args.tables,
{
string.format('COUNT(DISTINCT %s._pageID)=count', args.tables[1]),
args.id_field,
},
{
join=args.query.join,
where=args.query.where,
groupBy=args.id_field,
having=string.format('COUNT(DISTINCT %s._pageID) > 1', args.tables[1]),
}
)
if #dupes > 0 then
out = {}
for _, row in ipairs(dupes) do
out[#out+1] = string.format('%s (%s pages found)', row[args.id_field], row['count'])
end
error(string.format(i18n.errors.duplicate_ids, args.id_field, table.concat(out, '\n')))
end
local dupes = m_cargo.query(
args.tables,
{
string.format('COUNT(%s)=count', args.id_field),
string.format('%s._pageName=page', args.tables[1]),
},
{
join=args.query.join,
where=args.query.where,
groupBy=string.format('%s._pageName', args.tables[1]),
having=string.format('COUNT(%s) > 1', args.id_field),
}
)
if #dupes > 0 then
out = {}
for _, row in ipairs(dupes) do
out[#out+1] = string.format('"%s" (%s entries found)', row['page'], row['count'])
end
error(string.format(i18n.errors.duplicate_ids, args.id_field, table.concat(out, '\n')))
end
if not args.ignore_missing then
local missing = {}
for _, id in ipairs(id_array) do
missing[id] = true
end
for _, row in ipairs(results) do
missing[row[args.id_field]] = nil
end
local missing_ids = {}
for k, _ in pairs(missing) do
missing_ids[#missing_ids+1] = k
end
msg[#msg+1] = string.format(i18n.errors.missing_ids, args.id_field, table.concat(missing_ids, '\n'))
if args.warning_on_missing == nil then
error(msg[#msg])
else
mw.logObject(msg[#msg])
end
end
end
return results, msg
end
function m_cargo.replace_holds(args)
-- Replaces a holds query with a like or regexp equivalent.
--
-- required args:
-- string: string to replace
--
-- optional args:
-- mode : Either "like" or "regex"; default "regex"
-- like: Replaces the holds query with a LIKE equivalent
-- regex: Replaces the holds query with a REGEXP equivalent
-- field : Field pattern to use. Can be used to restrict the hold replacement to specific fields.
-- Default: all fields are matched.
-- separator: Separator for field entries to use in the REGEXP mode.
-- Default: ,
--
-- Returns the replaced query
local args = args or {}
-- if the field is not specified, replace any holds query
args.field = args.field or '[%w_\.]+'
if args.mode == 'like' or args.mode == nil then
return string.gsub(
args.string,
string.format('(%s) HOLDS ([NOT ]*)([LIKE ]*)"([^"]+)"', args.field),
'%1__full %2LIKE "%%%4%%"'
)
elseif args.mode == 'regex' then
args.separator = args.separator or ','
return string.gsub(
args.string,
string.format('(%s) HOLDS ([NOT ]*)"([^"]+)"', args.field),
string.format('%%1__full %%2REGEXP "(%s|^)%%3(%s|$)"', args.separator, args.separator)
)
else
error('Invalid mode specified. Acceptable values are like or regex.')
end
end
function m_cargo.parse_field(args)
-- Parse a cargo field declaration and returns a table containing the results
--
-- required args:
-- field: field to parse
--
-- Return
-- type - Type of the field
-- list - Separator of the list if it is a list type field
-- parameters - any parameters to the field itself
local field = args.field
local results = {}
local match
match = { string.match(field, 'List %(([^%(%)]+)%) of (.*)') }
if #match > 0 then
results.list = match[1]
field = match[2]
end
match = { string.match(field, '%s*(%a+)%s*%(([^%(%)]+)%)') }
if #match > 0 then
results.type = match[1]
field = match[2]
results.parameters = {}
for _, param_string in ipairs(m_util.string.split(field, ';')) do
local index = { string.find(param_string, '=') }
local key
local value
if #index > 0 then
key = string.sub(param_string, 0, index[1]-1)
value = m_util.string.strip(string.sub(param_string, index[1]+1))
else
key = param_string
value = true
end
results.parameters[m_util.string.strip(key)] = value
end
else
-- the reminder must be the type since there is no extra declarations
results.type = string.match(field, '%s*(%a+)%s*')
end
return results
end
return m_cargo