Editing Module:BucketGet

Warning: You are not logged in. Your IP address will be publicly visible if you make any edits. If you log in or create an account, your edits will be attributed to your username, along with other benefits.
The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
Latest revision Your text
Line 4: Line 4:
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs
local Bucket = mw.bucket
local Bucket = mw.bucket
local NULL = {}


local TYPE_ORDER = {
local TYPE_ORDER = { ["nil"] = 1, number = 2, string = 3, boolean = 4, table = 5 }
["nil"] = 1,
number = 2,
string = 3,
boolean = 4,
table = 5
}
 
--------------------------------------------------------------------------------
-- Comparison / sorting
--------------------------------------------------------------------------------


local function compareValues(a, b)
local function compareValues(a, b)
Line 37: Line 26:
if a_type == 1 then
if a_type == 1 then
return 0
return 0
elseif a_type == 2 then
elseif a_type == 2 then
if a < b then
if a < b then return -1 else return 1 end
return -1
else
return 1
end
 
elseif a_type == 3 then
elseif a_type == 3 then
if a < b then
if a < b then return -1 else return 1 end
return -1
else
return 1
end
 
elseif a_type == 4 then
elseif a_type == 4 then
if a == false and b == true then
if (a == false and b == true) then return -1 else return 1 end
return -1
else
return 1
end
 
elseif a_type == 5 then
elseif a_type == 5 then
local max_len = math.max(#a, #b)
local max_len = math.max(#a, #b)
for i = 1, max_len do
for i = 1, max_len do
local a_elem = a[i]
local a_elem = a[i]
local b_elem = b[i]
local b_elem = b[i]
local cmp = compareValues(a_elem, b_elem)
local cmp = compareValues(a_elem, b_elem)
if cmp ~= 0 then
if cmp ~= 0 then
return cmp
return cmp
end
end
end
end
if #a < #b then
if #a < #b then
return -1
return -1
Line 91: Line 61:


local cmp = compareValues(a_val, b_val)
local cmp = compareValues(a_val, b_val)
if cmp ~= 0 then
if cmp ~= 0 then
if order.direction == "ASC" then
if order.direction == "ASC" then
Line 100: Line 69:
end
end
end
end
return false
return false
end)
end)
end
end
--------------------------------------------------------------------------------
-- Table formatting
--------------------------------------------------------------------------------


local function formatTable(frame, results, columns, args)
local function formatTable(frame, results, columns, args)
local table_columns_str = args['table-columns']
local table_columns_str = args['table-columns']
if table_columns_str then
if table_columns_str then
local by_alias = {}
local by_alias = {}
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
by_alias[col.alias] = col
by_alias[col.alias] = col
end
end
local display_columns = {}
local display_columns = {}
for part in table_columns_str:gmatch('[^,]+') do
for part in table_columns_str:gmatch('[^,]+') do
local alias, new_label =
local alias, new_label = part:match('^%s*(.-)%s*=%s*(.-)%s*$')
part:match('^%s*(.-)%s*=%s*(.-)%s*$')
 
if not alias then
if not alias then
alias = part:match('^%s*(.-)%s*$')
alias = part:match('^%s*(.-)%s*$')
end
end
local col = by_alias[alias]
local col = by_alias[alias]
if col then
if col then
if new_label then
if new_label then
col.label = new_label
col.label = new_label
end
end
table.insert(display_columns, col)
table.insert(display_columns, col)
end
end
end
end
columns = display_columns
columns = display_columns
end
end


local html = mw.html.create('table'):addClass('wikitable')
local html = mw.html.create('table'):addClass('wikitable')
local header_row = html:tag('tr')
local header_row = html:tag('tr')
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
header_row:tag('th'):wikitext(col.label)
header_row:tag('th'):wikitext(col.label)
Line 153: Line 105:
for _, row in ipairs(results) do
for _, row in ipairs(results) do
local tr = html:tag('tr')
local tr = html:tag('tr')
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
local data = row[col.alias]
local data = row[col.alias]
if data == nil then
if data == nil then
data = '<i>null</i>'
data = '<i>null</i>'
Line 162: Line 112:
data = tostring(data)
data = tostring(data)
end
end
tr:tag('td'):wikitext(data)
tr:tag('td'):wikitext(data)
end
end
end
end


-- In case an earlier step introduced a template that needs to be preprocessed.
if args['table-preprocess'] then
if args['table-preprocess'] then
return frame:preprocess(tostring(html))
return frame:preprocess(tostring(html));
end
end


return tostring(html)
return tostring(html)
end
end
--------------------------------------------------------------------------------
-- Delimiters
--------------------------------------------------------------------------------


local function getDelimiter(args)
local function getDelimiter(args)
Line 182: Line 128:
return delimiter:gsub("\\n", "\n")
return delimiter:gsub("\\n", "\n")
end
end
--------------------------------------------------------------------------------
-- Template formatting
--------------------------------------------------------------------------------


local function formatTemplate(frame, results, columns, args)
local function formatTemplate(frame, results, columns, args)
local template_name = args['template']
local template_name = args['template']
if not template_name then
if not template_name then
error("Template name must be specified for format=template")
error("Template name must be specified for format=template")
end
end
local delimiter = getDelimiter(args)
local delimiter = getDelimiter(args)
local output = {}
local output = {}
for _, row in ipairs(results) do
for _, row in ipairs(results) do
local parts = {}
local parts = {}
table.insert(parts, ("{{%s"):format(template_name))
table.insert(parts, ("{{%s"):format(template_name))


for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
local value = row[col.alias]
local value = row[col.alias]
if value == nil then
if value == nil then
value = ''
value = ''
elseif type(value) == "table" then
elseif type(value) == "table" then
value = table.concat(value, '; ')
value = table.concat(value, '; ')
else
else
value = tostring(value)
value = tostring(value)
end
end
 
table.insert(parts, ("|%s=%s"):format(col.label, value))
table.insert(
parts,
("|%s=%s"):format(col.label, value)
)
end
end


Line 225: Line 156:
end
end


return frame:preprocess(
return frame:preprocess(table.concat(output, delimiter))
table.concat(output, delimiter)
)
end
end
--------------------------------------------------------------------------------
-- Column parsing
--------------------------------------------------------------------------------


local function parseColumns(columns_str)
local function parseColumns(columns_str)
local columns = {}
local columns = {}
for col_raw in columns_str:gmatch("[^,]+") do
for col_raw in columns_str:gmatch("[^,]+") do
local column
local column, alias
local alias
 
local col = col_raw:match("^%s*(.-)%s*$")
local col = col_raw:match("^%s*(.-)%s*$")


local eq_pos = col:find("=")
local eq_pos = col:find("=")
local as_pos, as_end =
local as_pos, as_end = col:lower():find("%sas%s")
col:lower():find("%sas%s")


if eq_pos then
if eq_pos then
column =
column = col:sub(1, eq_pos - 1):match("^%s*(.-)%s*$")
col:sub(1, eq_pos - 1):match("^%s*(.-)%s*$")
alias = col:sub(eq_pos + 1):match("^%s*(.-)%s*$")
 
