Refuse duplicate aggregate columns and page aggregates after the fold

A repeated extract function names one response column twice, which openvoxdb
aliases as <name>_2: unknown to the merge spec, it froze at the first backend's
value. A limit pushed upstream truncated each backend's groups before the
cross-backend fold, so a group could be partly counted or missed.

- Refuses any extract projecting one response column twice, naming the clash
- Fetches every group and applies limit/offset after the fold
- Documents the float64 avg divergence from Postgres numeric
This commit is contained in:
2026-09-06 23:58:29 +10:00
parent 66ed7b615c
commit b499e962af
7 changed files with 230 additions and 20 deletions
+22 -11
View File
@@ -93,9 +93,18 @@ func parseAggregate(query string) (*aggregateSpec, error) {
spec := &aggregateSpec{}
var avgArgs []json.RawMessage
sawFunction := false
projected := map[string]bool{}
fnColumns := map[string]bool{}
for _, col := range cols {
var name string
if json.Unmarshal(col, &name) == nil {
// A field repeating another field is harmless — the copy holds the same
// value as the key it duplicates — but one taking a function's name is
// the same clash the other way round.
if fnColumns[name] {
return nil, columnClash(name)
}
projected[name] = true
spec.keys = appendUnique(spec.keys, name)
continue
}
@@ -104,13 +113,14 @@ func parseAggregate(query string) (*aggregateSpec, error) {
continue
}
sawFunction = true
if projected[fn] {
return nil, columnClash(fn)
}
projected[fn], fnColumns[fn] = true, true
switch {
case rowFns[fn]:
spec.keys = appendUnique(spec.keys, fn)
case fn == avgColumn:
if spec.avg {
return nil, fmt.Errorf("extract projects %q more than once, which openvoxdb answers with order-dependent columns", avgColumn)
}
if len(args) == 0 {
return nil, fmt.Errorf("extract function avg needs a column to average")
}
@@ -120,7 +130,7 @@ func parseAggregate(query string) (*aggregateSpec, error) {
if !known {
return nil, fmt.Errorf("extract function %q cannot be merged across backends", fn)
}
spec.aggs = appendUniqueAgg(spec.aggs, aggColumn{name: fn, op: combine})
spec.aggs = append(spec.aggs, aggColumn{name: fn, op: combine})
}
}
if !sawFunction {
@@ -139,6 +149,14 @@ func parseAggregate(query string) (*aggregateSpec, error) {
return spec, nil
}
// columnClash refuses a projection naming one response column twice. openvoxdb
// aliases every extract column after its function, so a repeat comes back as an
// order-dependent "<name>_2" that is neither a grouping key nor a folded
// aggregate, leaving the first backend's value frozen in the merged row.
func columnClash(name string) error {
return fmt.Errorf("extract projects the column %q more than once: openvoxdb returns the repeat as %q, which pdbmux can neither key on nor fold", name, name+"_2")
}
// rewriteAvg replaces the client's avg column with the sum and count of the same
// expression, so the true weighted average can be computed from the shards:
// Postgres avg(x) is sum(x)/count(x), and both of those do combine.
@@ -242,13 +260,6 @@ func appendUnique(s []string, v string) []string {
return append(s, v)
}
func appendUniqueAgg(s []aggColumn, v aggColumn) []aggColumn {
if hasAgg(s, v.name) {
return s
}
return append(s, v)
}
func hasAgg(s []aggColumn, name string) bool {
for _, x := range s {
if x.name == name {