Module:UtilsCargo

local p = {} local h = {}

local frame = mw.getCurrentFrame

function p.query(tables, fields, args) return mw.ext.cargo.query(tables, fields, args) end

local function escape(str) return string.gsub(str, "'", "\\'") end

function p.allOf(...) local query for i, v in ipairs({...}) do		if type(v) == "table" then for k, v in pairs(v) do				v = escape(v) query = h.andClause(query, string.format("%s='%s'", k, v)) end else query = h.andClause(query, v)		end end return query end

function p.holdsAll(field, values) local query for i, value in ipairs(values) do		value = escape(value) if #values > 1 then -- Workaround for Cargo bug: https://phabricator.wikimedia.org/T267498 -- __full lists all the categories as a string separated by pipes. -- Specific regex is needed to account for categories which are at the start or end of the string, which will be missing a pipe. -- (^|\\|) matches the beginning -- ($|\\|) matches the end query = h.andClause(query, field..\\|)..value..\\|)') else query = h.andClause(query, string.format("%s HOLDS '%s'", field, value)) end end return query end

function p.holdsAny(field, values) local query for i, value in ipairs(values) do		value = escape(value) query = h.orClause(query, string.format("%s HOLDS '%s'", field, value)) end return query end

function p.IN(field, values) local inValues = {} for i, value in ipairs(values) do		value = escape(value) value = string.format("'%s'", value) inValues[i] = value end inValues = table.concat(inValues, ", ") if inValues == "" then inValues = "''" end return string.format("%s IN (%s)", field, inValues) end

function h.andClause(query, clause) return h.addClause("AND", query, clause) end

function h.orClause(query, clause) return h.addClause("OR", query, clause) end

function h.addClause(operator, query, clause) if not query or query == "" then return clause end return table.concat({query, operator, clause}, " ") end

function p.Schemas return { query = { tables = { type = "string", required = true, },			fields = { type = "string", required = true, },			args = { type = "record", properties = { {						name = "where", type = "string", },						{						name = "join", type = "string", },					{						name = "groupBy", type = "string", },					{						name = "having", type = "string", },					{						name = "orderBy", type = "string", },					{						name = "limit", type = "number", default = 100, },					{						name = "offset", type = "number", default = 0, },				}			},		},		allOf = { ["..."] = {				type = "array", required = true, items = { oneOf = { {							type = "string", },						{							type = "map", keys = { type = "string" }, values = { type = "string" }, },					},				}			},		},		holdsAll = { field = { type = "string", required = true, },			values = { type = "array", required = true, items = { type = "string" }, },		},		holdsAny = { field = { type = "string", required = true, },			values = { type = "array", required = true, items = { type = "string" }, }		},		IN = { field = { type = "string", required = true, },			values = { type = "array", required = true, items = { type = "string" }, },		}	} end

function p.Documentation return { sections = { {				heading = " wrapper", section = { query = { params = {"tables", "fields", "args"}, returns = "An array of the query results. Throws an error when query syntax is invalid.", cases = { {								args = { "Games", "code, shortName", {										where = "type='main'", orderBy = "releaseDate", limit = 3, },								},								expect = { { code = "TLoZ", shortName = "The Legend of Zelda"}, { code = "TAoL", shortName = "The Adventure of Link"}, { code = "ALttP", shortName = "A Link to the Past"}, },							},						}					},				}			},			{				heading = "Query builders", section = { allOf = { params = {"..."}, returns = "A WHERE clause with ANDed conditions and escaped quotation marks.", cases = { outputOnly = true, {								args = { {										game = "Link's Awakening", remakeNum = 2, },									"foo HOLDS 'bar'", "baz LIKE '%quux%'", },								expect = remakeNum='2' AND game='Link\'s Awakening' AND foo HOLDS 'bar' AND baz LIKE '%quux%' }						},					},					holdsAll = { params = {"field", "values"}, returns = "A query string of and'ed HOLDS clauses.", cases = { outputOnly = true, {								args = {"game", {"Link's Awakening"}}, expect = "game HOLDS 'Link\\'s Awakening'", },							{								desc = "As a workaround to a Cargo issue, multiple HOLDS statements are converted to an equivalent regex-based syntax.", args = {"game", {"OoT", "TP"}}, expect = "game__full REGEXP '(^|\\\\|)OoT($|\\\\|)' AND game__full REGEXP '(^|\\\\|)TP($|\\\\|)'", }						},					},					holdsAny = { params = {"field", "values"}, returns = "A query string of or'ed HOLDS clauses.", cases = { outputOnly = true, {								args = {"game", {"Link's Awakening"}}, expect = "game HOLDS 'Link\\'s Awakening'", },							{								args = {"game", {"OoT", "TP"}}, expect = "game HOLDS 'OoT' OR game HOLDS 'TP'", }						}					},					IN = { params = {"field", "values"}, returns = "A where clause using the SQL IN keyword.", cases = { outputOnly = true, {								args = {"_pageName", {"Link's Shadow", "Zelda", "Ganon"}}, expect = _pageName IN ('Link\'s Shadow', 'Zelda', 'Ganon'), },							{								args = {"_pageName", {}}, expect = _pageName IN (''), }						}					},				}			}		}	} end

return p