<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://nbittrpgwiki.pynappo.xyz/index.php?action=history&amp;feed=atom&amp;title=Module%3ABucketGet</id>
	<title>Module:BucketGet - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://nbittrpgwiki.pynappo.xyz/index.php?action=history&amp;feed=atom&amp;title=Module%3ABucketGet"/>
	<link rel="alternate" type="text/html" href="https://nbittrpgwiki.pynappo.xyz/index.php?title=Module:BucketGet&amp;action=history"/>
	<updated>2026-10-25T17:39:41Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://nbittrpgwiki.pynappo.xyz/index.php?title=Module:BucketGet&amp;diff=3233&amp;oldid=prev</id>
		<title>CStatic: Created page with &quot;local p = {} local getArgs = require(&#039;Module:Arguments&#039;).getArgs local Bucket = mw.bucket local NULL = {}  local TYPE_ORDER = { [&quot;nil&quot;] = 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 &lt; b_type and -1 or 1 	end...&quot;</title>
		<link rel="alternate" type="text/html" href="https://nbittrpgwiki.pynappo.xyz/index.php?title=Module:BucketGet&amp;diff=3233&amp;oldid=prev"/>
		<updated>2026-10-04T03:12:40Z</updated>

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