This wiki is still in active development, many areas of the site are unstable or broken.
In the meantime, consider adding content to pages you like!
Module:BucketGet
From NBITTRPG Wiki
Documentation for this module may be created at Module:BucketGet/doc
local p = {}
local getArgs = require('Module:Arguments').getArgs
local Bucket = mw.bucket
local NULL = {}
local TYPE_ORDER = { ["nil"] = 1, number = 2, string = 3, boolean = 4, table = 5 }
local function compareValues(a, b)
if a == b then
return 0
end
local function typeOrder(val)
local t = type(val)
return TYPE_ORDER[t] or 6
end
local a_type = typeOrder(a)
local b_type = typeOrder(b)
if a_type ~= b_type then
return a_type < b_type and -1 or 1
end
if a_type == 1 then
return 0
elseif a_type == 2 then
if a < b then return -1 else return 1 end
elseif a_type == 3 then
if a < b then return -1 else return 1 end
elseif a_type == 4 then
if (a == false and b == true) then return -1 else return 1 end
elseif a_type == 5 then
local max_len = math.max(#a, #b)
for i = 1, max_len do
local a_elem = a[i]
local b_elem = b[i]
local cmp = compareValues(a_elem, b_elem)
if cmp ~= 0 then
return cmp
end
end
if #a < #b then
return -1
elseif #a > #b then
return 1
else
return 0
end
end
return 0
end
local function sortResults(results, order_by)
table.sort(results, function(a, b)
for _, order in ipairs(order_by) do
local a_val = a[order.column]
local b_val = b[order.column]
local cmp = compareValues(a_val, b_val)
if cmp ~= 0 then
if order.direction == "ASC" then
return cmp == -1
else
return cmp == 1
end
end
end
return false
end)
end
local function formatTable(frame, results, columns, args)
local table_columns_str = args['table-columns']
if table_columns_str then
local by_alias = {}
for _, col in ipairs(columns) do
by_alias[col.alias] = col
end
local display_columns = {}
for part in table_columns_str:gmatch('[^,]+') do
local alias, new_label = part:match('^%s*(.-)%s*=%s*(.-)%s*$')
if not alias then
alias = part:match('^%s*(.-)%s*$')
end
local col = by_alias[alias]
if col then
if new_label then
col.label = new_label
end
table.insert(display_columns, col)
end
end
columns = display_columns
else
local visible_columns = {}
for _, col in ipairs(columns) do
if not col.hidden then
table.insert(visible_columns, col)
end
end
columns = visible_columns
end
local html = mw.html.create('table'):addClass('wikitable')
local header_row = html:tag('tr')
for _, col in ipairs(columns) do
header_row:tag('th'):wikitext(col.label)
end
for _, row in ipairs(results) do
local tr = html:tag('tr')
for _, col in ipairs(columns) do
local data = row[col.alias]
if data == nil then
data = '<i>null</i>'
else
data = tostring(data)
end
tr:tag('td'):wikitext(data)
end
end
return tostring(html)
end
local function getDelimiter(args)
local delimiter = args['delimiter'] or ""
return delimiter:gsub("\\n", "\n")
end
local function formatTemplate(frame, results, columns, args)
local template_name = args['template']
if not template_name then
error("Template name must be specified for format=template")
end
local delimiter = getDelimiter(args)
local output = {}
for _, row in ipairs(results) do
local parts = {}
table.insert(parts, ("{{%s"):format(template_name))
for _, col in ipairs(columns) do
local value = row[col.alias]
if value == nil then
value = ''
elseif type(value) == "table" then
value = table.concat(value, '; ')
else
value = tostring(value)
end
table.insert(parts, ("|%s=%s"):format(col.label, value))
end
table.insert(parts, "}}")
table.insert(output, table.concat(parts, ""))
end
return table.concat(output, delimiter)
end
local function parseColumns(columns_str)
local columns = {}
for col_raw in columns_str:gmatch("[^,]+") do
local column, alias
local col = col_raw:match("^%s*(.-)%s*$")
local eq_pos = col:find("=")
local as_pos, as_end = col:lower():find("%sas%s")
if eq_pos then
column = col:sub(1, eq_pos - 1):match("^%s*(.-)%s*$")
alias = col:sub(eq_pos + 1):match("^%s*(.-)%s*$")
elseif as_pos then
column = col:sub(1, as_pos - 1):match("^%s*(.-)%s*$")
alias = col:sub(as_end + 1):match("^%s*(.-)%s*$")
else
column = col
alias = col
end
table.insert(columns, { column = column, alias = alias })
end
return columns
end
-- Convert column names to their alias
local function renameColumns(results, columns)
for _, col in ipairs(columns) do
if col.alias ~= col.column then
for _, row in ipairs(results) do
row[col.alias] = row[col.column]
row[col.column] = nil
end
end
end
end
local function applySplitColumns(results, columns, args)
local split_specs = {}
for _, col in ipairs(columns) do
local val = args[col.alias .. '-split']
if val ~= nil then
local count
if val == 'auto' then
count = 'auto'
elseif tonumber(val) and tonumber(val) >= 1 then
count = tonumber(val)
else
error(string.format("Invalid %s-split value '%s': must be 'auto' or a positive integer", col.alias, val))
end
table.insert(split_specs, {alias=col.alias, count=count})
end
end
if #split_specs == 0 then
return
end
for _, spec in ipairs(split_specs) do
-- resolve 'auto' counts: max table length across all rows
if spec.count == 'auto' then
local max_n = 0
for _, row in ipairs(results) do
local v = row[spec.alias]
if type(v) == 'table' and #v > max_n then
max_n = #v
end
end
spec.count = max_n
end
-- create new columns based on count
for i = 1, spec.count do
local sub_alias = spec.alias .. i
table.insert(columns, {
alias=sub_alias,
label=args[sub_alias .. '-label'] or sub_alias
})
end
end
for _, row in ipairs(results) do
for _, spec in ipairs(split_specs) do
local v = row[spec.alias]
for i = 1, spec.count do
local val
if type(v) == 'table' then
val = v[i]
else
val = (i == 1) and v
end
row[spec.alias .. i] = val or ""
end
end
end
end
local function translateAliases(node, alias_to_column)
if node.type == 'and' or node.type == 'or' then
translateAliases(node.left, alias_to_column)
translateAliases(node.right, alias_to_column)
elseif node.type == 'not' then
translateAliases(node.operand, alias_to_column)
elseif node.type == 'comparison' or node.type == 'filter_op' then
local original = alias_to_column[node.field]
if original then
node.field = original
end
end
end
local function parseOrderBy(order_by_str)
local order_by = {}
for col_raw in order_by_str:gmatch("[^,]+") do
local col = mw.text.trim(col_raw)
local column, direction
local lower_col = col:lower()
local space_pos = lower_col:find("%s+")
if space_pos then
column = mw.text.trim(col:sub(1, space_pos - 1))
local dir = mw.text.trim(col:sub(space_pos + 1)):lower()
if dir == "desc" then
direction = "DESC"
elseif dir == "asc" then
direction = "ASC"
else
error(("Sort direction must be either ASC or DESC. %s provided."):format(dir))
end
else
column = col
direction = "ASC"
end
table.insert(order_by, { column = column, direction = direction })
end
return order_by
end
-- Note to Module developers:
-- This function is taken from dev:Module:Example. Changes to this function
-- should be synced with the source module.
local function unescape(text)
if not text:match("UNIQ") then
return text;
end
local unescaped = mw.text.unstripNoWiki(text);
local text = unescaped:gsub('<', '<')
:gsub('>', '>')
:gsub('"', '"')
-- Taken from Wikipedia's Module:Demo
:gsub('}%-', '}-')
:gsub('%-{', '-{')
:gsub("<//nowiki>", "</nowiki>");
return text;
end
local function formatUserformat(frame, results, columns, args)
local formatter = args['userformat']
if not formatter then
error("format=userformat expects an accompanying formatter")
end
formatter = unescape(formatter)
local delimiter = getDelimiter(args)
local output = {}
for _, row in ipairs(results) do
local formatted = formatter
for _, col in ipairs(columns) do
local value = row[col.alias]
if value == nil then
value = ''
elseif type(value) == "table" then
value = table.concat(value, '; ')
else
value = tostring(value)
end
local placeholder = "%%" .. col.label .. "%%"
formatted = formatted:gsub(placeholder, function() return value end)
end
table.insert(output, formatted)
end
return table.concat(output, delimiter)
end
local function formatJson(results, args)
local flags = 0
flags = flags + (args['json-preserve-keys'] and mw.text.JSON_PRESERVE_KEYS or 0)
flags = flags + (args['json-pretty'] and mw.text.JSON_PRETTY or 0)
return mw.text.jsonEncode(results, flags)
end
local function parseGroupBy(group_by_str)
local group_by = {}
for col_raw in group_by_str:gmatch("[^,]+") do
local col = mw.text.trim(col_raw)
table.insert(group_by, col)
end
return group_by
end
local function parseAggregateArg(aggregate_str, columns)
local agg_specs = {}
if not aggregate_str or aggregate_str:match('^%s*$') then
return agg_specs
end
for spec_str in aggregate_str:gmatch('[^,]+') do
spec_str = spec_str:match('^%s*(.-)%s*$')
-- Check for AS alias
local output_col, main_part
local as_pos, as_end = spec_str:lower():find('%s+as%s+')
if as_pos then
main_part = spec_str:sub(1, as_pos - 1):match('^%s*(.-)%s*$')
output_col = spec_str:sub(as_end + 1):match('^%s*(.-)%s*$')
else
main_part = spec_str
output_col = nil
end
-- Check for FUNC(args) pattern
local func_name, args_str = main_part:match('^(%w+)%s*%((.*)%)%s*$')
if func_name then
func_name = func_name:upper()
local args = {}
for arg in args_str:gmatch('%S+') do
table.insert(args, arg)
end
if not output_col then
output_col = args[1]
end
local is_new_col = true
for _, col_info in ipairs(columns) do
if col_info.alias == output_col then
is_new_col = false
break
end
end
table.insert(agg_specs, {
func = func_name,
args = args,
output_col = output_col,
is_new_col = is_new_col
})
else
-- Bare column name → TABLE
local col_name = main_part:match('^%s*(.-)%s*$')
if col_name ~= '' then
if not output_col then
output_col = col_name
end
local is_new_col = true
for _, col_info in ipairs(columns) do
if col_info.alias == output_col then
is_new_col = false
break
end
end
table.insert(agg_specs, {
func = 'TABLE',
args = { col_name },
output_col = output_col,
is_new_col = is_new_col
})
end
end
end
return agg_specs
end
local function aggCount(rows, column)
return #rows
end
local function aggTable(rows, column)
local result = {}
for _, row in ipairs(rows) do
local value = row[column]
if value ~= nil then
if type(value) == "table" then
for _, v in ipairs(value) do
table.insert(result, v)
end
else
table.insert(result, value)
end
end
end
return result
end
local function aggMin(rows, column)
local min_val = nil
for _, row in ipairs(rows) do
local v = tonumber(row[column]) or row[column]
if v ~= nil and (min_val == nil or v < min_val) then
min_val = v
end
end
return min_val
end
local function aggMax(rows, column)
local max_val = nil
for _, row in ipairs(rows) do
local v = tonumber(row[column]) or row[column]
if v ~= nil and (max_val == nil or v > max_val) then
max_val = v
end
end
return max_val
end
local function aggAny(rows, column)
return rows[1][column]
end
local function applyValueFormats(results, columns, args, frame)
local formats_to_apply = {}
local preserveRepeated = (args['format'] or "") == 'json'
local link_columns = { page_name = true, page_name_sub = true }
local table_name = args[1]
local data = mw.loadJsonData("Bucket:" .. table_name)
for column, info in pairs(data) do
if info['type'] == 'PAGE' then
link_columns[column] = true
end
end
for _, col in ipairs(columns) do
local format_key = col.alias .. "-format"
if args[format_key] or (type(results[1][col.alias]) == "table" and not preserveRepeated) then
local formatter = args[format_key] or (link_columns[col.column] and "[[%s]]" or "%s")
local delimiter = args[col.alias .. "-delimiter"] or "; "
delimiter = delimiter:gsub("\\n", "\n")
table.insert(formats_to_apply, {
column = col.alias,
format = unescape(formatter),
delimiter = delimiter
})
end
end
if #formats_to_apply == 0 then
return results
end
for _, row in ipairs(results) do
for _, fmt_info in ipairs(formats_to_apply) do
local value = row[fmt_info.column]
if value == nil then
value = ''
elseif type(value) == "table" then
local formatted_values = {}
for _, v in ipairs(value) do
local formatted = fmt_info.format:gsub("%%s", function() return v or '' end)
table.insert(formatted_values, formatted)
end
row[fmt_info.column] = table.concat(formatted_values, fmt_info.delimiter)
else
local formatted = fmt_info.format:gsub("%%s", function() return value end)
row[fmt_info.column] = formatted
end
end
end
return results
end
local function buildGroups(results, group_by_cols)
local groups = {}
local group_order = {}
for _, row in ipairs(results) do
local key_parts = {}
for _, col in ipairs(group_by_cols) do
local value = row[col]
if type(value) == 'table' then
table.insert(key_parts, table.concat(value, '\0'))
elseif value == nil then
table.insert(key_parts, '\0')
else
table.insert(key_parts, tostring(value))
end
end
local key = table.concat(key_parts, '\1')
if not groups[key] then
groups[key] = {}
table.insert(group_order, key)
end
table.insert(groups[key], row)
end
return groups, group_order
end
local function applyAggregation(group_rows, agg_specs, new_row)
local format_specs = {}
for _, spec in ipairs(agg_specs) do
if spec.func == 'FORMAT' then
table.insert(format_specs, spec)
elseif spec.func == 'COUNT' then
new_row[spec.output_col] = aggCount(group_rows, spec.args[1])
elseif spec.func == 'TABLE' then
new_row[spec.output_col] = aggTable(group_rows, spec.args[1])
elseif spec.func == 'MIN' then
new_row[spec.output_col] = aggMin(group_rows, spec.args[1])
elseif spec.func == 'MAX' then
new_row[spec.output_col] = aggMax(group_rows, spec.args[1])
elseif spec.func == 'ANY' then
new_row[spec.output_col] = aggAny(group_rows, spec.args[1])
end
end
return format_specs
end
local function applyFormatSpec(new_row, spec, fmt_template)
local array_len = nil
for _, arg_col in ipairs(spec.args) do
if type(new_row[arg_col]) == 'table' then
array_len = #new_row[arg_col]
break
end
end
if array_len then
local results = {}
for i = 1, array_len do
local result_str = fmt_template
for _, arg_col in ipairs(spec.args) do
local val = new_row[arg_col]
local sv
if type(val) == 'table' then
sv = tostring(val[i] ~= nil and val[i] or '')
elseif val == nil then
sv = ''
else
sv = tostring(val)
end
result_str = result_str:gsub('%%' .. arg_col .. '%%', function() return sv end)
end
table.insert(results, result_str)
end
new_row[spec.output_col] = results
else
local result_str = fmt_template
for _, arg_col in ipairs(spec.args) do
local val = new_row[arg_col]
result_str = result_str:gsub('%%' .. arg_col .. '%%',
function() return val == nil and '' or tostring(val) end)
end
new_row[spec.output_col] = result_str
end
end
local function groupResults(results, group_by_cols, agg_specs, columns, group_sort, args)
if #group_by_cols == 0 then
return results
end
local group_by_set = {}
for _, col in ipairs(group_by_cols) do
group_by_set[col] = true
end
local covered = {}
for _, spec in ipairs(agg_specs) do
covered[spec.output_col] = true
end
local groups, group_order = buildGroups(results, group_by_cols)
local grouped_results = {}
for _, key in ipairs(group_order) do
local group_rows = groups[key]
if group_sort then
sortResults(group_rows, group_sort)
end
local new_row = {}
for _, col in ipairs(group_by_cols) do
new_row[col] = group_rows[1][col]
end
local format_specs = applyAggregation(group_rows, agg_specs, new_row)
for _, col_info in ipairs(columns) do
local col_name = col_info.alias
if not group_by_set[col_name] and not covered[col_name] then
new_row[col_name] = aggAny(group_rows, col_name)
end
end
for _, spec in ipairs(format_specs) do
local fmt_template = args[spec.output_col .. '-agg']
if fmt_template then
applyFormatSpec(new_row, spec, unescape(fmt_template))
end
end
table.insert(grouped_results, new_row)
end
return grouped_results
end
local T = { PARENTHESIS_L = 1, PARENTHESIS_R = 2, AND = 3, OR = 4, NOT = 5, NULL = 6, COMP = 7, STR = 8, NUM = 9, BOOL = 10, ID = 11, EOF = 12 }
local KW = {
['AND'] = T.AND,
['OR'] = T.OR,
['NOT'] = T.NOT,
['true'] = T.BOOL,
['false'] = T.BOOL,
['NULL'] = T.NULL,
}
local FILTER_OPS = { CONTAINS = true, ICONTAINS = true, MATCH = true }
local function parseJoins(join_str)
local joins = {}
for join_part in join_str:gmatch("[^,]+") do
local join = {}
local parts = {}
for part in join_part:gmatch("[^=]+") do
table.insert(parts, mw.text.trim(part))
end
if #parts == 2 then
join.left = parts[1]
join.right = parts[2]
table.insert(joins, join)
end
end
return joins
end
local function tokenize(s)
local t, i = {}, 1
while i <= #s do
local c = s:sub(i, i)
if c:match('%s') then
i = i + 1
elseif c == '(' then
table.insert(t, { type = T.PARENTHESIS_L }); i = i + 1
elseif c == ')' then
table.insert(t, { type = T.PARENTHESIS_R }); i = i + 1
elseif c:match('%d') or c == '.' then
local n = s:match('^%-?%d*%.?%d+', i)
table.insert(t, { type = T.NUM, val = tonumber(n) })
i = i + #n
elseif c:match("[%w_]") then
local w = s:match('^[%w_.]+', i)
table.insert(t, { type = KW[w:upper()] or KW[w] or T.ID, val = w })
i = i + #w
elseif c:match('["\']') then
local q, str, closed = c, '', false
for j = i + 1, #s do
local ch = s:sub(j, j)
if ch == q then
table.insert(t, { type = T.STR, val = str })
i = j + 1
closed = true
break
end
str = str .. ch
end
if not closed then
error("Unterminated string starting at position " .. i)
end
elseif c:match('%d') or c == '.' then
local n = s:match('^%-?%d*%.?%d+', i)
table.insert(t, { type = T.NUM, val = tonumber(n) })
i = i + #n
elseif c:match('[=<>!]') then
local op = s:match('[=<>!]+', i)
if not op then
error("Invalid operator at position " .. i)
end
table.insert(t, { type = T.COMP, val = op })
i = i + #op
else
error("Unexpected character '" .. c .. "' at position " .. i)
end
end
return t
end
-- Break out of the dependency loop
local parsePrimary
local function parseNotExpr(toks, pos)
if toks[pos] and toks[pos].type == T.NOT then
local node, new_pos = parseNotExpr(toks, pos + 1)
return { type = 'not', operand = node }, new_pos
else
return parsePrimary(toks, pos)
end
end
local function parseAndExpr(toks, pos)
local left, new_pos = parseNotExpr(toks, pos)
while new_pos <= #toks and toks[new_pos].type == T.AND do
local right, next_pos = parseNotExpr(toks, new_pos + 1)
left = { type = 'and', left = left, right = right }
new_pos = next_pos
end
return left, new_pos
end
local function parseExpression(toks, pos)
local left, new_pos = parseAndExpr(toks, pos)
while new_pos <= #toks and toks[new_pos].type == T.OR do
local right, next_pos = parseAndExpr(toks, new_pos + 1)
left = { type = 'or', left = left, right = right }
new_pos = next_pos
end
return left, new_pos
end
parsePrimary = function(toks, pos)
if toks[pos].type == T.PARENTHESIS_L then
local node, new_pos = parseExpression(toks, pos + 1)
if toks[new_pos].type ~= T.PARENTHESIS_R then
error("Expected ')' at position " .. new_pos)
end
return node, new_pos + 1
elseif toks[pos].type == T.ID then
local op_tok = toks[pos + 1]
if op_tok and op_tok.type == T.ID and FILTER_OPS[op_tok.val:upper()] then
local v = toks[pos + 2]
if not v or (v.type ~= T.STR and v.type ~= T.NUM and v.type ~= T.BOOL and v.type ~= T.ID) then
error("Expected value after filter operator at position " .. (pos + 2))
end
local value = (v.type == T.NUM) and v.val or tostring(v.val)
return { type = 'filter_op', field = toks[pos].val, op = op_tok.val:upper(), value = value }, pos + 3
elseif op_tok and op_tok.type == T.COMP then
local v = toks[pos + 2]
if v.type ~= T.STR and v.type ~= T.NUM and v.type ~= T.BOOL and v.type ~= T.NULL then
error("Expected value after comparison operator at position " .. (pos + 2))
end
local value
if v.type == T.NULL then
value = NULL
elseif v.type == T.BOOL then
value = (v.val == 'true')
else
value = v.val
end
return { type = 'comparison', field = toks[pos].val, op = op_tok.val, value = value }, pos + 3
else
error("Expected comparison or filter operator after identifier at position " .. (pos + 1))
end
else
error("Unexpected token at position " .. pos .. ", expected identifier or '('")
end
end
local function parseCond(toks, pos)
local node, new_pos = parseExpression(toks, pos)
if new_pos <= #toks then
error("Unexpected token at position " .. new_pos)
end
return node, new_pos
end
local function toBucket(n)
if n.type == 'and' then
return Bucket.And(toBucket(n.left), toBucket(n.right))
elseif n.type == 'or' then
return Bucket.Or(toBucket(n.left), toBucket(n.right))
elseif n.type == 'not' then
return Bucket.Not(toBucket(n.operand))
elseif n.type == 'comparison' then
local val = (n.value == NULL) and Bucket.Null() or n.value
return { n.field, n.op, val }
end
end
local FILTER_FUNCS = {
CONTAINS = function(v, s) return type(v) == 'string' and mw.ustring.find(v, s, 1, true) ~= nil end,
ICONTAINS = function(v, s)
return
type(v) == 'string' and
mw.ustring.find(mw.ustring.lower(v), mw.ustring.lower(s), 1, true) ~= nil
end,
MATCH = function(v, s) return type(v) == 'string' and mw.ustring.match(v, s) ~= nil end,
}
local function applyFilter(fn, v, s)
if type(v) == 'table' then
for _, elem in ipairs(v) do
if fn(elem, s) then
return true
end
end
return false
end
return fn(v, s)
end
local COMP_FUNCS = {
['='] = function(a, b) if b == NULL then return a == nil end; return a == b end,
['!='] = function(a, b) if b == NULL then return a ~= nil end; return a ~= b end,
['<'] = function(a, b) return compareValues(a, b) == -1 end,
['>'] = function(a, b) return compareValues(a, b) == 1 end,
['<='] = function(a, b) return compareValues(a, b) <= 0 end,
['>='] = function(a, b) return compareValues(a, b) >= 0 end,
}
local function evalFilter(node, row)
if node.type == 'and' then
return evalFilter(node.left, row) and evalFilter(node.right, row)
elseif node.type == 'or' then
return evalFilter(node.left, row) or evalFilter(node.right, row)
elseif node.type == 'not' then
return not evalFilter(node.operand, row)
elseif node.type == 'filter_op' then
local fn = FILTER_FUNCS[node.op]
if not fn then error('Unknown filter op: ' .. node.op) end
return applyFilter(fn, row[node.field], node.value)
elseif node.type == 'comparison' then
local fn = COMP_FUNCS[node.op]
if not fn then error('Unknown comparison op: ' .. node.op) end
return fn(row[node.field], node.value)
end
end
local function applyFilter(results, ast)
local filtered = {}
for _, row in ipairs(results) do
if evalFilter(ast, row) then
table.insert(filtered, row)
end
end
return filtered
end
local function runQuery(columns, args)
local bucket_name = mw.text.trim(args[1] or args['bucket'] or '')
if bucket_name == '' then
error('Must specify the name of the bucket')
end
for _, col in ipairs(columns) do
col.label = args[col.alias .. '-label'] or col.alias
end
local all_originals = {}
for _, col in ipairs(columns) do
all_originals[col.column] = true
end
local alias_to_column = {}
for _, col in ipairs(columns) do
if col.alias ~= col.column and not all_originals[col.alias] then
alias_to_column[col.alias] = col.column
end
end
local column_names = {}
for _, col in ipairs(columns) do
table.insert(column_names, col.column)
end
local query = mw.bucket(bucket_name).select(unpack(column_names))
local join_str = args['join on'] or args['join']
if join_str then
local joins = parseJoins(join_str)
for _, join in ipairs(joins) do
join.left = alias_to_column[join.left] or join.left
join.right = alias_to_column[join.right] or join.right
local right_table = join.right:match("^[^.]+") or join.right
query = query.join(right_table, join.left, join.right)
end
end
local where_str = args['where']
if where_str then
local toks = tokenize(where_str)
local ast = parseCond(toks, 1)
translateAliases(ast, alias_to_column)
query = query.where(toBucket(ast))
end
local limit = tonumber(args['limit'] or '5000')
if limit ~= nil then
query = query.limit(limit)
end
local offset = tonumber(args['offset'])
if offset ~= nil then
query = query.offset(offset)
end
return query:run()
end
local function applyOutputLimit(results, args)
local offset = tonumber(args['output-offset'])
local limit = tonumber(args['output-limit'])
if args['output-offset'] and offset == nil then
error("output-offset must be a number")
end
if args['output-limit'] and limit == nil then
error("output-limit must be a number")
end
offset = offset or 0
local start_idx = offset + 1
local end_idx = limit ~= nil and (offset + limit) or #results
local sliced = {}
for i = start_idx, math.min(end_idx, #results) do
table.insert(sliced, results[i])
end
return sliced
end
local function assignRowNumbers(results)
for i, row in ipairs(results) do
row._index = i
end
end
function p.get(frame)
local args = getArgs(frame)
local debug_mode = args['debug'] or false;
local function debugLog(title, value)
if debug_mode then
mw.log("=========" .. title .. "=========")
mw.logObject(value)
end
end
local columns_str = args['select'] or args['fields']
if not columns_str then
error("Must specify which columns to select.")
end
local columns = parseColumns(columns_str)
local results = runQuery(columns, args)
debugLog("Result from raw Bucket query", results)
assignRowNumbers(results)
table.insert(columns, { column = '_index', alias = '_index', label = args['_index-label'] or '_index', hidden = true })
renameColumns(results, columns)
debugLog("Columns renamed", results)
local filter_str = args['filter']
if filter_str then
local toks = tokenize(filter_str)
local ast = parseCond(toks, 1)
results = applyFilter(results, ast)
debugLog("Filters applied", results)
end
if #results == 0 then
return args['default'] or "No result"
end
local group_by_str = args['group by']
if group_by_str then
local group_by_cols = parseGroupBy(group_by_str)
local agg_specs = parseAggregateArg(args['aggregate'] or '', columns)
for _, spec in ipairs(agg_specs) do
if spec.is_new_col then
table.insert(
columns,
{
column = spec.output_col,
alias = spec.output_col,
label = args[spec.output_col .. '-label'] or spec.output_col
}
)
end
end
local group_sort = parseOrderBy(args['group-sort'] or '')
results = groupResults(results, group_by_cols, agg_specs, columns, group_sort, args)
debugLog("Group by applied", results)
end
applySplitColumns(results, columns, args)
local order_by_str = args['order by']
if order_by_str then
local order_by_rules = parseOrderBy(order_by_str)
sortResults(results, order_by_rules)
debugLog("Order by applied", results)
end
if args['output-limit'] or args['output-offset'] then
results = applyOutputLimit(results, args)
debugLog("Output limit/offset applied", results)
end
-- FIXME: this inserts markup that may need to be preprocessed, but for performance reasons
-- we don't want to preprocess them row-by-row.
applyValueFormats(results, columns, args, frame)
debugLog("Value-specific format applied: last step before final formatting", results)
local format = args['format'] or "table"
local intro = unescape(args['intro'] or "")
local outro = unescape(args['outro'] or "")
local text = ""
local preprocess = false
if format == "table" then
text = formatTable(frame, results, columns, args)
preprocess = args['table-preprocess']
elseif format == "template" then
text = formatTemplate(frame, results, columns, args)
preprocess = true
elseif format == "userformat" then
text = formatUserformat(frame, results, columns, args)
preprocess = true
elseif format == "json" then
text = formatJson(results, args)
else
error(string.format("Invalid format %s specified.", format))
end
text = intro .. text .. outro
if preprocess then
return frame:preprocess(text)
end
return text
end
-- Exports for testing
p.sortResults = sortResults
p.aggCount = aggCount
p.aggTable = aggTable
p.groupResults = groupResults
p.parseAggregateArg = parseAggregateArg
p.T = T
p.NULL = NULL
p.tokenize = tokenize
p.parseCond = parseCond
p.applyFilter = applyFilter
p.renameColumns = renameColumns
p.applySplitColumns = applySplitColumns
p.translateAliases = translateAliases
p.assignRowNumbers = assignRowNumbers
p.applyOutputLimit = applyOutputLimit
return p
