跳转至

命令

命令是一个已注册、受治理的函数,它把外部计算纳入 Provisa 的治理、审计与血缘之中。联邦引擎原生处理 SQL。命令涵盖 SQL 无法表达的计算,例如数据增强微服务、Python 模型、shell 脚本或数据库原生存储过程。你只需注册一次,每个客户端接口(GraphQL、pgwire SQL、REST、Arrow Flight、gRPC、Bolt/Cypher)都能在完全相同的治理下调用它。

命令是一次受治理的 RPC。它的输入和输出都经过声明、定型、校验、追踪,并接入血缘。临时拼凑的 ETL 步骤、不受治理的 curl 调用或子进程,这些都不具备。

实现种类

支持六种 impl_kind 取值:

impl_kind 传输方式
source_procedure 已注册数据源上的原生存储过程
source_operation OpenAPI、远程 GraphQL 或 gRPC 来源的写入操作,原样透传(见远程来源的写入操作)
script 本地子进程,从 stdin 喂入 JSON,从 stdout 读取 JSON
http HTTP/S 终结点;JSON 请求体,JSON 响应
grpc gRPC 一元调用;无 proto 的 JSON 桥接
python 进程内的 Python 可调用对象(module:attr)

寻址(目录中的 name 与 function_name)与 binding(传输方式与位置)是解耦的。换掉 binding,命令的治理、血缘和调用方契约都保持不变。

参数种类

每个参数都声明一个 arg_kind:

arg_kind 行为
column_value 标量;直接在请求负载中传递
table_ref 惰性;Provisa 原样传递关系引用,由服务自行取数
result_set 急切;Provisa 物化被引用的关系并发送其行

http 和 grpc 命令必须至少声明一个 table_ref 或 result_set 参数。一个只收到标量参数的外部命令会被逐行调用一次,那就毁掉了批处理。分发器在调用时拒绝这种配置(422)。

返回集合的命令(通过 output_columns 和 return_schema 声明)是表值函数。可在 FROM 子句或 JOIN 中使用它。

数据集契约

每个 table_ref 或 result_set 参数都可以声明一份输入列契约:FunctionArgument.columns 中一份有序的、按 IR 定型的列清单。命令自身则在 Function.output_columns 中声明一份输出列契约。

两份契约在每一次调用时都会以失败即报的方式校验:

  • 输入(仅 result_set): 物化之后,Provisa 会依据所声明的列校验这些行。多出的字段、缺失的字段和类型不符都会引发 HTTP 422。

  • 输出: 命令返回的行在到达调用方之前,会依据 output_columns 校验。

  • 窄投影: 声明了输入契约后,物化查询只投影那些列(SELECT "id", "region" FROM ...),而不是 SELECT *。

IR 类型词汇表

契约中的列类型使用规范的 IR 类型名称:

smallint integer bigint text boolean float double numeric date timestamp time uuid bytea json

常见别名会自动解析(varchar → text、int4 → integer、jsonb → json 等)。

return_schema 是 output_columns 的 GraphQL 投影。真正的依据是 output_columns,所以请为校验和血缘声明它,再为生成 GraphQL 类型添加 return_schema。

编写命令

配置文件

functions:
  - name: enrich_orders
    description: Enrich orders inline — deterministic score + region label
    domain_id: sales-analytics
    kind: query
    impl_kind: python
    source_id: ""
    function_name: enrich_orders
    returns: ""
    binding:
      callable: demo.py_functions:enrich_orders
    arguments:
      - name: input
        type: String
        arg_kind: result_set
        columns:
          - {name: id, type: integer}   # narrow input contract
          - {name: region, type: text}
    visible_to: [admin]
    output_columns:
      - {name: id, type: integer}
      - {name: score, type: double}
      - {name: region_label, type: text}
    return_schema:
      type: array
      items:
        type: object
        properties:
          id: {type: integer}
          score: {type: number}
          region_label: {type: string}

gRPC 变体(enrich_grpc_set)遵循同样的模式,只是指定 impl_kind: grpc,并且 binding 用 target 和 method 键代替 callable:

  - name: enrich_grpc_set
    impl_kind: grpc
    binding:
      target: ${env:DEMO_GRPC_TARGET:-localhost:50071}
      method: /provisa.demo.Enrich/EnrichRows
    arguments:
      - name: input
        type: String
        arg_kind: result_set
        columns:
          - {name: id, type: integer}
          - {name: region, type: text}
    output_columns:
      - {name: id, type: integer}
      - {name: embedding, type: text}
      - {name: geo, type: text}

管理 UI

