S4 language-lane spec — aggregates, having, raw root, named predicates
Engine rule: IR kinds count_distinct, min, max (with agg), having: Group (root only, needs group_by or group_by_expr; aggregate expressions go through expr items), count + grouping = number of groups, group_count + grouping = grouped rows with row_count, kind: raw with raw: {sql, ps} (step role raw, no assemble), manifest predicates: {name: {expr, arity}} per entity. Details: docs/protocol.md "집계 확장 (S4)", docs/dsl.md.
1. API to add (one token per meaning; PHP camel / Go Pascal / Rust snake)
| token | notes |
|---|---|
countDistinct<Col>(db) min<Col>(db) max<Col>(db) | terminals for every non-styled, non-json/bytes column; min/max return the column's typed value (nullable → null/nil/None when no rows); countDistinct → int |
getCount() getsCount() | scalar count; with groupBy<Col>() or groupByExpr(expr, as), getCount returns the number of groups and getsCount returns grouped rows with row_count |
having(fn) | on the query type only; the closure gets the same Where builder; sets having group |
raw(sql, binds) + rawAll(db) | raw stores the statement; rawAll runs kind raw and returns rows as ordered maps by column name: PHP list<array<string,mixed>>, Go []map[string]any, Rust Vec<IndexMap<String, Val>> (values as the driver gives them, no codec) |
<name>(args…) | one method per manifest predicate: visible() / startedAfter(v) (PHP camel of the snake name; Go Visible(), StartedAfter(v); Rust visible(), started_after(v)); body = expr(predicate.expr, args); available on both the query and the Where builder |
2. Conformance vectors (add to YOUR runner, same names, in this order, before codec_roundtrip)
agg_min_max—Battle.serviceSeq(7).using(db).minSeq(),.using(db).maxSeq(),.using(db).countDistinctUserSeq()→{"min": 6, "max": 99906, "distinct_users": <n>}(n is whatever the data gives; identical across languages).group_count_having—Battle.serviceSeq(7).groupByUserSeq().having(w => w.expr("COUNT(*) > ?", [1])).using(db).getCount()→ number of users with more than one battle in service 7.predicate_named—Battle.visible().serviceSeq(7).using(db).getCount()andBattle.startedAfter("2026-01-01 00:00:00").serviceSeq(7).using(db).getCount()→{"visible": …, "started_after": …}.raw_root—Battle.raw("SELECT COUNT(*) AS n, MAX(seq) AS m FROM {table} WHERE service_seq = ? AND is_close = ?", [7, 0]).using(db).rawAll()→[{"n": …, "m": …}](ints as numbers).
3. Rules
Same as docs/lanes/s3.md §4. Regenerate after template changes; run your integration test and runner; commit on your lane branch; report.