OQ3: internal/duckstats + bin/qa/stats.go. Zig stays Ladybug-only. mikefarah/yq for small structured slices. Gitea #30.
48 lines
1.1 KiB
Go
48 lines
1.1 KiB
Go
// Package duckstats runs in-process DuckDB for columnar aggregates.
|
|
// Graph facts stay in Ladybug. Web-search KV cache stays modernc sqlite.
|
|
package duckstats
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
_ "github.com/duckdb/duckdb-go/v2"
|
|
)
|
|
|
|
type Stats struct {
|
|
N int `json:"n"`
|
|
Min float64 `json:"min"`
|
|
P50 float64 `json:"p50"`
|
|
P95 float64 `json:"p95"`
|
|
Max float64 `json:"max"`
|
|
Avg float64 `json:"avg"`
|
|
}
|
|
|
|
func Quantiles(samples []float64) (Stats, error) {
|
|
if len(samples) == 0 {
|
|
return Stats{}, fmt.Errorf("duckstats: empty samples")
|
|
}
|
|
db, err := sql.Open("duckdb", "")
|
|
if err != nil {
|
|
return Stats{}, err
|
|
}
|
|
defer db.Close()
|
|
var s Stats
|
|
err = db.QueryRow(`
|
|
SELECT count(v), min(v), quantile_cont(v, 0.5), quantile_cont(v, 0.95), max(v), avg(v)
|
|
FROM (SELECT unnest(?) AS v)`, samples).Scan(
|
|
&s.N, &s.Min, &s.P50, &s.P95, &s.Max, &s.Avg)
|
|
return s, err
|
|
}
|
|
|
|
func CountJSONL(path string) (int64, error) {
|
|
db, err := sql.Open("duckdb", "")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer db.Close()
|
|
var n int64
|
|
err = db.QueryRow(`SELECT count(*) FROM read_json_auto(?)`, path).Scan(&n)
|
|
return n, err
|
|
}
|