设置 → 命令中的命令表单包含一个按数据集划分的输入列编辑器(每个已声明的列一行,配有 IR 类型选择器)和一个输出列编辑器。保存表单即可注册或更新命令,无需重新加载配置。

内联组合

命令可以出现在更大的 SQL 语句内部,你可以对它做连接、子查询或投影;SELECT * FROM fn(args) 只是最简单的形式。例外是远程数据源的写操作,因为它只能单独调用(见为何不能被组合)。

-- Enrich the orders relation and join the result back inline.
SELECT o.id, o.amount, e.score, e.region_label
FROM   orders o
JOIN   enrich_orders('main.public.orders') e ON o.id = e.id
WHERE  e.score > 0.8;

在治理、校验或路由运行之前,管道会检测出已注册的命令调用,经由共享的受治理执行器逐一执行(因此 I/O 契约和身份模型的适用方式与直接调用完全一致),并把调用点改写为一个已定型的本地关系。

替换是随规模自适应的:不超过 1,000 行时,结果以已定型的 VALUES 列表内联;超过该阈值则在引擎中注册为具名的本地关系。

本地化后的语句照常路由。单源查询留在数据源上;只有真正的跨源查询才会走联邦引擎。

远程来源的写入操作

OpenAPI、远程 GraphQL 或 gRPC 来源会提供写入操作。将其中一个注册为命令后,便可从所有界面调用,并受治理、被审计。添加来源不会注册其中任何一个;你要逐个注册所需的操作,方式与注册数据表相同。注册即为策展。

来源提供的内容:

来源类型 提供的操作 操作名称
openapi 规范中的每个非 GET 操作 operationId
graphql_remote 远程 Mutation 类型的每个字段 字段名称
grpc_remote 每个被归类为 mutation 的方法 Service.Method

注册一个

  1. 打开 建模 → 命令 并添加一个命令。
  2. 选择远程来源。表单会切换为操作选择器,列出该来源所提供的操作。
  3. 选择操作、一个域,以及可以调用它的角色。
  4. 可选:开启 需要审批,并填写 写入表 字段。

通过管理 GraphQL API,availableFunctions(sourceId, schemaName) 会列出这些操作。模式名称依来源类型而定,为 openapi、graphql 或 grpc_remote。

其余一切都由该操作决定,与表单发送的内容无关。Provisa 会覆盖以下字段:

body.implKind = "source_operation"
body.kind = "mutation"
body.schemaName = OPERATION_SCHEMA[source_type]
body.returns = ""
body.binding = {}
body.materialize = False
body.arguments = [{"name": a, "type": "json"} for a in operation.arguments]

每个参数的类型均为 json。操作的参数即其 OpenAPI 路径参数(操作带请求体时另加 body)、其 GraphQL mutation 参数,或其 gRPC 请求字段。 来源未提供的操作会被拒绝,返回 422,functions.operation_not_offered。

调用

Provisa 不会对输入进行整形、类型化或检查。每个参数都原样连同来源的凭据发往远程服务,远程服务的应答也原样返回。Provisa 负责治理谁可以调用、在哪个域中调用、是否需要审批,并记录该调用。

远程服务如何接收参数:

  • OpenAPI. 路径参数填入路径模板。body 是 JSON 请求体。其余每个参数都放在查询字符串中。
  • GraphQL. 参数以类型化变量发送,每个变量均按远程模式给定的类型声明。mutation 会回取应答中的标量字段和枚举字段,以及其内部对象的这类字段,最多深入两层。
  • gRPC. 这些参数成为 Service.Method 的请求消息。

应答即命令的行:一个对象是一行,对象列表即其各行,其他任何内容都是一行 {"result": ...}。

在 GraphQL 中,命令是一个 mutation 字段,其应答是 JSON 标量。

mutation {
  createIssue(input: {repositoryId: "R_kgDO...", title: "Crash on save"})
}

在 SQL 界面(pgwire 及其他传递 SQL 的界面)上,将每个参数写成字符串中的 JSON 字面量。'{"title": "x"}' 是对象,'"text"' 是字符串,'3' 是数字。不是有效 JSON 的字面量会失败,返回 422,functions.json_argument_invalid。

SELECT * FROM create_issue('{"repositoryId": "R_kgDO...", "title": "Crash on save"}');

在 REST 上,向 /data/rest/{domain}/commands/{command} 以 POST 发送由参数组成的 JSON 对象。json 参数在生成的规范中被记录为任意值。

curl -X POST https://acme.provisa.org/data/rest/engineering/commands/create_issue \
  -H "Content-Type: application/json" \
  -d '{"input": {"repositoryId": "R_kgDO...", "title": "Crash on save"}}'

