Перейти к содержанию

Команды

Команда является зарегистрированной управляемой функцией, которая ставит внешние вычисления под управление, аудит и происхождение данных Provisa. Движок федерации обрабатывает SQL сам. Команда покрывает вычисления, которые SQL выразить не может: микросервис обогащения, модель на Python, shell-скрипт или собственную хранимую процедуру базы данных. Вы регистрируете её один раз, и любая клиентская поверхность (GraphQL, SQL через pgwire, REST, Arrow Flight, gRPC, Bolt/Cypher) может вызвать её с одинаковым управлением.

Команда является управляемым RPC. Её входы и выходы объявлены, типизированы, проверены, трассируются и включены в происхождение данных. У разового шага ETL, неуправляемого вызова curl или подпроцесса ничего этого нет.

Виды реализации

Поддерживаются шесть значений impl_kind:

impl_kind Транспорт
source_procedure Нативная хранимая процедура на зарегистрированном источнике
source_operation Операция записи источника OpenAPI, удалённого GraphQL или gRPC, передаваемая как есть (см. Операция записи удалённого источника)
script Локальный подпроцесс, получающий JSON на stdin и читающий JSON из stdout
http HTTP/S-эндпоинт; тело запроса JSON, ответ JSON
grpc Унарный gRPC; JSON-мост без proto
python Вызываемый объект Python в процессе (module:attr)

Адресация (name каталога и function_name) отделена от 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 может объявить входной контракт колонок: упорядоченный, типизированный в IR список колонок в FunctionArgument.columns. Сама команда объявляет выходной контракт колонок в 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 является проекцией GraphQL для output_columns. Источник истины здесь output_columns, поэтому объявляйте его для проверки и происхождения данных, а return_schema добавляйте для генерации типов GraphQL.

Написание команды

Файл конфигурации

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

Форма команды в Settings → Commands включает редактор входных колонок для каждого набора данных (одна строка на объявленную колонку, с селектором 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;

До того как отработают управление, валидация или маршрутизация, конвейер обнаруживает вызовы зарегистрированных команд, выполняет каждый через общий управляемый исполнитель (так что контракт ввода-вывода и модель личности применяются ровно так же, как при прямом вызове) и переписывает место вызова в типизированное локальное отношение.

Подстановка адаптируется к размеру: до 1 000 строк результат встраивается как типизированный список VALUES; выше этого порога он регистрируется в движке как именованное локальное отношение.

Локализованное выражение маршрутизируется обычным образом. Запросы к одному источнику остаются на источнике; в движок федерации уходят только по-настоящему межисточниковые запросы.

Операция записи удалённого источника

Источник OpenAPI, удалённого GraphQL или gRPC предлагает операции записи. Регистрация одной из них как команды делает её вызываемой, управляемой и аудируемой с любой поверхности. Добавление источника не регистрирует ни одной; нужные вы регистрируете по одной, как регистрируете таблицы. Регистрация и есть курирование.

Что предлагает источник:

Тип источника Предлагаемые операции Имя операции
openapi Каждая операция спецификации, кроме GET operationId
graphql_remote Каждое поле удалённого типа Mutation Имя поля
grpc_remote Каждый метод, классифицированный как мутация 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 или поля её запроса gRPC. Операция, которую источник не предлагает, отклоняется с кодом 422, functions.operation_not_offered.

Вызов

Provisa не формирует, не типизирует и не проверяет входные данные. Каждый аргумент уходит удалённому сервису без изменений, с учётными данными источника, и ответ удалённого сервиса возвращается без изменений. Provisa управляет тем, кто может вызывать, в каком домене, нужно ли утверждение, и записывает вызов.

Как удалённый сервис получает аргументы:

  • OpenAPI. Параметры пути заполняют шаблон пути. body — это тело запроса в JSON. Любой другой аргумент идёт в строку запроса.
  • GraphQL. Аргументы отправляются как типизированные переменные, каждая объявлена с типом, который задаёт ей удалённая схема. Мутация запрашивает обратно скалярные поля и поля-перечисления ответа, а также такие поля объектов внутри него до двух уровней вглубь.
  • gRPC. Аргументы становятся сообщением запроса Service.Method.

Ответ — это строки команды: объект — одна строка, список объектов — его строки, всё остальное — одна строка {"result": ...}.

В GraphQL команда — это поле мутации, а её ответ — скаляр 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 отправьте POST-запросом JSON-объект аргументов на /data/rest/{domain}/commands/{command}. Аргумент 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, errors GraphQL, отклонённый вызов 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 представлений и запросов, где команда выступает узлом, а ни одно сохранённое определение не может вызвать операцию источника.

Команды и происхождение данных

Поскольку каждая команда объявляет свои входные и выходные колонки, происхождение на уровне колонок замыкается через непрозрачную границу команды. Движок происхождения применяет taint-замыкание: каждая объявленная выходная колонка выводится из каждой объявленной входной колонки.

Практическое следствие: ширина входного контракта задаёт точность этого замыкания. Узкий вход, только со столбцами, нужными команде, даёт плотный и читаемый конус происхождения. Объявление каждого столбца исходного отношения широко расходится по всем выходам. Это по-прежнему корректно, потому что происхождение не теряется, но прослеживаемость размывается.

Практическое правило: передавайте минимальную проекцию, нужную команде, и возвращайте только производные столбцы. Входы, возвращённые без изменений, расширяют конус заражения, поэтому не включайте их.

О том, как узлы команд выглядят в DAG и как их читать, см. Происхождение данных.

Разрешительный список исходящего трафика

Команды http и grpc вызывают внешние конечные точки. Каждый целевой хост должен быть в udf_egress_allowlist развёртывания. Loopback (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, ту конечную точку, которую вызывает кнопка Publish now на вкладке Admin.

Используйте её, чтобы запускать экспорты по расписанию из cron или CI, когда настроенного расписания reconcile_cron недостаточно по гранулярности:

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

Код выхода 0 = полная публикация. Код выхода 1 = частичная публикация или сбой подключения.

Полный справочник флагов, варианты аутентификации, именование хостов при мультиарендности и пример для cron см. в Экспорт метаданных — из командной строки.

Команды присутствуют в git-проекции каждого окружения. О том, как команда и назначения её тегов переживают слияние и pull, см. Окружения.