Edit the documentation or categories for this module.
local util_args = require('Module:ArgsUtil')
local util_map = require('Module:MapUtil')
local util_sort = require('Module:SortUtil')
local util_table = require('Module:TableUtil')
local util_text = require('Module:TextUtil')
local util_vars = require('Module:VarsUtil')
local cargowiki = require('Module:CargoUtil/Wiki')
local bool_false = { ['false'] = true, ['0'] = true, ['no'] = true, [''] = true }
local argPrefix = 'q?'
local lang = mw.getLanguage('en')
local bool_to_str = { [true] = 'Yes', [false] = 'No' }
local p = {}
local h = {}
-- oneToMany structure:
--[[
{
groupBy = { set of things to group by here },
fields = {
key1_plural = singleFieldSingular,
key2_plural = { table, of, fields },
}
}
]]
function p.queryAndCast(query)
if not query.union then
return h.queryAndCastOne(query)
end
local result = util_map.inPlace(query, h.queryAndCastOne)
if query.uniqueKey then
-- currently query.uniqueKey MUST be a single constant key
-- use CONCAT in the query as needed
-- remember you STILL must groupBy your first result!!!
result = util_table.uniqueKeyMergeArrays(query.uniqueKey, unpack(result))
else
result = util_table.mergeArrays(unpack(result))
end
if query.sortKey then
util_sort.tablesByKeys(result, query.sortKey, query.sortOrder)
end
return result
end
function h.queryAndCastOne(query)
local copyQuery = h.getFinalizedCopyQuery(query)
local result = mw.ext.cargo.query(
copyQuery.tables,
copyQuery.fields,
copyQuery
)
h.cast(result, copyQuery)
return h.groupOneToManyFields(result, copyQuery)
end
function h.getFinalizedCopyQuery(query)
local copyQuery = mw.clone(query)
copyQuery.tables = util_table.concatIfTable(query.tables)
copyQuery.fields, copyQuery.types = h.parseAndConcatFieldNames(query.fields, query.oneToMany)
if copyQuery.oneToMany then
h.cleanupOneToManyFields(copyQuery.oneToMany.fields)
end
copyQuery.join = util_table.concatIfTable(query.join)
copyQuery.finalLimit = query.limit
copyQuery.limit = h.getLimit(copyQuery)
util_table.merge(copyQuery.types, query.types)
copyQuery.complexTypes = query.complexTypes or {}
h.lowercaseifyTypes(copyQuery)
h.setObjectTypes(copyQuery)
-- default to the where being an AND of its params if it's a table
if type(query.where) == 'table' then
copyQuery.where = p.concatWhere(query.where)
end
return copyQuery
end
function h.parseAndConcatFieldNames(fields, oneToMany)
if not oneToMany then oneToMany = {} end
oneToMany.allFields = h.getListOfAllOneToManyFields(oneToMany.fields)
if type(fields) == 'string' then
fields = util_text.split(fields)
end
util_table.mergeArrays(fields, oneToMany.allFields)
local parsedFields = {}
local types = {}
for _, field in ipairs(fields) do
local partiallyParsedField, parsedType
if field:find('%[') then
-- partially parsed field still needs to be parsed the rest of the way
-- can include for example both a table name and an alias
partiallyParsedField, parsedType = field:match('(.-) *%[(%w+)%]$')
else
partiallyParsedField = field
end
local parsedField = h.parseOneFieldName(partiallyParsedField)
parsedFields[#parsedFields+1] = parsedField
types[h.getFieldAlias(parsedField)] = parsedType
end
local finalParsedFields = util_table.concat(parsedFields, ', ')
return finalParsedFields, types
end
function h.getListOfAllOneToManyFields(fields)
if not fields then return nil end
local allFields = {}
for k, v in pairs(fields) do
util_table.merge(allFields, util_table.guaranteeTable(v))
end
return allFields
end
function h.parseOneFieldName(str)
if not str:find('%.') then
return str
elseif str:find('=') then
return str
end
local name = str:match('%.(.+)')
return ('%s=%s'):format(str, name)
end
function h.cleanupOneToManyFields(fields)
-- we need to get rid of any types that are here, like we did above for the full fields list
for k, listOfFields in pairs(fields) do
-- TODO: this is a messy workaround and should be fixed
if type(listOfFields) ~= 'table' then return end
for i, field in ipairs(listOfFields) do
local partiallyParsedField = field:match('(.-) *%[(%w+)%]$')
if partiallyParsedField then
listOfFields[i] = partiallyParsedField
end
end
end
end
function h.getLimit(copyQuery)
if copyQuery.rawlimit then return copyQuery.rawlimit end
if copyQuery.limit then return copyQuery.limit end
return 9999
end
function h.lowercaseifyTypes(copyQuery)
for k, v in pairs(copyQuery.types) do
copyQuery.types[k] = lang:lc(v)
end
for _, v in pairs(copyQuery.complexTypes) do
v.type = lang:lc(v.type)
end
end
function h.setObjectTypes(copyQuery)
-- fields that cannot be nil because they must be an object with an is_nil value
copyQuery.objectTypes = {}
for k, v_type in pairs(copyQuery.types) do
if cargowiki.objectTypes[v_type] then
copyQuery.objectTypes[k] = v_type
end
end
end
-- post query
function h.cast(result, copyQuery)
for i, row in ipairs(result) do
row.index = i
for k, v in pairs(row) do
row[k] = h.castField(v, copyQuery.types[k])
end
for k, v in pairs(copyQuery.complexTypes) do
row[k] = cargowiki.castComplexTypes(row, v)
end
for k, v_type in pairs(copyQuery.objectTypes) do
if row[k] == nil then
row[k] = cargowiki.castField(nil, v_type)
end
end
end
end
function h.castField(v, v_type)
if v == '' then return nil end
if not v_type then return v end
if v_type == 'boolean' then
return p.strToBool(v)
elseif v_type == 'number' then
return tonumber(v)
elseif v_type == 'namespace' then
return mw.site.namespaces[tonumber(v)].name
elseif v_type == 'unicodelowercase' then
return mw.ustring.lower(v)
end
return cargowiki.castField(v, v_type)
end
function h.groupOneToManyFields(result, copyQuery)
local oneToMany = copyQuery.oneToMany
if not oneToMany then return result end
local currentKey
-- fields is a blob
local fieldsBlob = h.parseFieldSetsForKeys(oneToMany.fields)
local groupedResult = {}
for _, row in ipairs(result) do
local newKey = h.getNewKey(row, util_table.guaranteeTable(oneToMany.groupBy))
if newKey == currentKey then
h.addRowToExistingGroup(groupedResult[#groupedResult], row, fieldsBlob)
else
h.addRowToNewGroup(groupedResult, row, fieldsBlob)
currentKey = newKey
end
end
-- finalLimit is the final limit to display after resolving all one to many stuff
local finalLimit = copyQuery.finalLimit or copyQuery.limit
if #groupedResult <= finalLimit then return groupedResult end
for i = finalLimit + 1, #groupedResult do
groupedResult[i] = nil
end
return groupedResult
end
function h.parseFieldSetsForKeys(fields)
-- fields is a blob, and we return a blob
return util_map.safe(fields, h.parseFieldsForKeys)
end
function h.parseFieldsForKeys(fieldSet)
return util_map.inPlace(
util_map.inPlace(
util_table.guaranteeTable(fieldSet),
h.parseOneFieldName
),
h.getFieldAlias
)
end
function h.getNewKey(row, groupBy)
local toConcat = {}
for _, v in ipairs(groupBy) do
toConcat[#toConcat+1] = row[v]
end
return table.concat(toConcat)
end
function h.getFieldAlias(str)
if not str:find('=') then return str end
-- in case we have a CASE statement, we need to make sure we're splitting on
-- the right = here to retrieve the field name
return str:match('= *([^=]+)$')
end
function h.addRowToExistingGroup(groupedRow, row, fieldsBlob)
for k, fieldSet in pairs(fieldsBlob) do
local curRowInGroup = groupedRow[k]
util_table.push(curRowInGroup, h.extractFieldsetFromDataRow(row, fieldSet, #curRowInGroup+1))
end
end
function h.extractFieldsetFromDataRow(row, fieldSet, indexInGroup)
local ret = {
index = indexInGroup,
}
for _, field in ipairs(fieldSet) do
ret[field] = row[field]
end
return ret
end
function h.addRowToNewGroup(groupedResult, row, fieldsBlob)
for k, fieldSet in pairs(fieldsBlob) do
row[k] = { h.extractFieldsetFromDataRow(row, fieldSet, 1) }
end
groupedResult[#groupedResult+1] = row
end
function p.getOneResult(query, field)
local result = p.queryAndCast(query)
if result[1] then
return result[1][field or h.getOneFieldName(query.fields)]
end
return nil
end
function h.getOneFieldName(field)
if type(field) == 'table' then field = field[1] end
return h.getFieldAlias(h.parseOneFieldName(field))
end
function p.getOneRow(query)
local result = p.queryAndCast(query)
return result[1]
end
function p.getOneField(query, field)
local result = p.queryAndCast(query)
local tbl = {}
for i, row in ipairs(result) do
tbl[#tbl+1] = row[field]
end
return tbl
end
function p.strToBool(v)
if not v then
return false
elseif bool_false[lang:lc(v)] then
return false
end
return true
end
function p.getConstDict(query, key, value)
return p.makeConstDict(p.queryAndCast(query), key, value)
end
function p.makeConstDict(result, key, value)
local tbl = {}
for _, row in ipairs(result) do
if row[key] then
tbl[row[key]] = row[value]
end
end
return tbl
end
function p.getRowDict(query, key)
local result = p.queryAndCast(query)
local ret = {}
for _, row in ipairs(result) do
if row[key] then
ret[row[key]] = row
end
end
return ret
end
function p.getOrderedDict(query, key, value)
return h.makeOrderedDict(p.queryAndCast(query), key, value)
end
function h.makeOrderedDict(result, key, value)
local tbl = {}
for _, row in ipairs(result) do
if row[key] then
tbl[#tbl+1] = row[key]
tbl[row[key]] = row[value]
end
end
return tbl
end
function p.getOrderedList(query, key)
local result = p.queryAndCast(query)
return h.makeOrderedList(result, key or query.fields)
end
function h.makeOrderedList(result, key)
local tbl = {}
for k, row in ipairs(result) do
tbl[#tbl+1] = row[key]
end
return tbl
end
function p.groupResultOrdered(result, key, f)
local data = {}
local this
local thisvalue
local thistab
local i = 1
for _, row in ipairs(result) do
if not row[key] then row[key] = 'Uncategorized' end
if row[key] ~= thisvalue then
data[#data+1] = { name = row[key], index = i }
i = i + 1
thistab = data[#data] or {}
thisvalue = row[key]
end
thistab[#thistab+1] = f and f(row) or row
end
return data
end
function p.groupResultByValue(result, key, f)
local data = {}
local this
local thisvalue
local i = 1
for _, row in ipairs(result) do
if row[key] ~= thisvalue then
thisvalue = row[key]
data[thisvalue] = { name = row[key] }
i = i + 1
thistab = data[thisvalue]
end
thistab[#thistab+1] = f and f(row) or row
end
return data
end
function p.queryFromArgs(args, defaults)
-- sometimes we want to specify query args in the template
-- this function parses them into args that cargo will understand
-- change argPrefix above to change the prefix for query params
local query = mw.clone(defaults or {})
for k, v in pairs(args) do
if string.sub(k, 0, 2) == argPrefix then
query[string.sub(k,3)] = v
end
end
return query
end
function p.store(tbl)
if CARGO_NAMESPACE and mw.title.getCurrentTitle().nsText ~= CARGO_NAMESPACE then
return
end
if not tbl then return end
local tbl2 = { '' }
for k, v in pairs(tbl) do
if type(v) == 'boolean' then
tbl2[k] = bool_to_str[v]
elseif type(v) == 'table' then
-- Lua Class System
tbl2[k] = tostring(v)
else
tbl2[k] = v
end
end
mw.getCurrentFrame():callParserFunction{
name = '#cargo_store',
args = tbl2
}
return
end
function p.setStoreNamespace(ns)
CARGO_NAMESPACE = ns
end
function p.doWeStoreCargo(nocargo, desiredNamespace,title)
local argOkay = not util_args.castAsBool(nocargo)
if not desiredNamespace then
return argOkay
end
if not title then
title = mw.title.getCurrentTitle()
end
return argOkay and title.nsText == desiredNamespace
end
function p.whereFromArg(str, ...)
-- if an arg is defined, formats a string with the arg to be included in a where table
-- if it's not defined, returns false and NOT nil so the table can be used
-- with util_table.concat
if #{...} == 0 then
return false
else
return str:format(...)
end
end
function p.whereFromArgList(str, argTbl, sep, f)
if not sep then sep = '%s*,%s*' end
if not argTbl then return nil end
argTbl = util_table.guaranteeTable(argTbl)
if #argTbl == 0 then return end
local splitArgs = {}
for _, arg in ipairs(argTbl) do
splitArgs[#splitArgs+1] = util_map.split(arg, sep, f)
end
local argsForFormat = {}
for lineIndex, v in ipairs(splitArgs[1]) do
argsForFormat[lineIndex] = {}
for i, arg in ipairs(splitArgs) do
argsForFormat[lineIndex][i] = arg[lineIndex]
end
end
local where = {}
for _, condition in ipairs(argsForFormat) do
where[#where+1] = p.whereFromArg(str, unpack(condition))
end
return ('(%s)'):format(p.concatWhereOr(where))
end
function p.whereFromCompoundEntity(str, argTbl)
if not argTbl then return nil end
if argTbl.is_nil then return nil end
local where = {}
for _, v in ipairs(argTbl) do
where[#where+1] = str:format(v:get())
end
return ('(%s)'):format(p.concatWhereOr(where))
end
function p.concatWhere(tbl)
local arr = {}
-- pairs because maybe some entries are nil, and since it's an AND, order doesn't matter
for _, v in pairs(tbl) do
if v then
arr[#arr+1] = ('(%s)'):format(v)
end
end
if #arr == 0 then return nil end
return '(' .. util_table.concat(arr, ' AND ') .. ')'
end
function p.concatWhereOr(tbl)
local arr = {}
-- pairs because maybe some entries are nil, and since it's an OR, order doesn't matter
for _, v in pairs(tbl) do
if v then
arr[#arr+1] = ('(%s)'):format(v)
end
end
return '(' .. util_table.concat(arr, ' OR ') .. ')'
end
function p.fakeHolds(field, str, sep)
if str == nil then return false end
sep = sep or ','
str = h.escape(str)
return ('%s__full RLIKE ".*(^|%s)%s($|%s).*"'):format(field, sep, str, sep)
end
function h.escape(str)
local tbl = { '%(', '%)' }
for _, v in ipairs(tbl) do
str = str:gsub(v, '.')
end
return str
end
function p.fakeHoldsVariable(field, str, sep)
sep = sep or ','
return ('%s__full RLIKE CONCAT(".*(^|%s)",%s,"($|%s).*")'):format(field, sep, str, sep)
end
function p.makeMinMaxQuery(query, field, orderby, order)
-- modifies a pre-existing query to add an extra set of conditions to get the max/min value of some field
-- order will be either MIN or MAX, and orderby is usually going to be a date/datetime
-- example: c.makeMinMaxQuery(query, 'SP.Champion','SP.Time','MAX')
--to get the most-recent played champions
local query2 = mw.clone(query)
query2.fields = ("%s(%s)=value, %s=field"):format(order or 'MAX', orderby, field)
local result = p.queryAndCast(query2)
util_map.inPlace(result, function(row)
return row.value and ('(%s="%s" AND %s="%s")'):format(field, row.field, orderby, row.value)
end)
local newwhere = {
next(result) and ("(%s)"):format(p.concatWhereOr(result)),
query.where and ("(%s)"):format(query.where)
}
return p.concatWhere(newwhere)
end
function p.getUniqueLine(...)
local args = {...}
for k, v in ipairs(args) do
if type(v) == 'string' then
args[k] = util_vars.getGlobalIndex(v) or v
end
end
table.insert(args, 1, mw.title.getCurrentTitle().text)
return util_table.concat(args, '_')
end
function p.concatQueriesAnd(original, new)
-- combine tables, fields, and join
-- "and" the wheres together
-- overwrite everything else with new
for _, v in ipairs({ 'tables', 'fields', 'join' }) do
original[v] = util_text.splitIfString(original[v])
util_table.mergeArrays(original[v], util_text.splitIfString(new[v]))
new[v] = nil
end
new.where = h.concatQueriesWhereAnd(original.where, new.where)
util_table.merge(new.types, original.types)
util_table.merge(original, new)
return original
end
function h.concatQueriesWhereAnd(original, new)
if not original then return new end
if not new then return original end
local tbl = { original, new }
return p.concatWhere(tbl)
end
function p.wikitextQuery(query)
local copyQuery = h.getFinalizedCopyQuery(query)
local text = ([[{{#cargo_query:tables=%s
|join on=%s
|fields=%s
|where=%s
|order by=%s
|group by=%s
}}]]):format(
copyQuery.tables or '',
copyQuery.join or '',
copyQuery.fields or '',
copyQuery.where or '',
copyQuery.orderBy or '',
copyQuery.groupBy or ''
)
return mw.text.nowiki(text)
end
function p.logQuery(query)
if query.union then
util_map.arraySafe(query, p.logQuery)
return
end
util_vars.log(p.wikitextQuery(query))
end
local kv_sep = ':@:'
local arg_sep = ';@;'
function p.concatArgsForStore(args)
local argArray = {}
for k, v in pairs(args) do
argArray[#argArray+1] = ('%s%s%s'):format(k, kv_sep, v)
end
return table.concat(argArray, arg_sep)
end
function p.extractArgs(argString)
local args = util_text.split(argString, arg_sep)
local newArgs = {}
for _, arg in ipairs(args) do
local k, v = arg:match(('(.-)%s(.*)'):format(kv_sep))
if not k then
error(("Can't properly parse arg %s"):format(arg))
end
newArgs[tonumber(k) or k] = v
end
return newArgs
end
return p