@docyrus/docyrus 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/main.js +1028 -540
- package/main.js.map +4 -4
- package/package.json +2 -1
- package/resources/pi-agent/prompts/agent-system.md +25 -0
- package/resources/pi-agent/prompts/coder-append-system.md +19 -0
- package/resources/pi-agent/skills/docyrus-ai/SKILL.md +28 -0
- package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +161 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +349 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +238 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/data-source-query-guide.md +2059 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/formula-design-guide-llm.md +320 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +592 -0
- package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +70 -0
- package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +588 -0
- package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +159 -0
- package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +275 -0
- package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +352 -0
- package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +2059 -0
- package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +320 -0
- package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +525 -0
- package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +466 -0
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +602 -0
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +463 -0
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +242 -0
- package/resources/pi-agent/skills/docyrus-apps/SKILL.md +54 -0
- package/resources/pi-agent/skills/docyrus-architect/SKILL.md +174 -0
- package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +410 -0
- package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +2059 -0
- package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +320 -0
- package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +145 -0
- package/resources/pi-agent/skills/docyrus-auth/SKILL.md +100 -0
- package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +279 -0
- package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +532 -0
- package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +248 -0
- package/resources/pi-agent/skills/docyrus-curl/SKILL.md +32 -0
- package/resources/pi-agent/skills/docyrus-discover/SKILL.md +63 -0
- package/resources/pi-agent/skills/docyrus-ds/SKILL.md +95 -0
- package/resources/pi-agent/skills/docyrus-env/SKILL.md +21 -0
- package/resources/pi-agent/skills/docyrus-studio/SKILL.md +369 -0
- package/resources/pi-agent/skills/docyrus-tui/SKILL.md +15 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# SQL Block Formula Reference
|
|
2
|
+
|
|
3
|
+
## Formula Types
|
|
4
|
+
|
|
5
|
+
Two block formula formats:
|
|
6
|
+
|
|
7
|
+
**Block Inline** — AST expression in SELECT: `{ alias?: string, inputs: IQueryFormulaBlock[] }`. Detected by `inputs` without `from`/`with`.
|
|
8
|
+
|
|
9
|
+
**Block Subquery** — correlated subquery on child table: `{ alias?, inputs, from: string, with: string | Record<string,string>, filters?: IQueryFilterGroup }`. Detected by `from`+`with`.
|
|
10
|
+
|
|
11
|
+
Compat wrapper: `{ expression: { from, with, inputs } }` is also accepted.
|
|
12
|
+
|
|
13
|
+
## Block Schema
|
|
14
|
+
|
|
15
|
+
Top-level requires exactly 1 element in `inputs[]`. Optional `alias` becomes SQL alias.
|
|
16
|
+
|
|
17
|
+
Every block has optional `tz?: string` (timezone) and `cast?: string` (type cast). Processing: compile → tz → cast.
|
|
18
|
+
|
|
19
|
+
## Block Kinds
|
|
20
|
+
|
|
21
|
+
### literal
|
|
22
|
+
|
|
23
|
+
`{ kind: "literal", literal: string|number|boolean|Date|null|Array }`
|
|
24
|
+
|
|
25
|
+
- Scalars → parameterized `$N`. Arrays → `($1, $2, ...)`.
|
|
26
|
+
- Inside `concat`/`concat_ws` parent: auto-casts (`::text`, `::boolean`, `::timestamptz`, `::jsonb`).
|
|
27
|
+
|
|
28
|
+
### column
|
|
29
|
+
|
|
30
|
+
`{ kind: "column", name: string|string[] }`
|
|
31
|
+
|
|
32
|
+
- Advanced DS: `"alias"."slug"`
|
|
33
|
+
- Simple DS custom fields: `"alias".data->>'<field-uuid>'` with auto-cast by field type:
|
|
34
|
+
- number/money/duration(decimal≠false) → `::decimal`, (decimal=false) → `::int`
|
|
35
|
+
- DB types jsonb/date/time/timestamptz/boolean/int* → `::<type>`, uuid[] → `::jsonb`
|
|
36
|
+
- Simple DS static/system fields (in `SIMPLE_STATIC_FIELD_SLUGS`): direct reference. Field not found → error.
|
|
37
|
+
|
|
38
|
+
### builtin
|
|
39
|
+
|
|
40
|
+
`{ kind: "builtin", name: "current_date"|"current_time"|"current_timestamp"|"now" }`
|
|
41
|
+
|
|
42
|
+
- Emitted as raw SQL. Other names → error.
|
|
43
|
+
|
|
44
|
+
### function
|
|
45
|
+
|
|
46
|
+
`{ kind: "function", name: string, inputs?: Block[] }`
|
|
47
|
+
|
|
48
|
+
- Validated against allowed functions whitelist. Inputs compiled recursively, joined by commas.
|
|
49
|
+
- **Gotcha:** Literal auto-cast only works inside `concat`/`concat_ws`. For `jsonb_build_object` and other functions, add explicit `"cast": "text"` to string literal blocks or Postgres will fail to determine parameter types.
|
|
50
|
+
|
|
51
|
+
### extract
|
|
52
|
+
|
|
53
|
+
`{ kind: "extract", part: "year"|"month"|"day"|"hour"|"minute"|"second", inputs: [Block] }`
|
|
54
|
+
|
|
55
|
+
- Exactly 1 input required.
|
|
56
|
+
- SQL: `extract(<part> from <expr>)`
|
|
57
|
+
|
|
58
|
+
### aggregate
|
|
59
|
+
|
|
60
|
+
`{ kind: "aggregate", name: "count"|"sum"|"avg"|"min"|"max"|"jsonb_agg"|"json_agg"|"array_agg", distinct?: boolean, inputs: Block[] }`
|
|
61
|
+
|
|
62
|
+
- `count` with empty or omitted inputs → `count(*)`. For count specifically, `inputs` is optional.
|
|
63
|
+
- `distinct` → `DISTINCT` keyword.
|
|
64
|
+
|
|
65
|
+
### math
|
|
66
|
+
|
|
67
|
+
`{ kind: "math", op: "+"|"-"|"*"|"/"|"%", inputs: Block[] }`
|
|
68
|
+
|
|
69
|
+
- Min 2 operands. Left-associative with parens: `((a op b) op c)`.
|
|
70
|
+
|
|
71
|
+
### case
|
|
72
|
+
|
|
73
|
+
`{ kind: "case", cases: [{when: Block, then: Block}], else?: Block }`
|
|
74
|
+
|
|
75
|
+
- Min 1 case required. `else` optional (defaults NULL).
|
|
76
|
+
|
|
77
|
+
### compare
|
|
78
|
+
|
|
79
|
+
`{ kind: "compare", op: "="|"!="|"<>"|">"|"<"|">="|"<="|"like"|"ilike"|"in"|"not in"|"not_in", left: Block, right: Block }`
|
|
80
|
+
|
|
81
|
+
- `in`/`not in`: `left in right`. `not_in` accepted as alias but prefer `"not in"` (with space).
|
|
82
|
+
- `ilike` auto-converted to `like` for MySQL dialect.
|
|
83
|
+
|
|
84
|
+
### boolean
|
|
85
|
+
|
|
86
|
+
`{ kind: "boolean", op: "and"|"or"|"not", inputs: Block[] }`
|
|
87
|
+
|
|
88
|
+
- `not`: exactly 1 input → `not (<expr>)`. `and`/`or`: min 2 → `((<a>) op (<b>))`.
|
|
89
|
+
|
|
90
|
+
## Subquery Details
|
|
91
|
+
|
|
92
|
+
- `from`: child table full slug (`appSlug_tableSlug`), matched via `dataSource.children`.
|
|
93
|
+
- `with` (string): child field joins to parent `id`. `with` (object): `{ childField: parentField }`.
|
|
94
|
+
- Simple child DS: table rewritten to `tenant_record`, fields use `data->>'uuid'` refs.
|
|
95
|
+
- Optional `filters` apply WHERE on child table.
|
|
96
|
+
- Child alias: `t0_child`. Parent alias: `t0`.
|
|
97
|
+
|
|
98
|
+
## Allowed Functions (Postgres)
|
|
99
|
+
|
|
100
|
+
**String**: length, lower, upper, substr, replace, concat, trim, ltrim, rtrim, btrim, split_part, initcap, reverse, strpos, lpad, rpad
|
|
101
|
+
|
|
102
|
+
**Number**: abs, ceil, floor, round, sqrt, power, mod, gcd, lcm, exp, ln, log, log10, log1p, pi, sign, width_bucket, trunc, greatest, least
|
|
103
|
+
|
|
104
|
+
**Date/Time**: now, age, clock_timestamp, date_part, date_trunc, extract, isfinite, justify_days, justify_hours, make_date, make_time, make_timestamp, make_timestamptz, timeofday, to_timestamp, to_char, to_date, to_time
|
|
105
|
+
|
|
106
|
+
**Utility**: coalesce
|
|
107
|
+
|
|
108
|
+
**JSON/JSONB**: jsonb_array_length, jsonb_extract_path, jsonb_extract_path_text, jsonb_object_keys, jsonb_build_object, json_build_object, jsonb_agg, json_agg, array_agg, array_to_json, row_to_json
|
|
109
|
+
|
|
110
|
+
**Aggregates**: count, sum, avg, min, max, jsonb_agg, json_agg, array_agg
|
|
111
|
+
|
|
112
|
+
## Cast Types
|
|
113
|
+
|
|
114
|
+
Allowed: int, int2, int4, int8, bigint, real, float, float4, float8, numeric, double, decimal, money, timestamp, timestamptz, date, time, interval, bool, boolean, uuid, text (+ array variants like `int[]`, `text[]`).
|
|
115
|
+
|
|
116
|
+
## Timezone
|
|
117
|
+
|
|
118
|
+
`tz` property: validated `/^[a-zA-Z0-9_]+$/`. SQL: `<expr> at time zone '<tz>'`. Column/function blocks omit outer parens.
|
|
119
|
+
|
|
120
|
+
## Validation Errors
|
|
121
|
+
|
|
122
|
+
| Condition | Error |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Empty inputs | "Formula must have at least one input block" |
|
|
125
|
+
| >1 root input | "Multiple input blocks not yet supported" |
|
|
126
|
+
| Bad function | `Function "${name}" is not allowed for dialect "${dialect}"` |
|
|
127
|
+
| Bad aggregate | `Aggregate function "${name}" is not allowed` |
|
|
128
|
+
| Extract ≠1 input | "EXTRACT requires exactly one input expression" |
|
|
129
|
+
| Math <2 ops | "Math operations require at least 2 operands" |
|
|
130
|
+
| NOT ≠1 op | "NOT operation requires exactly one operand" |
|
|
131
|
+
| AND/OR <2 ops | "${OP} operation requires at least 2 operands" |
|
|
132
|
+
| CASE 0 whens | "CASE expression must have at least one WHEN clause" |
|
|
133
|
+
| Bad tz | "Unsupported timezone: ${tz}" |
|
|
134
|
+
| Bad builtin | "Unsupported formula function: ${name}" |
|
|
135
|
+
|
|
136
|
+
## SelectQueryBuilder Integration
|
|
137
|
+
|
|
138
|
+
1. Formulas in `ISelectQueryParams.formulas` as `Record<string, IQueryFormula>`.
|
|
139
|
+
2. Column alias matching formula key → formula replaces column ref in SELECT.
|
|
140
|
+
3. Dispatch: `from`/`expression` → `buildBlockFormula()` (subquery), `inputs` only → `buildBlockFormula()` (inline).
|
|
141
|
+
4. Calculations with `func:"formula"` also route through `buildFormula()`.
|
|
142
|
+
5. `usedFormulas` set prevents duplicate application across SELECT and aggregations.
|
|
143
|
+
6. Subquery formulas trigger async `resolveChildDatasources()` before build.
|
|
144
|
+
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
**Inline math** (balance / 100):
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{ "inputs": [{ "kind": "math", "op": "/", "inputs": [{ "kind": "column", "name": "balance" }, { "kind": "literal", "literal": 100 }] }] }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Formatted date** (to_char):
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "inputs": [{ "kind": "function", "name": "to_char", "inputs": [{ "kind": "column", "name": "created_on" }, { "kind": "literal", "literal": "DD/MM/YYYY" }] }] }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Subquery count**:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{ "from": "app_child", "with": "parent_id", "inputs": [{ "kind": "aggregate", "name": "count", "inputs": [] }] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Subquery count with distinct**:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "expression": { "from": "app_child_table", "with": "parent_field", "inputs": [{ "kind": "aggregate", "name": "count", "distinct": true, "inputs": [{ "kind": "column", "name": "id" }] }] } }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Multi-field subquery join**:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{ "from": "app_child", "with": { "child_field1": "parent_field1", "child_field2": "parent_field2" }, "inputs": [{ "kind": "aggregate", "name": "sum", "inputs": [{ "kind": "column", "name": "amount" }] }] }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**CASE with AND**:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{ "inputs": [{ "kind": "case", "cases": [{ "when": { "kind": "boolean", "op": "and", "inputs": [{ "kind": "compare", "op": ">", "left": { "kind": "column", "name": "price" }, "right": { "kind": "literal", "literal": 100 } }, { "kind": "compare", "op": "ilike", "left": { "kind": "column", "name": "name" }, "right": { "kind": "literal", "literal": "%pro%" } }] }, "then": { "kind": "literal", "literal": "premium" } }], "else": { "kind": "literal", "literal": "standard" } }] }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Multi-branch CASE** (tier assignment):
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{ "inputs": [{ "kind": "case", "cases": [{ "when": { "kind": "compare", "op": ">=", "left": { "kind": "column", "name": "revenue" }, "right": { "kind": "literal", "literal": 100000 } }, "then": { "kind": "literal", "literal": "enterprise" } }, { "when": { "kind": "compare", "op": ">=", "left": { "kind": "column", "name": "revenue" }, "right": { "kind": "literal", "literal": 10000 } }, "then": { "kind": "literal", "literal": "business" } }, { "when": { "kind": "compare", "op": ">=", "left": { "kind": "column", "name": "revenue" }, "right": { "kind": "literal", "literal": 1000 } }, "then": { "kind": "literal", "literal": "starter" } }], "else": { "kind": "literal", "literal": "free" } }] }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Nested aggregate**: `round(sum(qty * price), 2)`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "alias": "total", "inputs": [{ "kind": "function", "name": "round", "inputs": [{ "kind": "aggregate", "name": "sum", "inputs": [{ "kind": "math", "op": "*", "inputs": [{ "kind": "column", "name": "qty" }, { "kind": "column", "name": "price" }] }] }, { "kind": "literal", "literal": 2 }] }] }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Timezone**: `to_char(now() at time zone 'UTC', 'YYYY-MM-DD')`:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{ "inputs": [{ "kind": "function", "name": "to_char", "inputs": [{ "kind": "function", "name": "now", "tz": "UTC" }, { "kind": "literal", "literal": "YYYY-MM-DD" }] }] }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**COALESCE** (null handling):
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{ "inputs": [{ "kind": "function", "name": "coalesce", "inputs": [{ "kind": "column", "name": "description" }, { "kind": "literal", "literal": "No description" }] }] }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Subquery with filters** (count active children):
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{ "from": "app_child_table", "with": "parent_id", "filters": { "rules": [{ "field": "status", "operator": "=", "value": "active" }] }, "inputs": [{ "kind": "aggregate", "name": "count", "inputs": [] }] }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**String concatenation with initcap**:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{ "inputs": [{ "kind": "function", "name": "initcap", "inputs": [{ "kind": "function", "name": "concat", "inputs": [{ "kind": "column", "name": "first_name" }, { "kind": "literal", "literal": " " }, { "kind": "column", "name": "last_name" }] }] }] }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Percentage with cast**: `round(completed/total * 100, 2)`:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{ "inputs": [{ "kind": "function", "name": "round", "inputs": [{ "kind": "math", "op": "*", "inputs": [{ "kind": "math", "op": "/", "inputs": [{ "kind": "column", "name": "completed_tasks", "cast": "decimal" }, { "kind": "function", "name": "greatest", "inputs": [{ "kind": "column", "name": "total_tasks", "cast": "decimal" }, { "kind": "literal", "literal": 1 }] }] }, { "kind": "literal", "literal": 100 }] }, { "kind": "literal", "literal": 2 }] }] }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Days since created**: `date_part('day', age(now, created_on))::int`:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{ "inputs": [{ "kind": "function", "name": "date_part", "inputs": [{ "kind": "literal", "literal": "day" }, { "kind": "function", "name": "age", "inputs": [{ "kind": "builtin", "name": "now" }, { "kind": "column", "name": "created_on" }] }], "cast": "int" }] }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Boolean NOT with OR** (is_active = not archived or deleted):
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{ "inputs": [{ "kind": "boolean", "op": "not", "inputs": [{ "kind": "boolean", "op": "or", "inputs": [{ "kind": "compare", "op": "=", "left": { "kind": "column", "name": "is_archived" }, "right": { "kind": "literal", "literal": true } }, { "kind": "compare", "op": "=", "left": { "kind": "column", "name": "is_deleted" }, "right": { "kind": "literal", "literal": true } }] }] }] }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Subquery sum with filters** (outstanding invoice amount):
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{ "from": "billing_invoice_line", "with": "invoice_id", "filters": { "combinator": "and", "rules": [{ "field": "status", "operator": "!=", "value": "paid", "filterType": "ALPHA" }, { "field": "amount", "operator": ">", "value": 0, "filterType": "NUMERIC" }] }, "inputs": [{ "kind": "function", "name": "coalesce", "inputs": [{ "kind": "aggregate", "name": "sum", "inputs": [{ "kind": "column", "name": "amount" }] }, { "kind": "literal", "literal": 0 }] }] }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Extract year-month** (concat year + padded month):
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{ "inputs": [{ "kind": "function", "name": "concat", "inputs": [{ "kind": "extract", "part": "year", "inputs": [{ "kind": "column", "name": "created_on" }], "cast": "text" }, { "kind": "literal", "literal": "-" }, { "kind": "function", "name": "lpad", "inputs": [{ "kind": "extract", "part": "month", "inputs": [{ "kind": "column", "name": "created_on" }], "cast": "text" }, { "kind": "literal", "literal": 2 }, { "kind": "literal", "literal": "0" }] }] }] }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**JSONB extraction**:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{ "inputs": [{ "kind": "function", "name": "jsonb_extract_path_text", "inputs": [{ "kind": "column", "name": "address" }, { "kind": "literal", "literal": "country" }] }] }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Weighted average**: `sum(score * weight) / greatest(sum(weight), 1)`:
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{ "inputs": [{ "kind": "math", "op": "/", "inputs": [{ "kind": "aggregate", "name": "sum", "inputs": [{ "kind": "math", "op": "*", "inputs": [{ "kind": "column", "name": "score" }, { "kind": "column", "name": "weight" }] }] }, { "kind": "function", "name": "greatest", "inputs": [{ "kind": "aggregate", "name": "sum", "inputs": [{ "kind": "column", "name": "weight" }] }, { "kind": "literal", "literal": 1 }] }], "cast": "decimal" }] }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Date truncation** (period grouping by month):
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{ "inputs": [{ "kind": "function", "name": "date_trunc", "inputs": [{ "kind": "literal", "literal": "month" }, { "kind": "column", "name": "order_date" }] }] }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Multiple subquery formulas** (project with total + open task counts, using compat wrapper):
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"columns": "id, name, total_tasks, open_tasks",
|
|
272
|
+
"formulas": [
|
|
273
|
+
{
|
|
274
|
+
"key": "total_tasks",
|
|
275
|
+
"expression": {
|
|
276
|
+
"expression": {
|
|
277
|
+
"from": "base_task", "with": "project",
|
|
278
|
+
"inputs": [{ "kind": "aggregate", "name": "count", "inputs": [{ "kind": "column", "name": "id" }] }]
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"key": "open_tasks",
|
|
284
|
+
"expression": {
|
|
285
|
+
"expression": {
|
|
286
|
+
"from": "base_task", "with": "project",
|
|
287
|
+
"inputs": [{ "kind": "aggregate", "name": "count", "inputs": [{ "kind": "column", "name": "id" }] }],
|
|
288
|
+
"filters": { "rules": [{ "field": "status", "operator": "not_in", "value": ["<completed_uuid>", "<cancelled_uuid>"] }], "combinator": "and" }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
→ Each formula produces a correlated subquery: `(SELECT count("t0_child"."id") FROM ... WHERE "t0_child"."project" = "t0"."id" [AND status filter])`. No GROUP BY needed.
|
|
297
|
+
|
|
298
|
+
**Combined aggregations via jsonb_build_object** (pack total + open counts into one JSON column, one subquery):
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"key": "task_stats",
|
|
303
|
+
"expression": {
|
|
304
|
+
"expression": {
|
|
305
|
+
"from": "base_task", "with": "project",
|
|
306
|
+
"inputs": [{
|
|
307
|
+
"kind": "function", "name": "jsonb_build_object",
|
|
308
|
+
"inputs": [
|
|
309
|
+
{ "kind": "literal", "literal": "total", "cast": "text" },
|
|
310
|
+
{ "kind": "aggregate", "name": "count", "inputs": [{ "kind": "column", "name": "id" }] },
|
|
311
|
+
{ "kind": "literal", "literal": "open", "cast": "text" },
|
|
312
|
+
{ "kind": "aggregate", "name": "count", "inputs": [{ "kind": "case", "cases": [{ "when": { "kind": "compare", "op": "not in", "left": { "kind": "column", "name": "status" }, "right": { "kind": "literal", "literal": ["<completed_uuid>", "<cancelled_uuid>"] } }, "then": { "kind": "column", "name": "id" } }] }] }
|
|
313
|
+
]
|
|
314
|
+
}]
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
→ Result: `{ "task_stats": { "total": 6, "open": 2 } }`. `count(CASE WHEN ... THEN id END)` skips NULLs (no `else`) to count conditionally. `"cast": "text"` on literal keys is **required** for `jsonb_build_object`.
|