diff --git a/src/columnar.h b/src/columnar.h index 80eddb8..456b57f 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -173,6 +173,8 @@ extern bool columnar_enable_bloom_filter; /* bloom equality skipping (I7) */ /* Phase 6 GUCs (spec 8.3) */ extern bool columnar_enable_vectorization; /* vectorized aggregate path */ +extern bool columnar_enable_group_vectorization; /* GROUP BY vectorized agg (#289) */ +extern int columnar_groupagg_max_groups; /* plan-time group-count cap (#289) */ extern bool columnar_enable_read_stream; /* stream/prefetch block reads (PG17+) */ extern bool columnar_enable_index_only_scan; /* allow index-only scans (gap 28) */ extern bool columnar_enable_projection_scan; /* scan a covering projection (gap 26) */ @@ -772,6 +774,7 @@ extern void ColumnarCustomScanInit(void); */ extern const CustomScanMethods columnar_scan_methods; extern Node *ColumnarCreateAggScanState(CustomScan *cscan); +extern Node *ColumnarCreateGroupAggScanState(CustomScan *cscan); /* * Build the chunk-group skip scan keys from a plan's restriction clauses. diff --git a/src/columnar_customscan.c b/src/columnar_customscan.c index 31883ad..c28f3c0 100644 --- a/src/columnar_customscan.c +++ b/src/columnar_customscan.c @@ -703,7 +703,17 @@ static Node * ColumnarCreateScanState(CustomScan *cscan) { if (cscan->scan.scanrelid == 0) + { + /* + * Both upper aggregate paths are scanrelid==0 custom scans sharing these + * registered methods. The grouped path (#289) carries a length-5 + * custom_private (rti, quals, relid, keys, output map); the ungrouped + * path carries length 3. + */ + if (list_length(cscan->custom_private) == 5) + return ColumnarCreateGroupAggScanState(cscan); return ColumnarCreateAggScanState(cscan); + } return ColumnarCreateBaseScanState(cscan); } diff --git a/src/columnar_tableam.c b/src/columnar_tableam.c index 034ee5e..6306eed 100644 --- a/src/columnar_tableam.c +++ b/src/columnar_tableam.c @@ -2244,6 +2244,28 @@ _PG_init(void) 0, NULL, NULL, NULL); + DefineCustomBoolVariable("pgcolumnar.enable_group_vectorization", + "Use the vectorized aggregate fast path for GROUP BY queries.", + NULL, + &columnar_enable_group_vectorization, + false, + PGC_USERSET, + 0, + NULL, NULL, NULL); + + DefineCustomIntVariable("pgcolumnar.groupagg_max_groups", + "Cap on the actual group count the grouped vectorized " + "aggregate builds before it stops with an error.", + "Enforced at execution against the real number of groups, " + "not the planner's estimate: over the cap the query errors " + "rather than falling back, since the plan is fixed by then. " + "Raise it, or turn off pgcolumnar.enable_group_vectorization.", + &columnar_groupagg_max_groups, + 1000000, 1, INT_MAX, + PGC_USERSET, + 0, + NULL, NULL, NULL); + DefineCustomBoolVariable("pgcolumnar.enable_bloom_filter", "Skip chunk groups on equality using per-chunk bloom filters.", NULL, diff --git a/src/columnar_vector.c b/src/columnar_vector.c index 9d204f5..bdd9f9f 100644 --- a/src/columnar_vector.c +++ b/src/columnar_vector.c @@ -61,6 +61,9 @@ #include "optimizer/planner.h" #include "optimizer/cost.h" #include "optimizer/restrictinfo.h" +#include "optimizer/tlist.h" +#include "access/sysattr.h" +#include "utils/selfuncs.h" #include "utils/builtins.h" #include "utils/snapmgr.h" #include "utils/datum.h" @@ -68,12 +71,23 @@ #include "utils/lsyscache.h" #include "access/stratnum.h" #include "access/tupmacs.h" +#include "common/hashfn.h" #include "utils/rel.h" #include "utils/typcache.h" /* GUC: use the vectorized aggregate path (spec 8.3 scan control) */ bool columnar_enable_vectorization = true; +/* + * GUC: extend the vectorized aggregate to GROUP BY (#289). Default off while the + * grouped path is built out incrementally; the ungrouped path is unaffected. + * groupagg_max_groups caps the plan-time group estimate the grouped path will + * accept, so a high-cardinality grouping routes to the spillable core HashAgg + * rather than this (non-spilling) path. + */ +bool columnar_enable_group_vectorization = false; +int columnar_groupagg_max_groups = 1000000; + /* ------------------------------------------------------------------------- * shared column-at-a-time filter * ------------------------------------------------------------------------- */ @@ -213,7 +227,18 @@ typedef enum ColumnarAggKind COLUMNAR_AGG_SUM_INT, COLUMNAR_AGG_AVG_INT, COLUMNAR_AGG_MIN, - COLUMNAR_AGG_MAX + COLUMNAR_AGG_MAX, + /* + * Extended kinds used by the grouped path (#289). The ungrouped + * metadata-fold path never produces these (its classifier still rejects + * int8/float/numeric sum/avg), so its switches never see them. + */ + COLUMNAR_AGG_SUM_INT8, /* sum(int8) -> numeric */ + COLUMNAR_AGG_SUM_FLOAT, /* sum(float4/float8) -> float8 */ + COLUMNAR_AGG_SUM_NUMERIC, /* sum(numeric) -> numeric */ + COLUMNAR_AGG_AVG_INT8, /* avg(int8) -> numeric */ + COLUMNAR_AGG_AVG_FLOAT, /* avg(float4/float8) -> float8 */ + COLUMNAR_AGG_AVG_NUMERIC /* avg(numeric) -> numeric */ } ColumnarAggKind; typedef struct ColumnarAggSpec @@ -233,6 +258,10 @@ typedef struct ColumnarAggSpec int64 sum; /* integer sum / avg sum */ bool sawValue; /* any non-null value contributed */ Datum extreme; /* min/max running value (in resultContext) */ + float8 fsum; /* float running sum (scan order, like float8_accum) */ + float8 fsxx; /* avg(float): Youngs-Cramer Sxx, for overflow parity */ + Datum nsum; /* numeric running total (in resultContext) */ + bool nsumSet; /* nsum initialized */ } ColumnarAggSpec; /* @@ -244,7 +273,8 @@ typedef struct ColumnarAggSpec * scalar fallback. */ static bool -columnar_classify_aggref(Aggref *agg, int expectedVarno, ColumnarAggSpec *spec) +columnar_classify_aggref(Aggref *agg, int expectedVarno, bool allowExtended, + ColumnarAggSpec *spec) { char *name; Oid nsp; @@ -309,7 +339,19 @@ columnar_classify_aggref(Aggref *agg, int expectedVarno, ColumnarAggSpec *spec) spec->kind = COLUMNAR_AGG_SUM_INT; return true; } - return false; /* int8->numeric, float, numeric: fall back */ + if (allowExtended) + { + if (spec->inputType == INT8OID) + spec->kind = COLUMNAR_AGG_SUM_INT8; + else if (spec->inputType == FLOAT4OID || spec->inputType == FLOAT8OID) + spec->kind = COLUMNAR_AGG_SUM_FLOAT; + else if (spec->inputType == NUMERICOID) + spec->kind = COLUMNAR_AGG_SUM_NUMERIC; + else + return false; + return true; + } + return false; /* ungrouped: int8/float/numeric fall back */ } if (strcmp(name, "avg") == 0) @@ -319,6 +361,18 @@ columnar_classify_aggref(Aggref *agg, int expectedVarno, ColumnarAggSpec *spec) spec->kind = COLUMNAR_AGG_AVG_INT; return true; } + if (allowExtended) + { + if (spec->inputType == INT8OID) + spec->kind = COLUMNAR_AGG_AVG_INT8; + else if (spec->inputType == FLOAT4OID || spec->inputType == FLOAT8OID) + spec->kind = COLUMNAR_AGG_AVG_FLOAT; + else if (spec->inputType == NUMERICOID) + spec->kind = COLUMNAR_AGG_AVG_NUMERIC; + else + return false; + return true; + } return false; } @@ -336,6 +390,116 @@ columnar_classify_aggref(Aggref *agg, int expectedVarno, ColumnarAggSpec *spec) return false; } +/* + * columnar_group_key_unsupported_walker + * Reject any node in a candidate GROUP BY key the grouped path cannot + * evaluate against a bare base-relation slot: aggregates, grouping-set + * constructs, window functions, sublinks/subplans, external parameters, + * and whole-row or system-column Vars. Everything the executor's ordinary + * expression machinery can evaluate from scan columns alone (Const, real + * Vars, OpExpr, FuncExpr, CoerceViaIO, CaseExpr, ...) is accepted, subject + * to the volatility and varno checks the caller also applies. + */ +static bool +columnar_group_key_unsupported_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + switch (nodeTag(node)) + { + case T_Var: + { + Var *var = (Var *) node; + + /* whole-row and system columns are not projectable here */ + if (var->varattno <= 0) + return true; + return false; + } + case T_Aggref: + case T_GroupingFunc: + case T_WindowFunc: + case T_SubLink: + case T_SubPlan: + case T_AlternativeSubPlan: + case T_Param: + return true; + default: + break; + } + + return expression_tree_walker(node, columnar_group_key_unsupported_walker, + context); +} + +/* + * columnar_classify_group_keys + * Decide whether every GROUP BY key can be computed and grouped by the + * grouped vectorized path, and if so return copies of the key expressions + * (original varnos, one per grouping column). A key must be computable from + * this relation alone, non-volatile, not set-returning, free of the node + * kinds above, and have both a hash function and an equality operator; a + * collatable key must use a deterministic collation, because grouping then + * matches the byte-exact semantics the scalar path would produce. Returns + * false (add no path, run the ordinary Agg) on anything unsupported. + */ +static bool +columnar_classify_group_keys(PlannerInfo *root, RelOptInfo *input_rel, + List **keysOut) +{ + Query *parse = root->parse; + List *keys = NIL; + ListCell *lc; + + foreach(lc, parse->groupClause) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); + Node *expr = get_sortgroupclause_expr(sgc, parse->targetList); + Oid type; + Oid coll; + TypeCacheEntry *tce; + + if (expr == NULL) + return false; + + /* + * Do NOT strip a wrapping RelabelType: it carries the cast's result type + * and collation. Stripping it would read the type/collation from the + * underlying value instead, which for an explicit COLLATE defeats the + * deterministic-collation check below and could group with the wrong + * equality. Group by the expression exactly as written. + */ + if (!bms_is_subset(pull_varnos(root, expr), input_rel->relids)) + return false; + if (contain_volatile_functions(expr)) + return false; + if (expression_returns_set(expr)) + return false; + if (columnar_group_key_unsupported_walker(expr, NULL)) + return false; + + type = exprType(expr); + coll = exprCollation(expr); + tce = lookup_type_cache(type, + TYPECACHE_HASH_PROC_FINFO | + TYPECACHE_EQ_OPR_FINFO); + if (!OidIsValid(tce->hash_proc_finfo.fn_oid)) + return false; + if (!OidIsValid(tce->eq_opr_finfo.fn_oid)) + return false; + if (OidIsValid(coll) && !ColumnarCollationIsDeterministic(coll)) + return false; + + keys = lappend(keys, copyObject(expr)); + } + + if (keys == NIL) + return false; + *keysOut = keys; + return true; +} + /* ------------------------------------------------------------------------- * vectorized aggregate: executor state * ------------------------------------------------------------------------- */ @@ -365,6 +529,85 @@ typedef struct ColumnarAggScanState static const CustomExecMethods columnar_agg_exec_methods; +/* ------------------------------------------------------------------------- + * grouped vectorized aggregate (#289): executor state + * + * Fires for SELECT , agg(col) ... [WHERE ...] GROUP BY over a + * single columnar relation. The reader (ColumnarReadNextRow) applies WHERE + * pushdown for group/vector skipping; each surviving row is rechecked against + * the full WHERE, its group keys are evaluated, and it is scattered into an + * open-addressing hash table whose per-group accumulators fold in scan order -- + * byte-identical to the scalar Agg the planner would otherwise run. Grouping + * uses each key type's own hash and equality functions, so -0.0/NaN, numeric + * scale, and deterministic-collation text all group exactly as core does. + * ------------------------------------------------------------------------- */ + +typedef struct ColumnarGroupKey +{ + Expr *expr; /* key expression (original varnos) */ + ExprState *exprState; /* evaluates it against the base slot */ + Oid type; + Oid collation; + int16 typlen; + bool byval; + FmgrInfo hashFn; /* type hash function */ + FmgrInfo eqFn; /* type equality operator function */ +} ColumnarGroupKey; + +typedef struct ColumnarGroupEntry +{ + uint32 hash; + bool used; + Datum *keys; /* nkeys key values, in keyContext */ + bool *keyNulls; /* nkeys null flags */ + ColumnarAggSpec *specs; /* naggs accumulators, in specContext */ +} ColumnarGroupEntry; + +typedef struct ColumnarGroupAggScanState +{ + CustomScanState css; + + Oid relid; /* base relation to scan */ + List *quals; /* WHERE clauses (original varnos) */ + Index scanrelid; /* their range-table index */ + + int nkeys; + ColumnarGroupKey *keys; + + int naggs; + ColumnarAggSpec *aggTemplate; /* classified once; copied per new group */ + + int nout; /* output tuple width */ + int *outMap; /* per output pos: >=0 key index, else agg -(v)-1 */ + + Bitmapset *projected; /* base columns the reader must return */ + TupleTableSlot *baseSlot; /* holds each read row for key/qual eval */ + ExprState *whereState; /* residual WHERE recheck, or NULL */ + + ColumnarGroupEntry *entries; /* open-addressing table (power-of-two) */ + int capacity; + int nGroups; + int maxGroups; /* GUC cap (planner guard) */ + + MemoryContext keyContext; /* copied key Datums */ + MemoryContext specContext; /* per-group specs + running min/max/numeric */ + MemoryContext hashContext; /* the entries array itself */ + + bool started; /* scan + build completed */ + int emitPos; /* next entry index to emit */ + + /* EXPLAIN */ + int npreds; + bool haveStats; + uint64 groupsRead; + uint64 groupsSkipped; + uint64 groupsTotal; +} ColumnarGroupAggScanState; + +static const CustomExecMethods columnar_groupagg_exec_methods; +static void ColumnarTryGroupAggPath(PlannerInfo *root, RelOptInfo *input_rel, + RelOptInfo *output_rel); + /* ------------------------------------------------------------------------- * vectorized aggregate: planning * ------------------------------------------------------------------------- */ @@ -431,13 +674,31 @@ ColumnarCreateUpperPaths(PlannerInfo *root, UpperRelationKind stage, if (!columnar_enable_vectorization || !columnar_enable_custom_scan) return; - /* plain, ungrouped aggregation only (spec 9) */ if (!parse->hasAggs) return; + + /* + * GROUP BY: try the grouped vectorized path (#289). It handles a plain + * grouped aggregate over one columnar relation, with an optional WHERE, and + * no grouping sets / HAVING / DISTINCT / window / SRF. Anything else, or an + * unsupported key or aggregate, adds no path and the ordinary Agg runs. + */ if (parse->groupClause != NIL || parse->groupingSets != NIL || parse->havingQual != NULL || parse->distinctClause != NIL || parse->hasWindowFuncs || parse->hasTargetSRFs) + { + if (columnar_enable_group_vectorization && + parse->groupClause != NIL && + parse->groupingSets == NIL && + parse->havingQual == NULL && + parse->distinctClause == NIL && + !parse->hasWindowFuncs && + !parse->hasTargetSRFs) + ColumnarTryGroupAggPath(root, input_rel, output_rel); return; + } + + /* plain, ungrouped aggregation only (spec 9) */ /* a single columnar base relation with no joins */ if (input_rel->reloptkind != RELOPT_BASEREL) @@ -468,7 +729,7 @@ ColumnarCreateUpperPaths(PlannerInfo *root, UpperRelationKind stage, if (!IsA(expr, Aggref)) return; if (!columnar_classify_aggref((Aggref *) expr, (int) input_rel->relid, - &specs[i])) + false, &specs[i])) return; i++; } @@ -624,6 +885,233 @@ ColumnarCreateUpperPaths(PlannerInfo *root, UpperRelationKind stage, add_path(output_rel, &cpath->path); } +/* + * ColumnarTryGroupAggPath + * Add a grouped vectorized aggregate path (#289) when the query is one we + * can answer exactly: a single columnar base relation, an optional WHERE, + * every output entry either a supported aggregate or a bare reference to a + * supported GROUP BY key, and an estimated group count within the cap. On + * anything unsupported it adds nothing and the ordinary Agg plan runs. + */ +static void +ColumnarTryGroupAggPath(PlannerInfo *root, RelOptInfo *input_rel, + RelOptInfo *output_rel) +{ + RangeTblEntry *rte; + Oid relid; + List *groupKeys = NIL; + List *outMap = NIL; + List *quals; + List *groupExprs; + ListCell *lc; + int naggs = 0; + int aggIdx = 0; + double dNumGroups; + Path *cheapest; + CustomPath *cpath; + + /* a single columnar base relation with no joins */ + if (input_rel->reloptkind != RELOPT_BASEREL) + return; + if (bms_membership(input_rel->relids) != BMS_SINGLETON) + return; + if (input_rel->relid == 0 || + input_rel->relid >= (Index) root->simple_rel_array_size) + return; + rte = root->simple_rte_array[input_rel->relid]; + if (rte == NULL || rte->rtekind != RTE_RELATION || + rte->relkind != RELKIND_RELATION) + return; + /* + * A legacy inheritance parent is a plain RELKIND_RELATION with rte->inh set; + * its children hold rows this single-relation scan would never see. Leave the + * whole tree to the ordinary Append + Agg plan. + */ + if (rte->inh) + return; + if (!OidIsValid(rte->relid) || !ColumnarIsColumnarRelation(rte->relid)) + return; + relid = rte->relid; + + /* every GROUP BY key must be one we can evaluate and group exactly */ + if (!columnar_classify_group_keys(root, input_rel, &groupKeys)) + return; + + /* + * Every output entry is either a supported aggregate or a bare reference to + * one of the group keys. An output expression built on top of a key (a + * function of a grouping column) is not handled here and forces the fallback. + * outMap records, per output position, the key index (>=0) or, encoded + * negative, the aggregate index in output order. + */ + foreach(lc, output_rel->reltarget->exprs) + { + Node *oexpr = (Node *) lfirst(lc); + + if (IsA(oexpr, Aggref)) + { + ColumnarAggSpec spec; + + if (!columnar_classify_aggref((Aggref *) oexpr, + (int) input_rel->relid, true, &spec)) + return; + outMap = lappend(outMap, makeInteger(-(aggIdx + 1))); + aggIdx++; + naggs++; + } + else + { + ListCell *kc; + int k = 0; + int found = -1; + + /* + * Match the output expression against a group key exactly as written + * (no RelabelType stripping): the classifier stores keys un-stripped + * too, and both come from the same target list, so equal() lines them + * up. An output built on top of a key (not a bare reference) matches + * nothing and forces the fallback. + */ + foreach(kc, groupKeys) + { + if (equal(oexpr, (Node *) lfirst(kc))) + { + found = k; + break; + } + k++; + } + if (found < 0) + return; + outMap = lappend(outMap, makeInteger(found)); + } + } + if (naggs == 0) + return; /* no aggregate: leave it to the ordinary plan */ + + /* + * estimate_num_groups only sizes the path's row estimate; it is deliberately + * NOT a gate. For an expression key such as date_trunc(...) the planner + * cannot estimate distinctness and returns a count near the input row count + * (here ~8.4M estimated against 48k actual), which would wrongly disable this + * path on exactly the large tables it helps. The unbounded-hash-table guard + * is the execution-time cap on the actual group count + * (pgcolumnar.groupagg_max_groups), which errors with guidance -- see + * columnar_groupagg_lookup -- rather than silently declining the feature. + */ + groupExprs = groupKeys; + dNumGroups = estimate_num_groups(root, groupExprs, + input_rel->rows > 0 ? input_rel->rows : 1.0, + NULL, NULL); + + /* + * Pseudoconstant (gating) quals -- e.g. WHERE (SELECT false) -- are one-time + * filters, not per-row predicates. The residual ExecQual recheck this path + * relies on runs per row and would never apply them, so a false gate would + * wrongly return rows. They are rare; leave such a query to the ordinary plan. + */ + foreach(lc, input_rel->baserestrictinfo) + { + if (lfirst_node(RestrictInfo, lc)->pseudoconstant) + return; + } + + /* + * WHERE is carried whole and rechecked per row, so correctness never depends + * on which clauses become scan keys; the scan keys only prune groups. + */ + quals = extract_actual_clauses(input_rel->baserestrictinfo, false); + + /* + * A WHERE clause referencing a system column or a whole-row Var cannot be + * answered from the projected data columns this path reads, so fall back + * rather than evaluate it against unset slot values. + */ + { + Bitmapset *whereAtts = NULL; + int m = -1; + + pull_varattnos((Node *) quals, input_rel->relid, &whereAtts); + while ((m = bms_next_member(whereAtts, m)) >= 0) + if (m + FirstLowInvalidHeapAttributeNumber <= 0) + return; + } + + /* + * Cost from a full columnar scan, not input_rel->cheapest_total_path: the + * cheapest overall path may be an index scan, but this node always performs a + * full columnar scan, so pricing it from an index scan would understate it. + * Take the cheapest non-index path (the columnar custom or sequential scan). + */ + cheapest = NULL; + foreach(lc, input_rel->pathlist) + { + Path *p = (Path *) lfirst(lc); + + if (p->pathtype == T_IndexScan || p->pathtype == T_IndexOnlyScan || + p->pathtype == T_BitmapHeapScan) + continue; + if (cheapest == NULL || p->total_cost < cheapest->total_cost) + cheapest = p; + } + if (cheapest == NULL) + cheapest = input_rel->cheapest_total_path; + if (cheapest == NULL) + return; + + cpath = makeNode(CustomPath); + cpath->path.pathtype = T_CustomScan; + cpath->path.parent = output_rel; + cpath->path.pathtarget = output_rel->reltarget; + cpath->path.param_info = NULL; + cpath->path.parallel_aware = false; + cpath->path.parallel_safe = false; + cpath->path.parallel_workers = 0; + cpath->path.rows = (dNumGroups < 1.0) ? 1.0 : dNumGroups; + + /* + * Price just above the scan the reader already does. The fold is one hash + * probe per row inside that scan, always cheaper than a separate Agg node's + * per-row advance over the same scan, so a negligible fixed bump keeps this + * reliably below the ordinary Agg-over-scan plan whenever the feature is + * enabled. This is an opt-in accelerator: when it is on it should be the + * plan, not a coin-flip against a HashAggregate whose cost is close. An + * earlier version charged per output group, which let autoanalyze flip the + * choice on large inputs -- so the node sometimes did not run at all. One row + * per group comes out only after the whole scan is folded, so there is no + * cheap partial start-up. + */ + { + Cost cost = cheapest->total_cost + cpu_tuple_cost; + + cpath->path.startup_cost = cost; + cpath->path.total_cost = cost; + } + cpath->path.pathkeys = NIL; + cpath->flags = 0; + cpath->custom_paths = NIL; +#if PG_VERSION_NUM >= 170000 + cpath->custom_restrictinfo = NIL; +#endif + + /* + * custom_private (length 5 marks the grouped path for the shared create-state + * dispatch): rti, WHERE quals, relid, group-key expressions, output map. The + * planner leaves custom_private untouched by setrefs, so the key and qual + * expressions keep their original varnos and evaluate against a base slot. + */ + cpath->custom_private = + list_make5(makeInteger((int) input_rel->relid), + copyObject(quals), + makeConst(OIDOID, -1, InvalidOid, sizeof(Oid), + ObjectIdGetDatum(relid), false, true), + groupKeys, + outMap); + cpath->methods = &columnar_agg_path_methods; + + add_path(output_rel, &cpath->path); +} + /* ------------------------------------------------------------------------- * vectorized aggregate: execution * ------------------------------------------------------------------------- */ @@ -653,7 +1141,7 @@ ColumnarCreateAggScanState(CustomScan *cscan) TargetEntry *tle = (TargetEntry *) lfirst(lc); /* classified successfully at plan time; -1 skips the varno check */ - (void) columnar_classify_aggref((Aggref *) tle->expr, -1, + (void) columnar_classify_aggref((Aggref *) tle->expr, -1, false, &state->specs[i]); i++; } @@ -730,6 +1218,38 @@ ColumnarBeginAggScan(CustomScanState *node, EState *estate, int eflags) table_close(rel, AccessShareLock); } +/* + * Running float additions that reproduce core's float4pl/float8pl exactly, + * including the overflow error: core raises "value out of range: overflow" when a + * finite + finite addition produces an infinity, and the grouped accumulators + * must do the same rather than silently carrying an Infinity the scalar Agg would + * never have produced. Same compiler and flags as the server (PGXS), so the + * arithmetic is bit-for-bit what float4pl/float8pl compute. + */ +static inline float8 +columnar_float8_pl(float8 a, float8 b) +{ + float8 r = a + b; + + if (unlikely(isinf(r)) && !isinf(a) && !isinf(b)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value out of range: overflow"))); + return r; +} + +static inline float4 +columnar_float4_pl(float4 a, float4 b) +{ + float4 r = a + b; + + if (unlikely(isinf(r)) && !isinf(a) && !isinf(b)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value out of range: overflow"))); + return r; +} + /* * columnar_apply_one * Fold one value (or a null) into an aggregate accumulator. This is the @@ -737,7 +1257,7 @@ ColumnarBeginAggScan(CustomScanState *node, EState *estate, int eflags) * the run path's fallback and min/max handling. */ static void -columnar_apply_one(ColumnarAggScanState *state, ColumnarAggSpec *spec, +columnar_apply_one(MemoryContext resultContext, ColumnarAggSpec *spec, Datum val, bool isnull) { switch (spec->kind) @@ -775,6 +1295,111 @@ columnar_apply_one(ColumnarAggScanState *state, ColumnarAggSpec *spec, } break; + case COLUMNAR_AGG_SUM_FLOAT: + if (!isnull) + { + /* + * sum(real) accumulates in real (core float4pl) and returns real; + * sum(double precision) accumulates in float8 (core float8pl). Core's + * sum has a strict transfn with a null initial state, so it assigns + * the FIRST non-null value directly -- preserving a signed zero -- + * rather than adding it to +0.0. Match that, then fold the rest with + * float4pl/float8pl semantics. For the float4 case fsum always holds + * a value exactly representable as float4, so the round-trip through + * (float4) reproduces float4pl step for step. + */ + if (!spec->sawValue) + spec->fsum = (spec->inputType == FLOAT4OID) + ? (float8) DatumGetFloat4(val) + : DatumGetFloat8(val); + else if (spec->inputType == FLOAT4OID) + spec->fsum = (float8) columnar_float4_pl((float4) spec->fsum, + DatumGetFloat4(val)); + else + spec->fsum = columnar_float8_pl(spec->fsum, + DatumGetFloat8(val)); + spec->sawValue = true; + } + break; + + case COLUMNAR_AGG_AVG_FLOAT: + if (!isnull) + { + /* + * avg is Sx/N, but core's float4_accum/float8_accum keep the + * Youngs-Cramer Sxx as well and raise overflow when EITHER Sx or Sxx + * goes finite+finite -> inf. Track Sxx only to reproduce that error + * exactly (Sxx can overflow on finite inputs while Sx stays finite); + * the returned average is Sx/N and is unaffected by it. + */ + float8 v = (spec->inputType == FLOAT4OID) + ? (float8) DatumGetFloat4(val) + : DatumGetFloat8(val); + float8 n = (float8) spec->count; /* N before this value */ + float8 newN = n + 1.0; + float8 newSx = spec->fsum + v; + + if (spec->count > 0) + { + float8 tmp = v * newN - newSx; + float8 newSxx = spec->fsxx + tmp * tmp / (n * newN); + + if ((isinf(newSx) || isinf(newSxx)) && + !isinf(spec->fsum) && !isinf(v)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value out of range: overflow"))); + spec->fsxx = newSxx; + } + spec->fsum = newSx; + spec->count++; + spec->sawValue = true; + } + break; + + case COLUMNAR_AGG_SUM_INT8: + case COLUMNAR_AGG_AVG_INT8: + case COLUMNAR_AGG_SUM_NUMERIC: + case COLUMNAR_AGG_AVG_NUMERIC: + if (!isnull) + { + bool is_int8 = (spec->kind == COLUMNAR_AGG_SUM_INT8 || + spec->kind == COLUMNAR_AGG_AVG_INT8); + MemoryContext old = MemoryContextSwitchTo(resultContext); + Datum nv = is_int8 + ? DirectFunctionCall1(int8_numeric, val) + : val; + + /* + * Keep live memory O(groups), not O(rows): free the previous + * running sum and each per-row int8->numeric intermediate. Without + * this every scanned row leaked one or two numerics into the + * per-group context, which is not reset until end of scan -- O(rows) + * on exactly the 100M-row shape this path targets. + */ + if (!spec->nsumSet) + { + /* int8: nv is freshly allocated and becomes the running sum; + * numeric: nv is borrowed from the reader, so copy it. */ + spec->nsum = is_int8 ? nv : datumCopy(nv, false, -1); + spec->nsumSet = true; + } + else + { + Datum newsum = DirectFunctionCall2(numeric_add, + spec->nsum, nv); + + pfree(DatumGetPointer(spec->nsum)); + spec->nsum = newsum; + if (is_int8) + pfree(DatumGetPointer(nv)); + } + spec->count++; + spec->sawValue = true; + MemoryContextSwitchTo(old); + } + break; + case COLUMNAR_AGG_MIN: case COLUMNAR_AGG_MAX: if (!isnull) @@ -789,14 +1414,20 @@ columnar_apply_one(ColumnarAggScanState *state, ColumnarAggSpec *spec, FunctionCall2Coll(&spec->cmpFn, spec->collation, val, spec->extreme)); + /* + * On a tie, take the later value, matching core's larger and + * smaller comparators (transfn(state, newval) returns newval + * when equal). Observable for numeric 1.0 vs 1.00, which tie + * by value but differ in display scale. + */ take = (spec->kind == COLUMNAR_AGG_MIN) - ? (cmp < 0) : (cmp > 0); + ? (cmp <= 0) : (cmp >= 0); } if (take) { MemoryContext old = - MemoryContextSwitchTo(state->resultContext); + MemoryContextSwitchTo(resultContext); if (spec->sawValue && !spec->byval) pfree(DatumGetPointer(spec->extreme)); @@ -863,18 +1494,57 @@ columnar_agg_finalize(ColumnarAggSpec *spec, bool *isnull) return DirectFunctionCall2(numeric_div, sumd, cntd); } - case COLUMNAR_AGG_MIN: - case COLUMNAR_AGG_MAX: + case COLUMNAR_AGG_SUM_FLOAT: if (!spec->sawValue) { *isnull = true; return (Datum) 0; } - return spec->extreme; - } + /* sum(real) -> real; sum(double precision) -> double precision */ + return (spec->inputType == FLOAT4OID) + ? Float4GetDatum((float4) spec->fsum) + : Float8GetDatum(spec->fsum); - *isnull = true; - return (Datum) 0; + case COLUMNAR_AGG_AVG_FLOAT: + if (spec->count == 0) + { + *isnull = true; + return (Datum) 0; + } + return Float8GetDatum(spec->fsum / (float8) spec->count); + + case COLUMNAR_AGG_SUM_INT8: + case COLUMNAR_AGG_SUM_NUMERIC: + if (!spec->nsumSet) + { + *isnull = true; + return (Datum) 0; + } + return spec->nsum; + + case COLUMNAR_AGG_AVG_INT8: + case COLUMNAR_AGG_AVG_NUMERIC: + if (spec->count == 0 || !spec->nsumSet) + { + *isnull = true; + return (Datum) 0; + } + return DirectFunctionCall2(numeric_div, spec->nsum, + DirectFunctionCall1(int8_numeric, + Int64GetDatum(spec->count))); + + case COLUMNAR_AGG_MIN: + case COLUMNAR_AGG_MAX: + if (!spec->sawValue) + { + *isnull = true; + return (Datum) 0; + } + return spec->extreme; + } + + *isnull = true; + return (Datum) 0; } /* @@ -1159,6 +1829,15 @@ columnar_fill_native_metadata_agg(ColumnarAggScanState *state, int *ndirty) MemoryContextSwitchTo(oldcx); } break; + default: + + /* + * The extended int8/float/numeric sum/avg kinds are produced + * only for the grouped path; the ungrouped classifier rejects + * them, so they never reach this metadata fold. + */ + Assert(false); + break; } } } @@ -1212,10 +1891,10 @@ columnar_native_scan_agg(ColumnarAggScanState *state, ColumnarAggSpec *spec = &state->specs[a]; if (spec->attidx >= 0) - columnar_apply_one(state, spec, values[spec->attidx], - nulls[spec->attidx]); + columnar_apply_one(state->resultContext, spec, + values[spec->attidx], nulls[spec->attidx]); else - columnar_apply_one(state, spec, (Datum) 0, true); + columnar_apply_one(state->resultContext, spec, (Datum) 0, true); } } ColumnarEndRead(rs); @@ -1306,6 +1985,17 @@ ColumnarReScanAggScan(CustomScanState *node) spec->sum = 0; spec->sawValue = false; spec->extreme = (Datum) 0; + /* + * Also clear the running float/numeric accumulators. resultContext was + * just reset, so nsum's storage is gone; leaving nsumSet true would make + * the next scan add to (and free) a dangling pointer. The ungrouped path + * does not classify these extended kinds today, but reset every field so a + * rescan is safe if it ever does. + */ + spec->fsum = 0; + spec->fsxx = 0; + spec->nsum = (Datum) 0; + spec->nsumSet = false; } } @@ -1339,6 +2029,564 @@ static const CustomExecMethods columnar_agg_exec_methods = { .ExplainCustomScan = ColumnarExplainAggScan, }; +/* ------------------------------------------------------------------------- + * grouped vectorized aggregate (#289): execution + * ------------------------------------------------------------------------- */ + +Node * +ColumnarCreateGroupAggScanState(CustomScan *cscan) +{ + ColumnarGroupAggScanState *state = + (ColumnarGroupAggScanState *) palloc0(sizeof(ColumnarGroupAggScanState)); + List *groupKeys; + List *outMapList; + ListCell *lc; + int i; + int naggs = 0; + + state->css.ss.ps.type = T_CustomScanState; + state->css.methods = &columnar_groupagg_exec_methods; + + /* custom_private: rti, quals, relid, group-key exprs, output map (length 5) */ + state->scanrelid = (Index) intVal(linitial(cscan->custom_private)); + state->quals = (List *) lsecond(cscan->custom_private); + state->relid = + DatumGetObjectId(((Const *) lthird(cscan->custom_private))->constvalue); + groupKeys = (List *) lfourth(cscan->custom_private); + outMapList = (List *) list_nth(cscan->custom_private, 4); + + state->nkeys = list_length(groupKeys); + state->keys = (ColumnarGroupKey *) + palloc0(sizeof(ColumnarGroupKey) * Max(state->nkeys, 1)); + i = 0; + foreach(lc, groupKeys) + state->keys[i++].expr = (Expr *) lfirst(lc); + + /* rebuild the aggregate template from the output tuple's aggregates */ + foreach(lc, cscan->custom_scan_tlist) + if (IsA(((TargetEntry *) lfirst(lc))->expr, Aggref)) + naggs++; + state->naggs = naggs; + state->aggTemplate = (ColumnarAggSpec *) + palloc0(sizeof(ColumnarAggSpec) * Max(naggs, 1)); + i = 0; + foreach(lc, cscan->custom_scan_tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (IsA(tle->expr, Aggref)) + { + (void) columnar_classify_aggref((Aggref *) tle->expr, -1, true, + &state->aggTemplate[i]); + i++; + } + } + + state->nout = list_length(outMapList); + state->outMap = (int *) palloc(sizeof(int) * Max(state->nout, 1)); + i = 0; + foreach(lc, outMapList) + state->outMap[i++] = intVal(lfirst(lc)); + + state->maxGroups = columnar_groupagg_max_groups; + state->capacity = 0; + state->nGroups = 0; + state->entries = NULL; + + return (Node *) state; +} + +static void +ColumnarBeginGroupAggScan(CustomScanState *node, EState *estate, int eflags) +{ + ColumnarGroupAggScanState *state = (ColumnarGroupAggScanState *) node; + Relation rel; + TupleDesc basedesc; + Bitmapset *proj = NULL; + Bitmapset *projected = NULL; + List *keyExprList = NIL; + bool allConvertible; + int k; + int a; + int x; + + state->specContext = AllocSetContextCreate(estate->es_query_cxt, + "columnar groupagg specs", + ALLOCSET_SMALL_SIZES); + state->keyContext = AllocSetContextCreate(estate->es_query_cxt, + "columnar groupagg keys", + ALLOCSET_SMALL_SIZES); + state->hashContext = AllocSetContextCreate(estate->es_query_cxt, + "columnar groupagg table", + ALLOCSET_DEFAULT_SIZES); + state->started = false; + state->emitPos = 0; + state->nGroups = 0; + state->capacity = 0; + state->entries = NULL; + state->haveStats = false; + + rel = table_open(state->relid, AccessShareLock); + basedesc = RelationGetDescr(rel); + + /* + * Count pushable filters for EXPLAIN before the EXPLAIN-only early return, so + * a plain EXPLAIN reports the real pushed-down filter count instead of 0. + */ + ColumnarCountConvertibleQuals(state->quals, state->scanrelid, basedesc, + &state->npreds, &allConvertible); + + if (eflags & EXEC_FLAG_EXPLAIN_ONLY) + { + table_close(rel, AccessShareLock); + return; + } + + /* guard the native format before decoding any value (#240) */ + ColumnarCheckNativeFormatVersion(ColumnarStorageId(rel), + RelationGetRelationName(rel)); + + /* a virtual slot holding each read row for key and qual evaluation */ + state->baseSlot = MakeSingleTupleTableSlot(CreateTupleDescCopy(basedesc), + &TTSOpsVirtual); + + /* group-key ExprStates and their hash/equality machinery */ + for (k = 0; k < state->nkeys; k++) + { + ColumnarGroupKey *key = &state->keys[k]; + Oid type = exprType((Node *) key->expr); + TypeCacheEntry *tce = lookup_type_cache(type, + TYPECACHE_HASH_PROC_FINFO | + TYPECACHE_EQ_OPR_FINFO); + + key->exprState = ExecInitExpr(key->expr, &node->ss.ps); + key->type = type; + key->collation = exprCollation((Node *) key->expr); + get_typlenbyval(type, &key->typlen, &key->byval); + fmgr_info_copy(&key->hashFn, &tce->hash_proc_finfo, estate->es_query_cxt); + fmgr_info_copy(&key->eqFn, &tce->eq_opr_finfo, estate->es_query_cxt); + keyExprList = lappend(keyExprList, key->expr); + } + + /* residual WHERE recheck (the scan keys only prune groups) */ + state->whereState = (state->quals != NIL) + ? ExecInitQual(state->quals, &node->ss.ps) + : NULL; + + /* finish min/max comparison setup on the per-agg template */ + for (a = 0; a < state->naggs; a++) + { + ColumnarAggSpec *spec = &state->aggTemplate[a]; + + if (spec->kind == COLUMNAR_AGG_MIN || spec->kind == COLUMNAR_AGG_MAX) + { + Form_pg_attribute att = TupleDescAttr(basedesc, spec->attidx); + TypeCacheEntry *tce = lookup_type_cache(att->atttypid, + TYPECACHE_CMP_PROC_FINFO); + + fmgr_info_copy(&spec->cmpFn, &tce->cmp_proc_finfo, + estate->es_query_cxt); + spec->collation = att->attcollation; + spec->byval = att->attbyval; + spec->typlen = att->attlen; + } + } + + /* project the columns the keys, WHERE and aggregates reference */ + pull_varattnos((Node *) keyExprList, state->scanrelid, &proj); + pull_varattnos((Node *) state->quals, state->scanrelid, &proj); + x = -1; + while ((x = bms_next_member(proj, x)) >= 0) + { + AttrNumber attno = x + FirstLowInvalidHeapAttributeNumber; + + if (attno > 0) + projected = bms_add_member(projected, attno - 1); + } + for (a = 0; a < state->naggs; a++) + if (state->aggTemplate[a].attidx >= 0) + projected = bms_add_member(projected, state->aggTemplate[a].attidx); + if (projected == NULL) + projected = bms_make_singleton(0); /* count(*) with no keys touched */ + state->projected = projected; + + table_close(rel, AccessShareLock); +} + +/* + * columnar_groupagg_keys_equal + * Whether a probing row's keys match a stored group's, by SQL grouping + * semantics: two nulls are equal, and non-nulls compare with the key type's + * equality operator (with collation) -- exactly how core groups. + */ +static bool +columnar_groupagg_keys_equal(ColumnarGroupAggScanState *state, + ColumnarGroupEntry *e, + Datum *keyvals, bool *keynulls) +{ + int k; + + for (k = 0; k < state->nkeys; k++) + { + if (e->keyNulls[k] != keynulls[k]) + return false; + if (keynulls[k]) + continue; + if (!DatumGetBool(FunctionCall2Coll(&state->keys[k].eqFn, + state->keys[k].collation, + e->keys[k], keyvals[k]))) + return false; + } + return true; +} + +/* + * columnar_groupagg_grow + * Double the open-addressing table and reinsert live entries. Entry structs + * (and the key/spec pointers they carry) move by value; the pointed-at key + * Datums and accumulators stay put in their own contexts. + */ +static void +columnar_groupagg_grow(ColumnarGroupAggScanState *state) +{ + int oldCap = state->capacity; + int newCap = (oldCap <= 0) ? 1024 : oldCap * 2; + ColumnarGroupEntry *newEntries; + MemoryContext old; + int i; + + if (newCap > (1 << 30)) + newCap = 1 << 30; + if (newCap <= oldCap) + return; /* already at the ceiling; let probing lengthen */ + + /* + * The table can grow past what a plain palloc allows (MaxAllocSize is 1 GB; + * this array reaches it well before the 1<<30 entry ceiling), so allocate it + * as a huge, zeroed chunk. Memory is still bounded by the actual group count + * via pgcolumnar.groupagg_max_groups. + */ + old = MemoryContextSwitchTo(state->hashContext); + newEntries = (ColumnarGroupEntry *) + MemoryContextAllocExtended(state->hashContext, + sizeof(ColumnarGroupEntry) * (Size) newCap, + MCXT_ALLOC_HUGE | MCXT_ALLOC_ZERO); + MemoryContextSwitchTo(old); + + for (i = 0; i < oldCap; i++) + { + ColumnarGroupEntry *e = &state->entries[i]; + uint32 idx; + + if (!e->used) + continue; + idx = e->hash & (uint32) (newCap - 1); + while (newEntries[idx].used) + idx = (idx + 1) & (uint32) (newCap - 1); + newEntries[idx] = *e; + } + + if (state->entries != NULL) + pfree(state->entries); + state->entries = newEntries; + state->capacity = newCap; +} + +/* + * columnar_groupagg_lookup + * Find the group for this row's keys, inserting a fresh one (with the key + * Datums copied into keyContext and accumulators seeded from the template) + * when it is new. + */ +static ColumnarGroupEntry * +columnar_groupagg_lookup(ColumnarGroupAggScanState *state, + Datum *keyvals, bool *keynulls) +{ + uint32 hash = 0; + uint32 idx; + int k; + ColumnarGroupEntry *e; + + /* grow before probing so the index is computed against the final table */ + if ((int64) (state->nGroups + 1) * 10 >= (int64) state->capacity * 7) + columnar_groupagg_grow(state); + + for (k = 0; k < state->nkeys; k++) + { + uint32 h; + + if (keynulls[k]) + h = 0x9e3779b9u; /* fixed contribution for a null key */ + else + h = DatumGetUInt32(FunctionCall1Coll(&state->keys[k].hashFn, + state->keys[k].collation, + keyvals[k])); + hash = hash_combine(hash, h); + } + + idx = hash & (uint32) (state->capacity - 1); + for (;;) + { + e = &state->entries[idx]; + if (!e->used) + break; + if (e->hash == hash && + columnar_groupagg_keys_equal(state, e, keyvals, keynulls)) + return e; + idx = (idx + 1) & (uint32) (state->capacity - 1); + } + + /* + * A new group. Bounding the actual group count keeps this no-spill hash table + * from growing without limit; over the cap we stop with guidance rather than + * exhaust memory, since the plan cannot fall back mid-scan. + */ + if (state->nGroups >= state->maxGroups) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("grouped vectorized aggregate exceeded pgcolumnar.groupagg_max_groups (%d)", + state->maxGroups), + errhint("Raise pgcolumnar.groupagg_max_groups, or set " + "pgcolumnar.enable_group_vectorization = off."))); + + /* insert a new group here */ + e->used = true; + e->hash = hash; + { + MemoryContext oldc = MemoryContextSwitchTo(state->keyContext); + + e->keys = (Datum *) palloc(sizeof(Datum) * Max(state->nkeys, 1)); + e->keyNulls = (bool *) palloc(sizeof(bool) * Max(state->nkeys, 1)); + for (k = 0; k < state->nkeys; k++) + { + e->keyNulls[k] = keynulls[k]; + if (keynulls[k]) + e->keys[k] = (Datum) 0; + else + e->keys[k] = datumCopy(keyvals[k], state->keys[k].byval, + state->keys[k].typlen); + } + MemoryContextSwitchTo(oldc); + } + { + MemoryContext oldc = MemoryContextSwitchTo(state->specContext); + + e->specs = (ColumnarAggSpec *) + palloc(sizeof(ColumnarAggSpec) * Max(state->naggs, 1)); + memcpy(e->specs, state->aggTemplate, + sizeof(ColumnarAggSpec) * state->naggs); + MemoryContextSwitchTo(oldc); + } + state->nGroups++; + return e; +} + +/* + * columnar_groupagg_build + * Scan the relation once and fold every surviving row into its group. The + * reader prunes groups and vectors with the pushed-down WHERE; each row is + * rechecked against the whole WHERE, its keys evaluated, and its values + * folded in scan order so accumulators match the scalar Agg byte for byte. + */ +static void +columnar_groupagg_build(ColumnarGroupAggScanState *state) +{ + EState *estate = state->css.ss.ps.state; + ExprContext *econtext = state->css.ss.ps.ps_ExprContext; + Relation rel = table_open(state->relid, AccessShareLock); + TupleDesc basedesc = RelationGetDescr(rel); + int natts = basedesc->natts; + Datum *values = (Datum *) palloc(sizeof(Datum) * natts); + bool *nulls = (bool *) palloc(sizeof(bool) * natts); + Datum *keyvals = (Datum *) palloc(sizeof(Datum) * Max(state->nkeys, 1)); + bool *keynulls = (bool *) palloc(sizeof(bool) * Max(state->nkeys, 1)); + ColumnarReadState *rs; + ScanKey keys; + int nScanKeys = 0; + uint64 rowNumber; + + ColumnarFlushWriteStateForRelation(state->relid); + ColumnarFlushDeleteVectorForRelation(rel); + + keys = ColumnarBuildScanKeys(state->quals, state->scanrelid, basedesc, + &nScanKeys); + rs = ColumnarBeginRead(rel, estate->es_snapshot, NULL, state->projected, + nScanKeys, keys); + + /* columns outside the projection stay null in the base slot */ + memset(state->baseSlot->tts_isnull, true, sizeof(bool) * natts); + + while (ColumnarReadNextRow(rs, values, nulls, &rowNumber)) + { + int x; + int k; + int a; + ColumnarGroupEntry *e; + + ResetExprContext(econtext); + + /* stage the projected columns into the base slot */ + ExecClearTuple(state->baseSlot); + x = -1; + while ((x = bms_next_member(state->projected, x)) >= 0) + { + state->baseSlot->tts_values[x] = values[x]; + state->baseSlot->tts_isnull[x] = nulls[x]; + } + ExecStoreVirtualTuple(state->baseSlot); + econtext->ecxt_scantuple = state->baseSlot; + + /* recheck the full WHERE against this row */ + if (state->whereState != NULL && !ExecQual(state->whereState, econtext)) + continue; + + /* + * Evaluate the group keys in the per-tuple context (reset each row at the + * top of this loop), not the query context ExecEvalExpr would use, so a + * byref key does not leak one allocation per scanned row. Datums that + * start a new group are datumCopy'd into keyContext by the lookup. + */ + for (k = 0; k < state->nkeys; k++) + keyvals[k] = ExecEvalExprSwitchContext(state->keys[k].exprState, + econtext, &keynulls[k]); + + e = columnar_groupagg_lookup(state, keyvals, keynulls); + + /* fold this row's values into the group's accumulators */ + for (a = 0; a < state->naggs; a++) + { + ColumnarAggSpec *spec = &e->specs[a]; + + if (spec->attidx >= 0) + columnar_apply_one(state->specContext, spec, + values[spec->attidx], nulls[spec->attidx]); + else + columnar_apply_one(state->specContext, spec, (Datum) 0, true); + } + } + + ColumnarReadStats(rs, &state->groupsRead, &state->groupsSkipped, + &state->groupsTotal); + state->haveStats = true; + + ColumnarEndRead(rs); + table_close(rel, AccessShareLock); +} + +static TupleTableSlot * +ColumnarExecGroupAggScan(CustomScanState *node) +{ + ColumnarGroupAggScanState *state = (ColumnarGroupAggScanState *) node; + TupleTableSlot *scanSlot = node->ss.ss_ScanTupleSlot; + ExprContext *econtext = node->ss.ps.ps_ExprContext; + + if (!state->started) + { + columnar_groupagg_build(state); + state->started = true; + state->emitPos = 0; + } + + while (state->emitPos < state->capacity) + { + ColumnarGroupEntry *e = &state->entries[state->emitPos++]; + int p; + + if (!e->used) + continue; + + ResetExprContext(econtext); + ExecClearTuple(scanSlot); + for (p = 0; p < state->nout; p++) + { + int m = state->outMap[p]; + + if (m >= 0) + { + scanSlot->tts_values[p] = e->keys[m]; + scanSlot->tts_isnull[p] = e->keyNulls[m]; + } + else + { + int a = -(m) - 1; + + scanSlot->tts_values[p] = + columnar_agg_finalize(&e->specs[a], &scanSlot->tts_isnull[p]); + } + } + ExecStoreVirtualTuple(scanSlot); + + if (node->ss.ps.ps_ProjInfo != NULL) + { + econtext->ecxt_scantuple = scanSlot; + return ExecProject(node->ss.ps.ps_ProjInfo); + } + return scanSlot; + } + + return NULL; +} + +static void +ColumnarEndGroupAggScan(CustomScanState *node) +{ + ColumnarGroupAggScanState *state = (ColumnarGroupAggScanState *) node; + + if (state->baseSlot != NULL) + ExecDropSingleTupleTableSlot(state->baseSlot); + state->baseSlot = NULL; + /* the memory contexts are children of es_query_cxt and freed with it */ +} + +static void +ColumnarReScanGroupAggScan(CustomScanState *node) +{ + ColumnarGroupAggScanState *state = (ColumnarGroupAggScanState *) node; + + state->started = false; + state->emitPos = 0; + state->nGroups = 0; + state->capacity = 0; + state->entries = NULL; + state->haveStats = false; + MemoryContextReset(state->keyContext); + MemoryContextReset(state->specContext); + MemoryContextReset(state->hashContext); +} + +static void +ColumnarExplainGroupAggScan(CustomScanState *node, List *ancestors, + ExplainState *es) +{ + ColumnarGroupAggScanState *state = (ColumnarGroupAggScanState *) node; + + ExplainPropertyInteger("Columnar Vectorized Group Keys", NULL, + state->nkeys, es); + ExplainPropertyInteger("Columnar Vectorized Aggregates", NULL, + state->naggs, es); + ExplainPropertyInteger("Columnar Pushed-Down Filters", NULL, + state->npreds, es); + + if (state->haveStats) + { + ExplainPropertyInteger("Columnar Chunk Groups Total", NULL, + (int64) state->groupsTotal, es); + ExplainPropertyInteger("Columnar Chunk Groups Read", NULL, + (int64) state->groupsRead, es); + ExplainPropertyInteger("Columnar Chunk Groups Removed by Filter", NULL, + (int64) state->groupsSkipped, es); + } +} + +static const CustomExecMethods columnar_groupagg_exec_methods = { + .CustomName = "ColumnarScan", + .BeginCustomScan = ColumnarBeginGroupAggScan, + .ExecCustomScan = ColumnarExecGroupAggScan, + .EndCustomScan = ColumnarEndGroupAggScan, + .ReScanCustomScan = ColumnarReScanGroupAggScan, + .ExplainCustomScan = ColumnarExplainGroupAggScan, +}; + /* ------------------------------------------------------------------------- * registration * ------------------------------------------------------------------------- */ diff --git a/test/native_groupagg.sh b/test/native_groupagg.sh new file mode 100644 index 0000000..74b55e5 --- /dev/null +++ b/test/native_groupagg.sh @@ -0,0 +1,339 @@ +#!/usr/bin/env bash +# +# pgColumnar #289: grouped vectorized aggregate. +# +# The grouped path fires for SELECT , agg(col) ... [WHERE ...] GROUP BY +# over one columnar relation. It reads each surviving row with +# ColumnarReadNextRow (WHERE pushed down for group/vector skipping), rechecks the +# full WHERE, evaluates the group keys, and scatters the row into an +# open-addressing hash table whose per-group accumulators fold in scan order. +# +# Two oracles prove it: +# +# * Heap mirror. Every query runs against a heap table with identical data. +# Exact aggregates (count, integer/numeric sums, min/max) compare byte-exact; +# float sums/averages compare rounded, because float summation order (not the +# grouping) is all that can differ and that is the executor, not a defect. +# +# * Toggle-differential. The same query runs against the columnar table with +# the path off (scalar Agg over the columnar scan) and on. Both read the same +# rows in the same order, so exact aggregates must be byte-identical -- this +# is what validates the order-preserving accumulators. +# +# Plan assertions confirm the node is actually chosen when supported and that +# every unsupported shape (group count over the cap, non-deterministic collation, +# an output built on a key, no aggregate) falls back to the ordinary Agg while +# still producing the oracle's answer. +# +# Runs on an assert server too, so a bad read or a mis-sized accumulator also +# trips a backend assertion under the oracle data. +# +# Usage: test/native_groupagg.sh [PG_CONFIG] +# Written fresh for pgColumnar. + +set -uo pipefail +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +pgc_setup "${1:-/usr/local/pg17/bin/pg_config}" + +# The grouped path is opt-in; set it at the database level so every new psql +# connection the oracle helpers open picks it up. +groupvec_on() { psql_run "ALTER DATABASE $PGC_DB SET pgcolumnar.enable_group_vectorization = on;"; } +groupvec_off() { psql_run "ALTER DATABASE $PGC_DB SET pgcolumnar.enable_group_vectorization = off;"; } + +# Is this query planned as the grouped vectorized node? Its own marker line is +# "Columnar Vectorized Group Keys"; no other node emits it, so a positive grep is +# proof of the node rather than an absence test that a fallback would also pass. +pgc_is_groupvec() { # query -> yes|no + env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -At -c "EXPLAIN (COSTS OFF) $1" 2>/dev/null \ + | grep -q 'Columnar Vectorized Group Keys' && echo yes || echo no +} + +# toggle_diff LABEL "QUERY on t_col": same query, path off vs on, byte-exact. +# Asserts the node actually fires with the GUC on -- without that premise a query +# the node quietly rejects runs the scalar Agg in both arms and the comparison is +# vacuously green (exactly how the float-sum defect slipped through before). +toggle_diff() { + local label="$1" query="$2" h_off h_on + groupvec_on + check "$label [node fires]" "$(pgc_is_groupvec "$query")" yes + groupvec_off + h_off="$(pgc_set_hash "$query")" + groupvec_on + h_on="$(pgc_set_hash "$query")" + check "$label" "$h_on" "$h_off" +} + +# oracle LABEL "TEMPLATE with %T": assert the grouped node is chosen for the +# columnar form, then compare it to the heap oracle. The node-fires assertion is +# the premise every comparison here needs and none used to state. +oracle() { + local label="$1" tmpl="$2" + check "$label [node fires]" "$(pgc_is_groupvec "${tmpl//%T/t_col}")" yes + diff_query "$label" "$tmpl" +} + +# ---- 1. the data: a q4/q5-shaped table with many group shapes --------------- + +make_pair "g int, + ts timestamp, + host text, + region text, + i2 smallint, + i4 int, + i8 bigint, + f4 real, + f8 double precision, + num numeric" +# small row groups so grouping spans many groups and vectors +psql_run "SELECT pgcolumnar.set_options('t_col', stripe_row_limit => 1000);" + +# 20000 rows: 50 hosts, 4 regions, timestamps spanning ~14 hours, values with +# interleaved NULLs in both keys and measures (a NULL host/ts must form its own +# group, exactly as GROUP BY does). +load_pair "SELECT g, + CASE WHEN g % 331 = 0 THEN NULL + ELSE timestamp '2024-01-01 00:00:00' + (g * interval '25 seconds') END, + CASE WHEN g % 197 = 0 THEN NULL ELSE 'host_' || (g % 50) END, + 'region_' || (g % 4), + CASE WHEN g % 11 = 0 THEN NULL ELSE (g % 97 - 48)::smallint END, + CASE WHEN g % 7 = 0 THEN NULL ELSE (g * 7 - 3) END, + CASE WHEN g % 5 = 0 THEN NULL ELSE (g::bigint * 1000003 - 5) END, + CASE WHEN g % 6 = 0 THEN NULL ELSE (g * 1.5)::real END, + CASE WHEN g % 9 = 0 THEN NULL ELSE (g::float8 * 0.125 - 3.5) END, + CASE WHEN g % 8 = 0 THEN NULL ELSE (g * 0.01)::numeric(12,4) END + FROM generate_series(1, 20000) g" + +# ---- 2. plan assertions: the node is chosen for the supported shapes -------- + +groupvec_on +Q_KEYHOST="SELECT host, count(*), sum(i4) FROM t_col GROUP BY host" +Q_KEYHOUR="SELECT date_trunc('hour', ts) h, count(*) FROM t_col GROUP BY date_trunc('hour', ts)" +Q_Q4="SELECT date_trunc('hour', ts) h, host, avg(f8), count(*) + FROM t_col WHERE ts >= timestamp '2024-01-01 02:00:00' + AND ts < timestamp '2024-01-01 06:00:00' + GROUP BY date_trunc('hour', ts), host" +check "plan: GROUP BY text key uses grouped node" "$(pgc_is_groupvec "$Q_KEYHOST")" yes +check "plan: GROUP BY hour expr uses grouped node" "$(pgc_is_groupvec "$Q_KEYHOUR")" yes +check "plan: q4-shape (WHERE+2 keys) uses grouped node" "$(pgc_is_groupvec "$Q_Q4")" yes +groupvec_off +check "plan: off -> ordinary Agg, not grouped node" "$(pgc_is_groupvec "$Q_KEYHOST")" no +groupvec_on + +# ---- 3. heap oracle: exact aggregates, every group shape ------------------- +# GUC on, so t_col runs the grouped path; t_heap is the reference. + +EXACT="count(*), count(i4), count(host), + sum(i2), sum(i4), sum(i8), sum(num), + min(f8), max(f8), min(host), max(host), min(ts), max(ts), min(i8), max(i8)" + +oracle "oracle exact: GROUP BY host" \ + "SELECT host, $EXACT FROM %T GROUP BY host" +oracle "oracle exact: GROUP BY hour" \ + "SELECT date_trunc('hour', ts), $EXACT FROM %T GROUP BY date_trunc('hour', ts)" +oracle "oracle exact: GROUP BY hour, host (q4/q5 shape)" \ + "SELECT date_trunc('hour', ts), host, $EXACT FROM %T GROUP BY date_trunc('hour', ts), host" +oracle "oracle exact: GROUP BY region, host (two text keys)" \ + "SELECT region, host, $EXACT FROM %T GROUP BY region, host" +oracle "oracle exact: GROUP BY integer expr key" \ + "SELECT (g % 7), $EXACT FROM %T GROUP BY (g % 7)" +oracle "oracle exact: GROUP BY smallint column key" \ + "SELECT i2, count(*), sum(i4), min(f8), max(f8) FROM %T GROUP BY i2" + +# with a WHERE that both prunes groups and needs a residual recheck +oracle "oracle exact: WHERE range + GROUP BY hour, host" \ + "SELECT date_trunc('hour', ts), host, $EXACT FROM %T + WHERE ts >= timestamp '2024-01-01 02:00:00' + AND ts < timestamp '2024-01-01 06:00:00' + GROUP BY date_trunc('hour', ts), host" +oracle "oracle exact: WHERE on measure + GROUP BY host" \ + "SELECT host, $EXACT FROM %T WHERE i4 > 40000 AND host IS NOT NULL GROUP BY host" + +# ---- 4. float and average accumulators, through the node -------------------- +# These are the accumulators the earlier suite left uncovered: rounding an +# aggregate IN the SELECT list makes the output an expression over an aggregate, +# which the node rejects, so both arms silently ran the scalar Agg and the check +# was vacuous (that is how sum(real) returning 0 got through). Cover them by a +# toggle-differential of the BARE aggregates: the GUC-off arm is core's Agg over +# the same columnar rows in the same fold order, so the node's result must be +# byte-identical -- and toggle_diff now asserts the node actually fires. + +FLOATAGG="sum(f4), sum(f8), sum(num), + avg(i2), avg(i4), avg(i8), avg(f4), avg(f8), avg(num)" + +toggle_diff "toggle float/avg: GROUP BY host" \ + "SELECT host, $FLOATAGG FROM t_col GROUP BY host" +toggle_diff "toggle float/avg: GROUP BY hour, host" \ + "SELECT date_trunc('hour', ts), host, $FLOATAGG FROM t_col GROUP BY date_trunc('hour', ts), host" +toggle_diff "toggle float/avg: WHERE + GROUP BY region, host" \ + "SELECT region, host, $FLOATAGG FROM t_col WHERE f8 IS NOT NULL GROUP BY region, host" + +# min/max tie-break: numeric values equal by value but differing in display scale +# (1.0 vs 1.00). Core's larger/smaller keep the later value on a tie; the node +# must match. Order-sensitive, so tested as a toggle-differential (same fold +# order in both arms), not against heap (which scans in a different order). +psql_run "DROP TABLE IF EXISTS tie_col; + CREATE TABLE tie_col (k int, n numeric) USING pgcolumnar; + INSERT INTO tie_col VALUES + (1, 1.0), (1, 1.00), (1, 1.000), (1, 0.5), (1, 0.50), + (2, 5.5), (2, 5.50), (2, 5.500);" >/dev/null +toggle_diff "toggle min/max numeric display-scale tie" \ + "SELECT k, min(n), max(n) FROM tie_col GROUP BY k" + +# ---- 5. toggle-differential: exact accumulators, off vs on ----------------- + +toggle_diff "toggle exact: GROUP BY host" \ + "SELECT host, $EXACT FROM t_col GROUP BY host" +toggle_diff "toggle exact: GROUP BY hour, host" \ + "SELECT date_trunc('hour', ts), host, $EXACT FROM t_col GROUP BY date_trunc('hour', ts), host" +toggle_diff "toggle exact: GROUP BY region, host, WHERE" \ + "SELECT region, host, $EXACT FROM t_col WHERE i8 IS NOT NULL GROUP BY region, host" + +# ---- 6. adversarial: NULL keys, deletes, ADD COLUMN ------------------------ + +groupvec_on +# NULL keys already present (g%197, g%331); confirm the node is still used and +# the NULL group matches the oracle. +oracle "oracle exact: NULL ts group folds like heap" \ + "SELECT date_trunc('hour', ts), count(*), sum(i4) FROM %T GROUP BY date_trunc('hour', ts)" + +psql_run "DELETE FROM t_heap WHERE g % 17 = 0;" +psql_run "DELETE FROM t_col WHERE g % 17 = 0;" +oracle "oracle exact: GROUP BY host after deletes" \ + "SELECT host, $EXACT FROM %T GROUP BY host" +toggle_diff "toggle exact: GROUP BY host after deletes" \ + "SELECT host, $EXACT FROM t_col GROUP BY host" + +psql_run "ALTER TABLE t_heap ADD COLUMN extra int DEFAULT 7;" +psql_run "ALTER TABLE t_col ADD COLUMN extra int DEFAULT 7;" +oracle "oracle exact: GROUP BY host, added column" \ + "SELECT host, count(*), sum(extra), sum(i4) FROM %T GROUP BY host" +oracle "oracle exact: GROUP BY added column" \ + "SELECT extra, count(*), sum(i4) FROM %T GROUP BY extra" + +# ---- 7. fallback shapes: not the node, still the right answer --------------- + +# The cap bounds the actual group count at execution, not the planner's group +# estimate (unreliable for expression keys): over the cap the node stops with +# guidance rather than building an unbounded hash table. The node is forced here +# (the only grouping path left with hashagg and sort off) so the cap-enforcement +# path is exercised deterministically regardless of how the planner would cost it +# on this small table; the node's real-world plan choice is covered above and by +# the benchmark. The cap is set in-session so the executor reads it directly. +check "plan: node still chosen (no estimate gate)" \ + "$(pgc_is_groupvec "SELECT g, count(*) FROM t_col GROUP BY g")" yes +# Capture to a variable before grepping: the query errors on purpose, so psql +# exits non-zero, and under `set -o pipefail` a `psql | grep` pipeline would +# report psql's failure even when grep matched -- flipping this check to a false +# negative. Grepping the captured text keeps psql's exit status out of it. +oc_out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -Atc "SET pgcolumnar.enable_group_vectorization=on; + SET enable_hashagg=off; SET enable_sort=off; + SET pgcolumnar.groupagg_max_groups=100; + SELECT g, count(*) FROM t_col GROUP BY g" 2>&1 || true)" +n_err="$(printf '%s' "$oc_out" | grep -q 'groupagg_max_groups' && echo yes || echo no)" +check "over-cap stops with a groupagg_max_groups error" "$n_err" yes +diff_query "oracle exact: default cap runs the high-cardinality key" \ + "SELECT g, count(*), sum(i4) FROM %T GROUP BY g" + +# non-deterministic collation key -> falls back +psql_run "DROP TABLE IF EXISTS t_ci;" +psql_run "CREATE COLLATION IF NOT EXISTS ci (provider = icu, locale = 'und-u-ks-level2', deterministic = false);" \ + 2>/dev/null || true +if [ "$(q "SELECT 1 FROM pg_collation WHERE collname = 'ci'")" = "1" ]; then + psql_run "CREATE TABLE t_ci (k text COLLATE ci, v int) USING pgcolumnar;" + # Keys where case is DECOUPLED from the suffix: case comes from g%2, the + # suffix from (g/2)%10, so for every suffix 0..9 both 'host'N and 'HOST'N + # exist -- 20 byte-distinct keys that case-fold to 10. A case-insensitive + # (level-2) collation yields 10 groups; a byte-equality path (which the node + # uses, and why it must fall back here) would yield 20. So this fixture fails + # if the fallback ever stops happening. (An earlier version used g%20, whose + # parity tracked g's, so nothing case-folded and the check was vacuous.) + psql_run "INSERT INTO t_ci + SELECT (CASE WHEN g % 2 = 0 THEN 'host' ELSE 'HOST' END) || ((g / 2) % 10), g + FROM generate_series(1, 5000) g;" + check "plan: non-deterministic collation key falls back" \ + "$(pgc_is_groupvec "SELECT k, count(*) FROM t_ci GROUP BY k")" no + check "answer: non-deterministic collation grouping is case-insensitive (10 groups)" \ + "$(q "SELECT count(*) FROM (SELECT k FROM t_ci GROUP BY k) s")" \ + "$(q "SELECT count(DISTINCT lower(k)) FROM t_ci")" +else + echo "SKIP non-deterministic collation (ICU unavailable)" +fi + +# an output expression built on a group key (not a bare key) -> falls back +check "plan: f(key) output falls back" \ + "$(pgc_is_groupvec "SELECT upper(host), count(*) FROM t_col GROUP BY host")" no +diff_query "oracle exact: f(key) output still correct" \ + "SELECT upper(host), count(*), sum(i4) FROM %T GROUP BY host" + +# GROUP BY with no aggregate -> falls back (nothing to vectorize) +check "plan: GROUP BY with no aggregate falls back" \ + "$(pgc_is_groupvec "SELECT host FROM t_col GROUP BY host")" no + +# ---- 7b. named regressions for the two reproduced wrong-answer blockers ----- + +# Blocker 1: sum(real) once returned 0 -- a float8 Datum handed to a float4 slot. +# With integer-valued reals the sum is exact regardless of fold order, so compare +# to heap directly; require the node and require the result be nonzero. +psql_run "DROP TABLE IF EXISTS r_col; DROP TABLE IF EXISTS r_heap;" +psql_run "CREATE TABLE r_col (h int, r real, d float8) USING pgcolumnar; + CREATE TABLE r_heap (h int, r real, d float8); + INSERT INTO r_heap SELECT g % 4, (g % 7)::real, (g % 7)::float8 + FROM generate_series(1, 4000) g; + INSERT INTO r_col SELECT * FROM r_heap;" >/dev/null +check "regress B1: sum(real) fires the node" \ + "$(pgc_is_groupvec "SELECT h, sum(r) FROM r_col GROUP BY h")" yes +check "regress B1: sum(real) matches heap, not 0" \ + "$(pgc_set_hash "SELECT h, sum(r), sum(d) FROM r_col GROUP BY h")" \ + "$(pgc_set_hash "SELECT h, sum(r), sum(d) FROM r_heap GROUP BY h")" +check "regress B1: sum(real) is nonzero" \ + "$(q "SELECT bool_and(s <> 0) FROM (SELECT sum(r) s FROM r_col GROUP BY h) x")" t + +# Blocker 2: a gating (pseudoconstant, one-time) WHERE was dropped, so a false +# gate wrongly returned rows. The node cannot honor a one-time filter, so it must +# fall back; either way the answer must match heap. +check "regress B2: gating WHERE falls back (not the node)" \ + "$(pgc_is_groupvec "SELECT host, count(*) FROM t_col WHERE (SELECT false) GROUP BY host")" no +diff_query "regress B2: gating WHERE (SELECT false) returns no rows" \ + "SELECT host, count(*) FROM %T WHERE (SELECT false) GROUP BY host" +diff_query "regress B2: gating WHERE (SELECT true) is a no-op" \ + "SELECT host, count(*), sum(i4) FROM %T WHERE (SELECT true) GROUP BY host" + +# Signed zero: core's sum assigns the first value directly, so sum of a lone -0.0 +# prints '-0'. Folding it into +0.0 would drop the sign. Toggle-diff over columnar +# rows containing -0.0 (node vs core's scalar Agg, same fold order) is byte-exact. +psql_run "DROP TABLE IF EXISTS z_col; + CREATE TABLE z_col (k int, r real, d float8) USING pgcolumnar; + INSERT INTO z_col VALUES (1,'-0','-0'), (2,'-0','-0'), (2,'-0','-0');" >/dev/null +toggle_diff "toggle sum signed-zero (-0 preserved)" \ + "SELECT k, sum(r), sum(d) FROM z_col GROUP BY k" + +# avg overflow parity: core's float accum keeps Youngs-Cramer Sxx and raises on +# finite inputs whose sum-of-squares overflows even if the running sum stays +# finite. The node must raise too. (diff_query can't compare two errors, so check +# the node fires and errors with the same message.) +psql_run "DROP TABLE IF EXISTS ov_col; + CREATE TABLE ov_col (k int, d float8) USING pgcolumnar; + INSERT INTO ov_col VALUES (1, -1e308), (1, 1e308);" >/dev/null +check "avg(float8) overflow: node fires" \ + "$(pgc_is_groupvec "SELECT k, avg(d) FROM ov_col GROUP BY k")" yes +ov_out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U postgres \ + -d "$PGC_DB" -Atc "SET pgcolumnar.enable_group_vectorization=on; + SELECT k, avg(d) FROM ov_col GROUP BY k" 2>&1 || true)" +check "avg(float8) overflow errors like core" \ + "$(printf '%s' "$ov_out" | grep -qi 'out of range' && echo yes || echo no)" yes + +# ---- 8. degenerate inputs -------------------------------------------------- + +diff_query "oracle exact: all rows filtered out -> 0 groups" \ + "SELECT host, count(*) FROM %T WHERE g < 0 GROUP BY host" + +# empty columnar table: GROUP BY yields no rows +psql_run "DROP TABLE IF EXISTS t_empty;" +psql_run "CREATE TABLE t_empty (k text, v int) USING pgcolumnar;" +check "empty table: grouped scan yields 0 rows" \ + "$(q "SELECT count(*) FROM (SELECT k, count(*) FROM t_empty GROUP BY k) s")" 0 + +pgc_summary diff --git a/test/run_all_versions.sh b/test/run_all_versions.sh index f056e3e..b863f27 100755 --- a/test/run_all_versions.sh +++ b/test/run_all_versions.sh @@ -208,7 +208,7 @@ SRCDIR="${PGC_RUN_SRCDIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" SUITES=(harness_selftest docs_style smoke phase2 phase3 phase4 phase5 phase6 audit concurrency unique_conc \ differential recovery replication native_backend_crash fuzz fuzz_parquet fuzz_arrow hardening concurrent_diff parallel sorted_projection \ arrow_export parquet_export read_stream corruption \ - generated_columns temporal arrow_import index_only projections arrow_nested parquet_import parquet_nested arrow_nested_import parquet_nested_import native_writer native_roundtrip native_encoding native_fastdecode native_zonemap write_minmax_fastpath write_fsst_compressed fsst_margin encode_invariants encode_effort native_skip pushdown_report native_agg native_agg_deletes native_agg_addcolumn native_bloom bloom_setting bloom_lazy native_vecskip native_index native_fetch_position native_dml alter_column_type native_ios native_projection native_cluster native_compact native_recluster recluster_extent native_vacuum_race native_sort_by sort_status native_reclaim native_ownership drop_cleanup pg_dump_roundtrip native_reclaim_cycles native_reclaim_frag native_reclaim_reconcile native_gap native_format native_truncate native_rewrite native_rewrite_conc rewrite_group_scan native_parquet_schema native_read_parquet native_parquet_fdw native_parquet_pushdown native_parquet_hardening server_file_privilege native_parquet_stack native_parquet_units native_parquet_flba native_parquet_codecs native_parquet_projection native_parquet_multifile native_parquet_streaming native_parquet_partition native_cancel cancel_decode wal_envelope decode_interrupts import_exclusion import_deferred fk_referencing row_triggers native_lazy_slot native_fetch_cache native_fetch_interrupt analyze_stats analyze_reltuples native_fetch_projection isolation) + generated_columns temporal arrow_import index_only projections arrow_nested parquet_import parquet_nested arrow_nested_import parquet_nested_import native_writer native_roundtrip native_encoding native_fastdecode native_zonemap write_minmax_fastpath write_fsst_compressed fsst_margin encode_invariants encode_effort native_skip pushdown_report native_agg native_agg_deletes native_agg_addcolumn native_groupagg native_bloom bloom_setting bloom_lazy native_vecskip native_index native_fetch_position native_dml alter_column_type native_ios native_projection native_cluster native_compact native_recluster recluster_extent native_vacuum_race native_sort_by sort_status native_reclaim native_ownership drop_cleanup pg_dump_roundtrip native_reclaim_cycles native_reclaim_frag native_reclaim_reconcile native_gap native_format native_truncate native_rewrite native_rewrite_conc rewrite_group_scan native_parquet_schema native_read_parquet native_parquet_fdw native_parquet_pushdown native_parquet_hardening server_file_privilege native_parquet_stack native_parquet_units native_parquet_flba native_parquet_codecs native_parquet_projection native_parquet_multifile native_parquet_streaming native_parquet_partition native_cancel cancel_decode wal_envelope decode_interrupts import_exclusion import_deferred fk_referencing row_triggers native_lazy_slot native_fetch_cache native_fetch_interrupt analyze_stats analyze_reltuples native_fetch_projection isolation) # Default matrix: one assert-enabled pg_config per major, 15 through 19. DEFAULT_CONFIGS=(