Module:Cargo query
Appearance
Documentation for this module may be created at Module:Cargo query/doc
-- ================================================================================
-- Module Dependencies
-- ================================================================================
local cargo = mw.ext.cargo
local html = mw.html.create()
local getArgs = require('Module:Arguments').getArgs -- for processing arguments
local ibf = require("Module:Infobox_functions")
local utils = require("Module:Utilities")
-- ================================================================================
-- Main Table
-- ================================================================================
local p = {}
-- ================================================================================
-- Thin Cargo wrapper
-- ================================================================================
--[[This function is perhaps a better Cargo wrapper than `cargo_query` and should eventually replace it. It is much more versatile and a thinner wrapper than the latter function.
Description:
A thin, general-purpose wrapper for the MediaWiki Cargo extension that
allows complex, flexible SQL-like querying via a structured approach.
It separates query logic (the WHERE clause) from dynamic user-supplied
values to improve safety, maintainability, and clarity.
This function is intended for advanced use cases where queries may contain
multiple clauses, negations, or complex combinations of filters.
For simpler one-field filters, see: `p.filter_cargo_table_enhanced`.
Usage:
Template call example:
{{#invoke:Cargo_query | thin_cargo_wrapper
| cargo_table=Resource
| cargo_fields=Name,Year_published
| cargo_display_fields=Name,Year_published
| cargo_where=Species_purview HOLDS __species__ AND Category HOLDS 'Media'
| cargo_where_values=species={{PAGENAME}}
| order_by=Year_published DESC
| row_cutoff=5
| character_cutoff=600
}}
Parameters:
- cargo_table: Name of the Cargo table to query.
- cargo_fields: Comma-separated list of fields to query from Cargo.
- cargo_display_fields: Fields to display in output (optional override).
- cargo_where: WHERE clause string with placeholder tokens like '__key__' or '__value__'.
- cargo_where_values: Comma-separated list of key=value pairs used to
replace placeholders in the WHERE clause.
Each value is SQL-escaped automatically before substitution.
If omitted, the placeholder __value__ will be replaced using
`filter_value`, or `PAGENAME` as a fallback.
- filter_value: A single fallback value (used with the __value__ token).
- order_by: Optional ORDER BY clause.
- limit: Optional result limit.
- row_cutoff: Row limit for collapsing tables (default: 3).
- character_cutoff: Max characters before collapsing content block.
- options: Optional JSON string for advanced display or link transformation.
Returns:
- A formatted wiki table of results if any records are found.
- A fallback message if no records match the query.
--]]
function p.thin_cargo_wrapper(frame)
local args = getArgs(frame)
local parent_args = frame:getParent().args
local cargo_table = args.cargo_table
local cargo_fields = args.cargo_fields
local display_fields_raw = args.cargo_display_fields or args.cargo_fields
local cargo_where = args.cargo_where
local options = args.options and mw.text.jsonDecode(args.options) or {}
local row_cutoff = (tonumber(args.row_cutoff or parent_args.row_cutoff) or 3) + 1
local character_cutoff = tonumber(args.character_cutoff or parent_args.character_cutoff) or 600
-- ========================================
-- 1. Placeholder substitution
-- ========================================
if cargo_where then
local cargo_where_values_raw = args.cargo_where_values
if cargo_where_values_raw then
for pair in mw.text.gsplit(cargo_where_values_raw, ",") do
local key, value = pair:match("^(.-)=(.*)$")
if key and value and value ~= "" then
value = mw.text.trim(value):gsub("[#%%%-%-]", "")
local safe_value = "'" .. utils.escape_string_for_sql(value) .. "'"
cargo_where = cargo_where:gsub("__" .. key .. "__", safe_value)
end
end
end
end
-- ========================================
-- 2. Schema and field parsing
-- ========================================
local cargo_table_schema = utils.get_cargo_table_schema(cargo_table)
local display_fields = mw.text.split(display_fields_raw, ",")
for i, v in ipairs(display_fields) do
display_fields[i] = mw.text.trim(v)
end
local column_label_map = {}
local column_labels_raw = args.column_labels or parent_args.column_labels
if column_labels_raw then
for pair in mw.text.gsplit(column_labels_raw, ",") do
local k, v = pair:match("([^=]+)=([^=]+)")
if k and v then
column_label_map[mw.text.trim(k)] = mw.text.trim(v)
end
end
else
for _, field in ipairs(display_fields) do
column_label_map[field] = string.gsub(field, "_", " ")
end
end
-- ========================================
-- 3. Run Cargo query
-- ========================================
local cargo_args = {
where = cargo_where,
orderBy = args.order_by,
limit = tonumber(args.limit),
groupBy = args.group_by
}
local cargo_results = cargo.query(cargo_table, cargo_fields, cargo_args)
-- ========================================
-- 4. Process and return
-- ========================================
local parsed = utils.parse_cargo_results(cargo_results, cargo_table_schema)
parsed.results = utils.transform_link_fields(parsed.results, display_fields, cargo_table_schema, options)
local collapse = (#parsed.results > row_cutoff)
if #parsed.results > 0 then
return utils.generate_wiki_table_enhanced(
parsed.results,
display_fields,
collapse,
character_cutoff,
cargo_table_schema,
column_label_map,
true,
nil,
row_cutoff,
options
) .. '<div style="clear: both;"></div>'
else
return "No results found in the database at this time—[[HopperWiki:About|please reach out]] if you have any to share!"
end
end
-- ================================================================================
-- New enhanced argument handling and added features
-- ================================================================================
--[[
filter_cargo_table_enhanced(frame)
A structured, opinionated Cargo query wrapper designed for simple filtering
of a single field by one or more values. The function is optimized for use
in MediaWiki templates where a single page title or user-supplied value is
used to filter a table, and optionally exclude some results.
This wrapper is safer and more predictable than the raw `cargo_query` call,
offering:
- Auto-escaping of filter values
- Support for both literal and list-style input
- Auto-detection of field type to determine correct operators (e.g., `HOLDS`)
- Optional support for fallback files in file fields
- Output as a table, bulleted list, or count
Parameters (via template or #invoke):
- cargo_table: The name of the Cargo table to query
- query_fields / cargo_fields: Fields to fetch from the Cargo table
- table_columns / cargo_display_fields: Fields to display in the output
- column_labels: Optional labels for output columns
- cargo_focal_field: The field to filter against
- filter_value / filter_values: The values to filter for
- not_value / not_values: Values to exclude
- *_literal: Whether to treat the input as a single literal (even if it has commas)
- row_cutoff: Number of rows before collapsing the table
- character_cutoff: Max character length before collapsing
- default_file_fields: Map of fallback files for file fields
- format: Output type: "table", "bulleted_list", or "count"
--]]
function p.filter_cargo_table_enhanced(frame)
-- ========================================
-- 1. Process and prioritize arguments
-- ========================================
local args = getArgs(frame)
local parent_args = frame:getParent().args
-- Required configuration
local cargo_table = args.cargo_table
local cargo_fields = args.query_fields or args.cargo_fields
local display_fields_raw = args.table_columns or args.cargo_display_fields or args.cargo_fields
local cargo_focal_field = args.cargo_focal_field
local format = args.format or "table"
-- Display behavior
local data_row_cutoff = tonumber(args.row_cutoff or parent_args.row_cutoff) or 3
local row_cutoff = data_row_cutoff + 1
local character_cutoff = tonumber(args.character_cutoff or parent_args.character_cutoff) or 600
-- ========================================
-- 2. Schema lookup
-- ========================================
local cargo_table_schema = utils.get_cargo_table_schema(cargo_table)
local cargo_focal_field_info = cargo_table_schema[cargo_focal_field]
-- Validate field exists
if not cargo_focal_field_info then
error("Field '" .. cargo_focal_field .. "' not found in the Cargo table schema. Did you pass a fully qualified name like 'Table.Field'? Use just the field name.")
end
local cargo_focal_field_type = cargo_focal_field_info.type
-- ========================================
-- 3. Value sanitization helpers
-- ========================================
local function sanitize_values(value_string)
if not value_string or value_string == "" then return {} end
local values = mw.text.split(value_string, ",")
for i, val in ipairs(values) do
values[i] = utils.escape_string_for_sql(val)
end
return values
end
local function sanitize_literal(value)
if not value then return "" end
return utils.escape_string_for_sql(value)
end
local function clean_list(list)
local cleaned = {}
for _, val in ipairs(list or {}) do
if val and mw.text.trim(val) ~= "" then
table.insert(cleaned, val)
end
end
return cleaned
end
-- ========================================
-- 4. Determine values and literal flags
-- ========================================
local page_title = mw.title.getCurrentTitle().text
local fallback_value = sanitize_literal(page_title)
local raw_filter_values = args.filter_value or args.filter_values or parent_args.filter_value or parent_args.filter_values or fallback_value
local raw_not_values = args.not_value or args.not_values or parent_args.not_value or parent_args.not_values
local filter_value_literal = args.filter_value_literal == "true" or parent_args.filter_value_literal == "true"
local not_value_literal = args.not_value_literal == "true" or parent_args.not_value_literal == "true"
local filter_values_list = filter_value_literal
and { sanitize_literal(raw_filter_values) }
or sanitize_values(raw_filter_values)
local not_values_list = not_value_literal
and { sanitize_literal(raw_not_values) }
or sanitize_values(raw_not_values)
-- Remove any empty strings that would break SQL
filter_values_list = clean_list(filter_values_list)
not_values_list = clean_list(not_values_list)
-- ========================================
-- 5. Parse display fields
-- ========================================
local display_fields = mw.text.split(display_fields_raw, ",")
for i, v in ipairs(display_fields) do
display_fields[i] = mw.text.trim(v)
end
-- ========================================
-- 6. Column label mapping
-- ========================================
local column_label_map = {}
local column_labels_raw = args.column_labels or parent_args.column_labels
if column_labels_raw then
for pair in mw.text.gsplit(column_labels_raw, ",") do
local key, value = pair:match("([^=]+)=([^=]+)")
if key and value then
column_label_map[mw.text.trim(key)] = mw.text.trim(value)
end
end
end
-- ========================================
-- 7. Default file fields
-- ========================================
local default_file_fields = {}
local default_files_raw = args.default_file_fields or parent_args.default_file_fields
if default_files_raw then
for pair in mw.text.gsplit(default_files_raw, ";") do
local field, file = pair:match("([^:]+):%s*(.+)")
if field and file then
default_file_fields[mw.text.trim(field)] = mw.text.trim(file)
end
end
end
-- ========================================
-- 8. Run query
-- ========================================
local where_clause = utils.build_where_clause(
cargo_focal_field_type, cargo_focal_field, filter_values_list, not_values_list
)
local cargo_args = { where = where_clause }
local cargo_results = cargo.query(cargo_table, cargo_fields, cargo_args)
-- Special format: count only
if format == "count" then
return tostring(#cargo_results)
end
-- ========================================
-- 9. Parse results
-- ========================================
local parsed = utils.parse_cargo_results(cargo_results, cargo_table_schema)
-- ========================================
-- 10. Normalize file fields
-- ========================================
if default_file_fields then
for _, row in ipairs(parsed.results) do
for field, default_file in pairs(default_file_fields) do
local schema = cargo_table_schema[field]
if schema and schema.type == "file" and (row[field] == "" or row[field] == nil) then
row[field] = default_file
elseif schema and schema.type == "list_of_file" and (not row[field] or #row[field] == 0) then
row[field] = { default_file }
end
end
end
end
-- ========================================
-- 11. Collapse logic
-- ========================================
local collapse = false
if row_cutoff and #parsed.results > row_cutoff then
collapse = true
end
-- ========================================
-- 12. Render
-- ========================================
if #parsed.results > 0 then
if format == "bulleted_list" then
local field_name = display_fields[1]
return utils.bulleted_list(parsed.results, field_name, cargo_table_schema, column_label_map)
else
return utils.generate_wiki_table_enhanced(
parsed.results,
display_fields,
collapse,
character_cutoff,
cargo_table_schema,
column_label_map,
false,
nil,
row_cutoff
) .. '<div style="clear: both;"></div>'
end
else
return "No results found in the database at this time—[[HopperWiki:About|please reach out]] if you have any to share!"
end
end
-- ================================================================================
-- Cargo querying function
-- ================================================================================
function p.filter_cargo_table(frame)
-- ========================================
-- Process and prioritize arguments
-- ========================================
local args = getArgs(frame)
local parent_args = frame:getParent().args
local title = mw.title.getCurrentTitle().text
title = title:match("^%s*(.-)%s*$") -- Trim leading/trailing whitespace
title = utils.escape_string_for_sql(title)
-- Get arguments, allowing for variant spellings and fallbacks
local filter_values = args.filter_value or args.filter_values or parent_args.filter_value or parent_args.filter_values or title
local not_values = args.not_value or args.not_values or parent_args.not_value or parent_args.not_values
local cargo_table = args.cargo_table
local cargo_fields = args.cargo_fields
local cargo_display_fields = args.cargo_display_fields or args.cargo_fields
-- Get table schema and validate
local cargo_table_schema, schema_debug = utils.get_cargo_table_schema(cargo_table)
if not cargo_table_schema then
error("Failed to retrieve schema for Cargo table '" .. cargo_table .. "'. Debug info:\n" .. (schema_debug or ""))
end
-- Focal field and type
local cargo_focal_field = args.cargo_focal_field or parent_args.cargo_focal_field
local cargo_focal_field_info = cargo_table_schema[cargo_focal_field]
local cargo_focal_field_type = cargo_focal_field_info and cargo_focal_field_info.type or "string"
-- Parse display fields
if type(cargo_display_fields) == "string" then
cargo_display_fields = mw.text.split(cargo_display_fields, ",")
for i, value in ipairs(cargo_display_fields) do
cargo_display_fields[i] = mw.text.trim(value)
end
end
-- Row and character cutoff values
local row_cutoff = tonumber(args.row_cutoff or parent_args.row_cutoff)
local character_cutoff = tonumber(args.character_cutoff or parent_args.character_cutoff)
-- Split and trim filter values
local filter_values_list = mw.text.split(filter_values, ",")
for i, value in ipairs(filter_values_list) do
filter_values_list[i] = mw.text.trim(value)
end
local not_values_list = {}
if not_values then
not_values_list = mw.text.split(not_values, ",")
for i, value in ipairs(not_values_list) do
not_values_list[i] = mw.text.trim(value)
end
end
-- ========================================
-- Run Cargo query and format
-- ========================================
local where_clause = utils.build_where_clause(cargo_focal_field_type, cargo_focal_field, filter_values_list, not_values_list)
local cargo_args = { where = where_clause }
local cargo_results = cargo.query(cargo_table, cargo_fields, cargo_args)
local field_formats = {}
for field, def in pairs(cargo_table_schema) do
field_formats[field] = def.type
end
-- Generate user-friendly labels by replacing underscores with spaces
local field_name_mappings = {}
for _, field in ipairs(cargo_display_fields) do
field_name_mappings[field] = string.gsub(field, "_", " ")
end
local cargo_results_formatted = utils.format_cargo_results(
cargo_results,
field_formats,
field_name_mappings
)
-- Determine if display should collapse
local collapse = false
if row_cutoff and #cargo_results_formatted > row_cutoff then
collapse = true
end
-- ========================================
-- Produce display table
-- ========================================
local header = ""
if #cargo_results_formatted > 0 then
return utils.generate_wiki_table_enhanced_new_collapse(
parsed.results,
display_fields,
collapse,
character_cutoff,
cargo_table_schema,
column_label_map,
false, -- compat_mode
nil, -- file_display_format
row_cutoff
) .. '<div style="clear: both;"></div>'
else
return "No results found in the database at this time—[[HopperWiki:About|please reach out]] if you have any to share!"
end
end
-- ================================================================================
-- Custom Cargo wrapper
-- ================================================================================
--[[This function is a base-level wrapper for Cargo to deal with future-proofing the need to modify raw Cargo queries
on the forward facing pages of the wiki. It should be used for more complex SQL querying. If only a simple SQL query is needed,
for instance querying a single keyword (or
a few keywords) from a single field from a single table then see: filter_cargo_table()--]]
function p.cargo_query(frame)
-- ========================================
-- 1. Process and prioritize arguments
-- ========================================
local args = getArgs(frame)
local parent_args = frame:getParent().args
local cargo_table = args.cargo_table
local cargo_fields = args.cargo_fields
local display_fields_raw = args.cargo_display_fields or args.cargo_fields
local cargo_where = args.cargo_where
-- Parse options table (JSON string)
local options = args.options and mw.text.jsonDecode(args.options) or {}
-- Add fallback defaults—CSS logic for toggle requires header count offset
local row_cutoff = (tonumber(args.row_cutoff or parent_args.row_cutoff) or 3) + 1
local character_cutoff = tonumber(args.character_cutoff or parent_args.character_cutoff) or 600
-- ========================================
-- 2. Schema lookup
-- ========================================
local cargo_table_schema = utils.get_cargo_table_schema(cargo_table)
-- ========================================
-- 3. Parse display fields
-- ========================================
local display_fields = mw.text.split(display_fields_raw, ",")
for i, v in ipairs(display_fields) do
display_fields[i] = mw.text.trim(v)
end
-- ========================================
-- 4. Column label mapping
-- Handles field renaming like `CONCAT(...) = URL` or custom labels
-- ========================================
local column_label_map = {}
for _, field in ipairs(display_fields) do
if field:find("=") then
local expr, label = field:match("(.+)%s*=%s*(.+)")
if expr and label then
column_label_map[label] = label
for i, orig in ipairs(display_fields) do
if orig == field then
display_fields[i] = label
end
end
end
else
column_label_map[field] = string.gsub(field, "_", " ")
end
end
-- ========================================
-- 5. Run query
-- ========================================
local cargo_args = { where = cargo_where }
local cargo_results = cargo.query(cargo_table, cargo_fields, cargo_args)
-- ========================================
-- 6. Parse structured results
-- ========================================
local parsed = utils.parse_cargo_results(cargo_results, cargo_table_schema)
-- ========================================
-- 6b. Transform results (e.g. URL → link text)
-- ========================================
parsed.results = utils.transform_link_fields(
parsed.results,
display_fields,
cargo_table_schema,
options
)
-- ========================================
-- 7. Collapse logic
-- ========================================
local collapse = false
if row_cutoff and #parsed.results > row_cutoff then
collapse = true
end
-- ========================================
-- 8. Output
-- ========================================
if #parsed.results > 0 then
return utils.generate_wiki_table_enhanced(
parsed.results,
display_fields,
collapse,
character_cutoff,
cargo_table_schema,
column_label_map,
true, -- compat_mode
nil, -- file_display_format
row_cutoff,
options -- still passed, but ignored by renderer
) .. '<div style="clear: both;"></div>'
else
return "No results found in the database at this time—[[HopperWiki:About|please reach out]] if you have any to share!"
end
end
p.cargo = p.cargo_query
p.cargo_enhanced = p.cargo_query
return p