Module:CargoUtil

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) 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 = :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) 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