Article
· Oct 28 2m read

Writing a user-defined aggregate function in IRIS - example: Median

User-defined aggregate functions have been supported in IRIS since 2021.1.0. I'd wished upon a star for these years ago before finding a secret hacky way to override MAX and MIN in a custom datatype, but didn't get a chance to actually try one out until today. I thought it was an interesting experience/example - the question of how to get a Median in IRIS SQL came up once before - so I'm sharing it here without too much further comment.

One caveat: UDAFs don't have the nice object/SQL parity that other types of functions do, so you actually need to run SQL to define the aggregate function (helpfully wrapped in a classmethod in the below example). Compiling the class alone isn't enough.

/// Class implementing a Median aggregate function for IRIS SQL
Class DC.Demo.Median
{

/// Returns a new global ref in IRISTEMP to use to store intermediate results
ClassMethod Initialize() As %String [ PublicList = ref, SqlProc ]
{
	New ref
	Set ref = $Name(^IRIS.Temp.UDAF.Median($Increment(^IRIS.Temp.UDAF.Median)))
	Set @ref = 0
	Quit ref
}

/// Updates temp global for a single record
ClassMethod Iterate(ref As %String, value As %Numeric) As %String [ SqlProc ]
{
	If (value '= "") {
		Do $Increment(@ref)
		Do $Increment(@ref@(+value))
	}
	Quit ref
}

/// Finds the actual median (possibly an average of the two middle values)
ClassMethod Finalize(ref As %String) As %Numeric [ SqlProc ]
{
	Set median = ""
	Set total = @ref
	Set position1 = (total+1)\2
	Set position2 = (total+2)\2
	Set val1 = ""
	Set val2 = ""
	Set reached = 0
	Set key = ""
	For {
		Set key = $Order(@ref@(key),1,refCount)
		Quit:key=""
		set reached = reached + refCount
		if (reached >= position1) && (val1 = "") {
			Set val1 = key
		}
		if (reached >= position2) && (val2 = "") {
			Set val2 = key
		}
		If (val1 '= "") && (val2 '= "") {
			Set median = (val1+val2)/2
			Quit
		}
	}
	Kill @ref
	Quit median
}

/// To actually define the UDAF from an SQL perspective, call this classmethod.
ClassMethod Define()
{
	// Drop the function in case something has changed
	&sql(DROP AGGREGATE DC_Demo.Median)
	&sql(CREATE AGGREGATE DC_Demo.Median(arg NUMERIC) RETURNS NUMERIC
	   INITIALIZE WITH DC_Demo.Median_Initialize
	   ITERATE WITH DC_Demo.Median_Iterate
	   FINALIZE WITH DC_Demo.Median_Finalize)
	$$$ThrowSQLIfError(SQLCODE,%msg)
}

}

Hopefully this helps someone!

Discussion (0)1
Log in or sign up to continue