Field names and descriptions¶
A column called LeadHours or cust_no tells a reader little. The report can show a field under
another name, explain it behind an "i" next to the field (in the field list, its menu and the table's
headers) with a "Learn more" link, sort its items in their own order and give it an aggregator and a
format when it is put into Values. Three parameters say it, from the most specific to the most general:
| Parameter | What | Where it comes from |
|---|---|---|
-FieldDefaults |
everything about a field: label, description, link, order, aggregator, formats, type | your app script |
-FieldDescriptions |
a description per field | SQL Server column descriptions, a CSV, a hashtable |
-Schema |
labels, descriptions, links, types and orders from a JSON Schema or an OpenAPI document | the API the data comes from |
Where they say something about the same field, -FieldDefaults wins over -FieldDescriptions, and
both win over -Schema, key by key. Field names match without regard to case. Views and templates
always keep the field's own name: a label changes what the reader sees, not what is stored.
Labels, descriptions with a link, the order of Status and the type of Created from a JSON Schema; one description from -FieldDescriptions (as Get-UDPivotSqlColumnDescription returns them), aggregator and format from -FieldDefaults. Hover or tap the "i" next to a field.
$rng = [System.Random]::new(13)
$rows = foreach ($i in 1..500) {
[pscustomobject]@{
Id = 20000 + $i
Created = ([datetime]'2026-10-31').AddDays(-$rng.Next(0, 200)).ToString('yyyy-MM-dd')
Team = ('Service Desk', 'Network', 'Server')[$rng.Next(0, 3)]
Status = ('Open', 'Waiting', 'Closed', 'Closed')[$rng.Next(0, 4)]
LeadHours = [math]::Round(2 + 30 * $rng.NextDouble(), 1)
}
}
$schema = @{
type = 'object'
properties = @{
Id = @{ type = 'integer'; title = 'Ticket number' }
Created = @{ type = 'string'; format = 'date'; description = 'The day the customer reported it.' }
Status = @{ type = 'string'; enum = @('Open', 'Waiting', 'Closed'); description = 'Open, waiting for the customer, or closed.' }
LeadHours = @{ type = 'number'; title = 'Lead time (h)'; externalDocs = @{ url = 'https://powershell-universal-components.eize.dev/pivot/fields/' } }
}
}
New-UDPivotReport -Id 'pivot-fields' -Data $rows -Schema $schema -Height 560px -TimeZone UTC `
-FieldDescriptions @{ Team = 'The team the ticket is assigned to.' } `
-FieldDefaults @{ LeadHours = @{ Agg = 'average'; Format = 'number:1'; Description = 'Hours from creation to closing.' } } `
-View @{ rows = @('Team'); cols = @('Status'); values = @(@{ field = 'LeadHours'; agg = 'average'; format = 'number:1' }) }
-FieldDefaults¶
New-UDPivotReport -Id 'tickets' -Data $tickets -FieldDefaults @{
LeadHours = @{ Label = 'Lead time (h)'; Description = 'Hours from creation to closing; empty while the ticket is open.'
Href = 'https://wiki.example.com/tickets/lead-time'; Agg = 'average'; Format = 'number:1' }
Priority = @{ Order = 'P1', 'P2', 'P3', 'P4'; Description = 'P1 is the most urgent.' }
Ticket = @{ Agg = 'count'; DrillFormat = 'text' }
}
| Key | What | Checked on the server |
|---|---|---|
Label |
the name shown for the field everywhere: field list, zones, headers, value names ("Average of Lead time (h)"), drill-down, chart, exports | text, cut at 120 characters |
Description |
plain text behind the "i"; screen readers hear it with the field | text, cut at 1000 characters |
Href |
a "Learn more" link under the description | an absolute https:// link without user name or password, at most 2000 characters |
Order |
the field's own order of items on Rows and Columns ("Custom order" in its menu); items not listed follow A to Z | a list of at most 1000 values |
Agg, Format |
aggregator and format when the field is put into Values (average, number:1, currency:EUR, ...) |
names only |
DrillFormat |
the format of the field's values in the drill-down: text shows an id as it is (40072, not 40,072), date a date without its time |
a name |
Type |
the type the column is read as: string, number, date, boolean (numbers that arrive as text, dates as numbers) |
one of these |
Anything else throws an error that names the field and the key: an unknown key, a number where text
belongs, an http:// or javascript: link. Two fields with the same label are told apart by their
names in parentheses. The report shows descriptions as text, never as HTML.
Readers can switch the "i" off in Options, Help ("Show field descriptions"); -FieldInfo Off
or -FieldInfo On decides it for them, and -ZoneInfo does the same for the "i" next to the zone
titles that explains Rows, Columns, Values and Filters.
-FieldDescriptions and SQL Server column descriptions¶
-FieldDescriptions takes field name to text, for example the descriptions a database already has.
In SQL Server they are the extended property MS_Description of each column (what SQL Server
Management Studio shows as a column's Description). Get-UDPivotSqlColumnDescription reads them for one
table or view and returns the hashtable -FieldDescriptions takes:
$connectionString = 'Server=sql01;Database=ServiceDesk;Integrated Security=true;TrustServerCertificate=true'
$tickets = Invoke-Sqlcmd -ConnectionString $connectionString -Query 'SELECT Ticket, Created, Team, Priority, LeadHours FROM dbo.Tickets'
$descriptions = Get-UDPivotSqlColumnDescription -ConnectionString $connectionString -Table 'dbo.Tickets'
New-UDPivotReport -Id 'tickets' -Data $tickets -FieldDescriptions $descriptions
With -ConnectionString the function opens its own connection with Microsoft.Data.SqlClient when that
is loaded (the SqlServer module loads it), else with System.Data.SqlClient, which PowerShell 7 ships; the
module needs neither to load. -Connection takes a connection your app has open already (any ADO.NET
provider for SQL Server) and leaves it open. Put the call where the data is read (the New-UDDynamic
of the report, or a cached variable), not into an event.
It sends one query, with the table name as a parameter of OBJECT_ID():
SELECT c.name AS ColumnName, CAST(ep.value AS nvarchar(4000)) AS Description
FROM sys.columns AS c
INNER JOIN sys.extended_properties AS ep
ON ep.class = 1 AND ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = @Name
WHERE c.object_id = OBJECT_ID(@Table)
ORDER BY c.column_id;
sys.extended_properties has one row per extended property; for class 1 (object or column) major_id
is the object's id and minor_id the column's id
(sys.extended_properties,
sys.columns).
SQL Server shows an account only the properties of objects it has permissions on; columns without a
description are left out. -PropertyName reads another extended property than MS_Description.
The same query works without the function, for example with Invoke-Sqlcmd: replace @Table and
@Name with literals you control (never with input from a reader) and turn the rows into a hashtable:
$descriptions = @{}
foreach ($row in Invoke-Sqlcmd -ConnectionString $connectionString -Query $query) {
if ($row.Description -isnot [System.DBNull]) { $descriptions[$row.ColumnName] = $row.Description }
}
-Schema: JSON Schema and OpenAPI¶
Data from a REST API usually has a schema. -Schema reads the row's properties from it: title
becomes the label, description the description, externalDocs.url the link (https only), type and
format the type (integer, number, date, date-time), enum the order of the items.
# a JSON Schema of one row (or of an array of rows)
New-UDPivotReport -Data $rows -Schema @{ properties = @{ LeadHours = @{ title = 'Lead time (h)'; type = 'number' } } }
# an OpenAPI 3 or Swagger 2 document: -SchemaName names the schema of a row
New-UDPivotReport -Data (Invoke-RestMethod 'https://api.example.com/tickets') -Schema 'https://api.example.com/openapi.json' -SchemaName 'Ticket'
New-UDPivotReport -Data $rows -Schema (Join-Path $Repository 'schemas/tickets.json')
-Schema takes a hashtable or an object, JSON text, a file path, or an https:// URL. A URL is
fetched by the server, never by the browser: once per PowerShell runspace every 10 minutes, with a
timeout of 15 seconds and at most 1 MB; other address schemes (http://, file://) are refused. The
schema must be one JSON object of at most 1 MB and 64 levels. -SchemaName is a name in
components.schemas (definitions in Swagger 2) or a local reference such as
#/components/schemas/Ticket.
The server only checks the size and that it is JSON and passes the schema on; pivotkit-core's
fieldDefaultsFromSchema reads it in the browser. It follows only references inside the document,
fetches and runs nothing, caps every text, and removes characters that would make a text read as
another (bidi controls, zero-width characters). What it cannot read - a reference to another document,
a cycle, a cap - is left out and named in the browser's console; the report works without it. A large
OpenAPI document travels to the browser with every page: for a big API, save the schema of the row as
a file and pass that.
Next: Views, Templates, view files and roles, Events, security and limits, Reference.