alias =
col:sub(eq_pos + 1):match("^%s*(.-)%s*$")
 
elseif as_pos then
elseif as_pos then
column =
column = col:sub(1, as_pos - 1):match("^%s*(.-)%s*$")
col:sub(1, as_pos - 1):match("^%s*(.-)%s*$")
alias = col:sub(as_end + 1):match("^%s*(.-)%s*$")
 
alias =
col:sub(as_end + 1):match("^%s*(.-)%s*$")
 
else
else
column = col
column = col
Line 266: Line 179:
end
end


table.insert(columns, {
table.insert(columns, { column = column, alias = alias })
column = column,
alias = alias
})
end
end
return columns
return columns
end
end


--------------------------------------------------------------------------------
-- Convert column names to their alias
-- Column aliases
--------------------------------------------------------------------------------
 
local function renameColumns(results, columns)
local function renameColumns(results, columns)
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
Line 289: Line 195:
end
end
end
end
--------------------------------------------------------------------------------
-- Split columns
--------------------------------------------------------------------------------


local function applySplitColumns(results, columns, args)
local function applySplitColumns(results, columns, args)
local split_specs = {}
local split_specs = {}
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
local val = args[col.alias .. '-split']
local val = args[col.alias .. '-split']
if val ~= nil then
if val ~= nil then
local count
local count
if val == 'auto' then
if val == 'auto' then
count = 'auto'
count = 'auto'
elseif tonumber(val) and tonumber(val) >= 1 then
elseif tonumber(val) and tonumber(val) >= 1 then
count = tonumber(val)
count = tonumber(val)
else
else
error(string.format(
error(string.format("Invalid %s-split value '%s': must be 'auto' or a positive integer", col.alias, val))
"Invalid %s-split value '%s': must be 'auto' or a positive integer",
col.alias,
val
))
end
end
 
table.insert(split_specs, {alias=col.alias, count=count})
table.insert(split_specs, {
alias = col.alias,
count = count
})
end
end
end
end
Line 329: Line 218:


for _, spec in ipairs(split_specs) do
for _, spec in ipairs(split_specs) do
 
-- resolve 'auto' counts: max table length across all rows
-- Resolve "auto" counts.
if spec.count == 'auto' then
if spec.count == 'auto' then
local max_n = 0
local max_n = 0
for _, row in ipairs(results) do
for _, row in ipairs(results) do
local v = row[spec.alias]
local v = row[spec.alias]
if type(v) == 'table' and #v > max_n then
if type(v) == 'table' and #v > max_n then
max_n = #v
max_n = #v
end
end
end
end
spec.count = max_n
spec.count = max_n
end
end
 
-- create new columns based on count
-- Create the split columns.
for i = 1, spec.count do
for i = 1, spec.count do
local sub_alias = spec.alias .. i
local sub_alias = spec.alias .. i
table.insert(columns, {
table.insert(columns, {
alias = sub_alias,
alias=sub_alias,
label = args[sub_alias .. '-label'] or sub_alias
label=args[sub_alias .. '-label'] or sub_alias
})
})
end
end
Line 359: Line 242:
for _, spec in ipairs(split_specs) do
for _, spec in ipairs(split_specs) do
local v = row[spec.alias]
local v = row[spec.alias]
for i = 1, spec.count do
for i = 1, spec.count do
local val
local val
if type(v) == 'table' then
if type(v) == 'table' then
val = v[i]
val = v[i]
Line 368: Line 249:
val = (i == 1) and v
val = (i == 1) and v
end
end
 
row[spec.alias .. i] = val or ""
-- Make missing split values genuinely empty.
if val == nil or val == false then
val = ""
elseif type(val) == "string" then
val = mw.text.trim(val)
end
 
row[spec.alias .. i] = val
end
end
end
end
end
end
end
end
--------------------------------------------------------------------------------
-- Alias translation
--------------------------------------------------------------------------------


local function translateAliases(node, alias_to_column)
local function translateAliases(node, alias_to_column)
Line 390: Line 259:
translateAliases(node.left, alias_to_column)
translateAliases(node.left, alias_to_column)
translateAliases(node.right, alias_to_column)
translateAliases(node.right, alias_to_column)
elseif node.type == 'not' then
elseif node.type == 'not' then
translateAliases(node.operand, alias_to_column)
translateAliases(node.operand, alias_to_column)
 
elseif node.type == 'comparison' or node.type == 'filter_op' then
elseif node.type == 'comparison'
or node.type == 'filter_op' then
 
local original = alias_to_column[node.field]
local original = alias_to_column[node.field]
if original then
if original then
node.field = original
node.field = original
Line 404: Line 268:
end
end
end
end
--------------------------------------------------------------------------------
-- Order parsing
--------------------------------------------------------------------------------


local function parseOrderBy(order_by_str)
local function parseOrderBy(order_by_str)
local order_by = {}
local order_by = {}
for col_raw in order_by_str:gmatch("[^,]+") do
for col_raw in order_by_str:gmatch("[^,]+") do
local col = mw.text.trim(col_raw)
local col = mw.text.trim(col_raw)
 
local column, direction
local column
local direction
 
local lower_col = col:lower()
local lower_col = col:lower()
local space_pos = lower_col:find("%s+")
local space_pos = lower_col:find("%s+")


if space_pos then
if space_pos then
column =
column = mw.text.trim(col:sub(1, space_pos - 1))
mw.text.trim(
local dir = mw.text.trim(col:sub(space_pos + 1)):lower()
col:sub(1, space_pos - 1)
)
 
local dir =
mw.text.trim(
col:sub(space_pos + 1)
):lower()
 
if dir == "desc" then
if dir == "desc" then
direction = "DESC"
direction = "DESC"
elseif dir == "asc" then
elseif dir == "asc" then
direction = "ASC"
direction = "ASC"
else
else
error(
error(("Sort direction must be either ASC or DESC. %s provided."):format(dir))
("Sort direction must be either ASC or DESC. %s provided.")
:format(dir)
)
end
end
else
else
column = col
column = col
Line 450: Line 292:
end
end


table.insert(order_by, {
table.insert(order_by, { column = column, direction = direction })
column = column,
direction = direction
})
end
end
return order_by
return order_by
end
end
--------------------------------------------------------------------------------
-- Unescape
--------------------------------------------------------------------------------


local function unescape(text)
local function unescape(text)
Line 467: Line 301:
return text
return text
end
end
local unescaped = mw.text.unstripNoWiki(text)
local unescaped = mw.text.unstripNoWiki(text)
 
local text, count = string.gsub(unescaped, "&lt;", "<")
local text, count =
text, count = string.gsub(text, "&gt;", ">")
string.gsub(unescaped, "&lt;", "<")
text, count = string.gsub(text, "<//nowiki>", "</nowiki>")
 
text, count =
string.gsub(text, "&gt;", ">")
 
text, count =
string.gsub(text, "<//nowiki>", "</nowiki>")
 
