Skip to content

Commit 288a0e4

Browse files
committed
feat: add MetadataHolder embedding for operator metadata reuse
Introduce MetadataHolder struct that stores DSL-declared field-name slices and provides a default SetMetadata implementation. Operators embed it via Go struct embedding instead of writing per-operator SetMetadata methods, reducing boilerplate. Migrated all engine operators (filter_condition, merge_dedup, reorder_sort, transform_normalize, transform_dispatch) to use MetadataHolder embedding. Updated tests, design docs, and codegen output. Bump to v0.2.5.
1 parent 2a3ec14 commit 288a0e4

23 files changed

Lines changed: 231 additions & 143 deletions

apple/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.4"
1+
__version__ = "0.2.5"

apple_generated/operators.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ class FilterConditionOp(BaseOp):
88
"""Operator: filter_condition"""
99
_name = "filter_condition"
1010
_params_schema = {
11-
"field": {"type": "string", "required": True},
1211
"value": {"type": "any", "required": True},
1312
}
1413

1514
def __call__(
1615
self,
1716
*,
18-
field: str = ...,
1917
value: Any = ...,
2018
common_input: list[str] | None = None,
2119
common_output: list[str] | None = None,
@@ -28,7 +26,6 @@ def __call__(
2826
) -> "FilterConditionOp":
2927
return self._apply(
3028
params={
31-
"field": field,
3229
"value": value,
3330
},
3431
common_input=common_input,
@@ -79,14 +76,12 @@ class MergeDedupOp(BaseOp):
7976
"""Operator: merge_dedup"""
8077
_name = "merge_dedup"
8178
_params_schema = {
82-
"dedup_by": {"type": "string", "required": True},
8379
"strategy": {"type": "string", "required": False, "default": "first"},
8480
}
8581

8682
def __call__(
8783
self,
8884
*,
89-
dedup_by: str = ...,
9085
strategy: str = "",
9186
common_input: list[str] | None = None,
9287
common_output: list[str] | None = None,
@@ -99,7 +94,6 @@ def __call__(
9994
) -> "MergeDedupOp":
10095
return self._apply(
10196
params={
102-
"dedup_by": dedup_by,
10397
"strategy": strategy,
10498
},
10599
common_input=common_input,
@@ -185,14 +179,12 @@ class ReorderSortOp(BaseOp):
185179
"""Operator: reorder_sort"""
186180
_name = "reorder_sort"
187181
_params_schema = {
188-
"field": {"type": "string", "required": True},
189182
"order": {"type": "string", "required": False, "default": "desc"},
190183
}
191184

192185
def __call__(
193186
self,
194187
*,
195-
field: str = ...,
196188
order: str = "",
197189
common_input: list[str] | None = None,
198190
common_output: list[str] | None = None,
@@ -205,7 +197,6 @@ def __call__(
205197
) -> "ReorderSortOp":
206198
return self._apply(
207199
params={
208-
"field": field,
209200
"order": order,
210201
},
211202
common_input=common_input,
@@ -262,15 +253,11 @@ class TransformDispatchOp(BaseOp):
262253
"""Operator: transform_dispatch"""
263254
_name = "transform_dispatch"
264255
_params_schema = {
265-
"common_field": {"type": "string", "required": True},
266-
"item_field": {"type": "string", "required": True},
267256
}
268257

269258
def __call__(
270259
self,
271260
*,
272-
common_field: str = ...,
273-
item_field: str = ...,
274261
common_input: list[str] | None = None,
275262
common_output: list[str] | None = None,
276263
item_input: list[str] | None = None,
@@ -282,8 +269,6 @@ def __call__(
282269
) -> "TransformDispatchOp":
283270
return self._apply(
284271
params={
285-
"common_field": common_field,
286-
"item_field": item_field,
287272
},
288273
common_input=common_input,
289274
common_output=common_output,
@@ -299,17 +284,13 @@ class TransformNormalizeOp(BaseOp):
299284
"""Operator: transform_normalize"""
300285
_name = "transform_normalize"
301286
_params_schema = {
302-
"field": {"type": "string", "required": True},
303287
"method": {"type": "string", "required": False, "default": "min_max"},
304-
"output_field": {"type": "string", "required": False},
305288
}
306289

307290
def __call__(
308291
self,
309292
*,
310-
field: str = ...,
311293
method: str = "",
312-
output_field: str = "",
313294
common_input: list[str] | None = None,
314295
common_output: list[str] | None = None,
315296
item_input: list[str] | None = None,
@@ -321,9 +302,7 @@ def __call__(
321302
) -> "TransformNormalizeOp":
322303
return self._apply(
323304
params={
324-
"field": field,
325305
"method": method,
326-
"output_field": output_field,
327306
},
328307
common_input=common_input,
329308
common_output=common_output,

design_doc/03_data_abstraction.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,81 @@ func (out *OperatorOutput) SetWarning(err error)
270270
- **统一接口,按需使用**:所有算子共享同一个 `Operator` 接口和 `OperatorOutput`。不同类型的算子使用不同的方法子集,不使用的方法不调用即可。引擎通过 JSON 元数据(`recall: true`、`sources` 等)或 Go 接口断言识别算子类别,决定写回策略。
271271
- **无状态可重入**:算子在 `Init` 后不持有可变状态,`Execute` 可被多个 goroutine 并发调用。算子可持有只读配置和线程安全资源(如连接池),不可持有请求级状态。
272272
- **错误约定**:`return nil` 表示正常执行;`output.SetWarning(err)` 表示可恢复错误(DAG 继续);`return err` 表示不可恢复错误(DAG 终止)。
273+
274+
#### MetadataAware — 字段名自省
275+
276+
算子操作的字段名已在 DSL 的 `common_input` / `common_output` / `item_input` / `item_output` 中声明。通过实现可选接口 `MetadataAware`,算子可以在初始化阶段获取这些声明的字段名,而无需通过 `Params` 重复指定。
277+
278+
```go
279+
// MetadataAware is an optional interface. The engine calls SetMetadata
280+
// after Init for operators that implement it.
281+
type MetadataAware interface {
282+
SetMetadata(commonInput, commonOutput, itemInput, itemOutput []string)
283+
}
284+
```
285+
286+
引擎在 `Init(params)` 之后自动检测算子是否实现 `MetadataAware`,若实现则调用 `SetMetadata` 注入 `$metadata` 中声明的字段名。
287+
288+
##### MetadataHolder — 嵌入式默认实现
289+
290+
引擎提供 `MetadataHolder` 结构体,存储四个字段名切片并提供默认的 `SetMetadata` 实现。算子通过 Go embedding 嵌入即可自动满足 `MetadataAware` 接口,无需每个算子手写 `SetMetadata`
291+
292+
```go
293+
// MetadataHolder 存储 DSL 声明的字段名,提供默认 SetMetadata。
294+
type MetadataHolder struct {
295+
CommonInput []string
296+
CommonOutput []string
297+
ItemInput []string
298+
ItemOutput []string
299+
}
300+
301+
func (m *MetadataHolder) SetMetadata(commonInput, commonOutput, itemInput, itemOutput []string) {
302+
m.CommonInput = commonInput
303+
m.CommonOutput = commonOutput
304+
m.ItemInput = itemInput
305+
m.ItemOutput = itemOutput
306+
}
307+
```
308+
309+
**使用方式**——算子嵌入 `pine.MetadataHolder`,在 `Execute` 中直接访问字段名:
310+
311+
```go
312+
type SortOp struct {
313+
pine.MetadataHolder // 自动实现 MetadataAware
314+
ascending bool
315+
}
316+
317+
func (o *SortOp) Execute(ctx context.Context, in *pine.OperatorInput, out *pine.OperatorOutput) error {
318+
field := o.ItemInput[0] // 直接从嵌入的 MetadataHolder 获取
319+
// ...
320+
}
321+
```
322+
323+
如果算子需要自定义 metadata 处理逻辑(极少见),可以覆写 `SetMetadata` 方法,此时嵌入的默认实现不再生效。
324+
325+
**何时用 `MetadataAware` vs `Params`**
326+
327+
| 场景 | 推荐方式 | 说明 |
328+
|------|---------|------|
329+
| 算子操作的字段名由调用方决定 | 嵌入 `MetadataHolder` | 字段名已在 DSL 声明中给出,不应重复 |
330+
| 算子有固定的业务语义字段 | 硬编码 |`filter_paginate` 固定读 `page`/`size` |
331+
| 算子有与字段名无关的配置 | `Params` |`reorder_sort``order="desc"` |
332+
333+
**反模式**:通过 `Params` 传入字段名,导致 DSL 调用时必须同时声明 `common_input=["x"]``field="x"`——信息重复,容易不一致。
334+
335+
**正确模式示例**(以 `reorder_sort` 为例):
336+
337+
```python
338+
# 反模式: field 名在 item_input 和 params 中重复
339+
flow.reorder_sort(
340+
item_input=["score"],
341+
field="score", # 冗余!
342+
ascending=False,
343+
)
344+
345+
# 正确模式: 算子通过 MetadataHolder 获取 item_input=["score"]
346+
flow.reorder_sort(
347+
item_input=["score"],
348+
order="desc",
349+
)
350+
```

doc/operators/filter_condition.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ Removes items where a specified field equals a given value.
88

99
| Name | Type | Required | Default | Description |
1010
|------|------|----------|---------|-------------|
11-
| field | string | Yes | - | Item field to check. |
1211
| value | any | Yes | - | Items where field == value are removed. |
1312

1413
## Metadata Contract
@@ -24,7 +23,6 @@ Removes items where a specified field equals a given value.
2423

2524
```python
2625
flow.filter_condition(
27-
field=...,
2826
value=...,
2927
common_input=[...],
3028
item_input=[...],

doc/operators/merge_dedup.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ Deduplicates items by a key field, keeping the first occurrence.
88

99
| Name | Type | Required | Default | Description |
1010
|------|------|----------|---------|-------------|
11-
| dedup_by | string | Yes | - | Field name to deduplicate on. |
1211
| strategy | string | No | `"first"` | Dedup strategy — "first" keeps first occurrence. |
1312

1413
## Metadata Contract
@@ -24,7 +23,6 @@ Deduplicates items by a key field, keeping the first occurrence.
2423

2524
```python
2625
flow.merge_dedup(
27-
dedup_by=...,
2826
strategy=...,
2927
common_input=[...],
3028
item_input=[...],

doc/operators/reorder_sort.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ Sorts items by a numeric field in ascending or descending order.
88

99
| Name | Type | Required | Default | Description |
1010
|------|------|----------|---------|-------------|
11-
| field | string | Yes | - | Item field to sort by. |
1211
| order | string | No | `"desc"` | Sort direction — "asc" or "desc". |
1312

1413
## Metadata Contract
@@ -24,7 +23,6 @@ Sorts items by a numeric field in ascending or descending order.
2423

2524
```python
2625
flow.reorder_sort(
27-
field=...,
2826
order=...,
2927
common_input=[...],
3028
item_input=[...],

doc/operators/transform_dispatch.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ Copies a common-side field value to every item as an item-side field.
88

99
| Name | Type | Required | Default | Description |
1010
|------|------|----------|---------|-------------|
11-
| common_field | string | Yes | - | Source common field to read. |
12-
| item_field | string | Yes | - | Target item field to write. |
1311

1412
## Metadata Contract
1513

@@ -24,8 +22,6 @@ Copies a common-side field value to every item as an item-side field.
2422

2523
```python
2624
flow.transform_dispatch(
27-
common_field=...,
28-
item_field=...,
2925
common_input=[...],
3026
item_input=[...],
3127
item_output=[...],

doc/operators/transform_normalize.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ Normalizes a numeric item field using min-max scaling to [0, 1].
88

99
| Name | Type | Required | Default | Description |
1010
|------|------|----------|---------|-------------|
11-
| field | string | Yes | - | Item field to normalize. |
1211
| method | string | No | `"min_max"` | Normalization method. |
13-
| output_field | string | No | `""` | Target field for normalized values. |
1412

1513
## Metadata Contract
1614

@@ -25,9 +23,7 @@ Normalizes a numeric item field using min-max scaling to [0, 1].
2523

2624
```python
2725
flow.transform_normalize(
28-
field=...,
2926
method=...,
30-
output_field=...,
3127
common_input=[...],
3228
item_input=[...],
3329
item_output=[...],

internal/types/operator.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,31 @@ type MetadataAware interface {
148148
SetMetadata(commonInput, commonOutput, itemInput, itemOutput []string)
149149
}
150150

151+
// MetadataHolder stores the four DSL-declared field-name slices and provides
152+
// a default SetMetadata implementation. Embed it in an operator struct to
153+
// satisfy MetadataAware automatically:
154+
//
155+
// type SortOp struct {
156+
// pine.MetadataHolder
157+
// ascending bool
158+
// }
159+
//
160+
// The operator can then access o.CommonInput, o.ItemInput, etc. directly.
161+
type MetadataHolder struct {
162+
CommonInput []string
163+
CommonOutput []string
164+
ItemInput []string
165+
ItemOutput []string
166+
}
167+
168+
// SetMetadata implements MetadataAware.
169+
func (m *MetadataHolder) SetMetadata(commonInput, commonOutput, itemInput, itemOutput []string) {
170+
m.CommonInput = commonInput
171+
m.CommonOutput = commonOutput
172+
m.ItemInput = itemInput
173+
m.ItemOutput = itemOutput
174+
}
175+
151176
// ParamSpec describes a single operator parameter for schema validation.
152177
type ParamSpec struct {
153178
Type string // "string", "int64", "float64", "bool", "any"

operator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ type OperatorSchema = types.OperatorSchema
3333
// MetadataAware is an optional interface for operators that need access to
3434
// their declared input/output field names from $metadata.
3535
type MetadataAware = types.MetadataAware
36+
37+
// MetadataHolder stores DSL-declared field-name slices and provides a default
38+
// SetMetadata. Embed it in operator structs for automatic MetadataAware compliance.
39+
type MetadataHolder = types.MetadataHolder

0 commit comments

Comments
 (0)