Halo Esports Wiki

We are currently performing extensive maintenance to this wiki and as a result there will likely be errors. Please be patient while we work to fix all problems.

READ MORE

Halo Esports Wiki
Advertisement

To edit the documentation or categories for this module, click here.


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
	-- h.queryAndCastOne will clone the table so this can be in place
	local result = util_map.arrayInPlace(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
	
	-- we normally already added an index, but it is completely untrustworthy now
	-- so just overwrite it entirely
	for i, row in ipairs(result) do
		row.index = i
	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
Advertisement