拒绝

远程服务的拒绝按其原样返回。

远程服务 Provisa 应答
拒绝该调用(HTTP 4xx、GraphQL errors、被拒绝的 gRPC 调用) 422,functions.remote_refused,附带 remote_status 和 answer
失败(HTTP 5xx) 502, functions.remote_refused

来源的凭据能否执行该操作,由远程服务在调用该操作时决定。Provisa 无法在注册时试写而不真正执行它。

审批

开启 需要审批 后,每次调用在运行前都会提交给部署的审批钩子。仅当钩子批准时才会运行。

  • 未配置钩子:403,functions.approval_unavailable。
  • 钩子拒绝:403,functions.approval_denied,附带钩子给出的原因。

钩子会收到调用者、角色、命令名称及其参数。参见ABAC批准钩子。该标志存储为 Function.requires_approval,该检查适用于任何设置了它的命令。

写入表

在 写入表 字段中填写该操作所写入的数据表,格式为 schema.table。它必须是该命令自身来源下的已注册数据表,否则保存会被拒绝,返回 422,actions.written_table_not_registered。该设置可选。

远程服务每接受一次调用,Provisa 就将其视为对该数据表的一次写入。它会丢弃该表的缓存应答,将基于它的物化视图标记为过期,发出变更事件,运行该表变更时运行的命令,并在该表被保持为热状态时重新加载它。

当该表从其整表副本读取时(运营方的设置将其置于副本上,或引擎无法就地读取其数据源),调用之后会请求构建该副本,原因为 write。读取方在新副本替换之前继续使用旧副本。逐行复制的表或带有参数列的表没有可重建的整表副本。

为何不能被组合

写操作是一个动作,它不转换数据;包含写操作的视图或物化视图会在每次读取或刷新时执行一次写入。因此这个调用只能单独出现。单独的 SELECT * FROM create_issue(...) 会执行它,而同一调用放在更大的语句中,无论位于连接、子查询还是投影中,都会被拒绝。

若视图或物化视图的定义调用了它,保存时会被拒绝。只要注册了任何写入操作,无法解析的定义也会被拒绝,因为无法证明它没有调用任何一个。

command 'create_issue' writes to its source and is called on its own: it cannot be composed in a query, a view or a materialized view

源操作也不是血缘节点。血缘是从视图和查询的 SQL 中读取的,命令在其中作为节点出现,而任何已保存的定义都不能调用源操作。

命令与血缘

由于每个命令都声明了自己的输入列和输出列,列级血缘得以跨越不透明的命令边界闭合。血缘引擎会施加一次污点闭包:每个已声明的输出列都派生自每个已声明的输入列。

由此带来的实际后果:输入契约的宽度决定了这一闭包的精度。窄的输入只包含命令真正需要的列,会得到紧凑、易读的血缘锥。声明源关系中的每一列,则会在每个输出上大面积扇入。这样依然正确,因为不会丢失血缘,但会模糊可追溯性。

经验法则:只传入命令所需的最小投影,只返回派生列。原样回传的输入会扩大污染锥,所以不要返回它们。

命令节点如何出现在 DAG 中以及如何解读它们,参见血缘。

出口允许列表

http 和 grpc 命令会调用外部端点。每个目标主机都必须出现在部署的 udf_egress_allowlist 中。回环地址(localhost、127.0.0.1、::1)始终允许。没有允许列表时,所有外部出站都会以 HTTP 403 拒绝,不存在静默的默认值。

调用追踪

每次调用都会产生一条追踪,无论结果如何。追踪包含命令名称、传输类型、身份模型(DEFINER 或 INVOKER)、输入关系引用、角色 id 和输出基数。它由分发器产生,所以任何 impl_kind 都无法绕过。

CLI:provisa metadata export

provisa metadata export 是 shell 层的作业,运行在受治理的 RPC 路径之外。它向 /admin/metadata-export/publish 发送请求,触发正在运行的服务器按需发布元数据,也就是 Admin 选项卡中 立即发布 按钮调用的端点。

当配置的 reconcile_cron 计划粒度不够时,可用它从 cron 或 CI 驱动定时导出:

provisa metadata export --api https://acme.provisa.org --token "$PROVISA_API_TOKEN"

退出码 0 = 完整发布。退出码 1 = 部分发布或连接失败。

完整的标志参考、认证选项、多租户主机命名以及一个 cron 示例,参见元数据导出——从命令行。

命令会出现在每个环境的 git 投影中。命令及其标签分配如何在合并与拉取中留存,参见环境。