return text
return text
end
end
--------------------------------------------------------------------------------
-- Userformat value normalization
--------------------------------------------------------------------------------
local function normalizeUserformatValue(value)
-- Missing values.
if value == nil or value == false then
return ''
end
-- Arrays.
if type(value) == "table" then
local values = {}
for _, v in ipairs(value) do
if v ~= nil and v ~= false then
local s = mw.text.trim(tostring(v))
-- Ignore empty/null array entries.
local lower = mw.ustring.lower(s)
if s ~= ''
and lower ~= 'null'
and lower ~= 'nil'
and lower ~= 'false'
and lower ~= '<i>null</i>' then
table.insert(values, s)
end
end
end
return table.concat(values, '; ')
end
-- Normal scalar value.
value = mw.text.trim(tostring(value))
-- Treat all common empty/null representations as empty.
local lower = mw.ustring.lower(value)
if lower == ''
or lower == 'null'
or lower == 'nil'
or lower == 'false'
or lower == '<i>null</i>' then
return ''
end
-- Remove non-breaking spaces.
value = value:gsub('\194\160', ' ')
return mw.text.trim(value)
end
--------------------------------------------------------------------------------
-- Userformat
--------------------------------------------------------------------------------


local function formatUserformat(frame, results, columns, args)
local function formatUserformat(frame, results, columns, args)
local formatter = args['userformat']
local formatter = args['userformat']
if not formatter then
if not formatter then
error("format=userformat expects an accompanying formatter")
error("format=userformat expects an accompanying formatter")
end
end
formatter = unescape(formatter)
formatter = unescape(formatter)
local delimiter = getDelimiter(args)


local delimiter = getDelimiter(args)
local output = {}
local output = {}
for _, row in ipairs(results) do
for _, row in ipairs(results) do
local formatted = formatter
local formatted = formatter
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
local value =
local value = row[col.alias]
normalizeUserformatValue(
if value == nil then
row[col.alias]
value = ''
)
elseif type(value) == "table" then
 
value = table.concat(value, '; ')
local placeholder =
else
"%%" .. col.label .. "%%"
value = tostring(value)
 
end
formatted =
local placeholder = "%%" .. col.label .. "%%"
formatted:gsub(
formatted = formatted:gsub(placeholder, function() return value end)
placeholder,
function()
return value
end
)
end
end
table.insert(output, formatted)
table.insert(output, formatted)
end
end


return frame:preprocess(
return frame:preprocess(table.concat(output, delimiter))
table.concat(output, delimiter)
)
end
end
--------------------------------------------------------------------------------
-- Grouping
--------------------------------------------------------------------------------


local function parseGroupBy(group_by_str)
local function parseGroupBy(group_by_str)
local group_by = {}
local group_by = {}
for col_raw in group_by_str:gmatch("[^,]+") do
for col_raw in group_by_str:gmatch("[^,]+") do
local col = mw.text.trim(col_raw)
local col = mw.text.trim(col_raw)
table.insert(group_by, col)
table.insert(group_by, col)
end
end
return group_by
return group_by
end
end
--------------------------------------------------------------------------------
-- Aggregation parsing
--------------------------------------------------------------------------------


local function parseAggregateArg(aggregate_str, columns)
local function parseAggregateArg(aggregate_str, columns)
local agg_specs = {}
local agg_specs = {}
 
if not aggregate_str or aggregate_str:match('^%s*$') then
if not aggregate_str
or aggregate_str:match('^%s*$') then
 
return agg_specs
return agg_specs
end
end


for spec_str in aggregate_str:gmatch('[^,]+') do
for spec_str in aggregate_str:gmatch('[^,]+') do
spec_str =
spec_str = spec_str:match('^%s*(.-)%s*$')
spec_str:match('^%s*(.-)%s*$')
 
local output_col
local main_part
 
local as_pos, as_end =
spec_str:lower():find('%s+as%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
if as_pos then
main_part =
main_part = spec_str:sub(1, as_pos - 1):match('^%s*(.-)%s*$')
spec_str:sub(1, as_pos - 1)
output_col = spec_str:sub(as_end + 1):match('^%s*(.-)%s*$')
:match('^%s*(.-)%s*$')
 
output_col =
spec_str:sub(as_end + 1)
:match('^%s*(.-)%s*$')
 
else
else
main_part = spec_str
main_part = spec_str
Line 635: Line 366:
end
end


local func_name, args_str =
-- Check for FUNC(args) pattern
main_part:match(
local func_name, args_str = main_part:match('^(%w+)%s*%((.*)%)%s*$')
'^(%w+)%s*%((.*)%)%s*$'
)
 
if func_name then
if func_name then
func_name = func_name:upper()
func_name = func_name:upper()
local args = {}
local args = {}
for arg in args_str:gmatch('%S+') do
for arg in args_str:gmatch('%S+') do
table.insert(args, arg)
table.insert(args, arg)
Line 654: Line 380:


local is_new_col = true
local is_new_col = true
for _, col_info in ipairs(columns) do
for _, col_info in ipairs(columns) do
if col_info.alias == output_col then
if col_info.alias == output_col then
Line 668: Line 393:
is_new_col = is_new_col
is_new_col = is_new_col
})
})
else
else
-- Bare column name → TABLE
-- Bare column name → TABLE
local col_name =
local col_name = main_part:match('^%s*(.-)%s*$')
main_part:match('^%s*(.-)%s*$')
 
