ResponseAggregateBatch
in package
Collects every aggregate needed for survey statistics and executes them as conditional aggregates over a single scan of the responses table.
Usage is two-phased:
- Registration: countValue()/countBlank()/countNonEmpty()/countTotal() each return an alias for the requested aggregate (deduplicated).
- execute() runs the merged SELECT (chunked only when the expression list is very large), after which value($alias) returns the count.
Table of Contents
Constants
- MAX_EXPRESSIONS_PER_QUERY = 1500
- Hard cap of SELECT expressions per query to bound per-row CASE cost
- FIELD_SEPARATOR = "\x1e"
- Separator joining the columns of a multi-field aggregate into one field key
- KIND_ANY_NON_EMPTY = 'anyNonEmpty'
- KIND_BLANK = 'blank'
- KIND_JSON_ELEMENT = 'jsonElement'
- KIND_MAX = 'max'
- KIND_MIN = 'min'
- KIND_NON_EMPTY = 'nonEmpty'
- KIND_NUMERIC = 'numeric'
- KIND_SUM = 'sum'
- KIND_SUM_SQUARES = 'sumSquares'
- KIND_TOTAL = 'total'
- KIND_VALUE = 'value'
- MAX_MEDIANS_PER_QUERY = 100
- NUMERIC_PATTERN = '^-?[0-9]*\\.?[0-9]+$'
- Plain decimal test for numeric answers stored in text columns
Properties
- $aliasIndex : array<string, string>
- $executed : bool
- $filters : StatisticsResponseFilters|null
- $medianRequests : array<string, array{field: string, countAlias: string}>
- Median cannot be expressed as a one-scan conditional aggregate, so these run as one ordered LIMIT/OFFSET query per field after the main pass.
- $requests : array<string, array{kind: string, field: string, value: string}>
- $results : array<string, int|float>
- $surveyId : int
Methods
- __construct() : mixed
- countAnyNonEmpty() : string
- Count of rows where at least one of the given columns is neither NULL nor empty
- countBlank() : string
- Count of rows where the column is NULL or empty.
- countJsonArrayValue() : string
- Count of rows where the JSON-array column holds the given value at the given zero-based position (rankings store the whole answer as one JSON array of codes, the element index being the rank).
- countNonEmpty() : string
- Count of rows where the column is neither NULL nor empty.
- countNumeric() : string
- Count of rows whose cell holds a numeric value. For numeric columns this equals countNonEmpty(); for text columns non-numeric cells are excluded.
- countTotal() : string
- Total row count (with the response filters applied).
- countValue() : string
- Count of rows where the column equals the given value.
- execute() : void
- Execute all registered aggregates over the responses table.
- isExecuted() : bool
- maxValue() : string
- Largest numeric value in a column (empty cells ignored).
- medianValue() : string
- minValue() : string
- Smallest numeric value in a column (empty cells ignored).
- sumSquares() : string
- Sum of the squared numeric values in a column (non-numeric/empty cells count as 0). Combined with sumValues() and countNonEmpty() this yields the population variance/standard deviation without a second scan.
- sumValues() : string
- Sum of the numeric values in a column (non-numeric/empty cells count as 0). Combined with countNonEmpty() this yields a column mean. The result preserves up to 4 decimal places, so fractional answers (e.g. 1.5) are not lost.
- value() : int|float
- Resolved value for a previously registered alias (0 before execute()).
- buildExpression() : string
- buildMedianSelect() : string
- buildSearchConditions() : array<string|int, string>
- One condition group per search term: the term must appear (case-insensitively) in at least one free-text answer column; the terms themselves combine with AND. A survey without any text answer column cannot match a term, so the whole filter becomes FALSE.
- buildWhere() : string
- executeMedians() : void
- Median via an ordered LIMIT/OFFSET sub-select: the middle value, or the mean of the two middle values for even counts. There is no portable single-scan SQL median, but the per-field sub-selects are merged into chunked UNION ALL statements so resolving N fields (e.g. every cell of an array question) does not cost N round trips.
- getDb() : CDbConnection
- getSearchableColumns() : array<string|int, string>
- isJsonColumn() : bool
- jsonArrayElementCheck() : string
- Predicate testing the element at a zero-based position of a JSON-array column against a value. Extracting in SQL keeps per-rank counts inside the shared single scan instead of pulling every row into PHP. The merged query must survive cells that are not valid JSON (encrypted rankings live in plain text columns): MySQL/MSSQL guard per row with JSON_VALID()/ISJSON(); Postgres only gets the ->> operator when the column really is json/jsonb, because on a text column the operator itself is a hard SQL error.
- nonEmptyCheck() : string
- Answered-cell predicate for a column. Numeric response columns (e.g.
- numericCellCheck() : string
- Castable-cell predicate guarding every CAST(... AS DECIMAL). Numeric answers of array questions live in text columns which may hold non-numeric junk (API-submitted or legacy data); casting that fails the whole query on Postgres and coerces to 0 (skewing sums) on MySQL, so text columns get a per-driver numeric test.
- register() : string
Constants
MAX_EXPRESSIONS_PER_QUERY
Hard cap of SELECT expressions per query to bound per-row CASE cost
public
mixed
MAX_EXPRESSIONS_PER_QUERY
= 1500
FIELD_SEPARATOR
Separator joining the columns of a multi-field aggregate into one field key
private
mixed
FIELD_SEPARATOR
= "\x1e"
KIND_ANY_NON_EMPTY
private
mixed
KIND_ANY_NON_EMPTY
= 'anyNonEmpty'
KIND_BLANK
private
mixed
KIND_BLANK
= 'blank'
KIND_JSON_ELEMENT
private
mixed
KIND_JSON_ELEMENT
= 'jsonElement'
KIND_MAX
private
mixed
KIND_MAX
= 'max'
KIND_MIN
private
mixed
KIND_MIN
= 'min'
KIND_NON_EMPTY
private
mixed
KIND_NON_EMPTY
= 'nonEmpty'
KIND_NUMERIC
private
mixed
KIND_NUMERIC
= 'numeric'
KIND_SUM
private
mixed
KIND_SUM
= 'sum'
KIND_SUM_SQUARES
private
mixed
KIND_SUM_SQUARES
= 'sumSquares'
KIND_TOTAL
private
mixed
KIND_TOTAL
= 'total'
KIND_VALUE
private
mixed
KIND_VALUE
= 'value'
MAX_MEDIANS_PER_QUERY
private
mixed
MAX_MEDIANS_PER_QUERY
= 100
NUMERIC_PATTERN
Plain decimal test for numeric answers stored in text columns
private
mixed
NUMERIC_PATTERN
= '^-?[0-9]*\\.?[0-9]+$'
Properties
$aliasIndex
private
array<string, string>
$aliasIndex
= []
dedup key => alias
$executed
private
bool
$executed
= false
$filters
private
StatisticsResponseFilters|null
$filters
$medianRequests
Median cannot be expressed as a one-scan conditional aggregate, so these run as one ordered LIMIT/OFFSET query per field after the main pass.
private
array<string, array{field: string, countAlias: string}>
$medianRequests
= []
alias => request
$requests
private
array<string, array{kind: string, field: string, value: string}>
$requests
= []
alias => request
$results
private
array<string, int|float>
$results
= []
alias => count, or fractional sum for KIND_SUM
$surveyId
private
int
$surveyId
Methods
__construct()
public
__construct(int $surveyId[, StatisticsResponseFilters|null $filters = null ]) : mixed
Parameters
- $surveyId : int
- $filters : StatisticsResponseFilters|null = null
countAnyNonEmpty()
Count of rows where at least one of the given columns is neither NULL nor empty
public
countAnyNonEmpty(array<string|int, string> $fields) : string
Parameters
- $fields : array<string|int, string>
Return values
stringcountBlank()
Count of rows where the column is NULL or empty.
public
countBlank(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringcountJsonArrayValue()
Count of rows where the JSON-array column holds the given value at the given zero-based position (rankings store the whole answer as one JSON array of codes, the element index being the rank).
public
countJsonArrayValue(string $field, int $position, string $value) : string
Parameters
- $field : string
- $position : int
- $value : string
Return values
stringcountNonEmpty()
Count of rows where the column is neither NULL nor empty.
public
countNonEmpty(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringcountNumeric()
Count of rows whose cell holds a numeric value. For numeric columns this equals countNonEmpty(); for text columns non-numeric cells are excluded.
public
countNumeric(string $field[, bool $numericColumn = false ]) : string
Use as the denominator of mean/variance so junk cells cannot skew them.
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringcountTotal()
Total row count (with the response filters applied).
public
countTotal() : string
Return values
stringcountValue()
Count of rows where the column equals the given value.
public
countValue(string $field, string $value) : string
Parameters
- $field : string
- $value : string
Return values
stringexecute()
Execute all registered aggregates over the responses table.
public
execute() : void
isExecuted()
public
isExecuted() : bool
Return values
boolmaxValue()
Largest numeric value in a column (empty cells ignored).
public
maxValue(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringmedianValue()
public
medianValue(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringminValue()
Smallest numeric value in a column (empty cells ignored).
public
minValue(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringsumSquares()
Sum of the squared numeric values in a column (non-numeric/empty cells count as 0). Combined with sumValues() and countNonEmpty() this yields the population variance/standard deviation without a second scan.
public
sumSquares(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringsumValues()
Sum of the numeric values in a column (non-numeric/empty cells count as 0). Combined with countNonEmpty() this yields a column mean. The result preserves up to 4 decimal places, so fractional answers (e.g. 1.5) are not lost.
public
sumValues(string $field[, bool $numericColumn = false ]) : string
Parameters
- $field : string
- $numericColumn : bool = false
Return values
stringvalue()
Resolved value for a previously registered alias (0 before execute()).
public
value(string $alias) : int|float
Counts are integers; KIND_SUM aggregates may be fractional.
Parameters
- $alias : string
Return values
int|floatbuildExpression()
private
buildExpression(CDbConnection $db, array<string|int, mixed> $request) : string
Parameters
- $db : CDbConnection
- $request : array<string|int, mixed>
Return values
stringbuildMedianSelect()
private
buildMedianSelect(CDbConnection $db, string $table, array<string|int, mixed> $request, int $count, string $alias) : string
Parameters
- $db : CDbConnection
- $table : string
- $request : array<string|int, mixed>
- $count : int
- $alias : string
Return values
stringbuildSearchConditions()
One condition group per search term: the term must appear (case-insensitively) in at least one free-text answer column; the terms themselves combine with AND. A survey without any text answer column cannot match a term, so the whole filter becomes FALSE.
private
buildSearchConditions(array<string|int, string> $terms) : array<string|int, string>
Parameters
- $terms : array<string|int, string>
Return values
array<string|int, string>buildWhere()
private
buildWhere() : string
Return values
stringexecuteMedians()
Median via an ordered LIMIT/OFFSET sub-select: the middle value, or the mean of the two middle values for even counts. There is no portable single-scan SQL median, but the per-field sub-selects are merged into chunked UNION ALL statements so resolving N fields (e.g. every cell of an array question) does not cost N round trips.
private
executeMedians(CDbConnection $db, string $table) : void
Parameters
- $db : CDbConnection
- $table : string
getDb()
private
getDb() : CDbConnection
Return values
CDbConnectiongetSearchableColumns()
private
getSearchableColumns() : array<string|int, string>
Return values
array<string|int, string>isJsonColumn()
private
isJsonColumn(string $field) : bool
Parameters
- $field : string
Return values
booljsonArrayElementCheck()
Predicate testing the element at a zero-based position of a JSON-array column against a value. Extracting in SQL keeps per-rank counts inside the shared single scan instead of pulling every row into PHP. The merged query must survive cells that are not valid JSON (encrypted rankings live in plain text columns): MySQL/MSSQL guard per row with JSON_VALID()/ISJSON(); Postgres only gets the ->> operator when the column really is json/jsonb, because on a text column the operator itself is a hard SQL error.
private
jsonArrayElementCheck(CDbConnection $db, string $field, int $position, string $value) : string
Parameters
- $db : CDbConnection
- $field : string
- $position : int
- $value : string
Return values
stringnonEmptyCheck()
Answered-cell predicate for a column. Numeric response columns (e.g.
private
nonEmptyCheck(CDbConnection $db, string $field, bool $numeric) : string
numerical input's DECIMAL column) must not be compared to '': Postgres rejects the comparison outright and MySQL coerces '' to 0, which would misclassify legitimate 0 answers as blank — for those, blank is NULL.
Parameters
- $db : CDbConnection
- $field : string
- $numeric : bool
Return values
stringnumericCellCheck()
Castable-cell predicate guarding every CAST(... AS DECIMAL). Numeric answers of array questions live in text columns which may hold non-numeric junk (API-submitted or legacy data); casting that fails the whole query on Postgres and coerces to 0 (skewing sums) on MySQL, so text columns get a per-driver numeric test.
private
numericCellCheck(CDbConnection $db, string $field, bool $numeric) : string
Parameters
- $db : CDbConnection
- $field : string
- $numeric : bool
Return values
stringregister()
private
register(string $kind, string $field, string $value[, bool $numeric = false ]) : string
Parameters
- $kind : string
- $field : string
- $value : string
- $numeric : bool = false