Module:Cargo Utility

From Caves of Qud Wiki
Revision as of 02:34, 18 June 2025 by Myrsta (talk | contribs) (Undo revision 100159 by Teamtoto (talk) - believe this broke a bunch of queries with "Identifier must not contain quote, dot or null characters", can undo if not the case)
Jump to navigation Jump to search

Taken from LoL's CargoUtil Module.


local util_args = require('Module:Args Utility')
local util_map = require('Module:Map Utility')
local util_table = require('Module:Table Utility')
local util_text = require('Module:Text Utility')
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 = {}

function p.queryAndCast(query)
	local tables = util_table.concatIfTable(query.tables)
	local fields = h.parseAndConcatFieldNames(query.fields, query.oneToMany)
	query.join = util_table.concatIfTable(query.join)
	query.limit = query.limit or 9999
	local result = mw.ext.cargo.query(tables, fields, query)

	h.cast(result, query.types or {}, query.coalesce or {})
	return h.groupOneToManyFields(result, query.oneToMany)
end

function h.parseAndConcatFieldNames(fields, oneToMany)
	if not oneToMany then oneToMany = {} end
	if type(fields) == 'string' then
		fields = util_text.split(fields)
	end
	util_table.mergeArrays(fields, oneToMany.fields)
	return util_table.concat(fields, ',', h.parseOneFieldName)
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.cast(result, types, coalesce)
	for i, row in ipairs(result) do
		row.index = i
		for k, v in pairs(row) do
			row[k] = h.castField(v, types[k])
			for coalesceKey, coalesceValueList in pairs(coalesce) do
				row[coalesceKey] = h.coalesce(row, coalesceValueList)
			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
	end
	error('Unrecognized Cargo value casting type')
end

function h.coalesce(row, tbl)
	for _, v in ipairs(tbl) do
		if row[v] then
			return row[v]
		end
	end
	return nil
end

function h.groupOneToManyFields(result, oneToMany)
	if not oneToMany then return result end
	local currentKey
	local groupedResult = {}
	local fields = h.parseFieldsForKeys(oneToMany.fields)
	for _, row in ipairs(result) do
		local newKey = h.getNewKey(row, oneToMany.groupBy)
		if newKey == currentKey then
			h.addRowToExistingGroup(groupedResult, row, fields)
		else
			h.addRowToNewGroup(groupedResult, row, fields)
			currentKey = newKey
		end
	end
	return groupedResult
end

function h.parseFieldsForKeys(fields)
	return util_map.safe(fields, h.parseOneFieldForKey)
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.parseOneFieldForKey(str)
	if not str:find('=') then return str end
	return str:match('=(.+)')
end

function h.addRowToExistingGroup(groupedResult, row, fields)
	for _, v in ipairs(fields) do
		util_table.push(groupedResult[#groupedResult][v], row[v])
	end
end

function h.addRowToNewGroup(groupedResult, row, fields)
	for _, v in ipairs(fields) do
		row[v] = { row[v] }
	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 query.fields]
	end
	return nil
end

function p.getOneRow(query)
	local result = p.queryAndCast(query)
	return result[1] or {}
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 = mw.clone(tbl)
	tbl2[1] = ''
	for k, v in pairs(tbl2) do
		if type(v) == 'boolean' then
			tbl2[k] = bool_to_str[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 not next({...}) 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 not next(argTbl) 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.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 not next(arr) 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 AND, 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.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

return p