if col_name ~= '' then
if col_name ~= '' then
if not output_col then
if not output_col then
output_col = col_name
output_col = col_name
end
end
local is_new_col = true
local is_new_col = true
for _, col_info in ipairs(columns) do
for _, col_info in ipairs(columns) do
if col_info.alias == output_col then
if col_info.alias == output_col then
Line 688: Line 407:
end
end
end
end
table.insert(agg_specs, {
table.insert(agg_specs, {
func = 'TABLE',
func = 'TABLE',
Line 698: Line 416:
end
end
end
end
return agg_specs
return agg_specs
end
end
--------------------------------------------------------------------------------
-- Aggregation functions
--------------------------------------------------------------------------------


local function aggCount(rows, column)
local function aggCount(rows, column)
Line 712: Line 425:
local function aggTable(rows, column)
local function aggTable(rows, column)
local result = {}
local result = {}
for _, row in ipairs(rows) do
for _, row in ipairs(rows) do
local value = row[column]
local value = row[column]
if value ~= nil then
if value ~= nil then
if type(value) == "table" then
if type(value) == "table" then
Line 726: Line 437:
end
end
end
end
return result
return result
end
end
Line 732: Line 442:
local function aggMin(rows, column)
local function aggMin(rows, column)
local min_val = nil
local min_val = nil
for _, row in ipairs(rows) do
for _, row in ipairs(rows) do
local v = tonumber(row[column]) or row[column]
local v = tonumber(row[column]) or row[column]
 
if min_val == nil or v < min_val then min_val = v end
if min_val == nil or v < min_val then
min_val = v
end
end
end
return min_val
return min_val
end
end
Line 746: Line 451:
local function aggMax(rows, column)
local function aggMax(rows, column)
local max_val = nil
local max_val = nil
for _, row in ipairs(rows) do
for _, row in ipairs(rows) do
local v = tonumber(row[column]) or row[column]
local v = tonumber(row[column]) or row[column]
 
if max_val == nil or v > max_val then max_val = v end
if max_val == nil or v > max_val then
max_val = v
end
end
end
return max_val
return max_val
end
end
Line 761: Line 461:
return rows[1][column]
return rows[1][column]
end
end
--------------------------------------------------------------------------------
-- Value formats
--------------------------------------------------------------------------------


local function applyValueFormats(results, columns, args, frame)
local function applyValueFormats(results, columns, args, frame)
local formats_to_apply = {}
local formats_to_apply = {}


local link_columns = {
local link_columns = { page_name = true, page_name_sub = true }
page_name = true,
page_name_sub = true
}


local table_name = args[1]
local table_name = args[1]
local data =
local data = mw.loadJsonData("Bucket:" .. table_name)
mw.loadJsonData("Bucket:" .. table_name)
 
for column, info in pairs(data) do
for column, info in pairs(data) do
if info['type'] == 'PAGE' then
if info['type'] == 'PAGE' then
Line 785: Line 476:


for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
local format_key =
local format_key = col.alias .. "-format"
col.alias .. "-format"
if args[format_key] or type(results[1][col.alias]) == "table" then
 
local formatter = args[format_key] or (link_columns[col.column] and "[[%s]]" or "%s")
if args[format_key]
local delimiter = args[col.alias .. "-delimiter"] or "; "
or type(results[1][col.alias]) == "table" then
delimiter = delimiter:gsub("\\n", "\n")
 
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, {
table.insert(formats_to_apply, {
Line 821: Line 497:
for _, fmt_info in ipairs(formats_to_apply) do
for _, fmt_info in ipairs(formats_to_apply) do
local value = row[fmt_info.column]
local value = row[fmt_info.column]
 
if value == nil then
-- Empty values stay empty.
value = ''
if value == nil
or value == false
or (
type(value) == "string"
and mw.text.trim(value) == ""
)
or (
type(value) == "string"
and mw.ustring.lower(
mw.text.trim(value)
) == "<i>null</i>"
) then
 
row[fmt_info.column] = ''
 
elseif type(value) == "table" then
elseif type(value) == "table" then
local formatted_values = {}
local formatted_values = {}
for _, v in ipairs(value) do
for _, v in ipairs(value) do
local clean = v
local formatted = fmt_info.format:gsub("%%s", function() return v or '' end)
 
table.insert(formatted_values, formatted)
if clean == nil or clean == false then
clean = ""
elseif type(clean) == "string" then
clean = mw.text.trim(clean)
end
 
local lower =
mw.ustring.lower(
tostring(clean)
)
 
if clean ~= ""
and lower ~= "null"
and lower ~= "nil"
and lower ~= "false"
and lower ~= "<i>null</i>" then
 
local formatted =
fmt_info.format:gsub(
"%%s",
function()
return clean
end
)
 
table.insert(
formatted_values,
formatted
)
end
end
end
 
row[fmt_info.column] = table.concat(formatted_values, fmt_info.delimiter)
row[fmt_info.column] =
table.concat(
formatted_values,
fmt_info.delimiter
)
 
else
else
local formatted =
local formatted = fmt_info.format:gsub("%%s", function() return value end)
fmt_info.format:gsub(
"%%s",
function()
return value
end
)
 
row[fmt_info.column] = formatted
row[fmt_info.column] = formatted
end
end
Line 898: Line 515:
return results
return results
end
end
--------------------------------------------------------------------------------
-- Group building
--------------------------------------------------------------------------------


local function buildGroups(results, group_by_cols)
local function buildGroups(results, group_by_cols)
local groups = {}
local groups = {}
local group_order = {}
local group_order = {}
for _, row in ipairs(results) do
for _, row in ipairs(results) do
local key_parts = {}
local key_parts = {}
for _, col in ipairs(group_by_cols) do
for _, col in ipairs(group_by_cols) do
local value = row[col]
local value = row[col]
if type(value) == 'table' then
if type(value) == 'table' then
table.insert(
table.insert(key_parts, table.concat(value, '\0'))
key_parts,
table.concat(value, '\0')
)
 
elseif value == nil then
elseif value == nil then
table.insert(key_parts, '\0')
table.insert(key_parts, '\0')
else
else
table.insert(
table.insert(key_parts, tostring(value))
key_parts,
tostring(value)
)
end
end
end
end
 
local key = table.concat(key_parts, '\1')
local key =
table.concat(key_parts, '\1')
 
if not groups[key] then
if not groups[key] then
groups[key] = {}
groups[key] = {}
table.insert(group_order, key)
table.insert(group_order, key)
end
end
table.insert(groups[key], row)
table.insert(groups[key], row)
end
end
return groups, group_order
return groups, group_order
end
end
--------------------------------------------------------------------------------
-- Aggregation
--------------------------------------------------------------------------------


local function applyAggregation(group_rows, agg_specs, new_row)
local function applyAggregation(group_rows, agg_specs, new_row)
local format_specs = {}
local format_specs = {}
for _, spec in ipairs(agg_specs) do
for _, spec in ipairs(agg_specs) do
if spec.func == 'FORMAT' then
if spec.func == 'FORMAT' then
table.insert(format_specs, spec)
table.insert(format_specs, spec)
elseif spec.func == 'COUNT' then
elseif spec.func == 'COUNT' then
new_row[spec.output_col] =
new_row[spec.output_col] = aggCount(group_rows, spec.args[1])
aggCount(
group_rows,
spec.args[1]
)
 
elseif spec.func == 'TABLE' then
elseif spec.func == 'TABLE' then
new_row[spec.output_col] =
new_row[spec.output_col] = aggTable(group_rows, spec.args[1])
aggTable(
group_rows,
spec.args[1]
)
 
elseif spec.func == 'MIN' then
elseif spec.func == 'MIN' then
new_row[spec.output_col] =
new_row[spec.output_col] = aggMin(group_rows, spec.args[1])
aggMin(
group_rows,
spec.args[1]
)
 
elseif spec.func == 'MAX' then
elseif spec.func == 'MAX' then
new_row[spec.output_col] =
new_row[spec.output_col] = aggMax(group_rows, spec.args[1])
aggMax(
group_rows,
spec.args[1]
)
 
elseif spec.func == 'ANY' then
elseif spec.func == 'ANY' then
new_row[spec.output_col] =
new_row[spec.output_col] = aggAny(group_rows, spec.args[1])
aggAny(
group_rows,
spec.args[1]
)
end
end
end
end
return format_specs
return format_specs
end
end
Line 998: Line 563:
local function applyFormatSpec(new_row, spec, fmt_template)
local function applyFormatSpec(new_row, spec, fmt_template)
local array_len = nil
local array_len = nil
for _, arg_col in ipairs(spec.args) do
for _, arg_col in ipairs(spec.args) do
if type(new_row[arg_col]) == 'table' then
if type(new_row[arg_col]) == 'table' then
Line 1,008: Line 572:
if array_len then
if array_len then
local results = {}
local results = {}
for i = 1, array_len do
for i = 1, array_len do
local result_str = fmt_template
local result_str = fmt_template
for _, arg_col in ipairs(spec.args) do
for _, arg_col in ipairs(spec.args) do
local val = new_row[arg_col]
local val = new_row[arg_col]
local sv
local sv
if type(val) == 'table' then
if type(val) == 'table' then
sv =
sv = tostring(val[i] ~= nil and val[i] or '')
tostring(
val[i] ~= nil
and val[i]
or ''
)
 
elseif val == nil then
elseif val == nil then
sv = ''
sv = ''
else
else
sv = tostring(val)
sv = tostring(val)
end
end
 
result_str = result_str:gsub('%%' .. arg_col .. '%%', function() return sv end)
result_str =
result_str:gsub(
'%%' .. arg_col .. '%%',
function()
return sv
end
)
end
end
table.insert(results, result_str)
table.insert(results, result_str)
end
end
new_row[spec.output_col] = results
new_row[spec.output_col] = results
else
else
local result_str = fmt_template
local result_str = fmt_template
for _, arg_col in ipairs(spec.args) do
for _, arg_col in ipairs(spec.args) do
local val = new_row[arg_col]
local val = new_row[arg_col]
 
result_str = result_str:gsub('%%' .. arg_col .. '%%',
result_str =
function() return val == nil and '' or tostring(val) end)
result_str:gsub(
'%%' .. arg_col .. '%%',
function()
return val == nil
and ''
or tostring(val)
end
)
end
end
new_row[spec.output_col] = result_str
new_row[spec.output_col] = result_str
end
end
end
end


local function groupResults(
local function groupResults(results, group_by_cols, agg_specs, columns, group_sort, args)
results,
group_by_cols,
agg_specs,
columns,
group_sort,
args
)
if #group_by_cols == 0 then
if #group_by_cols == 0 then
return results
return results
Line 1,079: Line 606:


local group_by_set = {}
local group_by_set = {}
for _, col in ipairs(group_by_cols) do
for _, col in ipairs(group_by_cols) do
group_by_set[col] = true
group_by_set[col] = true
Line 1,085: Line 611:


local covered = {}
local covered = {}
for _, spec in ipairs(agg_specs) do
for _, spec in ipairs(agg_specs) do
covered[spec.output_col] = true
covered[spec.output_col] = true
end
end


local groups, group_order =
local groups, group_order = buildGroups(results, group_by_cols)
buildGroups(
results,
group_by_cols
)


local grouped_results = {}
local grouped_results = {}
for _, key in ipairs(group_order) do
for _, key in ipairs(group_order) do
local group_rows = groups[key]
local group_rows = groups[key]
if group_sort then
if group_sort then
sortResults(
sortResults(group_rows, group_sort)
group_rows,
group_sort
)
end
end


local new_row = {}
local new_row = {}
for _, col in ipairs(group_by_cols) do
for _, col in ipairs(group_by_cols) do
new_row[col] =
new_row[col] = group_rows[1][col]
group_rows[1][col]
end
end


local format_specs =
local format_specs = applyAggregation(group_rows, agg_specs, new_row)
applyAggregation(
group_rows,
agg_specs,
new_row
)


for _, col_info in ipairs(columns) do
for _, col_info in ipairs(columns) do
local col_name = col_info.alias
local col_name = col_info.alias
 
if not group_by_set[col_name] and not covered[col_name] then
if not group_by_set[col_name]
new_row[col_name] = aggAny(group_rows, col_name)
and not covered[col_name] then
 
new_row[col_name] =
aggAny(
group_rows,
col_name
)
end
end
end
end


for _, spec in ipairs(format_specs) do
for _, spec in ipairs(format_specs) do
local fmt_template =
local fmt_template = args[spec.output_col .. '-agg']
args[spec.output_col .. '-agg']
 
if fmt_template then
if fmt_template then
applyFormatSpec(
applyFormatSpec(new_row, spec, unescape(fmt_template))
new_row,
spec,
unescape(fmt_template)
)
end
end
end
end


table.insert(
table.insert(grouped_results, new_row)
grouped_results,
new_row
)
end
end
return grouped_results
return grouped_results
end
end


--------------------------------------------------------------------------------
local T = { PARENTHESIS_L = 1, PARENTHESIS_R = 2, AND = 3, OR = 4, NOT = 5, COMP = 7, STR = 8, NUM = 9, BOOL = 10, ID = 11, EOF = 12 }
-- Filter parser
--------------------------------------------------------------------------------
 
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 = {
local KW = {
['AND'] = T.AND,
['AND'] = T.AND,
Line 1,182: Line 656:
['NOT'] = T.NOT,
['NOT'] = T.NOT,
['true'] = T.BOOL,
['true'] = T.BOOL,
['false'] = T.BOOL,
['false'] = T.BOOL
['NULL'] = T.NULL
}
}
 
local FILTER_OPS = { CONTAINS = true, ICONTAINS = true, MATCH = true }
local FILTER_OPS = {
CONTAINS = true,
ICONTAINS = true,
MATCH = true
}
 
--------------------------------------------------------------------------------
-- Joins
--------------------------------------------------------------------------------


local function parseJoins(join_str)
local function parseJoins(join_str)
local joins = {}
local joins = {}
for join_part in join_str:gmatch("[^,]+") do
for join_part in join_str:gmatch("[^,]+") do
local join = {}
local join = {}
local parts = {}
local parts = {}
for part in join_part:gmatch("[^=]+") do
for part in join_part:gmatch("[^=]+") do
table.insert(
table.insert(parts, mw.text.trim(part))
parts,
mw.text.trim(part)
)
end
end
if #parts == 2 then
if #parts == 2 then
join.left = parts[1]
join.left = parts[1]
join.right = parts[2]
join.right = parts[2]
 
table.insert(joins, join)
table.insert(
joins,
join
)
end
end
end
end
return joins
return joins
end
end
--------------------------------------------------------------------------------
-- Tokenizer
--------------------------------------------------------------------------------


local function tokenize(s)
local function tokenize(s)
local t, i = {}, 1
local t, i = {}, 1
while i <= #s do
while i <= #s do
local c = s:sub(i, i)
local c = s:sub(i, i)
if c:match('%s') then
if c:match('%s') then
i = i + 1
i = i + 1
elseif c == '(' then
elseif c == '(' then
table.insert(
table.insert(t, { type = T.PARENTHESIS_L }); i = i + 1
t,
{ type = T.PARENTHESIS_L }
)
 
i = i + 1
 
elseif c == ')' then
elseif c == ')' then
table.insert(
table.insert(t, { type = T.PARENTHESIS_R }); i = i + 1
t,
{ type = T.PARENTHESIS_R }
)
 
i = i + 1
 
elseif c:match('%d') or c == '.' then
elseif c:match('%d') or c == '.' then
local n =
local n = s:match('^%-?%d*%.?%d+', i)
s:match(
table.insert(t, { type = T.NUM, val = tonumber(n) })
'^%-?%d*%.?%d+',
i
)
 
table.insert(
t,
{
type = T.NUM,
val = tonumber(n)
}
)
 
i = i + #n
i = i + #n
elseif c:match("[%w_]") then
elseif c:match("[%w_]") then
local w =
local w = s:match('^[%w_.]+', i)
s:match(
table.insert(t, { type = KW[w:upper()] or KW[w] or T.ID, val = w })
'^[%w_.]+',
i
)
 
table.insert(
t,
{
type =
KW[w:upper()]
or KW[w]
or T.ID,
val = w
}
)
 
i = i + #w
i = i + #w
elseif c:match('["\']') then
elseif c:match('["\']') then
local q, str = c, ''
local q, str = c, ''
for j = i + 1, #s do
for j = i + 1, #s do
local ch = s:sub(j, j)
local ch = s:sub(j, j)
if ch == q then
if ch == q then
table.insert(
table.insert(t, { type = T.STR, val = str }); i = j + 1; break
t,
{
type = T.STR,
val = str
}
)
 
i = j + 1
break
end
end
str = str .. ch
str = str .. ch
end
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
elseif c:match('[=<>!]') then
local op =
local op = s:match('[=<>!]+', i)
s:match(
'[=<>!]+',
i
)
 
if not op then
if not op then
error(
error("Invalid operator at position " .. i)
"Invalid operator at position "
.. i
)
end
end
 
table.insert(t, { type = T.COMP, val = op })
table.insert(
t,
{
type = T.COMP,
val = op
}
)
 
i = i + #op
i = i + #op
else
else
error(
error("Unexpected character '" .. c .. "' at position " .. i)
"Unexpected character '"
.. c
.. "' at position "
.. i
)
end
end
end
end
return t
return t
end
end


--------------------------------------------------------------------------------
-- Break out of the dependency loop
-- Expression parser
--------------------------------------------------------------------------------
 
local parsePrimary
local parsePrimary


local function parseNotExpr(toks, pos)
local function parseNotExpr(toks, pos)
if toks[pos]
if toks[pos] and toks[pos].type == T.NOT then
and toks[pos].type == T.NOT then
local node, new_pos = parseNotExpr(toks, pos + 1)
 
return { type = 'not', operand = node }, new_pos
local node, new_pos =
parseNotExpr(
toks,
pos + 1
)
 
return {
type = 'not',
operand = node
}, new_pos
 
else
else
return parsePrimary(
return parsePrimary(toks, pos)
toks,
pos
)
end
end
end
end


local function parseAndExpr(toks, pos)
local function parseAndExpr(toks, pos)
local left, new_pos =
local left, new_pos = parseNotExpr(toks, pos)
parseNotExpr(
while new_pos <= #toks and toks[new_pos].type == T.AND do
toks,
local right, next_pos = parseNotExpr(toks, new_pos + 1)
pos
left = { type = 'and', left = left, right = right }
)
 
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
new_pos = next_pos
end
end
return left, new_pos
return left, new_pos
end
end


local function parseExpression(toks, pos)
local function parseExpression(toks, pos)
local left, new_pos =
local left, new_pos = parseAndExpr(toks, pos)
parseAndExpr(
while new_pos <= #toks and toks[new_pos].type == T.OR do
toks,
local right, next_pos = parseAndExpr(toks, new_pos + 1)
pos
left = { type = 'or', left = left, right = right }
)
 
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
new_pos = next_pos
end
end
return left, new_pos
return left, new_pos
end
end
Line 1,436: Line 756:
parsePrimary = function(toks, pos)
parsePrimary = function(toks, pos)
if toks[pos].type == T.PARENTHESIS_L then
if toks[pos].type == T.PARENTHESIS_L then
local node, new_pos =
local node, new_pos = parseExpression(toks, pos + 1)
parseExpression(
if toks[new_pos].type ~= T.PARENTHESIS_R then
toks,
error("Expected ')' at position " .. new_pos)
pos + 1
)
 
if toks[new_pos].type
~= T.PARENTHESIS_R then
 
error(
"Expected ')' at position "
.. new_pos
)
end
end
return node, new_pos + 1
return node, new_pos + 1
elseif toks[pos].type == T.ID then
elseif toks[pos].type == T.ID then
local op_tok = toks[pos + 1]
local op_tok = toks[pos + 1]
 
if op_tok and op_tok.type == T.ID and FILTER_OPS[op_tok.val:upper()] then
if op_tok
and op_tok.type == T.ID
and FILTER_OPS[
op_tok.val:upper()
] then
 
local v = toks[pos + 2]
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
if not v
error("Expected value after filter operator at position " .. (pos + 2))
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
end
 
local value = (v.type == T.NUM) and v.val or tostring(v.val)
local value =
return { type = 'filter_op', field = toks[pos].val, op = op_tok.val:upper(), value = value }, pos + 3
(v.type == T.NUM)
elseif op_tok and op_tok.type == T.COMP then
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]
local v = toks[pos + 2]
 
if v.type ~= T.STR and v.type ~= T.NUM and v.type ~= T.BOOL then
if v.type ~= T.STR
error("Expected value after comparison operator at position " .. (pos + 2))
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
end
 
return { type = 'comparison', field = toks[pos].val, op = op_tok.val, value = v.val }, pos + 3
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
else
error(
error("Expected comparison or filter operator after identifier at position " .. (pos + 1))
"Expected comparison or filter operator after identifier at position "
.. (pos + 1)
)
end
end
else
else
error(
error("Unexpected token at position " .. pos .. ", expected identifier or '('")
"Unexpected token at position "
.. pos
.. ", expected identifier or '('"
)
end
end
end
end


local function parseCond(toks, pos)
local function parseCond(toks, pos)
local node, new_pos =
local node, new_pos = parseExpression(toks, pos)
parseExpression(
toks,
pos
)
 
return node, new_pos
return node, new_pos
end
end
Line 1,553: Line 791:
local function toBucket(n)
local function toBucket(n)
if n.type == 'and' then
if n.type == 'and' then
return Bucket.And(
return Bucket.And(toBucket(n.left), toBucket(n.right))
toBucket(n.left),
toBucket(n.right)
)
 
elseif n.type == 'or' then
elseif n.type == 'or' then
return Bucket.Or(
return Bucket.Or(toBucket(n.left), toBucket(n.right))
toBucket(n.left),
toBucket(n.right)
)
 
elseif n.type == 'not' then
elseif n.type == 'not' then
return Bucket.Not(
return Bucket.Not(toBucket(n.operand))
toBucket(n.operand)
)
 
elseif n.type == 'comparison' then
elseif n.type == 'comparison' then
local val =
return { n.field, n.op, n.value }
(n.value == NULL)
and Bucket.Null()
or n.value
 
return {
n.field,
n.op,
val
}
end
end
end
end
--------------------------------------------------------------------------------
-- Filter evaluation
--------------------------------------------------------------------------------


local FILTER_FUNCS = {
local FILTER_FUNCS = {
CONTAINS = function(v, s)
CONTAINS = function(v, s) return type(v) == 'string' and mw.ustring.find(v, s, 1, true) ~= nil end,
return type(v) == 'string'
and mw.ustring.find(
v,
s,
1,
true
) ~= nil
end,
 
ICONTAINS = function(v, s)
ICONTAINS = function(v, s)
return type(v) == 'string'
return
and mw.ustring.find(
type(v) == 'string' and
mw.ustring.lower(v),
mw.ustring.find(mw.ustring.lower(v), mw.ustring.lower(s), 1, true) ~= nil
mw.ustring.lower(s),
1,
true
) ~= nil
end,
end,
 
MATCH     = function(v, s) return type(v) == 'string' and mw.ustring.match(v, s) ~= nil end,
MATCH = function(v, s)
return type(v) == 'string'
and mw.ustring.match(v, s)
~= nil
end
}
}


local COMP_FUNCS = {
local COMP_FUNCS = {
['='] = function(a, b)
['='] = function(a, b) return a == b end,
if b == NULL then
['!='] = function(a, b) return a ~= b end,
return a == nil
['<'] = function(a, b) return compareValues(a, b) == -1 end,
end
['>'] = function(a, b) return compareValues(a, b) == 1 end,
 
['<='] = function(a, b) return compareValues(a, b) <= 0 end,
return a == b
['>='] = function(a, b) return compareValues(a, b) >= 0 end,
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)
local function evalFilter(node, row)
if node.type == 'and' then
if node.type == 'and' then
return evalFilter(
return evalFilter(node.left, row) and evalFilter(node.right, row)
node.left,
row
) and evalFilter(
node.right,
row
)
 
elseif node.type == 'or' then
elseif node.type == 'or' then
return evalFilter(
return evalFilter(node.left, row) or evalFilter(node.right, row)
node.left,
row
) or evalFilter(
node.right,
row
)
 
elseif node.type == 'not' then
elseif node.type == 'not' then
return not evalFilter(
return not evalFilter(node.operand, row)
node.operand,
row
)
 
elseif node.type == 'filter_op' then
elseif node.type == 'filter_op' then
local fn =
local fn = FILTER_FUNCS[node.op]
FILTER_FUNCS[node.op]
if not fn then error('Unknown filter op: ' .. node.op) end
 
return fn(row[node.field], node.value)
if not fn then
error(
'Unknown filter op: '
.. node.op
)
end
 
return fn(
row[node.field],
node.value
)
 
elseif node.type == 'comparison' then
elseif node.type == 'comparison' then
local fn =
local fn = COMP_FUNCS[node.op]
COMP_FUNCS[node.op]
if not fn then error('Unknown comparison op: ' .. node.op) end
 
return fn(row[node.field], node.value)
if not fn then
error(
'Unknown comparison op: '
.. node.op
)
end
 
return fn(
row[node.field],
node.value
)
end
end
end
end
Line 1,710: Line 840:
local function applyFilter(results, ast)
local function applyFilter(results, ast)
local filtered = {}
local filtered = {}
for _, row in ipairs(results) do
for _, row in ipairs(results) do
if evalFilter(ast, row) then
if evalFilter(ast, row) then
table.insert(
table.insert(filtered, row)
filtered,
row
)
end
end
end
end
return filtered
return filtered
end
end
--------------------------------------------------------------------------------
-- Query
--------------------------------------------------------------------------------


local function runQuery(columns, args)
local function runQuery(columns, args)
local bucket_name =
local bucket_name = mw.text.trim(args[1] or args['bucket'] or '')
mw.text.trim(
args[1]
or args['bucket']
or ''
)
 
if bucket_name == '' then
if bucket_name == '' then
error(
error('Must specify the name of the bucket')
'Must specify the name of the bucket'
)
end
end


for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
col.label =
col.label = args[col.alias .. '-label'] or col.alias
args[col.alias .. '-label']
or col.alias
end
end


local all_originals = {}
local all_originals = {}
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
all_originals[col.column] = true
all_originals[col.column] = true
end
end
local alias_to_column = {}
local alias_to_column = {}
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
if col.alias ~= col.column
if col.alias ~= col.column and not all_originals[col.alias] then
and not all_originals[col.alias] then
alias_to_column[col.alias] = col.column
 
alias_to_column[col.alias] =
col.column
end
end
end
end


local column_names = {}
local column_names = {}
for _, col in ipairs(columns) do
for _, col in ipairs(columns) do
table.insert(
table.insert(column_names, col.column)
column_names,
col.column
)
end
end
local query = mw.bucket(bucket_name).select(unpack(column_names))


local query =
local join_str = args['join on'] or args['join']
mw.bucket(bucket_name)
.select(unpack(column_names))
 
local join_str =
args['join_on']
or args['join']
 
if join_str then
if join_str then
local joins =
local joins = parseJoins(join_str)
parseJoins(join_str)
 
for _, join in ipairs(joins) do
for _, join in ipairs(joins) do
join.left =
join.left = alias_to_column[join.left] or join.left
alias_to_column[join.left]
join.right = alias_to_column[join.right] or join.right
or join.left
local right_table = join.right:match("^[^.]+") or join.right
 
query = query.join(right_table, join.left, join.right)
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
end
end


local where_str = args['where']
local where_str = args['where']
if where_str then
if where_str then
local toks =
local toks = tokenize(where_str)
tokenize(where_str)
local ast = parseCond(toks, 1)
 
translateAliases(ast, alias_to_column)
local ast =
query = query.where(toBucket(ast))
parseCond(
toks,
1
)
 
translateAliases(
ast,
alias_to_column
)
 
query =
query.where(
toBucket(ast)
)
end
end


local limit =
local limit = tonumber(args['limit'])
tonumber(args['limit'])
 
if limit ~= nil then
if limit ~= nil then
query = query.limit(limit)
query = query.limit(limit)
end
end


local offset =
local offset = tonumber(args['offset'])
tonumber(args['offset'])
 
if offset ~= nil then
if offset ~= nil then
query = query.offset(offset)
query = query.offset(offset)
Line 1,846: Line 906:
return query:run()
return query:run()
end
end
--------------------------------------------------------------------------------
-- Main
--------------------------------------------------------------------------------


function p.get(frame)
function p.get(frame)
local args =
local args = getArgs(frame)
getArgs(frame)
local columns_str = args['select'] or args['fields']
 
local columns_str =
args['select']
or args['fields']
 
if not columns_str then
if not columns_str then
error(
error("Must specify which columns to select.")
"Must specify which columns to select."
)
end
end


local columns =
local columns = parseColumns(columns_str)
parseColumns(columns_str)
 
local results =
runQuery(
columns,
args
)
 
renameColumns(
results,
columns
)


local filter_str =
local results = runQuery(columns, args)
args['filter']
-- mw.logObject(results)
renameColumns(results, columns)


local filter_str = args['filter']
if filter_str then
if filter_str then
local toks =
local toks = tokenize(filter_str)
tokenize(filter_str)
local ast = parseCond(toks, 1)
 
results = applyFilter(results, ast)
local ast =
parseCond(
toks,
1
)
 
results =
applyFilter(
results,
ast
)
end
end


if #results == 0 then
if #results == 0 then
return args['default']
return args['default'] or "No result"
or "No result"
end
end


local group_by_str =
local group_by_str = args['group by']
args['group_by']
 
if group_by_str then
if group_by_str then
local group_by_cols =
local group_by_cols = parseGroupBy(group_by_str)
parseGroupBy(group_by_str)
local agg_specs = parseAggregateArg(args['aggregate'] or '', columns)
 
local agg_specs =
parseAggregateArg(
args['aggregate']
or '',
columns
)
 
for _, spec in ipairs(agg_specs) do
for _, spec in ipairs(agg_specs) do
if spec.is_new_col then
if spec.is_new_col then
Line 1,925: Line 942:
column = spec.output_col,
column = spec.output_col,
alias = spec.output_col,
alias = spec.output_col,
label =
label = args[spec.output_col .. '-label'] or spec.output_col
args[
spec.output_col
.. '-label'
]
or spec.output_col
}
}
)
)
end
end
end
end
 
local group_sort = parseOrderBy(args['group-sort'] or '')
local group_sort =
results = groupResults(results, group_by_cols, agg_specs, columns, group_sort, args)
parseOrderBy(
args['group-sort']
or ''
)
 
results =
groupResults(
results,
group_by_cols,
agg_specs,
columns,
group_sort,
args
)
end
end


applySplitColumns(
applySplitColumns(results, columns, args)
results,
columns,
args
)
 
local order_by_str =
args['order_by']
or args['order']


local order_by_str = args['order by']
if order_by_str then
if order_by_str then
local order_by_rules =
local order_by_rules = parseOrderBy(order_by_str)
parseOrderBy(
sortResults(results, order_by_rules)
order_by_str
)
 
sortResults(
results,
order_by_rules
)
end
end


applyValueFormats(
-- FIXME: this inserts markup that may need to be preprocessed, but for performance reasons
results,
-- we don't want to preprocess them row-by-row.
columns,
applyValueFormats(results, columns, args, frame)
args,
frame
)
 
local format =
args['format']
or "table"


local format = args['format'] or "table"
local prefix = ""
local prefix = ""
 
local resultsHeader = args['resultsHeader'] or "'''%TOTALRESULTS%''' result%S%:"
local resultsHeader =
local header = args['header'] or ""
args['results_header']
local footer = args['footer'] or ""
or "'''%TOTALRESULTS%''' result%S%:"
 
local header =
args['header']
or ""
 
local footer =
args['footer']
or ""
 
local text = ""
local text = ""
 
local resultsCount =
local resultsCount = #results
#results
    resultsHeader = resultsHeader
 
:gsub("%%TOTALRESULTS%%", resultsCount)
resultsHeader =
:gsub("%%ISARE%%", resultsCount == 1 and "is" or "are")
resultsHeader
:gsub("%%S%%", resultsCount == 1 and "" or "s")
:gsub(
:gsub("%%ES%%", resultsCount == 1 and "" or "es")
"%%TOTALRESULTS%%",
:gsub("%%HASHAVE%%", resultsCount == 1 and "has" or "have")
resultsCount
)
local yesResultsHeader = args['show resultsHeader']
:gsub(
"%%ISARE%%",
resultsCount == 1
and "is"
or "are"
)
:gsub(
"%%S%%",
resultsCount == 1
and ""
or "s"
)
:gsub(
"%%ES%%",
resultsCount == 1
and ""
or "es"
)
:gsub(
"%%HASHAVE%%",
resultsCount == 1
and "has"
or "have"
)
 
local yesResultsHeader =
args['show_results_header']
 
local showResultsHeader = false
local showResultsHeader = false
 
if (args['resultsHeader'] ~= nil and args['resultsHeader'] ~= "")
if
(
args['results_header']
~= nil
and args['results_header']
~= ""
)
or yesResultsHeader == true
or yesResultsHeader == true
or (
or (type(yesResultsHeader) == "string" and (yesResultsHeader:lower() == "true" or yesResultsHeader == "1")) then
type(yesResultsHeader)
showResultsHeader = true
== "string"
and (
yesResultsHeader:lower()
== "true"
or yesResultsHeader
== "1"
)
)
then
showResultsHeader = true
end
end


if showResultsHeader then
if showResultsHeader then
prefix =
  prefix = resultsHeader .. "\n"
resultsHeader
.. "\n"
end
end
 
if format == "table" then
if format == "table" then
text =
text = formatTable(frame, results, columns, args)
formatTable(
frame,
results,
columns,
args
)
 
elseif format == "template" then
elseif format == "template" then
text =
text = formatTemplate(frame, results, columns, args)
formatTemplate(
frame,
results,
columns,
args
)
 
elseif format == "userformat" then
elseif format == "userformat" then
text =
text = formatUserformat(frame, results, columns, args)
formatUserformat(
frame,
results,
columns,
args
)
 
else
else
error(
error(string.format("Invalid format %s specified.", format))
string.format(
"Invalid format %s specified.",
format
)
)
end
end


return
return prefix .. header .. text .. footer
prefix
.. header
.. text
.. footer
end
end


--------------------------------------------------------------------------------
-- Exports for testing
-- Exports for testing
--------------------------------------------------------------------------------
p.sortResults = sortResults
p.sortResults = sortResults
p.aggCount = aggCount
p.aggCount = aggCount
Line 2,122: Line 1,010:
p.parseAggregateArg = parseAggregateArg
p.parseAggregateArg = parseAggregateArg
p.T = T
p.T = T
p.NULL = NULL
p.tokenize = tokenize
p.tokenize = tokenize
p.parseCond = parseCond
p.parseCond = parseCond
Please note that all contributions to The Petit Planet Wiki are considered to be released under the Creative Commons Attribution-ShareAlike (see Petit Planet Wiki:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)
Preview page with this template
Below are commonly used wikitext from MediaWiki:Edittools. Click on any to insert them in the edit box.
File Pages==Summary====Licensing=={{Fairuse}}{{File|}}
Dynamic Page List¦²«»²{¦}²«»«/»«!----»
Array in DPL¹^2{}^2^2{¹}^2
Parser Functions{{#if:||}}
{{#ifeq:|||}}
{{#expr:}}
{{#switch:||#default=}}
{{#replace:||}}
{{#titleparts:}}
{{#tag:|}}
Markup{{}}|[][[]][[Category:]]#REDIRECT [[]] [[Category:Redirect Pages]]<code></code><includeonly></includeonly><noinclude></noinclude><nowiki></nowiki><!----><br>§
Notices{{Stub|}}{{Stub Dialogue}}{{Upcoming}}{{Under Construction}}{{Placeholder|}}
Magic Words{{!}}{{formatnum:}}{{lc:}}{{uc:}}{{PAGENAME}}{{FULLPAGENAME}}{{ROOTPAGENAME}}{{BASEPAGENAME}}{{SUBPAGENAME}}{{DISPLAYTITLE:|noreplace}}__TOC____NOTOC____EXPECTUNUSEDCATEGORY____HIDDENCAT__
HTML Entities&mdash;&nbsp;&comma;&shy;