# YTsaurus # YTsaurus **YTsaurus** (IPA: [waɪtiːsɔːrəs], pronounced _wai-tee-saw-ruhs_) is a distributed storage and processing platform for large amounts of data. It includes [MapReduce](http://en.wikipedia.org/wiki/MapReduce) computation model, a distributed file system and a NoSQL key-value storage.

Overview

System overview: YTsaurus purpose and key features of the platform.

Data storage

Storing data in YTsaurus: Cypress metadata storage, key system entities, static tables, transactions, data storage formats.

How to try?

Examples of basic actions with YTsaurus in the CLI and web interface.

Dynamic tables

NoSQL key-value database: transactions, query language, replicated dynamic tables.

API and reference

Commands and their parameters, SDK description, and sample code for platform interaction.

Data processing

Processing data with YTsaurus: scheduler, MapReduce paradigm, operations supported.

## Useful links { #links } * [GitHub](https://github.com/ytsaurus/ytsaurus) * [YTsaurus site](https://ytsaurus.tech) * [Telegram](https://t.me/ytsaurus) * [Stack Overflow](https://stackoverflow.com/tags/ytsaurus) * [Email for questions](https://ytsaurus.tech/docs/en/mailto:community@ytsaurus.tech) # YTsaurus **YTsaurus** (IPA: [waɪtiːsɔːrəs], pronounced _wai-tee-saw-ruhs_) is a distributed storage and processing platform for large amounts of data. It includes [MapReduce](http://en.wikipedia.org/wiki/MapReduce) computation model, a distributed file system and a NoSQL key-value storage.

Overview

System overview: YTsaurus purpose and key features of the platform.

Data storage

Storing data in YTsaurus: Cypress metadata storage, key system entities, static tables, transactions, data storage formats.

How to try?

Examples of basic actions with YTsaurus in the CLI and web interface.

Dynamic tables

NoSQL key-value database: transactions, query language, replicated dynamic tables.

API and reference

Commands and their parameters, SDK description, and sample code for platform interaction.

Data processing

Processing data with YTsaurus: scheduler, MapReduce paradigm, operations supported.

## Useful links { #links } * [GitHub](https://github.com/ytsaurus/ytsaurus) * [YTsaurus site](https://ytsaurus.tech) * [Telegram](https://t.me/ytsaurus) * [Stack Overflow](https://stackoverflow.com/tags/ytsaurus) * [Email for questions](https://ytsaurus.tech/docs/en/mailto:community@ytsaurus.tech) # Overview This section contains information about the basic features and structure of the YTsaurus system. ## Clusters { #clusters } A YTsaurus cluster is a group of instances located in containers on physical hosts and operating as a unit. Based on the tasks to be solved, clusters can be divided into: - Computing clusters for massive parallel big data processing with MapReduce operations. - Dynamic table clusters (key-value storage). - Geodistributed clusters for coordination. A YTsaurus computing cluster is able: - To store exabytes of data. - To use HDD, SDD, and RAM for data storage. - To process data on hundreds of thousands of CPU cores. - To solve machine learning problems on thousands of GPUs. - To store and process data from tens of thousands of platform tenants. ## Architecture ![](https://ytsaurus.tech/_images/architecture.png) The YTsaurus architecture contains three layers: - Distributed file system and metadata storage: [Cypress](https://ytsaurus.tech/docs/user-guide/storage/cypress.md). - Scheduler for distributed computing with [MapReduce](https://ytsaurus.tech/docs/user-guide/data-processing/operations/overview.md) model support. - High-level computing engines and streaming frameworks: [YQL](https://ytsaurus.tech/docs/yql/index.md), [Flow](https://ytsaurus.tech/docs/flow/about.md), [CHYT](https://ytsaurus.tech/docs/user-guide/data-processing/chyt/about-chyt.md), and [SPYT](https://ytsaurus.tech/docs/user-guide/data-processing/spyt/overview.md). ### Cypress { #cypress } [Cypress](https://ytsaurus.tech/docs/user-guide/storage/cypress.md) is a fault-tolerant tree-based storage whose capabilities can be briefly described as follows: - A tree-like namespace with directories, tables, and files as nodes. - Transparent sharding of large tabular data into chunks, enabling the table to be treated as a single entity without worrying too much about the details of its physical storage. - Support for columnar and row-based storage mechanisms for tabular data. - Support for compressed storage using various encoding codecs, such as lz4 and zstd, with varying levels of compression. - Support for erasure coding using various erasure codecs with different control sum calculation strategies that have different redundancy parameters and allowable loss types. - Expressive data schematization with support for hierarchical types and data sortedness signs. - Background replication and repair of erased data without manual intervention. - Transactional semantics with support for nested transactions and snapshot/shared/exclusive level locks. - Transactions that can affect many Cypress objects and last indefinitely. - A flexible disk quota accounting system. - A flexible system for delimiting data access permissions, including access to table columns. At the heart of Cypress is a replicated and horizontally scalable master server that stores metadata about the Cypress tree structure and the composition and location of chunk replicas for all tables on the cluster. Master servers are implemented as replicated state machines based on Hydra, a proprietary consensus algorithm similar to Raft. Cypress implements a fault-tolerant elastic data storage layer. In addition to handling storage, Cypress can act as a lock service similar to Apache Zookeeper. From a user's standpoint, Cypress looks like a Linux file system tree. The easiest way of working with Cypress is to use its web interface. ### Dynamic tables { #dyn-tables } [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md) are tables in YTsaurus that implement an interface for point reads and key-based data writes and have transaction support and a native SQL dialect. Key features of dynamic tables: - Storing data in the [MVCC](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) model. Users can look up values by key or by timestamp. - Scalability: dynamic tables are split into tablets (shards by key ranges) that are served by separate cluster nodes. - Transactionality: dynamic tables are OLTP storages that can change many rows from different tablets of different tables within a single transaction. - Fault tolerance: failure of a single cluster node serving a tablet causes that tablet to be moved to another cluster node with no data loss. - Isolation: instances serving tablets are grouped into bundles that reside on separate servers, ensuring load isolation. - Conflict checking at the individual key or even individual value level. - Hot data responses from RAM. - Deleting data by [TTL](https://en.wikipedia.org/wiki/Time_to_live). - A built-in SQL-like language for scanning analytical queries. In addition to dynamic tables with the k-v storage interface, the system supports dynamic tables that implement the message queue abstraction, namely topics and streams. These queues can also be considered tables because they consist of rows and have their own schema. In a transaction, you can modify rows in both the k-v dynamic table and the queue simultaneously. This enables you to build stream processing on top of YTsaurus's dynamic tables with exactly once semantics. ### MapReduce { #mapreduce } The YTsaurus compute architecture is based on the [MapReduce](https://ytsaurus.tech/docs/user-guide/data-processing/operations/overview.md) distributed computational model. The Map operation processes input data broken down into parts between the cluster's nodes without data being exchanged between such nodes. The Reduce operation groups data from different cluster nodes. The model helps process large amounts of data in a highly reliable manner. Parts of the computation are automatically restarted if individual cluster nodes are unavailable. MapReduce in YTsaurus has the following features: - A rich model of base operations: classic MapReduce (with different shuffle strategies and support for multi-phase partitioning), Map, Erase, Sort, and some extensions of the classic model that take into account the sortedness of input data. - Horizontal scalability of computations: operations are divided into jobs that run on separate cluster nodes. - Support for hundreds of thousands of jobs in a single operation. - A flexible model of hierarchical compute pools with instant and integral guarantees, as well as fair_share distribution of underutilized resources among consumers without guarantees. - A vector resource model that enables different compute resources (CPU, RAM, GPU) to be requested in different proportions. - Job execution on compute nodes in containers isolated by CPU, RAM, file system, and process namespace using the Porto containerization mechanism. - A scalable scheduler that can serve clusters with up to a million concurrent tasks. - Virtually all compute progress is preserved in the event of updates or scheduler node failures. You can execute not only MapReduce jobs but also arbitrary code. In YTsaurus terminology, running arbitrary code is achieved using Vanilla operations. ### YQL { #yql } [YQL](https://ytsaurus.tech/docs/yql/index.md) is an SQL-based language of universal declarative queries against data storage and processing systems, as well as an infrastructure to run such queries. YQL benefits include: - A powerful graph execution engine that can build MapReduce pipelines with hundreds of nodes and adapt during computation. - Ability to build complex data processing pipelines using SQL by storing subqueries in variables as chains of dependent queries and transactions. - Predictable parallel execution of queries of any complexity. - Efficient implementation of joins, subqueries, and window functions with no restrictions on their topology or nesting. - Extensive function library. - Support for custom functions in C++, Python, and JavaScript. - Support for using machine learning models via CatBoost and TensorFlow. - Automatic execution of small parts of queries on prepared compute instances, bypassing MapReduce operations to reduce latency. ### Flow { #flow } [Flow](https://ytsaurus.tech/docs/flow/about.md) is a framework for real-time streaming event processing in the YTsaurus ecosystem. It supports stateful processing with persistent state in YTsaurus dynamic tables. ### CHYT { #chyt } ClickHouse over YTsaurus ([CHYT](https://ytsaurus.tech/docs/user-guide/data-processing/chyt/about-chyt.md)) is a technology that enables you to create a cluster of ClickHouse servers directly on YTsaurus computational nodes. ClickHouse is created within a Vanilla operation and works with the data in YTsaurus. The YTsaurus cluster acts as a compute cloud with respect to the running CHYT clusters within it. This technology enables different users to run multiple CHYT clusters on a single YTsaurus cluster, completely isolated from each other, solving the problem of resource separation in a cloud-like manner. In the YTsaurus ecosystem, CHYT is responsible for: - Fast analytical queries on static tables in YTsaurus with sub-second latency. - Reusing existing data in the YTsaurus cluster without having to copy it to a separate ClickHouse cluster. - Ability to integrate via ClickHouse's native ODBC and JDBC drivers, for example, with third-party visualization systems: Tableau, Qlik, Power BI, Oracle Bi, and DataLens. The integration is done at a fairly low level. This enables us to use the full potential of both YTsaurus and ClickHouse. This integration implements: - Support of reading both static and dynamic tables. - Partial support of the YTsaurus transactional model. - Support of distributed inserts. - CPU-efficient conversion of columnar data from the internal YTsaurus format to the in-memory ClickHouse representation. - Aggressive data caching, which in some cases enables query execution data to be read exclusively from instance memory. ### SPYT { #spyt } [SPYT](https://ytsaurus.tech/docs/user-guide/data-processing/spyt/overview.md) is a technology that integrates Apache Spark as a compute engine for data stored in YTsaurus. Similar to CHYT, vanilla YTsaurus operations provide computational resources for the SPYT cluster. Using SPYT helps minimize IO overheads and increase the performance of complex analytical pipelines manifold. SPYT can read both static and dynamic YTsaurus tables, perform computations with them, and capture the output in a static YTsaurus table. ### SDK { #sdk } All APIs in popular languages (C++, Python, Java, Go) were developed by the YTsaurus team, so all the nuances of interacting with the system are considered and well thought out in them. The YTsaurus client part for the mentioned languages enables you to read or write large amounts of data, despite possible network failures and other error. ### Web interface { #ui } The YTsaurus system implements a unified web interface for users and administrators. Use it to: - Navigate Cypress and view data. - Execute operations with tables, folders, and files. - Execute and view MapReduce computations. - Execute and view the history of SQL queries across all engines: YQL, CHYT, dynamic tables SQL. - Administer the system: - Monitor cluster component health. - Create and delete users and groups. - Manage quotas. - View access control settings. - View cluster component versions and much more. # How to try YTsaurus This guide offers a look at YTsaurus in action and describes the process of installing and starting a cluster. You'll deploy a local YTsaurus cluster, create a table, and run a simple SELECT query. Then, you'll deal with a slightly more complex challenge and solve the classic Word Count problem with a MapReduce operation. {% note info %} The fastest way to familiarize yourself with the product's features is by using the [Demo Stand](https://ytsaurus.tech/docs/en/#demo). It provides temporary access to a demo cluster that includes all the required YTsaurus components. All you need is a web browser. {% endnote %} ## Before you start {#prerequisites} - We recommend using an x86_64 Linux operating environment for YTsaurus. If you're using MacOS with an Apple Silicon processor, you'll need to use the x86 emulation mode to install YTsaurus locally. This can be achieved by using the [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) virtualization platform with [Rosetta 2](https://support.apple.com/en-us/102527) enabled. However, keep in mind that YTsaurus **isn't guaranteed** to work in emulation mode. - In this guide, you'll start a YTsaurus cluster with a minimal configuration, which means it will offer no guarantees of fault tolerance. Don't use this configuration in a production environment or for performance testing. Examples of how to configure your cluster configuration are available in the [Administrator manual](https://ytsaurus.tech/docs/admin-guide/prepare-spec.md). - For the examples to work correctly, Python 3.8+ must be installed on your system. ## Installing and starting a YTsaurus cluster { #launch-cluster} This guide offers two methods for installing a YTsaurus cluster: using [Docker](https://www.docker.com/), [Minikube](https://minikube.sigs.k8s.io/docs/) and using [Kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation). Regardless of the installation method, the required system components will be deployed in the process, including the [master server](https://ytsaurus.tech/docs/en/*about-master), [scheduler](https://ytsaurus.tech/docs/en/*about-scheduler), [YQL](https://ytsaurus.tech/docs/en/*about-yql), [Query Tracker](https://ytsaurus.tech/docs/en/*about-qt), and others. All examples in this guide — table creation, data upload, and running MapReduce — apply regardless of your preferred installation method and will be the same for both Docker and Minikube. {% list tabs dropdown %} - Docker 1. Install Docker: - If you're using Linux x86_64, install [Docker Engine](https://docs.docker.com/engine/install/ubuntu/). - If you're using MacOS, install either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [Podman](https://podman.io/docs/installation). Make sure that you have [Rosetta 2](https://support.apple.com/en-us/102527) installed and enabled. 2. Download the [run_local_cluster.sh](https://github.com/ytsaurus/ytsaurus/tree/main/yt/docker/local) script for deploying the cluster, and set execution permissions: ```bash prompt="$" prompt="$" $ mkdir ~/yt-local && cd ~/yt-local $ curl -s https://raw.githubusercontent.com/ytsaurus/ytsaurus/main/yt/docker/local/run_local_cluster.sh > run_local_cluster.sh $ chmod +x run_local_cluster.sh ``` 3. Run the script to deploy the cluster: ```bash prompt="$" prompt="$" $ ./run_local_cluster.sh ``` The script creates and runs [docker containers](https://ytsaurus.tech/docs/en/*about-docker-containers) for deploying YTsaurus. If the operation is successful, you'll see the following message: ``` Congratulations! Local cluster is up and running. To use the cluster web interface, point your browser to http://localhost:8001. Or, if you prefer command-line tool 'yt', use it like this: 'yt --proxy localhost:8000 '. ``` Remember the addresses listed in this message — you'll need them later. - `localhost:8001` is the web interface address. You can open it in your browser. - `localhost:8000` is the cluster's backend address. You'll need to [specify](https://ytsaurus.tech/docs/en/#set-vars) it as the proxy address to access the cluster via the CLI. 4. To make sure everything works correctly, run the following command: ```bash prompt="$" $ docker ps | grep yt CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2c254e35037c ghcr.io/ytsaurus/local:stable "--fqdn localhost --…" 2 minutes ago Up 2 minutes 80/tcp, 8002/tcp yt.backend 5235b5077b5b ghcr.io/ytsaurus/ui:stable "" 2 minutes ago Up 2 minutes 80/tcp yt.frontend ``` You should have two containers running: - `yt.frontend`: Handles processes related to the web interface. - `yt.backend`: Hosts YTsaurus cluster components. {% cut "About YTsaurus components" %} To find out what YTsaurus components are deployed on your system, check the list of processes running within the container: ```bash prompt="$" $ docker exec -it yt.backend /bin/bash $ ps -axo command | grep ytserver /mnt/rosetta /usr/bin/python3.8 /usr/local/bin/yt_local start --proxy-port 80 --local-cypress-dir /var/lib/yt/local-cypress --fqdn localhost --ytserver-all-path /usr/bin/ytserver-all --sync --fqdn localhost --proxy-config {coordinator={public_fqdn="localhost:8000"}} --rpc-proxy-count 0 --rpc-proxy-port 8002 --node-count 1 --queue-agent-count 1 --address-resolver-config {enable_ipv4=%true;enable_ipv6=%false;} --native-client-supported --id primary -c {name=query-tracker} -c {name=yql-agent;config={path="/usr/bin";count=1;artifacts_path="/usr/bin"}} /mnt/rosetta /primary/bin/ytserver-http-proxy --pdeathsig 9 --config /primary/configs/http-proxy-0.yson --pdeathsig 15 --setsid /mnt/rosetta /primary/bin/ytserver-master --pdeathsig 9 --config /primary/configs/master-0-0.yson --pdeathsig 15 --setsid /mnt/rosetta /primary/bin/ytserver-queue-agent --pdeathsig 9 --config /primary/configs/queue_agent-0.yson --pdeathsig 15 --setsid /mnt/rosetta /primary/bin/ytserver-node --pdeathsig 9 --config /primary/configs/node-0.yson --pdeathsig 15 --setsid /mnt/rosetta /primary/bin/ytserver-scheduler --pdeathsig 9 --config /primary/configs/scheduler-0.yson --pdeathsig 15 --setsid /mnt/rosetta /primary/bin/ytserver-controller-agent --pdeathsig 9 --config /primary/configs/controller_agent-0.yson --pdeathsig 15 --setsid /mnt/rosetta /primary/bin/ytserver-query-tracker --pdeathsig 9 --config /primary/configs/query_tracker-0.yson --pdeathsig 15 --setsid /mnt/rosetta /usr/bin/ytserver-yql-agent --pdeathsig 9 --config /primary/configs/yql_agent-0.yson --pdeathsig 15 --setsid /mnt/rosetta /usr/bin/grep --color=auto ytserver ``` As you can see, we launched the following components: - The [master server](https://ytsaurus.tech/docs/admin-guide/components#master) is responsible for fault-tolerant storage of the cluster's metadata. This includes information about system users, stored objects, and the location of the data itself. - The node combines the functionality of a [data node](https://ytsaurus.tech/docs/admin-guide/components.md#data-nodes) and an [exec node](https://ytsaurus.tech/docs/admin-guide/components.md#exec-nodes). It is responsible for data storage, dynamic tables, and job execution. - The [scheduler](https://ytsaurus.tech/docs/admin-guide/components.md#planirovshik) plans data processing operations, such as Map and Reduce. - The [controller agent](https://ytsaurus.tech/docs/admin-guide/components.md#planirovshik) schedules the jobs of individual operations. - The [HTTP proxy](https://ytsaurus.tech/docs/admin-guide/components.md#proxy) is a server used for external communication. - The [YQL agent](https://ytsaurus.tech/docs/admin-guide/components.md#yql-agent) is an execution engine for SQL-like queries. - The [YTsaurus queue](https://ytsaurus.tech/docs/user-guide/dynamic-tables/queues.md) contains ordered dynamic tables. - [Query Tracker](https://ytsaurus.tech/docs/user-guide/query-tracker/about.md) is a component for running queries in different SQL dialects (YQL, QT, CHYT, SPYT). {% endcut %} 5. Done! Now YTsaurus is deployed and ready for use. You may proceed to the next step. After you finish working with the examples, remember to [delete](https://ytsaurus.tech/docs/en/#delete-cluster) the cluster. - Minikube In this example, you'll deploy a local Kubernetes cluster consisting of a single node and run a YTsaurus cluster within it. We'll use Docker as the container execution engine. {% note warning "Resource requirements" %} To successfully deploy YTsaurus on a Kubernetes cluster, the host machine must have: - at least 4 CPU cores; - at least 8 GB of RAM; - at least 30 GB of disk space. {% endnote %} To install YTsaurus in Minikube, follow these steps: 1. [Set up the environment](https://ytsaurus.tech/docs/en/#setup) 1. [Deploy a Kubernetes cluster](https://ytsaurus.tech/docs/en/#k8s-start) 1. [Install cert-manager](https://ytsaurus.tech/docs/en/#cert-manager-apply) 1. [Install the YTsaurus operator](https://ytsaurus.tech/docs/en/#operator-install) 1. [Start the YTsaurus cluster](https://ytsaurus.tech/docs/en/#yt-start) 1. [Check network access](https://ytsaurus.tech/docs/en/#set-url) {% note info %} For a more detailed description of the installation process, watch [this webinar](https://www.youtube.com/watch?v=LTKtY5okG4c). {% endnote %} #### 1. Set up the environment {#setup} - Install Docker: - If you're using Linux, install [Docker Engine](https://docs.docker.com/engine/install/ubuntu/). - If you're using MacOS, install either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [Podman](https://podman.io/docs/installation). Make sure that you have Rosetta 2 installed and enabled. - Install [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl), a utility program for managing Kubernetes clusters. - Install [Minikube](https://kubernetes.io/ru/docs/tasks/tools/install-minikube/), a utility program for running a simple Kubernetes cluster on a local machine. - Install [Helm](https://helm.sh/docs/intro/install/), a package manager for installing YTsaurus components in Kubernetes. #### 2. Deploy a Kubernetes cluster {#k8s-start} ```bash prompt="$" $ minikube start --cpus=6 --memory=8192 --driver=docker # If you're using Podman # minikube start --cpus=6 --memory=8192 --driver=podman ``` Once your Kubernetes cluster is deployed, the following command should execute successfully: ```bash prompt="$" $ kubectl cluster-info Kubernetes control plane is running at https://127.0.0.1:36399 CoreDNS is running at https://127.0.0.1:36399/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy ``` #### 3. Install cert-manager { #cert-manager-apply } ```bash prompt="$" $ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.yaml ``` Wait for the `cert-manager-webhook` pod to enter the `Running` state: ```bash prompt="$" $ kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-7b5cdf866f-5lfth 1/1 Running 0 2m12s cert-manager cert-manager-cainjector-7c9788477c-xdp8l 1/1 Running 0 2m12s cert-manager cert-manager-webhook-764949f558-dldzp 1/1 Running 0 2m12s kube-system coredns-668d6bf9bc-774xg 1/1 Running 0 2m57s ... ``` #### 4. Install the YTsaurus operator {#operator-install} The [YTsaurus operator](https://github.com/ytsaurus/ytsaurus-k8s-operator) is a program that manages YTsaurus execution in a Kubernetes cluster. The operator ensures that all YTsaurus components are up and running correctly. Install the chart: ```bash prompt="$" $ helm install ytsaurus oci://ghcr.io/ytsaurus/ytop-chart --version 0.31.0 ``` {% cut "If you see the error 'Internal error occurred: failed calling webhook "webhook.cert-manager.io"'" %} Check the status of the `cert-manager-webhook` pod: ``` $ kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-7b5cdf866f-5lfth 1/1 ContainerCreating 0 2m12s cert-manager cert-manager-cainjector-7c9788477c-xdp8l 1/1 ContainerCreating 0 2m12s cert-manager cert-manager-webhook-764949f558-dldzp 1/1 ContainerCreating 0 2m12s ... ``` If the pod's status is `ContainerCreating`, wait for its installation to complete and try restarting the command: `helm install ytsaurus oci://ghcr.io/ytsaurus/ytop-chart --version 0.31.0`. If the pod's status is `ImagePullBackOff`, it means that the system can't download the required images. Most likely, this is caused by the network settings within Minikube. Click [here](https://cert-manager.io/docs/troubleshooting/webhook/#error-connect-connection-refused) for possible solutions. {% endcut %} Wait for the operator to enter the `Running` state: ```bash prompt="$" $ kubectl get pod NAME READY STATUS RESTARTS AGE ytsaurus-ytop-chart-controller-manager-5765c5f995-dntph 2/2 Running 0 7m57s ``` #### 5. Start the YTsaurus cluster {#yt-start} ```bash prompt="$" $ curl -s https://raw.githubusercontent.com/ytsaurus/ytsaurus/refs/heads/main/yt/docs/code-examples/cluster-config/cluster_v1_local.yaml > cluster_v1_local.yaml $ kubectl apply -f cluster_v1_local.yaml ``` It usually takes a few minutes for a YTsaurus cluster to start. If everything is successful, the list of running pods will look like this: ```bash prompt="$" $ kubectl get pod NAME READY STATUS RESTARTS AGE ca-0 1/1 Running 0 8m43s dnd-0 1/1 Running 0 8m44s dnd-1 1/1 Running 0 8m44s dnd-2 1/1 Running 0 8m44s ds-0 1/1 Running 0 11m end-0 1/1 Running 0 8m43s hp-0 1/1 Running 0 8m44s hp-control-0 1/1 Running 0 8m44s ms-0 1/1 Running 0 11m rp-0 1/1 Running 0 8m43s rp-heavy-0 1/1 Running 0 8m43s sch-0 1/1 Running 0 8m39s strawberry-controller-679786577b-4p5kz 1/1 Running 0 7m17s yt-client-init-job-user-ljfqf 1/1 Running 0 8m39s yt-master-init-job-default-hdnfm 1/1 Running 0 9m23s yt-master-init-job-enablerealchunks-575hk 1/1 Running 0 8m50s yt-strawberry-controller-init-job-cluster-l5gns 1/1 Running 0 8m17s yt-strawberry-controller-init-job-user-nn9lk 1/1 Running 0 8m34s yt-ui-init-job-default-6w5zv 1/1 Running 0 8m43s ytsaurus-ui-deployment-7b469d5cc8-596sf 1/1 Running 0 8m35s ytsaurus-ytop-chart-controller-manager-859b7bbddf-jc5sv 2/2 Running 0 14m ``` {% cut "If pods are stuck in the `Pending` status" %} Most likely, this is due to insufficient resources. Delete the Minikube cluster and try creating it again, this time allocating more resources at launch. ```bash prompt="$" $ kubectl delete -f cluster_v1_local.yaml $ minikube delete $ minikube start --cpus=8 --memory=10000 --driver=docker ``` If the specified resources exceed the limit set in your Podman configurations, increase this limit in the settings section. ![](https://ytsaurus.tech/_images/try-yt-podman-settings.png) {% endcut %} #### 6. Check network access {#set-url} To check the address at which the YTsaurus cluster will be available, run the following commands: ```bash prompt="$" # Network access to the web interface $ minikube service ytsaurus-ui --url http://192.168.49.2:30539 # Network access to the proxy $ minikube service http-proxies-lb --url http://192.168.49.2:30228 ``` The web interface is available at the first link. To log in, use: ``` Login: admin Password: password ``` {% cut "How to access the web interface if the cluster is deployed on a remote host" %} 1. On the remote host, run the command: ```bash prompt="$" $ minikube service http-proxies-lb --url : ``` You'll need the `` and `` values for the next step. 2. On the local host, start a new terminal session and run the following: ```bash prompt="$" $ ssh -fnNT -L 127.0.0.1:8080:: ``` The web interface will be available at [127.0.0.1:8080](http://127.0.0.1:8080). {% endcut %} You can use the second link to connect to the cluster from the command line. For more information, see the examples section below. #### Done! YTsaurus is now deployed and ready for use. You may proceed to the next step. After you finish working with the examples, remember to [delete](https://ytsaurus.tech/docs/en/#delete-cluster) the cluster. - Kind {selected} In this example, you will locally deploy a Kubernetes cluster consisting of a single node and run a YTsaurus cluster in it. Docker will be used as the container runtime engine. {% note warning "Resource requirements" %} To successfully deploy YTsaurus in a Kubernetes cluster, the host machine must have: - at least 4 CPU cores; - at least 8 GB of RAM; - at least 30 GB of disk space. {% endnote %} To install YTsaurus in Kind, follow these steps: 1. [Prepare the environment](https://ytsaurus.tech/docs/en/#kind-setup) 1. [Start a Kubernetes cluster](https://ytsaurus.tech/docs/en/#kind-k8s-start) 1. [Install cert-manager](https://ytsaurus.tech/docs/en/#kind-cert-manager-apply) 1. [Install the YTsaurus operator](https://ytsaurus.tech/docs/en/#kind-operator-install) 1. [Launch the YTsaurus cluster](https://ytsaurus.tech/docs/en/#kind-yt-start) 1. [Configure network access to the cluster](https://ytsaurus.tech/docs/en/#kind-network-access) #### 1. Prepare the environment {#kind-setup} - Install Docker: - If you are on Linux — install [Docker Engine](https://docs.docker.com/engine/install/ubuntu/); - If you are on Mac — install [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [Podman](https://podman.io/docs/installation) as an alternative. Make sure you have Rosetta 2 installed and enabled. - Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) — a utility for managing a Kubernetes cluster. - Install [Kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) — a tool that allows you to run a simple Kubernetes cluster on your local machine. - Install [Helm](https://helm.sh/docs/intro/install/) — a package manager for installing YTsaurus components in Kubernetes. #### 2. Start a Kubernetes cluster {#kind-k8s-start} ```bash prompt="$" $ kind create cluster --name ytsaurus ``` When the Kubernetes cluster is up, switch the k8s context to the created cluster: ```bash prompt="$" $ kubectl cluster-info --context kind-ytsaurus ``` Check that the cluster is accessible and running: ```bash prompt="$" $ kubectl cluster-info Kubernetes control plane is running at https://127.0.0.1:38797 CoreDNS is running at https://127.0.0.1:38797/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy ``` {% note warning %} In some cases — for example, when using Ubuntu 22.04 — there may be issues starting the k8s cluster. In this case, try using Kind version 0.20.0: ```bash prompt="$" $ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 $ chmod +x ./kind $ sudo mv ./kind /usr/local/bin/kind $ kind create cluster --name ytsaurus ``` {% endnote %} #### 3. Install cert-manager { #kind-cert-manager-apply } ```bash prompt="$" $ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.yaml ``` Wait until the `cert-manager-webhook` pod is in the `Running` state: ```bash prompt="$" $ kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-7b5cdf866f-5lfth 1/1 Running 0 2m12s cert-manager cert-manager-cainjector-7c9788477c-xdp8l 1/1 Running 0 2m12s cert-manager cert-manager-webhook-764949f558-dldzp 1/1 Running 0 2m12s kube-system coredns-668d6bf9bc-774xg 1/1 Running 0 2m57s ... ``` #### 4. Install the YTsaurus operator {#kind-operator-install} The [YTsaurus operator](https://github.com/ytsaurus/ytsaurus-k8s-operator) is a program that manages the execution of YTsaurus in a Kubernetes cluster. The operator ensures that all YTsaurus components are launched and running correctly. Install the chart: ```bash prompt="$" $ helm install ytsaurus oci://ghcr.io/ytsaurus/ytop-chart --version 0.31.0 ``` {% cut "If you encounter the error 'Internal error occurred: failed calling webhook "webhook.cert-manager.io"'" %} Check the status of the `cert-manager-webhook` pod: ``` $ kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-7b5cdf866f-5lfth 1/1 ContainerCreating 0 2m12s cert-manager cert-manager-cainjector-7c9788477c-xdp8l 1/1 ContainerCreating 0 2m12s cert-manager cert-manager-webhook-764949f558-dldzp 1/1 ContainerCreating 0 2m12s ... ``` If the pod is in `ContainerCreating` state, wait for the installation to finish and try rerunning `helm install ytsaurus oci://ghcr.io/ytsaurus/ytop-chart --version 0.31.0`. If the pod is in `ImagePullBackOff` state, it means the system cannot pull the required images. This is most likely related to networking settings inside Kind. Possible solutions are described [here](https://cert-manager.io/docs/troubleshooting/webhook/#error-connect-connection-refused). {% endcut %} Wait until the operator is in `Running` status: ```bash prompt="$" $ kubectl get pod NAME READY STATUS RESTARTS AGE ytsaurus-ytop-chart-controller-manager-5765c5f995-dntph 2/2 Running 0 7m57s ``` #### 5. Launch the YTsaurus cluster {#kind-yt-start} ```bash prompt="$" $ curl -s https://raw.githubusercontent.com/ytsaurus/ytsaurus/refs/heads/main/yt/docs/code-examples/cluster-config/cluster_v1_local.yaml > cluster_v1_local.yaml $ kubectl apply -f cluster_v1_local.yaml ``` Usually, launching the YTsaurus cluster takes several minutes. If everything is successful, the list of running pods will look like this: ```bash prompt="$" $ kubectl get pod NAME READY STATUS RESTARTS AGE ca-0 1/1 Running 0 8m43s dnd-0 1/1 Running 0 8m44s dnd-1 1/1 Running 0 8m44s dnd-2 1/1 Running 0 8m44s ds-0 1/1 Running 0 11m end-0 1/1 Running 0 8m43s hp-0 1/1 Running 0 8m44s hp-control-0 1/1 Running 0 8m44s ms-0 1/1 Running 0 11m rp-0 1/1 Running 0 8m43s rp-heavy-0 1/1 Running 0 8m43s sch-0 1/1 Running 0 8m39s strawberry-controller-679786577b-4p5kz 1/1 Running 0 7m17s yt-client-init-job-user-ljfqf 1/1 Running 0 8m39s yt-master-init-job-default-hdnfm 1/1 Running 0 9m23s yt-master-init-job-enablerealchunks-575hk 1/1 Running 0 8m50s yt-strawberry-controller-init-job-cluster-l5gns 1/1 Running 0 8m17s yt-strawberry-controller-init-job-user-nn9lk 1/1 Running 0 8m34s yt-ui-init-job-default-6w5zv 1/1 Running 0 8m43s ytsaurus-ui-deployment-7b469d5cc8-596sf 1/1 Running 0 8m35s ytsaurus-ytop-chart-controller-manager-859b7bbddf-jc5sv 2/2 Running 0 14m ``` #### 6. Configure network access to the cluster {#kind-network-access} By default, Kind creates an internal network for the k8s cluster and the pods inside it. To access this network, you can use [port-forwarding](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/). Below is an example of how to use this mechanism to access the web interface and API of the YTsaurus cluster. To access the cluster web interface at `localhost:8080`, run this command in a separate terminal: ```bash prompt="$" $ kubectl port-forward service/ytsaurus-ui 8080:80 ``` Use the following credentials: ``` Login: admin Password: password ``` To access the cluster API at `localhost:8081`, run this command in a separate terminal: ```bash prompt="$" $ kubectl port-forward service/http-proxies-lb 8081:80 ``` To work via [RPC proxy](https://ytsaurus.tech/docs/user-guide/proxy/rpc.md) at `localhost:8082`, run this command in a separate terminal: ```bash prompt="$" $ kubectl port-forward service/rpc-proxies-lb 8082:9013 ``` {% note info %} For the RPC proxy to be accessible through Discovery, you need to register it as a balancer in `//sys/rpc_proxies/@balancers`: ```bash prompt="$" $ yt --proxy localhost:8081 set --format json //sys/rpc_proxies/@balancers '{ "default": { "internal_rpc": { "default": ["localhost:8082"]} } }' ``` {% endnote %} Now the web interface will be available at `localhost:8080`, the cluster HTTP proxy at `localhost:8081`, and the RPC proxy at `localhost:8082`. How to work with the cluster from the command line is described below, in the [examples](https://ytsaurus.tech/docs/en/#launch-example) section. {% cut "How to configure native port forwarding with Kind" %} There is an alternative solution for configuring the YTsaurus cluster and the Kind cluster that avoids explicit `kubectl port-forward` commands. To do this: 1. Configure port forwarding in Kind. Example config: ```yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane extraPortMappings: - containerPort: 30080 hostPort: 30080 - containerPort: 30081 hostPort: 30081 - containerPort: 30082 hostPort: 30082 - containerPort: 30083 hostPort: 30083 - containerPort: 30084 hostPort: 30084 ``` Start the Kind cluster with the config: ```bash prompt="$" $ kind create cluster --name ytsaurus --config=kind-config.yaml ``` 2. Configure port forwarding for the web interface and proxies in the YTsaurus cluster. In the YTsaurus cluster config, specify the `httpNodePort` option in the proxies and web interface. For RPC proxy specify the `nodePort` option: ```bash prompt="$" $ grep nodePort: -iB 5 cluster_v1_local_with_ports.yaml httpProxies: - serviceType: NodePort loggers: *loggers instanceCount: 1 role: default httpNodePort: 30080 - serviceType: NodePort loggers: *loggers instanceCount: 1 role: control httpNodePort: 30081 rpcProxies: - serviceType: NodePort instanceCount: 1 loggers: *loggers role: default nodePort: 30083 - serviceType: NodePort instanceCount: 1 loggers: *loggers role: heavy nodePort: 30084 -- ui: image: ghcr.io/ytsaurus/ui:stable serviceType: NodePort instanceCount: 1 httpNodePort: 30082 ``` Now the web interface will be available at `localhost:30082`, the cluster proxy at `localhost:30080` and the RPC proxy at `localhost:30083`. {% endcut %} #### Done! Now YTsaurus is up and running — you can move on to the next step. After you finish working with the examples, don’t forget to [delete](https://ytsaurus.tech/docs/en/#delete-cluster) the cluster. {% endlist %} ## Installing the YTsaurus CLI {#install-cli} The most convenient way to interact with the YTsaurus system is through the console. The [YTsaurus CLI](https://ytsaurus.tech/docs/api/cli/cli.md) utility isn't installed as part of the cluster deployment process. You need to install it on your system separately. First, install the pip3 package manager if you don't already have it installed: ```bash prompt="$" $ sudo apt update $ sudo apt install python3-pip ``` Make sure that everything worked correctly: ```bash prompt="$" $ pip3 --version pip 22.0.2 from ... ``` Install the `ytsaurus-client` utility: ```bash prompt="$" $ pip3 install --user ytsaurus-client ``` Add the path to `$HOME/.local/bin` to the `PATH` variable: ```bash prompt="$" $ export PATH="$PATH:$HOME/.local/bin" ``` {% cut "How to save this change after system reboot" %} ```bash prompt="$" $ echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.bashrc # This command appends the string to the end of the ~/.bashrc file $ source ~/.bashrc # Apply the changes now ``` {% endcut %} Check if the YTsaurus CLI was successfully installed: ```bash prompt="$" $ yt --version Version: YT wrapper 0.13.20 ``` For more information about working with the CLI, watch the [introductory webinar](https://youtu.be/LTKtY5okG4c?t=1456) (the timestamp is 24:30). ## Executing examples { #launch-example } In this guide, you'll create a table, write data to it, and run a simple SELECT query. The section concludes with a more complex [example](https://ytsaurus.tech/docs/en/#mr): running a MapReduce operation. ### Set environment variables { #set-vars } You'll need this to access the cluster via the CLI for the examples that follow. {% list tabs dropdown %} - Docker ```bash prompt="$" $ export YT_PROXY=localhost:8000 ``` - Minikube ```bash prompt="$" $ export YT_PROXY=`minikube service http-proxies-lb --url` # Disable automatic proxy server detection YTsaurus $ export YT_CONFIG_PATCHES='{proxy={enable_proxy_discovery=%false}}' $ export YT_TOKEN=password ``` {% note warning %} Here, the token is set in an environment variable. This is done intentionally for the sake of simplicity and clarity of the example. Avoid this practice in real-world scenarios: YTsaurus provides dedicated commands for managing tokens. For more information, see [Authentication](https://ytsaurus.tech/docs/user-guide/storage/auth.md#upravlenie-tokenami). {% endnote %} - Kind {selected} ```bash prompt="$" $ export YT_PROXY='localhost:8081' # Disable automatic YTsaurus proxy discovery $ export YT_CONFIG_PATCHES='{proxy={enable_proxy_discovery=%false}}' $ export YT_TOKEN=password ``` {% note warning %} Here the token is set in an environment variable — this is intentional, for simplicity and clarity of the example. In real scenarios, this approach should not be used; YTsaurus provides special commands for working with tokens. For more details, see the [Authentication](https://ytsaurus.tech/docs/user-guide/storage/auth.md#upravlenie-tokenami) section. {% endnote %} {% endlist %} ### Create a table { #create-table } In YTsaurus, all data is stored in [tables](https://ytsaurus.tech/docs/user-guide/storage/static-tables.md). Let's create one! ```bash prompt="$" $ yt create table //home/input_table --attributes '{schema = [{name = id; type = int64}; {name = text; type = string}]}' > 16-64ca-10191-47007b7d ``` The value `16-64ca-10191-47007b7d` is the ID of the created [Cypress](https://ytsaurus.tech/docs/en/*about-cypress) node. Node IDs are useful when working with [transactions](https://ytsaurus.tech/docs/user-guide/storage/transactions.md) in YTsaurus. You won't need these IDs in this example. You can view the created table in the web interface. In your web browser, open the address that you received when starting the cluster, go to the **Navigation** tab, and click the created table: ![](https://ytsaurus.tech/_images/try-yt-new-table.png) ### Write data { #insert-data } Now write some data to the table by calling the [write-table](https://ytsaurus.tech/docs/api/cli/commands.md#write-table) command: ```bash prompt="$" $ echo '{ "id": 0, "text": "Hello" } { "id": 1, "text": "World!" }' | yt write-table //home/input_table --format json ``` ### Read the result { #get-result } To verify that the data has indeed been written to the table, run the following command: ```bash prompt="$" $ yt read-table //home/input_table --format json {"id":0,"text":"Hello"} {"id":1,"text":"World!"} ``` Another way to read a table is by running a [SELECT](https://ytsaurus.tech/docs/yql/syntax/select/index.md) query in the web interface. To do this, go to the **Queries** tab and enter the following query: ```sql SELECT * FROM `//home/input_table`; ``` ![](https://ytsaurus.tech/_images/try-yt-hello-world-select.png) {% cut "I get the error 'Attribute "cluster_name" is not found'" %} If you deployed your YTsaurus cluster via Docker, follow these steps: 1. In the web interface, go to the **Queries** tab. 2. Click the settings icon at the top right of the page. Delete your current **Settings**. 3. Click **Add setting** and specify the field values "cluster" and "primary", respectively. Click the checkmark. ![](https://ytsaurus.tech/_images/try-yt-fix-ui-cluster.png) If you deployed your YTsaurus cluster via Minikube, please let us know about this error in the [community chat](https://t.me/ytsaurus). {% endcut %} ### Advanced example: running MapReduce { #mr } This section explains how to run a MapReduce operation, using a [Word Count](https://en.wikipedia.org/wiki/Word_count) problem as an example. 1. [Prepare the data](https://ytsaurus.tech/docs/en/#mr-prepare-data) 2. [Create a table and write the data to it](https://ytsaurus.tech/docs/en/#mr-create-table) 3. [Run MapReduce](https://ytsaurus.tech/docs/en/#mr-run) 4. [Read the result](https://ytsaurus.tech/docs/en/#mr-get-result) {% cut "How MapReduce works" %} For a Word Count problem, the MapReduce operation is executed according to the following algorithm: 1. The source text is split into strings, with each string written to the table as a separate record. 1. A Map operation is performed for each record, emitting a pair of columns for each word: `(, 1)`. 2. The output of the previous step is sorted by the first column. 3. A Reduce operation is performed on the first column, summing the values from the second column. The resulting output is a set of pairs: `(, )`. {% endcut %} #### 1. Prepare the data { #mr-prepare-data } Download the [source text](https://github.com/ytsaurus/ytsaurus/blob/main/yt/docs/code-examples/source/moem.txt) and convert it into a tab-separated format: ```bash prompt="$" $ curl -s https://raw.githubusercontent.com/ytsaurus/ytsaurus/refs/heads/main/yt/docs/code-examples/source/moem.txt > source.txt $ awk '{gsub(/\t/, "\\t"); print "lineno="NR"\ttext="$0}' source.txt > source.tsv ``` {% cut "About the tab-separated format" %} - Table rows are separated by line breaks, `\n`. - Columns are separated by tabs, `\t`. - Column names and their corresponding contents are separated by an equals sign `=`. For example, the string `lineno=1\tsize=6\tvalue=foobar` describes a row with columns `lineno`, `size`, and `value`, which contain the values `1`, `6`, and `foobar`, respectively. Tab characters are escaped. {% endcut %} Prepare the source code of the program that will run the MapReduce operation. Download the Python 3 [script](https://github.com/ytsaurus/ytsaurus/blob/main/yt/docs/code-examples/python/word-count.py) and save it locally: ```bash prompt="$" $ curl -s https://raw.githubusercontent.com/ytsaurus/ytsaurus/refs/heads/main/yt/docs/code-examples/python/word-count.py > word-count.py ``` #### 2. Create a table { #mr-create-table } Create two tables, one for the source data and another for the results of executing the MapReduce operation: ```bash prompt="$" $ yt create table //home/mapreduce_input --attributes '{schema = [{name = lineno; type = string}; {name = text; type = string}]}' $ yt create table //home/mapreduce_result --attributes '{schema = [{name = count; type = string}; {name = word; type = string}]}' ``` If you get 'Cannot determine backend type: either driver config or proxy url should be specified,' [set](https://ytsaurus.tech/docs/en/#set-vars) the environment variable `YT_PROXY`. Now write some data to the source table by calling the [write-table](https://ytsaurus.tech/docs/api/cli/commands.md#write-table) command: ```bash prompt="$" $ cat source.tsv | yt write-table //home/mapreduce_input --format dsv ``` To verify that the data has been written to the table, use the [read-table](https://ytsaurus.tech/docs/api/cli/commands.md#read-table) command. The half-interval specified in the square brackets indicates that we want to get the first six rows of the table: ```bash prompt="$" $ yt read-table '//home/mapreduce_input[:#6]' --format dsv ``` #### 3. Run MapReduce { #mr-run } Run the MapReduce operation using the [map-reduce](https://ytsaurus.tech/docs/api/cli/commands.md#map-reduce) command: ```bash prompt="$" $ yt map-reduce --mapper "python3 word-count.py map" --reducer "python3 word-count.py reduce" --map-local-file word-count.py --reduce-local-file word-count.py --src //home/mapreduce_input --dst //home/mapreduce_result --reduce-by word --format dsv ``` You can track the status of a running operation in the **Operations** section of the web interface. {% cut "Where to find this section" %} ![](https://ytsaurus.tech/_images/try-yt-operations.png) {% endcut %} #### 4. Read the result { #mr-get-result } Now you can read the resulting table by executing a simple SELECT query. In the web interface, go to the **Queries** tab and enter the following query: ```sql SELECT * FROM `//home/mapreduce_result` ORDER BY count LIMIT 30; ``` ## Deleting a cluster { #delete-cluster } {% list tabs dropdown %} - Docker To delete a YTsaurus cluster, shut down the `yt.frontend` and `yt.backend` containers. To do this, run the command: ```bash prompt="$" $ ./run_local_cluster.sh --stop ``` This command stops (executes `docker stop`) and then removes (`docker rm`) the containers. - Minikube 1. Delete the YTsaurus cluster: ```bash prompt="$" $ kubectl delete -f cluster_v1_local.yaml ``` 2. Uninstall the operator: ```bash prompt="$" $ helm uninstall ytsaurus ``` 3. Stop the Kubernetes cluster: ```bash prompt="$" $ minikube stop ``` 4. Delete the Kubernetes cluster: ```bash prompt="$" $ minikube delete ``` 5. Clear the Minikube cache: ```bash prompt="$" $ rm -rf ~/.minikube/ ``` 6. If you used Podman: ```bash prompt="$" $ podman rm -f minikube $ podman volume rm minikube ``` - Kind {selected} 1. Delete the YTsaurus cluster: ```bash prompt="$" $ kubectl delete -f cluster_v1_local.yaml ``` 2. Delete the operator: ```bash prompt="$" $ helm uninstall ytsaurus ``` 3. Delete the Kubernetes cluster: ```bash prompt="$" $ kind delete cluster --name ytsaurus ``` {% endlist %} ## Demo Stand { #demo } This is an online demonstration of the capabilities offered by YTsaurus. To get access to the demo cluster, [fill out this form](https://ytsaurus.tech/#demo). After that, an email with information for accessing the cluster will be sent to your specified address. The demo stand features several environments for interacting with YTsaurus: {% cut "Jupyter Notebook" %} The notebook provides numerous examples for working with YTsaurus, including operations for creating tables, uploading data, and using CHYT, SPYT, and YQL, as well as SDK examples. For an overview of all available examples, see **About YTsaurus demo**, the notebook home page. A link to a deployed Jupyter Notebook will be included in the email. ![](https://ytsaurus.tech/_images/try-yt-jupyter.png) {% endcut %} {% cut "Web interface" %} Here you can test out the features of the YTsaurus web interface: explore the file system, see the list of pools, and run queries in [Query Tracker](https://ytsaurus.tech/docs/user-guide/query-tracker/about.md). A link to the web interface of a deployed cluster will be included in the email. ![](https://ytsaurus.tech/_images/try-yt-ui.png) For more information about using the web interface, watch the [introductory webinar](https://youtu.be/LTKtY5okG4c?t=929) (the timestamp is 15:30). {% endcut %} ## Troubleshooting If you're having trouble getting something to work, don't hesitate to ask your questions in our [community Telegram chat](https://t.me/ytsaurus). We'll do our best to help you. To share your suggestions or comments on the documentation, [create an issue](https://github.com/ytsaurus/ytsaurus/issues) in the project's GitHub repository. Your feedback is always welcome; it helps us make the documentation more informative. [*about-cypress]: Cypress is a distributed file system and metadata storage. Cypress stores tables and files. To learn more, see the [documentation](https://ytsaurus.tech/docs/user-guide/storage/cypress.md). [*about-docker-containers]: Container images are collected from the Dockerfile available [here](https://github.com/ytsaurus/ytsaurus/blob/main/yt/docker/ytsaurus/Dockerfile). [*about-qt]: A tool for running SQL-like queries. Query Tracker is available in the web interface, under **Queries**. To learn more, see the [documentation](https://ytsaurus.tech/docs/user-guide/query-tracker/about.md). [*about-yql]: A declarative query language for data storage and processing systems. It's based on SQL syntax. [*about-master]: The master server is responsible for fault-tolerant storage of the cluster's metadata. This includes information about system users, stored objects, and the location of the data itself. To learn more, see the [documentation](https://ytsaurus.tech/docs/admin-guide/components.md#master). [*about-scheduler]: The scheduler is responsible for allocating resources between operations as well as for their execution on the cluster. To learn more, see the [documentation](https://ytsaurus.tech/docs/user-guide/data-processing/scheduler/scheduler-and-pools.md). # User guide The guide covers the main YTsaurus workflows: data storage and processing, dynamic tables, proxies, and troubleshooting. - [Data storage](https://ytsaurus.tech/docs/en/storage/index.md) - [Dynamic tables](https://ytsaurus.tech/docs/en/dynamic-tables/overview.md) - [Data processing](https://ytsaurus.tech/docs/en/data-processing/overview.md) - [Proxy](https://ytsaurus.tech/docs/en/proxy/about.md) - [Troubleshooting](https://ytsaurus.tech/docs/en/problems/index.md) # Authentication and passwords To authenticate users, YTsaurus allows setting user passwords for accessing the system. A password can be changed either by the system administrator or by the user in question. To manage a user's password, go to the password reset page or run the `set-user-password` command in the YTsaurus CLI. ## Selecting a username {#user-selection} For token authentication, the token authenticates a user. A username sent separately by a client does not replace the token: an RPC proxy checks that the username matches the user associated with the token and returns the `Manually specified and authenticated users mismatch` error if they differ. The `YT_USER` environment variable is not a shared setting for all clients. Support depends on the client library and connection method. The table describes regular user authentication. | Client or connection method | `YT_USER` support | How the user is selected | | --- | --- | --- | | `yt` CLI and Python API with an HTTP proxy | Not supported | The HTTP proxy determines the user from the token. | | Python API with the RPC or native driver | Not supported | To specify a username explicitly, use `config["driver_user_name"]`. With RPC, the username must match the user associated with the token. | | Java API `ytsaurus-client` | Supported when `setAuth` is not passed to the builder | The client uses `YT_USER`, or the value of the Java system property `user.name` (`System.getProperty("user.name")`) if the variable is not set. If the local username differs from the YTsaurus username, set `YT_USER` explicitly. | | C++ API `yt/cpp/mapreduce` | Not supported | The token determines the user. | | C++ NApi, `GetClientOptionsFromEnv` | Supported only by this option factory | A non-empty `YT_USER` value is stored in `TClientOptions::User`. If the variable is unset or empty, the field remains unset; the system username is not used. An RPC proxy can determine the user from the token, while a native connection requires an explicit user. | To check which user the CLI sends requests as, configure the proxy and token and request the authenticated username from the server: ```bash $ id -un $ export YT_PROXY= $ yt whoami ``` The `yt whoami` command does not use `YT_USER`. In this HTTP scenario, its output is the user authenticated by the token and may differ from the local username. New users don't have a password, so their first password must be set by the administrator. Let's consider the following example: ```bash $ yt create user --attr '{name=alex}' $ yt set-user-password alex New password: Retype new password: ``` The administrator creates a new user named `alex` and sets their password to `cone`. The password is prompted interactively and entered securely (without echo to the terminal), and must be retyped for confirmation. The user can then run the `set-user-password` command to change their password to `cube`. ```bash $ yt set-user-password alex Current password for alex: New password: Retype new password: ``` Note that unlike the administrator, the user is required to enter their current password in order to change it. The administrator doesn't need to enter the current password, neither when setting the user password for the first time nor when changing it later. All passwords are prompted interactively for security. ## Token management Users need tokens to interact with YTsaurus via the CLI or the API. To facilitate token management, the CLI supports the `issue-token`, `revoke-token`, and `list-user-tokens` commands. The `issue-token` command issues a new token to the user. Unlike with passwords, a single user can have multiple active tokens. This allows for seamless replacement of one token with another. ```bash $ yt issue-token alex Current password for alex: ytct-2c59-56daecdff8dd45d2561a8679acf5 ``` User `alex` was issued token `ytct-2c59-56daecdff8dd45d2561a8679acf5`. Similar to the `set-user-password` command, the user must enter their password (prompted interactively), while the administrator isn't required to do so. Use the `list-user-tokens` command to see the information about user's active tokens. Note that YTsaurus doesn't store user tokens. In particular, the `list-user-tokens` command returns the SHA-256 hashes of tokens rather than the tokens themselves. For instance, ```bash $ yt list-user-tokens alex Current password for alex: ["87a5d9406ccf6a42cca510d86e43b20e2943aa7ade7e9129f4f4f947e1b02574"] $ echo -n 'ytct-2c59-56daecdff8dd45d2561a8679acf5' | sha256sum 87a5d9406ccf6a42cca510d86e43b20e2943aa7ade7e9129f4f4f947e1b02574 - ``` With the `revoke-token` command, you can revoke the user's token. To revoke a token, you can specify its SHA-256 hash using the `--token-sha256` flag, or if the flag is omitted, the token value will be prompted interactively. The password is always prompted interactively. Using `--token-sha256` allows using the output of the `list-user-tokens` command to revoke specific tokens. ```bash $ yt revoke-token alex --token-sha256 87a5d9406ccf6a42cca510d86e43b20e2943aa7ade7e9129f4f4f947e1b02574 Current password for alex: $ yt revoke-token alex Current password for alex: Token to revoke: $ yt list-user-tokens alex Current password for alex: [] ``` {% note warning "Note" %} Although the user password is required to manage a user's tokens, changing it doesn't automatically revoke the tokens. This means you can replace tokens gradually if the password is changed. If a user's password is compromised, it's highly recommended that you revoke all of their tokens once the new password has been set. {% endnote %} # Data storage This section describes the metadata tree, storage objects, access control, quotas, and read and write settings. - [Metadata tree](https://ytsaurus.tech/docs/en/cypress.md) - [Objects](https://ytsaurus.tech/docs/en/objects.md) - [Access control](https://ytsaurus.tech/docs/en/access-control.md) - [Quotas](https://ytsaurus.tech/docs/en/quotas.md) - [Data schema](https://ytsaurus.tech/docs/en/static-schema.md) - [Data formats](https://ytsaurus.tech/docs/en/formats.md) # Metadata tree This section describes Cypress, the meta information tree. Cypress contains various system information as well as indications of where user data is stored. This section includes three subsections describing a [general tree view](https://ytsaurus.tech/docs/en/#description), [Cypress node attributes](https://ytsaurus.tech/docs/en/#attributes), and [TTL for Cypress nodes](https://ytsaurus.tech/docs/en/#TTL). ## General tree view { #description } To a user, Cypress looks like a Linux [file system](https://en.wikipedia.org/wiki/File_system) tree but with a number of significant differences. First, every tree node has an associated [collection of attributes](https://ytsaurus.tech/docs/en/attributes.md), including some user-defined ones. Second, the tree is [transactional](https://ytsaurus.tech/docs/en/transactions.md). Third, files and directories, as well as [other objects](https://ytsaurus.tech/docs/en/objects.md), can serve as Cypress nodes. Like a file system, Cypress supports an [access control system](https://ytsaurus.tech/docs/en/access-control.md). Cypress is rooted at `/` which has **map_node** type (that is, it's a directory). Cypress nodes are addressed using [YPath](https://ytsaurus.tech/docs/en/ypath.md). The number of child nodes in a directory is limited. By default, the limit is 50,000, but it depends on the cluster configuration. The depth of Cypress is theoretically unlimited, but keep in mind that there is a limit on the path resolution depth in [YPath](https://ytsaurus.tech/docs/en/ypath.md). Example paths: `//tmp` is the temporary directory, `//tmp/@` is a pointer to the directory attributes, `//tmp/table/@type` is the path to the `type` attribute of the `//tmp/table` node. Using YPath, you can represent Cypress as follows: ``` / /home /user1 /table /@id /@chunk_ids /@type ... ... /user2 ... /tmp /sys /chunks ... ... ... ``` You can manipulate Cypress via a [CLI](https://ytsaurus.tech/api/cli/cli.md). ## Cypress node attributes { #attributes } In addition to attributes common to all objects, Cypress nodes have additional attributes listed in the table: | **Attribute** | **Type** | **Value** | | -------------------------- | ------------------ | ------------------------------------------------------------ | | `parent_id` | `string` | Parent node ID (none for the root) | | `locks` | `array` | [List of locks](https://ytsaurus.tech/docs/en/transactions.md) taken out on a node | | `lock_mode` | `LockMode` | Current node [lock mode](https://ytsaurus.tech/docs/en/transactions.md) (transaction-dependent) | | `path` | `string` | Node absolute path | | `key` | `string` | Key to access this node in its parent folder (if the node is so nested) | | `creation_time` | `DateTime` | Node [create](https://ytsaurus.tech/docs/en/#time_attributes) time | | `modification_time` | `DateTime` | Node [most recent modification](https://ytsaurus.tech/docs/en/#time_attributes) time | | `access_time` | `DateTime` | Node [most recent access](https://ytsaurus.tech/docs/en/#time_attributes) time | | `expiration_time` | `DateTime` | Time [automatically to delete](https://ytsaurus.tech/docs/en/#TTL) a node. Optional attribute | | `expiration_timeout` | `DateTime` | A timeout for the [automatic deletion](https://ytsaurus.tech/docs/en/#TTL) of a node if it has not been accessed. Optional attribute | | `touch_time` | `DateTime` | The last time the node was accessed, used for [automatic deletion](https://ytsaurus.tech/docs/en/#TTL) via `expiration_timeout`. Present only if `expiration_timeout` is set | | `access_counter` | `integer` | Number of times a node has been accessed since being created | | `revision` | `integer` | Node [revision](https://ytsaurus.tech/docs/en/#time_attributes) | | `resource_usage` | `ClusterResources` | Cluster resources appropriated by a node | | `recursive_resource_usage` | `ClusterResources` | Cluster resources appropriated by a node and its entire subtree | | `account` | `string` | Account used to keep track of the resources being used by a specific node | | `annotation` | `string` | Human-readable [summary description](https://ytsaurus.tech/docs/en/annotations.md) of an object | Each node has its own attributes responsible for access control. Therefore, its attributes include `inherit_acl`, `acl`, and `owner`. For more information, see [Access control system](https://ytsaurus.tech/docs/en/access-control.md). ### Time attributes { #time_attributes } The `creation_time` attribute stores the node create time. The `modification_time` attribute stores the time of the last update of the node, node user attribute or some node system attribute. `modification_time` does not track child node updates, that is, `modification_time` for `map_node` does not change if there are changes somewhere deep in the tree. When a node is created and every time a node is modified, the system updates its `revision` attribute. It stores a non-negative integer. The revision number is guaranteed to increase in a strictly monotonous manner over time. You can use revisions to verify that a node has not updated. `revision` updates together with `modification_time`. The `access_time` attribute stores the time of the last access to the node's content. Access to attributes is not taken into account (access to an attribute is considered to be specifying the full path to the attribute within [YPath](https://ytsaurus.tech/docs/en/ypath.md)). Requests with the `suppress_access_tracking` option enabled are also not taken into account. To improve efficiency, the system does not change this attribute with each access, but accumulates access records and updates the `access_time` attribute at intervals of about a second. {% note warning "Attention" %} In rare cases, an attribute may have been accessed without an `access_time` update because of a master server fault. {% endnote %} {% note warning "Attention" %} The `modification_time` and `access_time` attributes for dynamic tables currently do not carry any useful information. Their updates (or lack thereof) are independent of user-initiated read and write operations. This behavior may change in the future. {% endnote %} Most commands used for reads and writes include the `suppress_access_tracking` and the `suppress_modification_tracking` options that disable `access_time`, `modification_time`, and `revision` updates, respectively. for reading and writing. In particular, the web interface uses `suppress_access_tracking`, so viewing the contents via the web UI doesn't trigger `access_time` updates. {% note info "Note" %} In the event that a transaction creates or modifies a node, the above attributes are set once during updates within the transaction. Thus, a node may become visible in parent transactions much later than its `creation_time`: only after a commit of the relevant transaction. {% endnote %} ## Cypress node TTL { #TTL } Cypress can delete nodes automatically at a specified moment in time or if nodes are not accessed for a certain length of time. This feature is controlled by the `expiration_time` and the `expiration_timeout` attributes. By default, these attributes are not there, so the system will not delete a node automatically. For TTL to function, you need: - to set `expiration_time` to a moment in time when the node is to be deleted. If it is a composite node, this will also delete its entire subtree. - to set `expiration_timeout` to a time interval during which there have to be no attempts to access the node (and its entire subtree if it is a composite node) for it to be deleted. The time from the `touch_time` attribute is used to track the time of the last node access. The `touch_time` attribute, similar to `access_time`, stores the time of the last access to the node. This attribute is present only for nodes that have the `expiration_timeout` attribute. Unlike `access_time`, `touch_time` is updated when accessing the node's attributes, as well as when accessing the node via web interface. The `touch_time` attribute does not take into account changes to the node's children, i.e., the `touch_time` attribute of `map_node` does not change if there are accesses somewhere deep in the tree. Also, `touch_time` is not updated in requests with the `suppress_expiration_timeout_renewal` option enabled. {% note warning "Attention" %} You cannot restore data deleted using this mechanism. Use it with caution. {% endnote %} The moment in time has to be either an isoformat string or an integer denoting the number of milliseconds since the epoch. These two methods are equivalent: ```bash yt set //home/project/path/table/@expiration_time '"2020-05-16 15:12:34.591+03:00"' yt set //home/project/path/table/@expiration_time '1589631154591' ``` A time interval is specified in milliseconds: ```bash # Delete a node if "left alone" for a week. yt set //home/project/path/table/@expiration_timeout 604800000 ``` {% note warning "Attention" %} Exercise extreme caution when setting `expiration_timeout` for directories. A directory's time to live is extended only by direct access to that directory, not to any of its subtrees. For example, reading a table inside a directory with a set `expiration_timeout` *does not extend* this directory's TTL. {% endnote %} You can modify these attributes within transactions; however, only their committed values will take effect. To be able to set these attributes for a node, you need to have the [right](https://ytsaurus.tech/docs/en/access-control.md) to `write` to the node itself same as for many other attributes as well as the `remove` privilege to the node and its entire subtree because a delete is being requested in effect, albeit a deferred one. The `write` privilege is sufficient to delete these attributes. The system provides no guarantee that the delete will occur exactly at the time requested. In real life, the delete occurs within single seconds of the specified moment in time. A node is not automatically deleted if at the specified moment in time it is subject to locks other than `snapshot`. The system will delete the node when all locks are released. You can use this property to artificially extend a node's time to live. When you copy and move a node, `expiration_time` and `expiration_timeout` are reset by default, so the copy will not automatically delete. Commands include the `preserve-expiration-time` and the `preserve-expiration-timeout` options that enable you to change their behavior. {% note warning "Attention" %} A number of API calls that create temporary tables set such tables' `expiration_time`/`expiration_timeout` to purge them automatically. You must keep that in mind and not store important data in such tables. {% endnote %} Deletion may occur earlier if the node is located in a subtree with a smaller `expiration_time`/`expiration_timeout` value at the root. To get the actual deletion time of a node, use the `effective_expiration` attribute: ```bash $ yt get //home/project/path/table/@effective_expiration { "time": {"value": 42, "path": //testator/path} "timeout": {"value": 42, "path": //testator/path} } ``` If the path from the root to the node doesn't contain `expiration_time` or another relevant attribute, a YSON entity is written to the `“time”` field instead. # Attributes This section contains information about Cypress attributes. ## General information { #common } Each object in YTsaurus has a set of **attributes** associated with it. The set of attributes can be thought of as a key-value dictionary that is both readable and modifiable. Arbitrary non-empty strings can serve as attribute names. Random non-empty strings can be attribute names. Arbitrary [YSON](https://ytsaurus.tech/docs/en/yson.md) structures can be values. There are two kinds of attributes: **[system](https://ytsaurus.tech/docs/en/#system_attr)** attributes and **[user](https://ytsaurus.tech/docs/en/#user_attr)** attributes. The special path modifier `/@` and the `get`, `list`, `set`, and `remove` commands are used to access object attributes. In addition to accessing an attribute as a whole, you can address a part of it if the attribute value is a composite type. For more information about path modifiers, see [YPath](https://ytsaurus.tech/docs/en/ypath.md). For the `get` and `list` commands, you can optionally specify a list of attributes that you need to return along with the objects found. ## System attributes { #system_attr } Different types of objects have their own set of type-specific system attributes. For example, all objects have the `id` attribute, tables have the `compression_codec` and `compressed_data_size` attributes. For more information about the attributes inherent to all objects, see [Objects](https://ytsaurus.tech/docs/en/objects.md). System attributes are either modifiable (can be changed by the user), such as `compression_codec`, or unmodifiable, such as `compressed_data_size`. If you try to set an unmodifiable attribute, the system will return an error. Attempting to remove a system attribute will also cause an error. Some attributes are **inheritable**: these attributes can be set on directories (a.k.a. map nodes) where they are nullable, i.e. may lack (and, indeed, do lack by default) a value. If no value for an inheritable attribute is specified when creating a new object, that attribute gets the value of the closest ancestor node, or the default value if no ancestor up to the root has the attribute set. {% note warning "Attention" %} As follows from the above, attribute inheritance only works when creating new nodes. Setting an inheritable attribute on a directory has no effect on already existing nodes is the subtree. In order to apply the change recursively, one has to set the attribute on each node (of relevant type) in the subtree. {% endnote %} Different types of objects inherit different sets of attributes. The following attributes are inherited by **tables, files, and journals:** - `chunk_merger_mode`; - `compression_codec`; - `enable_striped_erasure`; - `erasure_codec`; - `hunk_media`; - `hunk_primary_medium`; - `media`; - `primary_medium`; - `replication_factor`; - `vital`. In addition, the following attributes are inherited by **tables only:** - `atomicity`; - `hunk_erasure_codec`; - `commit_ordering`; - `in_memory_mode`; - `optimize_for`; - `profiling_mode`; - `profiling_tag`; - `tablet_cell_bundle`. **Chaos replicated tables** inherit the following attributes: - `chaos_cell_bundle`. System attributes can also be **opaque** (or **computed**). The value of an **opaque** attribute will be available only when explicitly requested with the `get` command, but will not be available when all attributes are requested. Specifically, when all attributes are requested, the value of an opaque attribute will be set to **entity**. Most often, attributes are declared as opaque when they have a large value (in terms of occupied memory) or take a long time to compute. In practical terms, if the value of an attribute is 100 KB or more, you should probably make it opaque. In this case, when all attributes are requested, it will not be returned, making it fast to read all attributes. To get the value of an opaque attribute, you must explicitly request it. An example of an opaque attribute will be a list of all table chunks (the `chunk_ids` attribute) or the total usage of resources by Cypress subtree (the `recursive_resource_usage` attribute). Below is a list of attributes of YTsaurus system objects: - [Attributes of any object](https://ytsaurus.tech/docs/en/objects.md#attributes). - [Attributes of Cypress nodes](https://ytsaurus.tech/docs/en/cypress.md#attributes). - [Attributes of static tables](https://ytsaurus.tech/docs/en/static-tables.md#attributes). - [Attributes of dynamic tables](https://ytsaurus.tech/docs/dynamic-tables/overview.md#attributes). - [Attributes of files](https://ytsaurus.tech/docs/en/files.md#attributes). - The [compression_codec](https://ytsaurus.tech/docs/en/compression.md#get_compression) and [erasure_codec](https://ytsaurus.tech/docs/en/replication.md#erasure) attributes for tables and files. - [Attributes of transactions](https://ytsaurus.tech/docs/en/transactions.md#attributes). - [Attributes of links](https://ytsaurus.tech/docs/en/links.md#attributes). - [Attributes of accounts](https://ytsaurus.tech/docs/en/accounts.md#account_attributes). - [Attributes of media](https://ytsaurus.tech/docs/en/media.md#atributy). ## User attributes { #user_attr } The user can set user-defined attributes. {% note warning "Attention" %} The names (keys) of user attributes must not coincide with the names of system attributes. We recommend starting user attribute names with an underscore to avoid potential problems in the future if the set of system attributes is extended. {% endnote %} {% note warning "Attention" %} Attributes are metadata that is being stored in the Cypress master server memory. It is recommended that the total volume of all user attributes does not exceed hundreds of megabytes. {% endnote %} ## How attributes are stored in the system { #how_stored } System attributes can be stored in various ways, depending on the object type. Some attributes are computed on the fly instead of being stored. All user attributes are stored as a single key-value dict. This dictionary is encoded as YSON, so accessing the value of a specific attribute requires YSON-decoding. ```bash yt set //tmp/@my_attribute '{x=10; y=["hello"; "world"]}' yt get //tmp/@my_attribute/x 10 yt get //tmp/@my_attribute/y/1 "world" yt set //tmp/@my_attribute/y/@inner_attribute attr yt get //tmp/@my_attribute/y/@inner_attribute "attr" ``` ## Examples { #examples } ```bash # Getting all object attributes yt get //tmp/@ { "creation_time" = "2013-12-06T19:00:48.256444Z"; "locks" = []; ... } # Getting a specific object attribute yt get //tmp/@type "map_node" # Setting a specific object attribute yt set //tmp/@my_attribute "my_value" # Removing an attribute yt remove //tmp/@my_attribute # creating a table echo '{"a": 10, "b": "hello"}' | yt write //tmp/my_table --format json # Getting the first table chunk yt get //tmp/my_table/@chunk_ids/0 "3ab1-cd56f-3ec0064-c8998820" # Removing a table yt remove //tmp/my_table --force # Requesting a list with attributes yt list / --attribute type --format 'yson' [ < "type" = "map_node" > "home"; < "type" = "map_node" > "kiwi"; .... ] ``` ## Attributes on paths { #path_attr } A popular scenario for using attributes is annotating the path to tables and files when making requests. For more information, see [Operation options](https://ytsaurus.tech/docs/data-processing/operations/operations-options.md#path_attributes) and [YPath](https://ytsaurus.tech/docs/en/ypath.md#known_attributes). # YSON This section contains information about YSON, a JSON-like data format. The main differences between YSON and JSON are: 1. Support for binary representation of scalar types (numbers, strings, and boolean types). 2. [Attributes](https://ytsaurus.tech/docs/en/#attributes): an arbitrary dict that can be set additionally on a literal of any type, including scalar ones. Besides that, there are syntactic differences: 1. A semicolon is used as a separator instead of a comma. 2. In dicts, a key is not separated from a value by a colon, but by an equal sign: `=`. 3. String literals only have to be enclosed in quotes if there is a parsing. The following set of **scalar** types is available: 1. [Strings](https://ytsaurus.tech/docs/en/#string) (`string`). 2. [Signed](https://ytsaurus.tech/docs/en/#int) and [unsigned](https://ytsaurus.tech/docs/en/#uint) 64-bit integers (`int64` and `uint64` ). 3. Double-precision [floating-point numbers](https://ytsaurus.tech/docs/en/#double) (`double`). 4. [Boolean](https://ytsaurus.tech/docs/en/#boolean) (logical) type (`boolean`). 5. A [special entity type](https://ytsaurus.tech/docs/en/#entity) with only one value, a literal (`#`). Scalar types usually have both textual and binary representation. There are two **composite** types: 1. [List](https://ytsaurus.tech/docs/en/#list) (`list`). 2. [Dict](https://ytsaurus.tech/docs/en/#map) (`map`). ## Scalar types { #scalar_types } ### Strings { #string } There are three types of string tokens: 1. **Identifiers** are strings that match the regular expression ` [A-Za-z_][A-Za-z0-9_.\-]*`. It describes the set of possible C identifiers, extended with the `-` and `.` characters. An identifier specifies a string with identical content and is used primarily for brevity (no need to use quotes). Examples: - `abc123`; - `_`; - `a-b`. 2. **Text strings**: [C-escaped](https://en.wikipedia.org/wiki/Escape_sequences_in_C) strings in double quotes. Examples - `"abc123"`; - `""`; - `"quotation-mark: \", backslash: \\, tab: \t, unicode: \xEA"`. 3. **Binary strings**: `\x01 + length (protobuf sint32 wire format) + data ( bytes)`. ### Signed 64-bit integers (`int64`) { #int } Two methods of writing: 1. **Text** (`0`, `123`, `-123`, `+123`). 2. **Binary**: `\x02 + value (protobuf sint64 wire format)`. ### Unsigned 64-bit integers (`uint64`) { #uint } Two methods of writing: 1. **Text** (`10000000000000`, `123u`). 2. **Binary**: `\x06 + value (protobuf uint64 wire format)`. ### Floating-point numbers (`double`) { #double } Two methods of writing: 1. **Text**: `0.0`, `-1.0`, `1e-9`, `1.5E+9`, `32E1`. 2. **Binary**: `\x03 + protobuf double wire format`. {% note warning "Attention" %} Textual representation of floating-point numbers involves rounding. The result value may become different after a round of serialization and parsing. To store an accurate value, use binary representation. {% endnote %} ### Boolean literals (`boolean`) { #boolean } Two methods of writing: 1. **Text** (`%false`, `%true`). 2. **Binary** (`\x04`, `\x05`). ### Entity (`entity`) { #entity } Entity is an atomic scalar value with no content of its own. There are various scenarios in which this type can be useful. For example, entity often means null. In addition, when a `get` request is made to a [Cypress](https://ytsaurus.tech/docs/en/cypress.md) subtree, [files](https://ytsaurus.tech/docs/en/objects.md#files) and [tables](https://ytsaurus.tech/docs/en/objects.md#tables) are returned as entities (actual data is stored in the [attributes](https://ytsaurus.tech/docs/en/#attributes) of that node). Lexically, entity is encoded by the `#` symbol. ### Special literals { #special_literals } Special tokens: `;`, `=`, `#`, `[`, `]`, `{`, `}`, `<`, `>`, `)`, `/`, `@`, `!`, `+`, `^`, `:`, `,`, `~`. Not all of these symbols are used in YSON, some are used in [YPath](https://ytsaurus.tech/docs/en/ypath.md). ## Composite types { #composite_types } ### List (`list`) { #list } Set as follows: `[value; ...; value]` where `value` is a literal of some scalar or composite type. Example: `[1; "hello"; {a=1; b=2}]`. ### Dict (`map`) { #map } Set as follows: `{key = value; ...; key = value}`. Here `*key*` is a string literal and `value` is a literal of some scalar or composite type. Example: `{a = "hello"; "38 parrots" = [38]}`. ### Attributes { #attributes } It is possible to set attributes on any literal in YSON, in the following format: ` value`. Inside angle brackets, the syntax is similar to the dict. For example, `"some-string"` or `<"44" = 44>44`. But most often attributes can be found on literals like `entity`, for example, `#`. ## Grammar {#grammar} YSON has three data types: 1. **Node** (single tree; `` in the example) 2. **ListFragment** (`semicolon`-separated values; `` in the example) 3. **MapFragment** (`semicolon`-separated key-value pairs; `` in the example) Grammar (whitespace characters are ignored and may appear in any number between tokens): ```antlr = [ ], ; = | | | ; = | | | | ; = "[", , "]"; = "{", , "}"; = "#"; = "<", , ">"; = { , ";" }, [ ]; = ; = { , ";" }, [ ]; = , "=", ; % Key cannot be empty ``` You may omit the semicolon ('`;`') after the last element inside `` and ``. The following constructs are considered valid when reading data: #| || Ending with `;` | Shorthand syntax || || ```yson c {a=b;} 1;2;3; ``` | ```yson c {a=b} 1;2;3 ``` || |# ## Examples {#examples} - Map (Node) ```yson { performance = 1 ; precision = 0.78 ; recall = 0.21 } ``` - Map (Node) ```yson { cv-precision = [ 0.85 ; 0.24 ; 0.71 ; 0.70 ] } ``` - List (Node) ```yson [ 1; 2; 3; 4; 5 ] ``` - String (Node) ```yson foobar ``` ```yson "hello world" ``` - Int64 (Node) `42` - Double (Node) `3.1415926` - ListFragment ```yson { key = a; value = 0 }; { key = b; value = 1 }; { key = c; value = 2; unknown_value = [] } ``` - MapFragment ```yson do = create; type = table; scheme = {} ``` - HomeDirectory (Node) ```yson { home = { sandello = { mytable = # ; anothertable = # } ; monster = { } } } ``` ## Working with YSON from code { #working_from_code } Users usually do not have to work directly with YSON. When using one of the official YTsaurus clients, YSON structures will be expressed as follows: 1. **C++**:[`TNode`](https://github.com/ytsaurus/ytsaurus/blob/main/library/cpp/yson/node/node.h) is a class that provides dynamic DOM-like representation of a YSON document. 2. **Python**: [`YsonType`](https://github.com/ytsaurus/ytsaurus/blob/main/yt/python/yt/yson/yson_types.py): YSON types mimic Python types. You can get YSON attributes of object `x` like this: `x.attributes`, this is a Python dict. 3. **Java**: [`YTreeNode`](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/yson-tree/src/main/java/tech/ytsaurus/ysontree/YTreeNode.java) is an interface that provides dynamic DOM-like representation of a YSON document. # YPATH This section contains information about YPath, a language that describes paths to objects in the YTsaurus system. YPath is a language for describing paths that identify objects in the YTsaurus system. The language enables you to refer to nodes and specify annotations that may be useful when performing operations on nodes, such as writing to a table. You can use annotations to determine whether new data will be added to the end of the table or whether the table will be completely overwritten. For example: - `//home/user/table`: The path to `table` in the user's home folder. - `#0-25-3ec012f-406daf5c/@type`: The path to the `type` attribute of the object with the `0-25-3ec012f-406daf5c` ID. - `//home/user/table[#10:#20]`: Rows 10 through 19 of `table` in the user's home folder. There are several types of YPath. In the simplest case, YPath looks like a filesystem-like path, starting with a double slash `//`. ## Simple YPath { #simple_ypath } ### Lexis { #simple_ypath_lexis } The string that encodes simple YPath is split into the following **tokens**: 1. **Special characters**: Forward slash (`/`), at sign (`@`), ampersand (`&`), and asterisk (`*`). 2. **Literals**: The maximum non-empty sequence of non-special characters. Literals allow escaping of the `\` form where `` can be one of the characters: `\`, `/`, `@`, `&`, `*`, `[`, or `{`. Literals also allow an expression of the `x` form where `` and `` are hexadecimal digits. ### Syntax and semantics { #simple_ypath_syntax } YPath is structured as ``. Here `` can be of two types: 1. **Cypress root**: `/` token. Example: `//home/user`. Here `` is `/` and `` is `/home/user`. 2. **Object root**: A token literal that encodes a string of an `#` form. Example: `#1-2-3-4/@type`. Here `` is `#1-2-3-4` and `` is `/@type`. Thus, `` defines a starting point to which `` applies. The latter is disassembled sequentially from left to right, resulting in tree movement steps of the following types: - **Transition to descendant**: A sequence of a `/` token and a literal. This step type applies to dicts and lists. In case of a dict, the literal must contain the descendant name. Example: `/child`: Transition to a descendant named `child`. In case of a list, the literal must contain an integer in the decimal system — the descendant number. Descendants in the list are numbered from zero. Negative numbers that enumerate descendants from the end of the list are also allowed. Example: `/1` is transition to the second descendant in the list, `/-1` is transition to the last descendant in the list. - **Transition to attribute**: A sequence of `/@` tokens and a literal. This type of step is applicable at any point in the path and means transition to an attribute with the given name. Example: `/@attr`: Transition to an attribute named `attr`. {% note info "Note" %} In YPath, a relative path starts with a slash. Thus, the slash serves not as a separator as in the case of file systems, but as a start of a command to traverse the metainformation tree. Consequently, the usual string concatenation is sufficient to merge two YPaths. {% endnote %} ``` {% note warning %} The number of *child transitions* contained in the path is limited. An attempt to access a path exceeding this limit will result in an error. The limit is 256, but it may be changed in the future. {% endnote %} ``` ### Examples { #simple_ypath_examples } ``` % Cypress root / % The root directory has a home node, it has a user node. //home/user % Initial element in the //home/user/list list //home/user/list/0 % Last element in the //home/user/list list //home/user/list/-1 % The attr attribute of the //home/user node //home/user/@attr % The object with ID 1-2-a-b #1-2-a-b % The child descendant of the object with ID 1-2-a-b #1-2-a-b/child % The attr attribute of the object with ID 1-2-a-b #1-2-a-b/@attr ``` ### Behavior specifics { #simple_ypath_behavior } In addition to the above rules, there are some special agreements. Most of them are due to the fact that YPath can identify not only existing metainformation entities, but also new ones created at the moment of execution of the command the argument of which is this YPath. - **Specify all descendants**: You can use the `remove_node` command to remove all descendants of a dict or list. To do this, use the `*` token instead of the name. Example: ``` % Clear the user's home folder yt remove //home/user/* ``` - **Specify all attributes**: `/@` is a YPath form that designates all attributes of an object. It can be used with read (`get_node`, `list_nodes`) commands to access the full collection of all attributes of an object. Examples: ``` % Get the values of all attributes of the user's home folder yt get //home/user/@ % Get the names of all attributes of the user's home folder yt list //home/user/@ ``` The full collection of all attributes can also be *modified* via the `set_node` command. However, one should be warned that doing so will remove all attributes not mentioned in the value being set. Examples: ``` % Remove all attributes yt set //home/user/@ '{}' % Set two attributes and remove all other existing attributes yt set //home/user/@ '{attr1=value1; attr2=value2}' ``` This way of modifying attributes should not be used when working with Cypress (as opposed to an abstract YSON document) because it may lead to an inadvertent removal of system attributes of a node. Instead, individual attributes should be set. Examples: ``` % Set one attribute, do nothing with other attributes yt set //home/user/@attr value % Set the nested attribute yt set //home/user/@attr/some/key value ``` - **Specify insertion position in the list**: Use commands that create new nodes (for example, `set_node`) to specify the position of the node to be created in the list in relation to existing ones. To do this, use the special `begin` (beginning of the list), `end` (end of the list), `before:` (position before the descendant with the `` number), and `after:` (position after the descendant with the `` number) strings. Examples: ``` % Add the "value" element to the end of the //home/user/list list yt set //home/user/list/end value % Insert the "value" element at the beginning of the //home/user/list list yt set //home/user/list/before:0 value % Or yt set //home/user/list/begin value % Insert the "value" element before the second element in the //home/user/list list yt set //home/user/list/before:2 value % Insert the "value" element after the fifth element in the //home/user/list list yt set //home/user/list/after:5 value ``` - **Disable redirection**: The `&` token can be used to suppress redirections via a [symbolic link](https://ytsaurus.tech/docs/en/links.md). Examples: ``` % Find the ID of the link object itself, not the object the link points to yt get //home/user/link&/@id ``` ## Rich YPath { #rich_ypath } **Rich** YPath is an YPath extended with additional annotations. These annotations can be specified in two ways: either as **attributes** (key-value pairs) or **syntactically** by adding special prefixes and suffixes to a simple YPath, which turns it into the `` string. If there is no `` and ``, rich YPath can have, for example, the following form: ```yson < append = %true; compression_codec = lz4; > //home/user/table ``` This path points to the user's home folder and has two attributes. An arbitrary set of attributes can be specified on any path. Their exact meaning depends on how this path is used in the application. For example, when writing to a table, it is natural to support the `append` attribute. For input tables of some mapreduce operations, you can specify the `foreign` flag, which means that this table is secondary (a reference table), so the system needs to perform join of a special form. Attributes whose names are unknown to the system are ignored by the system, but still may be interpreted by the application. The path prefix and suffix enable you to encode attributes in a more compact form. Both of these parts use [YSON](https://ytsaurus.tech/docs/en/yson.md). Learn more about them: ### Prefix { #rich_ypath_prefix } The `` part is either empty or sets attributes as a map-fragment in [YSON](https://ytsaurus.tech/docs/en/yson.md) format. Example: `//home/user/table` sets the path to the table in the user's home folder and also passes the additional `append` attribute to us. In addition to this attribute map, there is a shorthand to specify `cluster` attribute: `my-cluster://path/to/object`. This shorthand is equivalent to specifying `cluster` manually in the attribute map: `//path/to/object`. If both `cluster` attribute and this shorthand are used, the shorthand has the preference. The main purpose of the prefix is to make it easier to pass attributes when using commands from the console. ### Suffix { #rich_ypath_suffix } The ``part is either empty or indicates columns and/or ranges of rows in the referenced table. The column selection modifier, if any, is always specified first, followed by the row selection modifier, if there is one. The **column selection modifier** is a comma-separated set of column names, specified in curly brackets. Each name is represented by a YSON string. Examples of column selection modifiers: `{a}`, `{a,b}`, and `{}` . The formal grammar of column selection modifiers: ``` = '{' { ',' } [ ] '}' = ``` The **row selection modifier** is a comma-separated set of table row ranges, specified in square brackets. Each range is a pair of lower- and upper-range boundaries separated by a colon. Either or both boundaries may be missing. In that case, the range extends without a boundary on that side. The boundary can either be a number with the `#` symbol appended to the left (in this case, it is the row number in the table) or a list of values (in this case, it is a boundary on the values of the key columns by which the table is sorted). The specified list can be shorter than the number of fields by which the table is sorted. If the list of columns contains just one element, that element does not have to be enclosed in parentheses. That element, however, must be a valid YSON value of scalar type: `int64`, `uint64`, `string`, `double`, or `bool`. For information about the modifier semantics, see [this section](https://ytsaurus.tech/docs/en/#key_selectors). Examples of row selection modifiers: `[:]`, `[#10:#100]`, `[a:m]`, `[(abc,8):(xyz,5)]`, `[:5.0,10.0:]`, `[#10]`, `[a,#100,#1:#2]`, and `[100u:200u]`. In the latter case, the data contains unsigned numbers. The formal grammar of row selection modifiers: ``` = '[' { ',' } [ ] ']' = | | | | '' = [ ] ':' [ ] = '#' = [ ] ':' [ ] = | = '(' { ',' } [ ] ')' = | | | | ``` ### Examples { #rich_ypath_examples } {% note info "Note" %} Rich paths oftentimes need to be enclosed in single quotes to be parsed by the shell properly. {% endnote %} ``` % Read the entire '//home/user/table' table yt read '//home/user/table' % Read all rows of the '//home/user/table' table without selecting any columns yt read '//home/user/table{}' % Read column 'a' yt read '//home/user/table{a}' % Read columns 'a' and 'b' yt read '//home/user/table{a,b}' % Read the first 100 rows of the '//home/user/table' table and select all columns yt read '//home/user/table[#0:#100]' % Read all rows, skipping the first 100 yt read '//home/user/table[#100:]' % Read row 100 (counting from 0) yt read '//home/user/table[#100]' % Read rows with the 'abc' key (or the first key coordinate) yt read '//home/user/table["abc":"abc\x00"]' % The query above can also be expressed in a more concise way: yt read '//home/user/table["abc"]' % Read rows with keys no smaller than '(a,1)' yt read '//home/user/table[(a,1):]' % Read rows with the first two key components equal to 'a' and '1', respectively yt read '//home/user/table[(a,1)]' % Read rows where the first key is 'a' and the second key starts with 'b'. If the second key is 'b', then the third key must be no smaller than '1'. yt read '//home/user/table[(a,b,1):(a,c)]' % Read rows where the first key starts with 'a' and rows where the first key is 'b' and the second is smaller than '1'. yt read '//home/user/table[a:(b,1)]' % Read rows where the first key starts with 'a', rows where the first key is 'b' and the second is smaller than '1', and rows where the first key is 'b', the second is '1', and the third has a negative value of the double type. Select columns 'ab', 'ac' yt read '//home/user/table{ab,ac}[a:(b,1,0.0)]' % Convert table1 with columns a,b,c,d to table2 with columns a,b,c and sort by fields a,b yt sort --src '//home/users/table1{a,b,c}' --dst //home/users/table2 --sort-by a --sort-by b --spec '{schema_inference_mode = from_output}' % Convert table1 with columns a,b,c,d to table2 with columns a,b,c yt merge --src '//home/users/table1{a,b,c}' --dst //home/users/table2 --spec '{schema_inference_mode = from_output}' ``` ## YPath canonical form { #ypath_canonical_form } [Rich YPath](https://ytsaurus.tech/docs/en/#rich_ypath) can be brought into **canonical form**. In canonical form, there is no prefix and suffix. Instead, all auxiliary information is explicitly put in the attributes assigned to the [simple YPath](https://ytsaurus.tech/docs/en/#simple_ypath). Example: after canonicalizing the `//home/user/table[#10:#20]` path, you get the following YSON structure: ```yson < append = true; ranges = [ { lower_limit = {row_index = 10}; upper_limit = {row_index = 20} } ] > "//home/user/table" ``` Internally, the YTsaurus system prefers to work with the canonical form of paths. Canonical form is larger, so the system allows to use the more convenient form with a prefix and suffix in the input paths: ``` //home/user/table[#10:#20] ``` Prefix: ``. Suffix: `[#10:#20]`. This will be converted to a more verbose string inside the system: ```yson ranges = [ { lower_limit = {row_index = 10}; upper_limit = {row_index = 20} } ] ``` The `parse_ypath` driver [command](https://ytsaurus.tech/api/commands.md#parse_ypath) canonicalizes a rich YPath. ## Supported attributes { #known_attributes } [Rich YPath](https://ytsaurus.tech/docs/en/#rich_ypath) can be annotated with arbitrary attributes, allowing for expansion. The table contains a list of attributes recognized by the system. | **Attribute** | **Type** | **Description** | | ------------------- | ----------------------------- | ------------------------------------------------------------ | | `append` | `bool` | Recognized by the data write commands (`write_table`, `write_file`, `upload_parquet` and `upload_orc`) and means that new data is added to the end of the existing data, but does not overwrite it. It is also recognized on the output paths of tables in the specifications of all scheduler operations and has similar semantics. | | `sorted_by` | `array` | Recognized by the table data write command (`write_table`). By specifying this attribute, the user promises that the written data is ordered by the specified set of columns. The system checks this property. The keys must go in non-descending order. When new data is being written, it is also checked that the first key to be written is no smaller than the last existing one. The attribute is also recognized by the system on the output paths of tables in the specifications of all scheduler operations and has similar semantics. In the latter case, it is also additionally required that the ranges of keys generated by different jobs have no intersections on the inner point. Intersections on the boundaries are allowed. | | `ranges` | `array` | Recognized by the data read commands (`read_table` and `read_file`) and specifies a list of ranges for reading. If it is missing, the entire dataset is read. It is also recognized on the input paths of tables in the specifications of all scheduler operations. A description of the `ReadRange` dict structure is given in the table below. | | `columns` | `array` | Recognized by the table data read command (`read_table`) and specifies a list of names of readable columns. If it is missing, all columns are read. It is also recognized on the input paths of tables in the specifications of all scheduler operations. | | `optimize_for` | `string` | Sets the storage format for the created table. The attribute can be specified with the `write_table` command and on the output paths of operations. This attribute **is not compatible** with ``. | | `compression_codec` | `string` | Sets the compression format for the created table. The attribute can be specified with the `write_file` and `write_table` commands and on the output paths of operations. This attribute **is not compatible** with ``. | | `erasure_codec` | `string` | Enables erasure coding for the created table. The attribute can be specified with the `write_file` and `write_table` commands and on the output paths of operations. This attribute **is not compatible** with ``. | | `schema` | `yson-list` with a valid schema | Validates data with respect to the specified schema and places this schema on the table at the end of the operation. The attribute can be specified with the `write_table` command and on the output paths of operations. This attribute **is not compatible** with ``. | | `transaction_id` | `string` of the id form | Specifies to the scheduler under which transaction the input table should be accessed if this attribute is specified on it. | | `rename_columns` | `yson-map` | Recognized on the input paths of tables. Renames the column names according to the mapping before feeding the table to the input of the operation. This attribute **is not compatible** with `teleport` tables. | | `cluster` | `string` | Recognized on the input paths of tables. Makes the scheduler to treat the table as a remote table from another cluster. The `ranges` attribute specifies which ranges of table rows should be read. The specified ranges will be read sequentially, in the order in which they are specified in the attribute. If the ranges overlap, the output will be repeated. | **Attribute** | **Type** | **Description** | | ------------- | ----------- | ------------------------------------------------------------ | | `lower_limit` | `ReadLimit` | Lower read limit | | `upper_limit` | `ReadLimit` | Upper read limit | | `exact` | `ReadLimit` | Exact read limit value (cannot be specified with lower_limit or upper_limit) | All limit types except for `key_bound` are inclusive in the `lower_limit` attribute and exclusive in the `upper_limit` attribute. Whether a `key_bound` limit is inclusive or exclusive is specified explicitly within the key bound itself. Learn more in the section on [slices by key](https://ytsaurus.tech/docs/en/#key_selectors). `ReadLimit` describes a separate read limit. Limits can be defined using one of the six **selectors**: key, key bound, row index, chunk index, tablet index, or byte offset. You can specify multiple selectors for a single limit. In this case, they all apply together, reinforcing each other. | **Attribute** | **Type** | **Description** | | ----------- | ------- | ---------------------------------------------------- | | `key` | `list` | List of values that form the key (for sorted tables only) Learn more in [this section](https://ytsaurus.tech/docs/en/#key_selectors) | | `key_bound` | `list` | Inclusive or exclusive key prefix limit. Cannot be used with exact limits. Learn more in [this section](https://ytsaurus.tech/docs/en/#key_selectors) | | `row_index` | `int` | Row number (for tables only) | | `offset` | `int` | Byte offset (for files only) | | `chunk_index` | `int` | Chunk index | | `tablet_index` | `int` | Tablet index (for ordered dynamic tables only) | YPath attributes on the paths of **files** supported by the YTsaurus system | **Attribute** | **Type** | **Description** | | ----------------------- | ----------------------------- | ------------------------------------------------------------ | | `file_name` | `string` | Recognized when ordering a file in the `sandbox` of the job. The attribute specifies the relative path on which to place the file. | | `executable` | `bool` | Recognized when ordering a file in the `sandbox` of the job. The attribute tells the system to set an executable bit on the ordered file. | | `bypass_artifact_cache` | `bool` | Recognized when ordering a file in the `sandbox` of the job. The attribute enables direct file download to the `sandbox` of the job, bypassing the file cache on the node. | | `format` | `string` | Recognized when ordering a table file in the `sandbox` of the job. The attribute specifies the format in which the table should be formatted when it is downloaded to the `sandbox` of the job. | ## Sorted table slices by key { #key_selectors } The system supports two types of selectors based on key column values. These selectors appear similar but differ in behavior. The first type is historically older and is implemented by the `key` selector. You can also specify it in the [YPath suffix](https://ytsaurus.tech/docs/en/#rich_ypath_suffix). The second type is newer and is implemented by the `key_bound` selector. It is only available via the `ranges` attribute in the YPath prefix. For new applications, we recommend using the `key_bound` selector because of its more transparent behavior and greater flexibility in use, even though it is more verbose. {% note info "Note" %} As a reminder, YTsaurus defines a linear order for all possible values that may occur in a table. Specifically, int64, uint64, double, and boolean values are ordered naturally within their respective types, while string values are ordered lexicographically. Two values of different data types are compared according to the implementation-defined rules for how the types themselves are compared. For example, any int64 is less than any uint64, and any null is less than any non-null (`#`) value. Because of this, you should always pay attention to the respective data types when comparing values. Specifically, unsigned integers must be specified with the suffix `u`, and fractions should have a decimal point. {% endnote %} ### `key` selector The `key` selector can only be used for sorted tables where all key columns are sorted in ascending order. The `key` selector's type is a list of values, with its behavior defined as follows. Let's examine various tuples consisting of YTsaurus type system values. We will define a linear lexicographic order for these tuples. For example, as follows: `[] < ["a"] < ["a"; 3] < ["a"; 4] < ["b"] < ["b"; 2]` Using this order, we can compare two tuples of any length. The comparison result is determined by the first differing tuple components, provided they exist. If not, the shorter tuple is considered smaller. If the `key` selector is specified as part of the `lower_limit` attribute (or as a lower limit preceding the colon in a row selection modifier within the YPath suffix), it only returns those rows where the **full** key, represented as a tuple, is greater than or equal to the `key` tuple based on the comparison logic described above. This means that the definition of the `key` selector remains valid even if it includes a tuple with fewer or more values than the number of key columns in the table. If the `key` selector is specified as part of the `upper_limit` attribute (or as an upper limit following the colon in a row selection modifier within the YPath suffix), it only returns those rows where the **full** key, represented as a tuple, is **strictly** smaller than the `key` tuple. If the `key` selector is specified as part of the `exact` attribute (or as the only limit in a colonless row selection modifier within the YPath suffix), it only returns those rows where the full key contains the `key` tuple as its prefix. #### Examples Suppose the `//tmp/t` table has two key columns and contains the following keys: ```yson ["a"; 1]; ["a"; 3]; ["a"; 5]; ["b"; 2]; ["b"; 4]; ["c"; 0]; ``` The `//tmp/t[(b):]` slice, also expressed as `//tmp/t`: ```yson ["b"; 2]; ["b"; 4]; ["c"; 0]; ``` The `//tmp/t[(b, 2, 56):]` slice (note that a key within a slice can have more values than there are key columns present in the table): ```yson ["b"; 4]; ["c"; 0]; ``` The `//tmp/t[:(c, 0)]` slice: ```yson ["a"; 1]; ["a"; 3]; ["a"; 5]; ["b"; 2]; ["b"; 4]; ``` The `//tmp/t[(b)]` slice: ```yson ["b"; 2]; ["b"; 4]; ``` {% note info "Note" %} Obtaining an exclusive lower limit or an inclusive upper limit using the `key` selector requires specifying the tuple's successor according to the linear comparison rules defined for the tuples above, rather than the tuple itself. Doing so can often be challenging. For this reason, the service introduced the `key_bound` selector, which is detailed in the following section. {% endnote %} ### `key_bound` selector The `key_bound` selector is available for use in sorted tables where key columns may be sorted in both descending and ascending order. The value of the `key_bound` selector is a list containing two values. The first one specifies the **relation**, which is represented by one of the four strings — `">", ">=", "<", "<="`, — while the second one is a list of values that defines the key **prefix**. The `>` and `>=` relational operators can only occur in the `lower_limit` attribute, while `<` and `<=` only occur in `upper_limit`. The `exact` attribute does not allow `key_bound` selectors. Examples of `key_bound` selectors: - `[">"; ["a"; 3]]` - `["<="; ["b"]]` - `[">="; []]` Let's examine the semantics of this selector. Consider a table with `N` key columns and a key bound. Suppose that the length of the key bound prefix is `K`. To apply the key bound to this table, `K` must be between 0 and `N`. Attempting to specify a longer prefix in the key bound will result in an error. To determine if a row with a specific key matches the selector, we need to extract a prefix of length `K` from that key and perform a lexicographic comparison of the resulting value against the key bound prefix. If the result of the comparison matches the specified relation, the selector returns the row, otherwise it does not. {% note info "Note" %} If a key column's `sort_order = descending`, the results of all comparisons involving the corresponding key tuple components are inverted. For instance, in this case, the relations `5u < 2u` and `"foo" < #` are true. Also note that the behavior of the `key_bound` selector only defines the comparison of tuples of the same length (represented as `K`), so defining the comparison of tuples of different lengths with sorting in descending order is not necessary. {% endnote %} Specifically, according to the definition above, the selectors `[">="; []]` and `["<="; []]` match any row because the tuple `[]` corresponds to a key prefix of length 0, while the selectors `[">"; []]` and `["<"; []]` do not match any rows. #### Examples Let's examine the `//tmp/t` table with the keys from the example above: ```yson ["a"; 1]; ["a"; 3]; ["a"; 5]; ["b"; 2]; ["b"; 4]; ["c"; 0]; ``` The `"; ["a"; 3]]}}]>//tmp/t` slice: ```yson ["a"; 5]; ["b"; 2]; ["b"; 4]; ["c"; 0]; ``` The `//tmp/t` slice: ```yson ["a"; 1]; ["a"; 3]; ["a"; 5]; ["b"; 2]; ["b"; 4]; ``` {% note info "Note" %} Note that although the tuple `["b"; 2]` is lexicographically greater than the tuple `["b"]`, truncating the key `["b"; 2]` to a length of 1 transforms it into the tuple `["b"]`, allowing it to match the key bound `[“<=”; [“b”]]`. This behavior of the `key_bound` selector differs from that of the `key` selector, providing the former with greater flexibility, since specifying a similar slice using the `key` selector is a non-trivial task. {% endnote %} The `="; ["b"; 2; 56]]}}]>//tmp/t` slice is not valid, and attempting to use it results in the error: `Key bound length must not exceed schema key prefix length`. # Objects This section describes the main [object types](https://ytsaurus.tech/docs/en/objects.md#object_types) in the YTsaurus system. A table with the key [attributes of each object](https://ytsaurus.tech/docs/en/objects.md#attributes) is given and the concept of an [object ID](https://ytsaurus.tech/docs/en/objects.md#object_ids) is described. ## Object types { #object_types } The main object types in YTsaurus are: * [Files](https://ytsaurus.tech/docs/en/objects.md#files). * [Tables](https://ytsaurus.tech/docs/en/objects.md#tables). * [Links](https://ytsaurus.tech/docs/en/objects.md#links). * [YSON documents](https://ytsaurus.tech/docs/en/objects.md#yson_doc). * [Primitive types](https://ytsaurus.tech/docs/en/objects.md#primitive_types). * [Internal objects](https://ytsaurus.tech/docs/en/#internal_objects). The description of objects takes up most of the meta-information which is stored in replicated form in the memory of the [Cypress](https://ytsaurus.tech/docs/en/cypress.md) master servers. Cypress contains various system information as well as indications of where user data is stored. For any Cypress node, there may be several versions, because the state of the node may look different in the context of each [transaction](https://ytsaurus.tech/docs/en/transactions.md). ### Files { #files } These are Cypress nodes of the **file** type designed to store large binary data in the system. Files consist of multiple chunks and [are chunk owners](https://ytsaurus.tech/docs/en/chunks.md#attributes). Chunks are organized as a special tree-like data structure in which leaves are chunks and intermediate nodes are chunklists. For more information, see [Files](https://ytsaurus.tech/docs/en/files.md). ### Tables { #tables } These are nodes of the **table** type designed to store large user data. There are two types of tables: [static](https://ytsaurus.tech/docs/en/static-tables.md) and [dynamic](https://ytsaurus.tech/docs/dynamic-tables/overview.md). Logically, a table is a sequence of rows. Each row consists of columns. Column values are typified and can contain random structured data supported by the [YSON](https://ytsaurus.tech/docs/en/yson.md) language. ### Links { #links } Links are nodes of the **link** type that refer to other objects along the path specified when the object is created. All accesses to the node links are automatically redirected to the target object. For more information, see [Links](https://ytsaurus.tech/docs/en/links.md). ### YSON documents { #yson_doc } A YSON document is a Cypress node of the **document** type designed to store random YSON structures. Requests and modifications within the document are supported. Addressing within the document is performed using the [YPath](https://ytsaurus.tech/docs/en/ypath.md) language. For more information, see [YSON documents](https://ytsaurus.tech/docs/en/yson-docs.md). For more information about the YSON format, see [YSON.](https://ytsaurus.tech/docs/en/yson.md) ### Primitive types { #primitive_types } A Cypress node can contain a value of the primitive type: string, number, dict, list. The table contains a description of the primitive types of Cypress nodes. | Type name | Description | | -------------- | --------------------------------| | `string_node` | A Cypress node containing a string | | `int64_node` | A Cypress node containing a signed integer | | `uint64_node` | A Cypress node containing an unsigned integer | | `double_node` | A Cypress node containing a real number | | `map_node` | Dict in Cypress (keys are strings, values are other nodes). By analogy with a file system, this is a folder | | `boolean_node` | A Cypress node containing a Boolean value | ### Internal objects { #internal_objects } In addition to the objects listed above, there are internal objects: transactions, chunks, accounts, users, and groups. Such objects are not subject to versioning. The internal objects listed in the table: | Object name | Description | | -------------- | ------------------------------------------------------------ | | `transaction` | [Transaction](https://ytsaurus.tech/docs/en/transactions.md) | | `chunk` | Chunk | | `chunk_list` | Chunklist | | `erasure_chunk` | Erasure-chunk | | `account` | Account | | `user` | User | | `group` | Group | ## Object IDs { #object_ids } Each object has a unique **ID** that is a 128-bit number whose format coincides with the GUID. This number can be represented as four 32-bit numbers: `a-b-c-d`. Usually when an ID is typed, components `a`, `b`, `c`, and `d` are written in hexadecimal form. Each of these components has its own meaning and is described in the table. | Component | Purpose | | :---------: | ------------------------------------------------------------ | | `a` | The number of the Hydra epoch in which the object was created | | `b` | The mutation number within the epoch in which the object was created | | `c[16..31]` | The master group ID (cell id) is a unique 16-bit number that unambiguously identifies the cluster. This ID can be found in the cluster web interface opposite the master servers | | `c[0..15]` | Object type | | `d` | Hash is a pseudo-unique random number selected at the time of object creation. | Thus, the object ID depends on the cluster on which the object was created, as well as on the point in time (in terms of mutation numbers) when the object was created. More than one object can be created within a single mutation. They will differ in hash value. This difference is guaranteed by the hash generation mechanism. ID example: `28bb5-75b04b-3fe012f-8b4eda94`. ## Object attributes { #attributes } All system objects have the attributes listed in the table: | **Attribute** | **Type** | **Value** | | :---------------------- | --------------- | --------------------------------------------------------- | | `id` | `string` | Object ID | | `type` | `string` | Object type | | `ref_counter` | `integer` | The number of [strong links](https://ytsaurus.tech/docs/en/objects.md#ref_counter) to the object | | `supported_permissions` | `array` | List of supported access permissions | | `effective_acl` | `Acl` | Effective object ACL | Some system objects have additional attributes: `inherit_acl`, `acl`, and `owner`. They affect access to this object. These attributes are called an **access control descriptor** (ACD). For more information about the ACD, see [Managing access](https://ytsaurus.tech/docs/en/access-control.md). ### Reference counters { #ref_counter } **Reference counters** are used to track the lifetime of an object. An object lives in the system as long as there are **strong links** to it. The number of strong links can be found in the ref_counter attribute. As soon as this number reaches zero, the object turns into a **zombie** and is subject to removal. It is not removed instantly: there is a special **removal queue** (GC queue) from which zombies are taken in portions of a controlled size and destroyed. The size is set by the internal configuration parameters of the system. Once the object has become a zombie, you can no longer access it, even by explicitly building a path by ID. # Static tables This section provides an introduction to static tables, including their types and [schema](https://ytsaurus.tech/docs/en/#schema). In addition to static tables, the YTsaurus system supports dynamic tables. For more information about the types and features of dynamic tables, see [Dynamic tables](https://ytsaurus.tech/docs/dynamic-tables/overview.md). ## General description { #common } Static tables are a classic table type in the YTsaurus system. Such tables are physically divided into parts ([chunks](https://ytsaurus.tech/docs/en/chunks.md)) and each part contains a fragment of table entries. The name “static” implies that you cannot update existing table data without an overwrite. However, you can still append new records to the end of the table. In addition, table data can be modified using the following operations (note that in a typical case you will need to read and overwrite entire chunks of the table): - [Merge](https://ytsaurus.tech/docs/data-processing/operations/merge.md) - [Delete](https://ytsaurus.tech/docs/data-processing/operations/erase.md) Static tables can be [sorted](https://ytsaurus.tech/docs/en/#sorted_tables) and [unsorted](https://ytsaurus.tech/docs/en/#unsorted_tables). ### Sorted tables { #sorted_tables } For a sorted table, a set of immutable key columns is known. The table entries appear to be physically or logically ordered by key. Therefore, sorted tables enable you to effectively search for data by key. A sorted table has at least one key column, the `sorted` attribute of the table is `true`. The attribute is read-only. There are key (sorted) columns in the sorted table schema. They are marked with the `sort_order` field in the schema. A list of all key columns is available in the `key_columns` and `sorted_by` attributes of the table. The sequence of columns is defined in the table schema. The attributes are read-only. {% note info "Note" %} Entries added to the end of the sorted table must not violate the sort order. {% endnote %} ### Unsorted tables { #unsorted_tables } For unsorted tables, the concept of a key is not defined, so searching for data by key is possible only if you read the whole table. However, you can access it by row numbers. {% note info "Note" %} Appending entries to the end of an unsorted table is the basis of many ways to load data into YTsaurus clusters. For this operation to be effective, consider the following: * Any change request is processed by the master server. This schema does not scale well, so you should not make more than 100 writes per second. * Writing a small number of rows within a single request results in small chunks which overload the master server with a large amount of metadata and make reading less efficient. {% endnote %} ## Attributes { #attributes } Any static table has the attributes represented in the table: | **Name** | **Type** | **Description** | **Mandatory** | |----------------|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| | `sorted` | `bool` | Whether the table is sorted. | No | | `key_columns` | `array` | Key column names. | Yes | | `dynamic` | `bool` | Whether the table is dynamic. | No | | `schema` | `TableSchema` | Table schema. | No | | `row_count` | `integer` | Number of rows in the table. | Yes | | `data_weight` | `integer` | A "logical" amount of uncompressed data written to a table. Depends only on the values in the table cells and the number of rows. Calculated as `row_count + sum(data_weight(value))` for all values in the table cells. `data_weight` for a value depends on the physical type of the value: for `int64`, `uint64`, and `double` — 8 bytes; for `bool` and `null` — 1 byte; for `string` — string length; for `any` — length of the value serialized in binary YSON. | Yes | | `optimize_for` | `string` | The format for the physical storage of the table in chunks: `lookup` (row-based) and `scan` (columnar). If this attribute is not specified when creating a table, the `lookup` format will be used. It is recommended to explicitly specify `scan` to get additional optimizations for data storage and reading. [Read more](https://ytsaurus.tech/docs/en/chunks.md#optimize_for) | No | Besides that, a table is an object that owns chunks, meaning it has the associated attributes featured in the [table](https://ytsaurus.tech/docs/en/chunks.md#attributes). ## Static table schema { #schema } A static table schema is a list of column descriptions. For a detailed description of the schema format, see [Data schema](https://ytsaurus.tech/docs/en/static-schema.md). ## Limitations { #limitations } A number of size and content type limitations are imposed on table rows and schema: - The number of columns in a static table cannot exceed 32,768. We do not recommend using more than a thousand columns. - The column name is an arbitrary byte sequence that can contain from 1 to 256 characters. In addition, it cannot start with the prefix `@`, which is reserved by the system. - The maximum length of `string` values in a static table is limited by the maximum row weight. - The maximum row weight is 128 megabytes. The row weight is the sum of the lengths of all values in the given row in bytes. The lengths of the values are counted depending on the type: - `int64`, `uint64`, and `double`: 8 bytes. - `boolean`: 1 byte. - `string`: String length. - `any`: The length of the structure serialized in binary [yson](https://ytsaurus.tech/docs/en/yson.md), in bytes. - `null`: 0 bytes. - The maximum key weight in a sorted table is 256 kilobytes. The default limitation is 16 kilobytes. The key weight is counted similarly to the row weight. {% note warning "Attention" %} We strongly advise against raising the limit on the key weight, because there is a risk of exhaustion of the master server memory. You should only change the `max_key_weight` setting as an extreme measure. {% endnote %} # Files This section contains information on YTsaurus system objects designed to store large amounts of binary data. You can also store binary data in [tables](https://ytsaurus.tech/docs/en/blobtables.md). ## General information { #common } A file is a [Cypress](https://ytsaurus.tech/docs/en/cypress.md) node of type **file** designed to store large binary data in the system. You can store binary data both in files and in tables. Sometimes, jobs need to save large binary data. The obvious solution is to write the file from each job to Cypress. However, this method has some drawbacks: - It creates high proxy server load. - A large number of objects appears in Cypress which makes working with them less efficient. There is another solution that we propose: writing binary data to a single table. ## Usage { #usage } Files support appends via the `write_file` [command](https://ytsaurus.tech/api/commands.md#write_file). This performs a write by adding [chunks](https://ytsaurus.tech/docs/en/chunks.md); therefore, we recommend against performing many small writes. To read files, use the `read_file` [command](https://ytsaurus.tech/api/commands.md#read_file). You can either read an entire file or specify a read range in the form of an offset in bytes. To do this, use the `offset` selector in the [YPath](https://ytsaurus.tech/docs/en/ypath.md) language. Each read requires a request for metadata from the master server, so multiple reads are not recommended. ### Using in jobs { #usage_in_jobs } One way to use files is to deliver the same data, such as lookups, for [jobs](https://ytsaurus.tech/docs/data-processing/operations/overview.md). This causes file chunks to be downloaded into the local cache of every node requesting the file. If a file consists of multiple chunks or has `compression_codec` set, the cache rebuilds the original file from the [chunks](https://ytsaurus.tech/docs/en/chunks.md). This file is referred to as an artifact. There are two ways to deliver an artifact into a file in a job `sandbox`, which is a special directory that every job creates: - By default, a [link](https://ytsaurus.tech/docs/en/links.md) to the artifact will be created in the job `sandbox`. This behavior is preferred since it works faster and creates less hard drive IO load. - If you include the `copy_file` option in the specification, the file will be copied into the `sandbox`. It does not make sense to use this behavior unless you need to mount the entire `sandbox` in [tmpfs](https://en.wikipedia.org/wiki/Tmpfs). If a file's `executable` attribute is set to `true`, it will be executable in the job `sandbox`. By default, the attribute is set to `false`. You can use the `file_name` attribute to manage the name of a file or link created in the `sandbox`. If this attribute is absent, the [Cypress](https://ytsaurus.tech/docs/en/cypress.md) filename is used instead. ## System attributes { #attributes } Any file has the attributes shown in the table below. The number of bytes in a file is written in the `uncompressed_data_size` attribute that every chunk owner has. | **Attribute** | **Type** | **Description** | **Mandatory** | | ------------ | --------- | -------------------------------------------------- |------------------| | `executable` | `bool` | Whether the file is executable. | No | | `file_name` | `string` | Filename when a file is moved into a job `sandbox`. | No | All the files are chunk owners, so they get the appropriate [attributes](https://ytsaurus.tech/docs/en/chunks.md#attributes). # Symbolic links This section provides information on [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link). ## General information { #common } The system supports `soft links` in [Cypress](https://ytsaurus.tech/docs/en/cypress.md). This link is a node of type `link` referencing another `target` object. The path to it is specified at link creation time. While the address is being computed for the operation, all attempts to access the link node are automatically redirected to the target object. A link to a target object depends on its path. If a target path is deleted or moved, the link will break. You will see an error message saying `Link target does not exist`. A link node does not take up space on disk. Its `resource_usage/disk_space` attribute is equal to 0. Therefore, creating links cannot cause you to exceed your disk quota. However, each link is actually a separate `node`, so it's possible to exceed your account's limit on the number of `nodes`. ## Creating { #create } You can create a symbolic link with the `link` [command](https://ytsaurus.tech/api/commands.md#link). Use the path to the target node and the path to the link node in that order as arguments: CLI ```bash yt link //tmp/target_table //tmp/target_table_link ``` A link may reference arbitrary nodes and objects. Such as the `//tmp` subtree in the home directory: CLI ```bash yt link //tmp //home/username/my_own_tmp ``` {% note info "Note" %} Relative links are not supported. {% endnote %} ## Redirects { #redirects } Automatic redirects from a link node to a target object have a number of exceptions: - The `remove`, `create`, and `link` commands for the target path are processed in the link node. When deleting `//tmp/target_table_link`, for example, a user will not damage `//tmp/target_table`. - The `move` command will affect the target node invalidating the link node. You can also disable redirection by appending the `&` (ampersand) marker to the end of the link name. This will help you explore the attributes of a link node, for example, rather than those of a target object. Thus, to retrieve the target object path, run `yt get //tmp/target_table_link&/@`. You will get the path through the `target_path` attribute. ## System attributes { #attributes } In addition to [attributes](https://ytsaurus.tech/docs/en/attributes.md) common to all nodes, link nodes also have the following properties: | **Name** | **Type** | **Value** | | ------------- | -------- | ------------------------------------------------------------ | | `target_path` | `string` | Path to the object referenced by a link. | | `broken` | `bool` | `true` if a link references a non-existent object, `false` otherwise. | # YSON document This section contains information about YSON documents. ## General information { #common } A YSON document is a [Cypress](https://ytsaurus.tech/docs/en/cypress.md) node of the **document** type designed to store random YSON structures. The document behaves as a whole in terms of Cypress-specific features: locks, owners, the `revision`, `creation_time`, `modification_time`, `expiration_time` attributes, and other features. You can work with documents using standard [commands](https://ytsaurus.tech/api/commands.md): `get`, `list`, `exists`, `set`, and `remove`, just like with other objects in Cypress. Queries and modifications within the document are supported. This means that a document can be requested as a whole, or only some of its individual parts can be requested. Similarly, when modifying a document (the `set` command), you can change the entire document, or you can only change some of its fields. See [usage examples](https://ytsaurus.tech/docs/en/#usage). Addressing within the document is performed using the [YPath](https://ytsaurus.tech/docs/en/ypath.md) language. ## Usage { #usage } Create a YSON document: ```bash $ yt create document //tmp/my_test_doc 3f08-5b920c-3fe01a5-e0c12642 ``` By default, a YSON entity is stored in the document when it is created. Read a YSON document: ```bash $ yt get //tmp/my_test_doc ``` Specify the initial value of the YSON document when it is created: ```bash $ yt create document //tmp/my_test_doc --attributes '{value=hello}' 3f08-6c0ee0-3fe01a5-2c4f6104 $ yt get //tmp/my_test_doc "hello" ``` Write a number to the YSON document and read it: ```bash $ yt set //tmp/my_test_doc 123 # $ yt get //tmp/my_test_doc 123 ``` Write a complex structure to a YSON document and read it fully or partially: ```bash $ yt set //tmp/my_test_doc '{key1=value1;key2={subkey=456}}' # $ yt get //tmp/my_test_doc { "key1" = "value1"; "key2" = { "subkey" = 456; }; } $ yt get //tmp/my_test_doc/key2 { "subkey" = 456; } ``` The node will have the `document` type: ```bash $ yt get //tmp/my_test_doc/@type "document" ``` Partially change a YSON document: ```bash $ yt set //tmp/my_test_doc/key1 newvalue1 ``` Remove a YSON document: ```bash $ yt remove //tmp/my_test_doc ``` ## Limits { #limits } YSON documents are read and written via the Cypress master server, so they cannot be used as a high-load object database. A reasonable limit is single-digit [RPS](https://en.wikipedia.org/wiki/Queries_per_second). Note that since the master server memory stores this data as a tree, the amount of data that can be saved to a YSON document is very limited. Kilobytes can be considered a reasonable limit for a single document. The total volume of all user documents must not exceed single-digit megabytes. Such nodes are usually used to store small pieces of structured metadata, configuration, etc. ## System attributes { #attributes } Besides the attributes inherent to all Cypress nodes, documents have the following additional attributes: | **Attribute** | **Type** | **Description** | | ----------- | ------- | ------------------------------------------------------------ | | `value` | `any` | Full contents of the document. This attribute enables you to specify the contents of the document when it is created. The attribute is [opaque](https://ytsaurus.tech/docs/en/attributes.md#system_attr), which means that when you read all attributes of a node without a filter, it will be displayed as [entity](https://ytsaurus.tech/docs/en/yson.md#entity). | # Transactions This section describes transactions as applied to [Cypress](https://ytsaurus.tech/docs/en/cypress.md), and [static tables](https://ytsaurus.tech/docs/en/static-tables.md), [locks](https://ytsaurus.tech/docs/en/#locks), and [versioning](https://ytsaurus.tech/docs/en/#versioning) of Cypress objects. The transactional model of dynamic tables is described in the [Multiversioning and transaction processing of dynamic tables](https://ytsaurus.tech/docs/dynamic-tables/transactions.md) section. ## General information { #transactions } The YTsaurus system supports transactions, but differs in a number of ways from the classic [transaction processing model](https://en.wikipedia.org/wiki/Online_transaction_processing): - Transactions can last minutes or hours. - Isolation is configurable. - There are no foreign table keys. - Transactions can affect other Cypress nodes in addition to tables. YTsaurus ensures the following properties of the transaction processing system: - **Atomicity**. YTsaurus guarantees that the transaction will not be partially committed in the system. Either all or none of its sub-operations will be executed. Changing the data of a single Cypress node within a single command (for example, the set command) is atomic. - **Consistency**. Changes introduced by committing a transaction maintain data consistency in Cypress nodes and static tables. - **Isolation**. Unlike traditional transaction processing systems, YTsaurus enables you to choose isolation on a per-transaction basis. YTsaurus behavior corresponds to the [Read committed](https://en.wikipedia.org/wiki/Isolation_(database_systems)) isolation level when interacting with Cypress and [Serializable](https://en.wikipedia.org/wiki/Isolation_(database_systems)) when interacting with static tables within transactions. - **Durability**. YTsaurus guarantees the safety of changes after the transaction is committed. A single server hardware failure, power outage, or system shutdown cannot cause a loss of made changes. Transactions in the system can survive a system shutdown and continue running after the system is restored. When you perform most actions in the system, you can specify which transaction those actions should be performed under. If a transaction is not explicitly specified, basic actions involving Cypress nodes, such as creating a new node or reading and writing a node's attributes, are performed atomically. However, more complex actions require the transaction to be initiated explicitly. One such example is reading a table: without a transaction and a snapshot lock on the table, there are no guarantees that data will not be deleted while you are working with it. However, if the data physically exists and can be read with a single query, that query returns a consistent state of the table. Transactions are objects of the `transaction` type. A list of all system transactions can be found in Cypress at `//sys/transactions`. A transaction can have another transaction as a parent. Transactions form a tree whose roots are transactions without parents, also called topmost transactions. A list of all topmost transactions is available at `//sys/topmost_transactions`. Transactions are divided into **master transactions** and **tablet transactions**. Master transactions enable you to perform operations on the master meta-information. Tablet transactions enable you only to write data to dynamic tables. ## Master transactions { #master_transactions } Transactional processing within master servers applies to versionable [objects](https://ytsaurus.tech/docs/en/objects.md). Examples of such objects are files, tables, and folders. ### Creating a transaction { #create } To create a transaction, run the `start_tx` command. You can specify the parent transaction in the `parent_id` option and define the transaction's time to live (TTL) in the `timeout` option. By default, the upper TTL limit for a transaction within the system is one hour. If you specify a `timeout` of more than one hour, it will be equal to the limit. Time to live starts from the moment of the `start_tx` call or from the last [`ping_tx`](https://ytsaurus.tech/docs/en/#ping) call. ### Extending a transaction's TTL { #ping } To extend a transaction's TTL, run the `ping_tx` command. Each execution extends the transaction's TTL by a time interval equal to `timeout`. If the time since transaction creation or the last execution of the `ping_tx` command exceeds `timeout`, the transaction will be aborted. ### Completing a transaction { #commit } You can abort a transaction using the `abort_tx` or successfully complete it using the `commit_tx` command. Aborting a transaction also aborts all its nested transactions. To successfully complete a transaction that includes nested transactions, you must first run the `commit_tx` command on all nested transactions. Attempting to run `commit_tx` on the parent transaction first will result in an error. In all other cases, completing a transaction cannot cause an error. YTsaurus uses [pessimistic locks](https://ytsaurus.tech/docs/en/#locks), so possible conflicts are detected as they occur — when locks are acquired and objects are created within transactions rather than when a transaction is completed. ### Transaction attributes { #attributes } In addition to the attributes [inherent to all objects](https://ytsaurus.tech/docs/en/objects.md#attributes), transactions have the following attributes: | **Attribute** | **Type** | **Description** | **Mandatory** | | ------------------------ | ------------------ | ------------------------------------------------------------ |-----------------------| | `timeout` | `integer` | Transaction timeout in ms. May be omitted for some system transactions. | No | | `title` | `string` | Text description string. This attribute is filled in automatically for all system transactions and for user transactions only if the user specifies it themselves when creating a transaction. | No | | `last_ping_time` | `DateTime` | Time when the transaction's TTL was last extended. May be missing for some system transactions. | No | | `parent_id` | `Guid` | Parent transaction ID. | Yes | | `start_time` | `DateTime` | Transaction creation time. | Yes | | `nested_transaction_ids` | `array` | A list of nested transaction IDs. | Yes | | `staged_object_ids` | `array` | A list of IDs of objects that the transaction temporarily owns. | Yes | | `branched_node_ids` | `array` | A list of branched Cypress node IDs. | Yes | | `locked_node_ids` | `array` | A list of locked Cypress node IDs. | Yes | | `lock_ids` | `array` | A list of IDs of locks created in the transaction. | Yes | | `resource_usage` | `ResourceUsageMap` | An attribute that shows the use of resources in a given transaction for each affected account. | Yes | {% note info "Note" %} Transactions created by the system always have the filled in `title` attribute. It contains a description of the process that created the transaction. Users are also encouraged to use this attribute to describe the purpose of the transaction. {% endnote %} ## Locks { #locks } Versioning of Cypress nodes is related to the concept of locks. By acquiring a lock on the node, the transaction expresses its intention to work with the node in [locking mode](https://ytsaurus.tech/docs/en/#locking_mode). If the transaction manages to acquire a lock, it is guaranteed that: - It is allowed to work with this node in the specified manner. - The node for this transaction is branched. {% note info "Note" %} Note that acquiring a lock on a node creates a branched version of that node. Furthermore, the node may already have existing branches. The presence of a lock on a node always means that the node has a branched version. However, the opposite is not always true: in certain scenarios, a node may be branched but not locked. An attempt to work with such a node will always result in acquiring a lock on it. Branching and acquiring a lock are related, but different things. {% endnote %} A lock is a full-fledged object that has its own ID. A list of all locks in the system is available at `//sys/locks`. We recommend using an address of the `#lock-id` form to access a specific lock. A list of locks acquired by a transaction is displayed in its `lock_ids` attribute. ### Locking modes { #locking_mode } The available locking modes are: `snapshot`, `exclusive`, and `shared`. A locking mode defines the list of allowed transaction actions, as well as the ability to acquire other locks: - `snapshot`: The transaction can read but not modify the node. The lock is used to obtain a read-only copy of a Cypress node state in the context of the transaction and freeze the state of that node. {% note info "Note" %} A snapshot lock is acquired only on the node itself, but not on the path to it in Cypress. If you continue accessing the node using its path, you can get a new node placed on the same path. To ensure access to the snapshot version of a node, use the node's `id`, which is returned by the `lock` [command](https://ytsaurus.tech/api/commands.md#lock). {% endnote %} - `exclusive`: The transaction can modify the node state. Other transactions cannot change the node. - `shared`: The transaction can modify a certain part of the node state. Other transactions can still change other parts of this node. There are three standard scenarios for using this lock: - Concurrently appending data to a table or file from multiple transactions. In this case, you can only append data, but not overwrite it. - Concurrently creating several differently named subfolders within the same folder from multiple transactions. For example, transaction T1 can be started and create (or delete) the `//tmp/a` node and transaction T2 can be started and create (or delete) the `//tmp/b` node. Each of them will acquire a separate `shared` lock on `//tmp`. To detect conflicts, each lock has the `child_key` attribute indicating which key (subfolder) is locked by it. - Concurrently creating several differently named attributes within the same node from multiple transactions. For example, transaction T1 can be started and set (change, delete) the `//tmp/@a` attribute and transaction T2 can be started and set (change, delete) the `//tmp/@b` attribute. To detect conflicts, each lock has the `attribute_key` attribute indicating which key (attribute name) is locked by it. {% note info "Note" %} Transactions can be nested. "Other transactions" refers to transactions that are unrelated to this transaction, meaning that they do not share a common ancestor with it. In particular, nested transactions can result in more than one `exclusive` lock on a node. {% endnote %} ### Implicit locks { #implicit_locks} A transaction can acquire locks either explicitly using the `lock` command or implicitly. Implicit acquisition of locks can occur in case of certain interactions with Cypress nodes, for example: - Creating a node is accompanied by an acquisition of an `exclusive` lock. - Writing to a table or file results in acquiring a `shared` lock if you are adding data to a table and an `exclusive` lock if you are overwriting it. - Creating a new entry in the folder, as well as changing or deleting an existing entry is accompanied by an acquisition of a `shared` lock with the corresponding `child_key`. - Creating a new node attribute, as well as changing or deleting an existing attribute is accompanied by an acquisition of a `shared` lock with the corresponding `attribute_key`. ### Lock compatibility { #locks_compatibility} Some lock combinations can be acquired concurrently. The formal rules are as follows: - A `snapshot` lock can always be acquired. If this transaction has already acquired a `snapshot` lock, an attempt to acquire the lock again is completed without errors and has no effect. - A `shared` or `exclusive` lock cannot be acquired if the transaction or any of its ancestors has already acquired a `snapshot` lock. - A `shared` or `exclusive` lock cannot be acquired if another transaction that is not an ancestor of the given one has already acquired an `exclusive` lock. - An `exclusive` lock cannot be acquired if another transaction that is not an ancestor of the given one has already acquired a `shared` lock. - A `shared` lock with the specified `child_key` cannot be acquired if another transaction that is not an ancestor of the given one has already acquired a `shared` lock with the same `child_key`. - A `shared` lock with the specified `attribute_key` cannot be acquired if another transaction that is not an ancestor of the given one has already acquired a `shared` lock with the same `attribute_key`. - A `shared` lock without `child_key` and `attribute_key` can be acquired despite any other `shared` locks. ### Lock operations { #lock_operations} There are two commands to work with locks: `lock` and `unlock`. The `lock` command enables you to acquire a lock on a Cypress node in a specified transaction. The `unlock` command does the opposite: it removes all explicit locks from the node for a given transaction, both those already acquired and those still in the [lock queue](https://ytsaurus.tech/docs/en/#locking_queue). The lock can only be removed if the locked branched version of the node contains no changes compared to the original. Consequently, an explicit `snapshot` lock can always be removed. Otherwise the command will end with an error. Locks are automatically removed at the end of the transaction, whether successful or unsuccessful. Therefore, there is usually no need to remove them manually. We recommend using the `unlock` command only when you need to acquire and release locks without completing the transaction. Such transactions are usually designed to manage the synchronization of third-party services. ### Lock queue { #locking_queue } Each Cypress node can have its own lock queue. By default, the `lock` command tries to acquire a lock and returns an error if the node is already locked. If you specify the `waitable` parameter equal to `true` in the `lock` command, the lock will be queued. To find out whether a lock is in the queue, request the `state` attribute. If the lock is in the queue, it will be `pending`, if not, it will be `acquired`. {% note warning "Attention" %} For a lock to be actually acquired, for example exclusively, its state must become `acquired`. To acquire a lock: - Call the `lock` command. - Monitor the `state` in the cycle until it takes on the `acquired` value. {% endnote %} ### Lock attributes { #lock_attributes} In addition to the attributes inherent to all objects, locks have the following attributes: | **Attribute** | **Type** | **Description** | | ---------------- | -------- | ------------------------------------------------------------ | | `state` | `string` | Lock state: `pending` or `acquired`. | | `transaction_id` | `Guid` | ID of the transaction that took the lock. | | `mode` | `string` | Locking mode: `shared`, `exclusive`, or `snapshot`. | | `child_key` | `string` | The key on which the lock is acquired. For the `shared` type only. | | `attribute_key` | `string` | The name of the attribute on which the lock is acquired (for the `shared` type only) | ## Versioning { #versioning } Changing the node state by a transaction is the following three-phase process: 1. Transaction `T` acquires the lock on node `N`. Version `N:T` appears for node `N` and it is formed as follows: - For a `snapshot` lock, it branches off from version `N:T'` where `T'` is the closest ancestor of `T` that branched off `N`. - For `shared` and `exclusive` locks, `N:T''` branches off from `N:T'` where `T''` is the child of `T'` ; `N:T'''` branches off from `N:T''` where `T'''` is the child of `T''` and so on up to `N:T`. In other words, a chain of branched node versions is created for each transaction from `T'` to `T`. If there is no such `T'`, the real version of `N` is used. It does not matter for this description whether the lock was acquired explicitly or implicitly. 2. Transaction `T` works with node `N`, with the actual changes accumulated in its version `N:T`. 3. Transaction `T` completes successfully and the changes it made to `N:T`, if any, get merged into the version from which version `N:T` was branched. Thus, these changes become visible to the parent transaction or to all if transaction `T` was a topmost transaction. ## Using transactions in operations When starting operations, the scheduler creates a set of transactions to provide some atomicity of data processing in the operation. To learn more about how this works, see [Transactions in data processing](https://ytsaurus.tech/docs/data-processing/operations/overview.md#transactions). # Chunks This section contains information about chunks — parts of a table or file that store data. ## General information { #common } Data stored in [tables](https://ytsaurus.tech/docs/en/static-tables.md), [files](https://ytsaurus.tech/docs/en/files.md), and logs is split into parts that are called chunks. A chunk stores a continuous range of data. Specifically, each chunk of the table contains a range of its rows. A chunk is an unmodifiable entity: once a chunk is created, the data in it cannot be changed. The chunk data is physically stored on the cluster nodes. YTsaurus master servers monitor the location of the chunks and keep them in [replicated](https://ytsaurus.tech/docs/en/replication.md) state. YTsaurus master servers store only metadata. However, metadata has a significant size, so in order to limit the memory consumption by the master servers, the number of chunks available to the account is [quoted](https://ytsaurus.tech/docs/en/quotas.md). ### Chunk size { #chunk-size } Chunks are not monolithic, they consist of blocks so that you can read and write chunks in parts. By default, static table blocks are 16 megabytes and dynamic table blocks are 256 kilobytes. You can change the block size in the [`table_writer`](https://ytsaurus.tech/docs/en/io-configuration.md) settings, the `block_size` attribute. A chunk usually occupies hundreds of megabytes or several gigabytes on a cluster node. The size of the chunks to be created can be configured via the [`table_writer`](https://ytsaurus.tech/docs/en/io-configuration.md) section, the `desired_chunk_size` attribute. We do not recommend making chunks too small. This leads to an increase in their number, which consumes the master server memory. Small chunks also slow down reading: the number of requests to the master and the disk that need to be made to read all the data increases. ### Reading data { #reading } When the client calls the procedure for reading data from a static table, the following steps are performed on the proxy server: 1. Getting a list of chunks that make up the table from the master server. 2. Getting a list of cluster nodes where chunk replicas are located from the master server. 3. Downloading data from cluster nodes and transmitting it to the client. ### Writing data { #writing } Writing is performed in a similar way: 1. Splitting the input data stream into parts. 2. Designing each part in the form of a chunk. 3. Transferring data to cluster nodes and binding it to a table. ## Chunk format { #optimize_for } There are two chunk formats in YTsaurus: row-by-row and column-by-column. The chunk format is determined by the `optimize_for` parameter. For row-by-row format, `optimize_for` has the `lookup` value, for column-by-column format — the `scan` value. ### Row-by-row format: optimize_for=lookup { #lines } In row-by-row format, each row is stored entirely in one block. The row-by-row format is suitable for [dynamic tables](https://ytsaurus.tech/docs/dynamic-tables/overview.md) from which data is selected by key. ### Column-by-column format: optimize_for=scan { #columns } The column-by-column format does not store information about the type next to each value and uses lightweight column compression techniques. By default, the data of each column specified in the schema gets into a separate sequence of blocks within a chunk. This speeds up the reading of a small number of columns. If you need to read one row or a small range of rows by key, you have to make as many disk accesses as there are columns in the table. To minimize disk accesses, you can use the `group` attribute in the description of columns in the schema, which will allow you to store related data in one block. For more information, see the [Data schema](https://ytsaurus.tech/docs/en/static-schema.md) section. {% note info "Note" %} Using a column-by-column format without a schema makes no sense. {% endnote %} ### Example of specifying a format { #format-example } CLI ```bash yt set //tmp/table/@optimize_for scan ``` Setting the attribute affects only future chunks and does not convert tables (similar to setting the `@erasure_codec` and `@compression_codec` attributes). To convert already written data into a new format, perform the Merge operation: CLI ```bash yt merge --mode ordered --src //tmp/table --dst //tmp/table --spec '{force_transform = %true}' ``` For **dynamic tables**, this is solved by forced compaction. Before using it, make sure to read the article on [Background compaction](https://ytsaurus.tech/docs/dynamic-tables/compaction.md#forced_compaction). Example of running the operation via the CLI: ```bash yt set //tmp/table/@forced_compaction_revision 1 ``` In addition, for columnar **dynamic tables**, note the `group` attribute in the column schema. If your use case involves frequent lookups of table rows without column filtering, it is a good idea to set the same group for all columns. For more information about groups, see the documentation page on [table schemas](https://ytsaurus.tech/docs/en/static-schema.md#schema_overview). ## Chunk owners { #attributes } Cypress nodes consisting of chunks — tables, files, and logs — are called chunk owners. A set of chunks belonging to the chunk owners is organized as a tree-like data structure. The leaves in this structure are chunks, and the intermediate nodes are chunk lists (`chunk_list`). Besides the attributes inherent to all Cypress nodes, chunk owners have the following additional attributes: | **Name** | **Type** | **Description** | **Mandatory** | | ------------------------- | ----------------------- | ------------------------------------------------------------ | ---------------- | | `chunk_list_id` | `Guid` | Root chunk list ID. | Yes | | `chunk_ids` | `array` | List of IDs of all chunks. | | `chunk_count` | `int` | Number of chunks. | | `compression_statistics` | `CompressionStatistics` | Statistics on data sizes and the types of [compression codecs](https://ytsaurus.tech/docs/en/compression.md) used. | Yes | | `erasure_statistics` | `ErasureStatistics` | Statistics on the types of used [erasure codecs](https://ytsaurus.tech/docs/en/replication.md) and data sizes. | Yes | | `optimize_for_statistics` | `OptimizeForStatistics` | Statistics on the types of used [chunk types](https://ytsaurus.tech/docs/en/#optimize_for) (`optimize_for`). | Yes | | `multicell_statistics` | `MulticellStatistics` | Statistics on the distribution of chunks on master servers. | Yes | | `uncompressed_data_size` | `int` | Total uncompressed data volume of all chunks (not including replication and erasure coding). | Yes | | `compressed_data_size` | `int` | Total compressed data volume of all chunks (not including replication and erasure coding). | Yes | | `compression_ratio` | `double` | Compression ratio, the ratio of compressed volume to uncompressed volume. | Yes | | `update_mode` | `string` | The way to change data under a transaction: `none`, `append`, or `overwrite`. For nodes outside a transaction, it is `none`. | Yes | | `replication_factor` | `integer` | [Replication](https://ytsaurus.tech/docs/en/replication.md) factor (equals 1 for erasure coding). | Yes | | `compression_codec` | `string` | [Compression codec](https://ytsaurus.tech/docs/en/compression.md) name. | Yes | | `erasure_codec` | `string` | [Erasure codec](https://ytsaurus.tech/docs/en/replication.md) name. | Yes | | `vital` | `bool` | Whether the chunks of this node are [vital](https://ytsaurus.tech/docs/en/#vitality) | No | | `media` | `Media` | Replication factors and other [medium](https://ytsaurus.tech/docs/en/media.md) settings. | Yes | | `primary_medium` | `string` | [Primary medium](https://ytsaurus.tech/docs/en/media.md#primary). | Yes | ## Data vitality { #vitality } The YTsaurus system divides chunks into two types: vital and non-vital. By default, data in the system is vital (the `vital` attribute is `true`) and its loss is a serious incident. Data loss can occur due to failure of cluster nodes. Non-vital data is data whose loss is not critical. YTsaurus cluster settings are such that inaccessibility of non-vital chunks does not trigger monitorings and exploitation intervention. Typical examples of non-vital data are intermediate results of operations: the scheduler will notice losses and perform recomputation. For non-vital data, the `vital` attribute is `false`. {% note info "Note" %} The system does not consider chunks written without erasure coding and having a single replication factor to be vital. {% endnote %} Data may be lost when cluster nodes fail. To avoid this, three copies of data are stored in YTsaurus by default. However, if multiple hosts fail, the system may lose access to all copies. If computations were interrupted when vital data was lost, restart them. # General information This section contains information about the system of access control to tables and other [Cypress](https://ytsaurus.tech/docs/en/cypress.md) nodes (users, groups, accounts, chunks, transactions, and other). Any user request passes through proxies that [**authenticate**](https://en.wikipedia.org/wiki/Authentication) the user, that is, verify their authenticity. Based on the token contained in the request and obtained using the [OAuth](https://en.wikipedia.org/wiki/OAuth) protocol, the proxy determines the name of the user who initiated the request. If no token is specified in the request, it is assumed that the request was made by the `guest` user. After authentication, the token is not used at the subsequent user request processing stages. For more information, see [Authentication](https://ytsaurus.tech/docs/en/auth.md). **Authorization** (granting permissions) is performed by the Cypress master server. The decision to grant or deny access depends on: 1. The access type (read, write, etc.). 2. The user who initiated the request. 3. The object to which access is requested. If access is denied, a message with details is generated: the name of the user to whom access is denied, the object to which access was requested, and the access type. If the proxy sends a request marked with the name of a non-existent user, the master server returns the error `No such user`. This can happen because obtaining a token and registering a user in the system are two separate actions performed by different services (OAuth provides a token, while the YTsaurus master server handles registration). The YTsaurus system supports lists of **users** and **groups**. The common name for users and groups is **subjects**. Group members can be random subjects: both users and other groups. The system guarantees acyclic membership relation. In the process of authorization, decisions are made on the basis of a transitive closure. Thus, user `A` can be a member of group `C` either directly (being a member of group `С`) or indirectly (`A` being a member of group `B` and group `B` being a member of group `C`). Each object in the YTsaurus system has an **Access Control List** (**ACL**). This list is stored in the `@acl` attribute of the object and consists of individual entries (**Access Control Entry** or **ACE**) where each entry contains a list of subjects, access type, and a number of other parameters listed in the table in the [Authorization](https://ytsaurus.tech/docs/en/access-control.md#authorization) section. For more information, see [How to use the ACL correctly](https://ytsaurus.tech/docs/en/#acl_usage). ## Users, groups { #users_groups } Cypress stores: * The list of all registered users. It is located at `//sys/users` and constitutes a `user_map` node. * The list of all registered groups. It is located at `//sys/groups` and constitutes a `group_map` node. {% note warning "Attention" %} Each user and group is defined by a unique name. Since these entities share the same namespace, duplicate names are not allowed. {% endnote %} #### How to view lists of users and groups {% list tabs %} - In the web interface - To view a list of users, go to **Users** in the side panel or type `https:////users` in the browser address bar - To view a list of groups, go to **Groups** or type `https:////groups` in the browser address bar - Via the CLI - Get a list of users: ```bash yt --proxy list //sys/users ``` - Get a list of groups: ```bash yt --proxy list //sys/groups ``` {% endlist %} {% note warning %} The `//sys/users` and `//sys/groups` nodes are not tables, so you cannot get lists of users and groups using a `SELECT` query. {% endnote %} Users have the `user` system object type. Groups have the `group` object type. #### How to get information about a user or group {% list tabs %} - In the web interface Go to the user or group page and click ![](https://ytsaurus.tech/_images/attrs-icon.png%20=20x20) on the right. - Via the CLI To get information about a particular user, use the command `yt --proxy get //sys/users//@`. For example: ``` $ yt --proxy get //sys/users/john/@type "user" ``` To see a list of all available attributes for a user, use the command: ```bash $ yt --proxy get //sys/users/john/@ { "id" = "ffffffff-fffffffe-101f5-1407420e"; "type" = "user"; "builtin" = %true; ... } ``` Similarly, to get information about a particular group, use the command `yt --proxy get //sys/groups//@`. For example: ```bash $ yt --proxy get //sys/groups/admins/@type "group" ``` {% endlist %} ## System subjects { #system_subjects } In YTsaurus, there is a set of subjects that perform system functions. You cannot delete such subjects. System functions include: 1. The `guest`, `root`, `scheduler`, and `job` users. 2. The `everyone`, `users`, and `superusers` groups. ## Subject attributes { #subject_attributes } The table below shows a list of attributes that are inherent to all subjects. | **Attribute** | **Type** | **Description** | | ------------------- | --------------- | ------------------------------------------------------------ | | `name` | `string` | Subject name (non-empty string) | | `member_of` | `array` | A list of names of groups to which the subject directly belongs | | `member_of_closure` | `array` | A list of names of groups to which the subject belongs (directly or indirectly) | | `aliases` | `array` | A list of names that can be used in the ACL as a reference to the subject | ## User attributes { #user_attributes } In addition to the inherent attributes that are shared by all subjects, users have the attributes listed in the table below. | **Attribute** | **Type** | **Description** | **Mandatory** | | -------------------------- | --------------- | ------------------------------------------------------------ | ------------------- | | `banned` | `bool` | Whether the user is blocked | No | | `access_time` | `DateTime` | The time of the last request from the user. | Yes | | `access_counter` | `integer` | The total number of requests made by the user. | Yes | | `request_rate` | `double` | The number of requests per second from the user. | Yes | | `request_rate_limit` | `double` | The limit on the number of requests per second from the user. The default value is 100. | Yes | | `request_queue_size_limit` | `double` | The request queue size. The default value is 100. | Yes | | `usable_accounts` | `array` | A list of accounts that the user is allowed to use. | Yes | ## Group attributes { #group_attributes } In addition to the inherent attributes that are shared by all subjects, groups have the attributes listed in the table below. | **Attribute** | **Type** | **Description** | | ----------- | --------------- | --------------------------------------------------------- | | `members` | `array` | A list of names of group members (users and other groups). | ## Managing groups { #group_control } {% note info "Note" %} On large clusters, only YTsaurus administrators can directly manage groups. {% endnote %} To create a new group, use the `create` command. When creating an object, you do not need to specify its path, but you need to specify the `name` attribute. ```bash yt create group --attributes '{name=my_group}' ``` You can remove objects using the `remove` command. When a group is removed, it is automatically removed from all ACLs where it was present. ```bash yt remove //sys/groups/my_group ``` ## Authorization { #authorization } The authorization module on the master server solves the following problem: should user `U` be granted access of type `P` for object `O`? There are two possible answers: to grant or not to grant. Let us review all three components (U, P, and O) separately. Any system user can act as `U`. Note that `U` cannot be a group, although group membership is taken into account when making a decision. If `U` is `root`, the access request is automatically approved. Access type `P` is also called **permission**. The permissions supported by YTsaurus are shown in the table. The **Applies to** column lists the object types for which the permission is meaningful; for other object types the permission is ignored. | Permission | Applies to | Description | | ------------------------- | -------------------------------------------- | ------------------------------------------------------------ | | `read` | All objects | Reading a value or getting information about an object and its attributes. See [details](https://ytsaurus.tech/docs/en/#permission_read). | | `full_read` | Tables (behaves as `read` for other objects) | Reading the whole table with no columnar or row-level restrictions. See [details](https://ytsaurus.tech/docs/en/#permission_full_read). | | `write` | All objects | Changing an object state or its attributes. | | `use` | Accounts, pools, tablet cell bundles | Using an object's resources. See [details](https://ytsaurus.tech/docs/en/#permission_use). | | `administer` | All objects | Changing the object access descriptor (its ACL and related attributes). | | `create` | Object schemas | Creating objects of a corresponding type. | | `remove` | All objects | Removing an object. | | `mount` | Dynamic tables | Mounting, unmounting, remounting, freezing, unfreezing, and resharding a dynamic table. | | `manage` | Operations | Managing an operation and its jobs. See [Managing operations](https://ytsaurus.tech/docs/en/#managing-operations). | | `modify_children` | Composite Cypress nodes, accounts, pools | Adding and removing children of a composite object. See [details](https://ytsaurus.tech/docs/en/#permission_modify_children). | | `register_queue_consumer` | Queues | Registering a consumer for a queue. See [details](https://ytsaurus.tech/docs/en/#permission_register_queue_consumer). | Object `O` means a random system object: Cypress node, user, group, account, chunk, transaction, and so on. To make a decision, the system implicitly builds an **effective access control list** (effective ACL) for object `O`. An effective access control list is a combination of the access control list specified on the node and the access control lists inherited from its parents. Any **access control list** (ACL) is a list of **access control entries** (ACE). The order of entries in this list does not matter. The object can inherit its ACL, which is controlled by the `inherit_acl = %true` attribute. When inheriting, the inheritance mode (the `inheritance_mode` key in the `acl` entry) is defined for each ACE. The latter can be `object_only`, `object_and_descendants`, `descendants_only`, and `immediate_descendants_only`. The `object_only` value means that this entry affects only the object itself. The `object_and_descendants` value means that this entry affects the object and all its descendants, including indirect ones. The `descendants_only` value means that this entry affects only descendants, including indirect ones. The `immediate_descendants_only` value means that this entry affects only direct descendants (sons). Each entry has a structure presented in the table. | **Attribute** | **Type** | **Description** | | ------------------ | ------------------- | ------------------------------------------------------------ | | `action` | `SecurityAction` | Either `allow` (allowing entry) or `deny` (denying entry). | | `subjects` | `array` | A list of names of subjects to which the entry applies. | | `permissions` | `array` | A list of access permissions to which the action specified in the `action` attribute applies. | | `inheritance_mode` | `InheritanceMode` | The inheritance mode of this ACE, by default `object_and_descendants`. | Once an effective list is built, the decision to grant or deny access is made according to the following schema: 1. If there is at least one allowing entry for `U` and `P` and no denying entries for `U` and `P`, access is granted. 2. Otherwise, access is denied. “Entry for `U` and `P`” means that `P` is mentioned in the `permissions` list and either user `U` or at least one group in which user U is a direct or indirect member is mentioned in the `subjects` list. From that description, it follows specifically that if the effective list is empty, access will be denied. ### read { #permission_read } For tables, `read` grants access to the data, but the visible set of columns and rows can be further narrowed by [columnar](https://ytsaurus.tech/docs/en/columnar-acl.md) and [row-level](https://ytsaurus.tech/docs/en/row-level-security.md) ACLs: even with `read` for the whole table, a user must be explicitly allowed to read particular columns. ### full_read { #permission_full_read } Grants unrestricted read access to the entire table, ignoring any columnar or row-level restrictions. For all other object types, `full_read` is equivalent to `read`. It is required for actions that need the complete data set, such as `copy`ing and `move`ing a table. Because this permission implies access to every column, the `columns` attribute cannot be specified in the same ACE. ### use { #permission_use } Grants the ability to consume the object's resources: * for an account, to create new objects in that account's quota; * for a pool, to run operations in that pool; * for a tablet cell bundle, to create a dynamic table in that bundle. ### modify_children { #permission_modify_children } Grants the ability to add and remove children of a composite object. For a Cypress map node, creating or removing a child requires either `write` or `modify_children` on the parent; granting only `modify_children` lets a subject manage the children of a node without granting broader `write` access to the node itself. ### register_queue_consumer { #permission_register_queue_consumer } Grants the ability to [register a consumer for a queue](https://ytsaurus.tech/docs/dynamic-tables/queues.md#register_queue_consumer). An ACE with this permission has two special rules: it must specify vitality via the `vital` attribute (`%true` or `%false`), and it must contain `register_queue_consumer` as its only permission. For example: ```bash { action = allow; subjects = [my_group]; permissions = [register_queue_consumer]; vital = %true; } ``` ## Object owner and owner user { #owner} When user `U` creates an object, user U also becomes the owner of that object, which is shown in the `owner` attribute. {% note info "Note" %} Only the superuser (member of the `superusers` group) can change the owner. {% endnote %} There is also a special fictitious `owner` user in the system. You cannot authenticate with it, but you can refer to it in the ACL as a subject. `owner` is replaced by the actual object owner when permissions are checked. This enables you to do such things as describe the "in this folder, only those who created the nodes can remove them" restriction by the ACE:`{action=allow; permissions=[remove]; subjects=[owner]; inheritance_mode = descendants_only}`. Remember to disable permission inheritance by specifying `inherit_acl = %false`. ## Managing operations { #managing-operations } Any operation has an ACL associated with it, similar to Cypress nodes. You can obtain this ACL from the attribute at `runtime_parameters/acl` of the operation. Access permissions have the following semantics: - The `read` permission is responsible for reading the live preview of the output and intermediate data of the operation, as well as the "artifacts" of its jobs: stderr, fail context, and the input data of individual jobs. - The `manage` permission is responsible for managing actions that change the state of the operation, including `Abort`, `Complete`, `Suspend`, and `Resume` (the corresponding buttons are located in the upper-right corner of the operation page in the web interface), as well as for managing actions with jobs, including `Abort`, `Abandon`, and `Send signal` (the corresponding buttons are located in the drop-down menu on the right side of the `Jobs` page in the YTsaurus web interface). - Using [Job Shell](https://ytsaurus.tech/docs/problems/jobshell-and-slowjobs.md) requires `read` and `manage` permissions at the same time , since having access via the console enables you to read or randomly change the job state. You can specify the ACL for an operation when it starts. To do this, in the operation specification, indicate an `"acl"` section as follows: ```python yt.run_map(..., spec={"acl": [{ "action": "allow", "subjects": ["u1", "u2"], "permissions": ["read", "manage"], }]}) ``` The user who started the operation and the default administrators are added to the resulting ACL of the operation. ## How to use the ACL correctly { #acl_usage} The access management schema presented above is very general and flexible. When using it, try to assign roles at the highest level with inheritance once, rather than manage access at the level of individual tables. ACL inheritance encourages grouping data with common access rules into folders. In normal mode, you should have only allowing entries. Denying entries can be set to solve the problem quickly if it turns out that access is overextended. Then, the ACL should be revised, and the problem should be solved by adjusting the allowing entries. We do not recommend mentioning specific users in the ACL. When access is granted, we recommend using groups with the same type of needs instead of individual users. ACL inheritance should be used everywhere, except in places where the nature of access changes radically. For example, on the Cypress root, the default entry is set to allow all users (except `guest`) to read. When overriding access to parts of Cypress that store security-sensitive information, such as token lists, the access definitions must be redefined from scratch. ## Checking the ACL { #check_acl} To check if a user has a certain permission to a certain Cypress node, use the `check-permission` command. Example: ```bash yt check-permission ``` Example of command launch: ```bash $ yt check-permission pavel-kulenov write //tmp { "action" = "allow"; "object_id" = "1-3-411012f-1888ce1f"; "object_name" = "node //tmp"; "subject_id" = "c4-8aaa-41101f6-bec6113b"; "subject_name" = "YTsaurus"; } ``` ## Requesting access { #request_access } To gain access to an existing account or directory in YTsaurus, contact your system administrator. # Managing access to table columns This section describes how ACLs on columns work and gives examples of their use. ## Brief description { #brief_desc } ACLs on columns enable you to specify access rules for individual table columns. This enables you to store both public and private data in the same table. Processes and users who have access will be able to read all columns in the table, including private ones, while other users will only be able to read public columns. ## Operation principle { #details } The general operation principles of the access control system in YTsaurus are described in the [Access control](https://ytsaurus.tech/docs/en/access-control.md) section. To set an ACL on columns, use ACEs of a special type where the additional `columns` attribute is specified in the ACLs of the nodes, both tables and directories. Such ACEs (**columnar**) do not take part in ordinary checks of permissions for objects in any way. Such ACEs are taken into account at the stage of checking access to individual table columns. Suppose that the system is checking read access permissions for table `T` on behalf of user `U` and this user wants to read columns from set `Cs`. Such reads occur when the `read-table` command is executed or when table `T` is used as input to a MapReduce operation or others. This feature is only available for [schematic data.](https://ytsaurus.tech/docs/en/static-schema.md). In particular, we assume that `Cs` is a subset of all columns explicitly specified in the schema of table `T`. Set `Cs` is usually specified by the user when setting a column filter, for example, via [YPath](https://ytsaurus.tech/docs/en/ypath.md). When no filter is specified, `Cs` are considered a set of all columns described in the table schema. If the table schema is not strict, additional columns can be found in the data, but no special checks are made for them. In addition to having a `read` permission for the entire table, the user must also have a `read` permission for each column of set `Cs`. The latter property is checked as follows: let's consider the name of column `C` from `Cs`. Let's build an effective ACL of table `T`. From the ACEs that make it up, select those that are columnar and applicable to `C` (contain `C` in the `columns` attribute). Label the result `L`. If `L` is empty, this means that no special restrictions apply to column `C` and checking access to this column is considered successful. Otherwise, if at least one suitable ACE is found, the following logic applies. From the previously built list `L`, select those entries that concern the read operation (contain `read` in `permissions`) and relate to the current user `U` (`U` is either explicitly mentioned in `subjects` or belongs to one of the groups mentioned in `subjects`). Denote the new list by `L'`. If `L'` has at least one allowing entry and no denying entires, then the check is successful. Otherwise, access will be denied. ## Compatibility mode { #compatibility_mode } Sometimes, even though they only need a small part of the columns, users order reading of the entire table (that is, no column filter is specified). If you set a columnar ACL on a table in a scenario like this, users who do not have access to the protected columns will receive an authorization error. For such cases, there is a special `omit_inaccessible_columns` flag (the default value is `%false`) which can be specified for read operations. In the CLI, use `--omit-inaccessible-columns` with the `read` or `read-table` command. A setting with the same name is also available in the operation specification. When this option is enabled, if you cannot get read access to any table column (if there is read access to the table as a whole), the system hides it completely from the user and the requested action is completed successfully. In the response to the read request (in so called `output parameters`), a list of columns that were skipped during reading due to lack of permissions is then passed. In case of an operation, such information is displayed in a warning from the scheduler. This feature is actively used in the YTsaurus web interface to enable you to view data in columns to which the user has access and warn that some columns are hidden due to lack of access. ## Example { #example } Suppose an ACL with the following ACE is set on the table: ```bash { action = allow; subjects = [username]; permissions = [read]; columns = [money]; } ``` Let's also assume that there are no more columnar ACEs all the way up to the root from this table. The following is then performed: 1. Any user who has `read` access to this table will be able to read all columns, except `money`. 2. If a user other than `username` attempts to read the `money` column, an authorization error will occur. This will happen both when you explicitly specify this column name in the filter and when you attempt to read without any filter. 3. User `username` will be able to read the `money` column (provided that this user also has `read` access to the entire table). ## Notes { #notes } {% note warning "Attention" %} Setting an ACE with a column read permission causes all users not specified in the ACE to automatically lose the read permission for the column. Therefore, before restricting access to the column, make sure that doing so will not break the processes working with the table. {% endnote %} 1. In the current implementation, the feature applies only to column reading. Therefore, do not specify permissions other than `read` in the `permissions` attribute. 2. If the table has no schema or the schema is empty and weak, you cannot restrict access to its individual columns. You cannot restrict access to columns that are not specified in the schema if the schema is not strict, even if any restrictions for them are described in the ACL. Do not forget to schematize your data and describe the protected columns explicitly in the schema. 3. Columnar ACEs must be specified not only on the tables, but also on the directories. ACE data is inherited in a standard way and its inheritance is broken specifically in those nodes where `inherit_acl = %false` is set. 4. Only YTsaurus administrators can manage columnar ACEs. # Managing row-level access to tables This section describes how Row-Level Security (RLS) works and provides examples of its use. ## Brief description { #brief_desc } Row-Level Security (RLS) enables you to specify access rules for individual table rows based on predicates evaluated against row data. This enables you to store both public and private data in the same table and define access through arbitrary expressions in the YTsaurus [Query language](https://ytsaurus.tech/docs/dynamic-tables/dyn-query-language.md). ## Operation principle { #details } The general operating principles of the access control system in YTsaurus are described in the [Access control](https://ytsaurus.tech/docs/en/access-control.md) section. To set up RLS, use ACEs of a special type where the additional `row_access_predicate` attribute is specified in the ACLs of nodes. Such ACEs do not take part in ordinary checks of permissions for objects in any way. These ACEs are taken into account at the stage of checking access to individual table rows. The `row_access_predicate` expression must evaluate to a boolean value. The expression can reference any columns from the table schema and use standard operators and functions. Suppose that the system is checking read access permissions for table `T` on behalf of user `U`. Such reads occur when the `read-table` command is executed or when table `T` is used as input to in operations. In addition to having a `read` permission for the entire table, the user must also satisfy at least one row access predicate for each row they want to read. The latter property is checked as follows: we build an effective ACL of table `T`. From the ACEs that make it up, select those that are row-level (contain `row_access_predicate` attribute) and applicable to user `U` (`U` is either explicitly mentioned in `subjects` or belongs to one of the groups mentioned in `subjects`). Call the result `L`. If `L` is empty, this means that no row-level allowances apply to table `T` for user `U`, and the user will not be able to read any rows (unless they have `full_read` permission). Otherwise, if at least one suitable ACE is found, for each row in the table, at least one predicate from `L` must evaluate to `true` for the user to be able to read that row. ## Compatibility mode { #compatibility_mode } If you set a row-level ACL on a table, users who do not have the `full_read` permission will receive an authorization error when attempting to read the table. To ensure that users explicitly understand they may not receive all data, they must specify the `omit_inaccessible_rows` flag (the default value is `%false`) for read operations. In the CLI, use `--omit-inaccessible-rows` with the `read` or `read-table` command. A setting with the same name is also available in the operation specification. When this option is enabled, if the user does not have access to a table row (if there is read access to the table as a whole), the system hides that row completely from the user and the requested action is completed successfully. ## Limitations and special features RLS is not supported for dynamic tables. Any read attempt will return an error. Due to the dynamic nature of access rules, the system cannot always perform certain actions efficiently. For example, the amount of data read from disk is not always proportional to the number of accessible rows. But the dynamic nature affects not only speed, but also some APIs: 1. `row_index` specified in `ranges` in the `read-table` command counts indices relative to rows written to disk, not rows accessible to the user. For example, if you request the first 100 rows (`read-table //path/to/table[:#100]`) while not having access to any of these rows, an empty result will be returned. 2. Similarly, in operations over tables that the user cannot read completely, `row_index` cannot be specified. The expression specified in `row_access_predicate` must be an expression that depends solely on the values in the row. You cannot use `JOIN`, `GROUP BY`, current user in it. You also cannot use QL UDFs. ### Information leakage through metadata When evaluating the predicate, restrictions on access to columns (see [Managing access to table columns](https://ytsaurus.tech/docs/en/columnar-acl.md)) are ignored. Since access is managed by the data owner, this is not a vulnerability per se, but may lead to a data leakage based on various metadata, so you need to be careful when designing predicates. Moreover, even if there are no column restrictions, metadata can still provide information that may be sensitive. Consider a toy example: Suppose a table contains two rows and has the following ACE: ```yson { action = allow; subjects = [vasya]; permissions = [read]; row_access_predicate = "region != 'RU' or income < 1000"; } ``` Vasya reads the table and sees only one row. From this, it can be concluded that for the remaining row the predicate does not hold, which means that `region = 'RU' and income >= 1000` holds for it. Whether this information is sensitive and whether it is even possible (the example is intentionally toy, in real conditions the table will likely contain more rows) is at the discretion of the data owner. ## Example { #example } Suppose an ACL with the following ACE is set on the table: ```yson { action = allow; subjects = [username]; permissions = [read]; row_access_predicate = "user_id = 12345"; } ``` Assume that there are no more row-level ACEs all the way up to the root from this table. The following then holds: 1. Any user who has `read` access to this table will not be able to read any rows by default. However, such a user will be able to read the table attributes. 2. User `username` will be able to read only those rows where the `user_id` column equals `12345` (provided that this user also has `read` access to the entire table). 3. If a user attempts to read rows without the `omit_inaccessible_rows` flag, an authorization error will occur. ## Full read permission { #full_read } The `full_read` permission allows reading the entire table regardless of other ACEs with `row_access_predicate`. This permission is necessary for some actions that either work at the control plane level or do not interpret (and therefore do not unpack data blocks) rows. Such actions include `copy`, `concatenate`, `move` commands, as well as the `RemoteCopy` operation. Note: A trivial predicate (an expression that always returns `true`) is not equivalent to `full_read`. Since in the general case checking access to all rows requires reading all rows, YTsaurus makes no attempts to check whether the predicate is trivial. Symmetrically, with an always-false predicate, the system will be forced to read all rows of the table and check for each one whether the expression still evaluates to false. ## Notes { #notes } {% note warning "Attention" %} Setting a row-level ACE restricts access to rows for all users not matching the predicate. Therefore, before restricting access at the row level, make sure that doing so will not break the processes working with the table. {% endnote %} 1. In the current implementation, the feature applies only to row reading. Therefore, do not specify permissions other than `read` in the `permissions` attribute. 2. You can only use columns specified in schema in the predicate. 3. Row-level ACEs are inherited in a standard way and their inheritance is broken specifically in those nodes where `inherit_acl = %false` is set. 4. If the effective ACL of a table contains an ACE in which the expression is invalid (for example, refers to a non-existent column, the return type differs from boolean, or it adds a number and a string), any read operation will return an error. To avoid this, it is recommended to set such ACEs only in those directories in which all tables have a homogeneous schema. 5. To test a predicate, you can use the Merge operation with `input_query`, placing the expression after the `WHERE` keyword. Both mechanisms use the same syntax for expressions, so the result of the operation will match which rows the user affected by the corresponding ACE will see. 6. Only YTsaurus administrators can manage row-level ACEs. # Quotas This section describes resource quotas in the YTsaurus system. ## Resource types { #resources } Most YTsaurus clusters are shared. Cluster resources are shared out to all projects that require data processing and storage. These quotas are displayed but not enabled. The following types of resource quotas exist: * Storage: * HDD (SATA) disk quota not accounting for compression or replication. * SATA SSD/NVMe SSD disk quota not accounting for compression or replication. * Quota on the number of nodes in the [metadata tree](https://ytsaurus.tech/docs/en/cypress.md) (tables, files, locks). Not booked separately in the annual planning process but provided upon request and approval. There is a limit on the number of tree nodes that applies to an entire cluster. Branched nodes actually consume the quota of the account in which they were created, but since this is not always correct, only trunk nodes are taken into account in the final resource quota. * Quota on chunks that all the stored data are split into. Issued in proportion to disk quotas based on 512 MB of space per chunk for static tables and 64 MB per chunk for dynamic tables plus a margin of 200,000 chunks or as may be agreed on a per-project basis. * Quota on the master server RAM used by the nodes in the metadata tree. Not requested as a separate resource but rather issued subject to agreement. Takes into account chunk and trunk node metadata, trunk node attributes, [dynamic table](https://ytsaurus.tech/docs/dynamic-tables/overview.md) pivot keys as well as other data. Rule-of-thumb estimate: 100 to 200 bytes per node and the same amount per chunk. The above values may vary greatly depending on attribute and node name lengths, the lengths of the values in chunk key columns, and so on. * Quota on the number of dynamic table [tablets (shards)](https://ytsaurus.tech/docs/dynamic-tables/overview.md#tablets). Not requested separately but issued upon request. The quota is allocated for the [tablet_cell_bundle](https://ytsaurus.tech/docs/dynamic-tables/concepts.md#tablet_cell_bundles) object. * Processing: * Guaranteed number of HyperThreading cores for MapReduce operations. * Guaranteed amount of RAM for MapReduce operations. * Limit on the number of running MapReduce operations and the total number of operations, including those queued to run. * Dedicated computational resources for querying [dynamic tables](https://ytsaurus.tech/docs/dynamic-tables/overview.md) (write/select/lookup): CPU, RAM. This includes the quota on the amount of data in dynamic tables in memory (tablet_static_memory). Quotas are allocated for the [tablet_cell_bundle](https://ytsaurus.tech/docs/dynamic-tables/concepts.md#tablet_cell_bundles) object. ## Data storage { #account_quotas } Most types of data storage quotas are linked to a project **account**. An account is a service object in Cypress. Each Cypress node has a readable/writable `account` attribute storing the name of the account the node in question is linked to. In the case of files and tables, assigning an account causes its disk quota to be consumed. This takes into account the space physically allocated on the cluster nodes for data storage, so it depends on the replication degree (the `replication_factor` attribute), erasure coding method (the `erasure_codec` attribute), and the compression utilized (the `compression_codec` attribute). If the user exceeds the account quota for certain resource types, the system starts denying the account further access to the exceeded resource type. This does not mean that an account never exceeds its limits when used. For example, the limits may have been forcibly set below usage. Besides that, disk and chunk quota consumption tracking works asynchronously, which can also result in exceeding allocated limits. For more information, see [Accounts](https://ytsaurus.tech/docs/en/accounts.md). ### Media { #medium } Different media types (HDD, SDD, RAM) are logically combined in special entities referred to as [media](https://ytsaurus.tech/docs/en/media.md). Media types differ by the amount of available space and read/write performance. The higher the performance of a medium, the smaller the medium is. Media divide the entire set of chunk locations into non-intersecting subsets. At the same time, a single cluster node may have locations on different media. Each such subset does its own chunk balancing and handles its own quota. An account's disk quota is issued for a specific medium. A user may select the medium to use to store different types of data. YTsaurus clusters host the following media types: - default: integrates the set of a cluster's HDDs. - ssd_blobs: set of cluster's SSDs. - ssd_journals: set of SSDs for dynamic table log storage. - in_memory: dedicated space allocated in cluster node RAM. There is a special page in the YTsaurus web interface to view account quotas. For more information on media, see the [Media](https://ytsaurus.tech/docs/en/media.md) section. ### Dynamic table resources { #dyntable_resources } Quotas on the number of tablets and the amount of tablet static memory are linked to tablet cell bundles. Bundle quotas display in a separate web interface section. Monitoring settings and viewing account and bundle quota histories are described in the Quota monitoring section. ## Data processing { #data_processing } Data processing computational resources are linked to [pools](https://ytsaurus.tech/docs/data-processing/scheduler/scheduler-and-pools.md) (except tablet_cell_bundle). You can view computational pool settings, including guarantees and operation number limits on the **Scheduling** page (**Details** tab, **CPU** section, `Strong guar` column). If a project uses dynamic tables for production, you need to have a separate [tablet cell bundle](https://ytsaurus.tech/docs/dynamic-tables/concepts.md#tablet_cell_bundles) to isolate from other users. A tablet cell bundle is a group of containers (instances) that processes queries to a project's dynamic tables. {% note info "Note" %} Dynamic tables require a disk quota to write journals (medium ssd_journals) and snapshots (medium default). The required quotas are a quota on file system objects (node_count) and a quota on the number of tablets (table shards, or tablet_count). {% endnote %} # Accounts This section contains information about accounts and related quotas in the YTsaurus system. A description of the account hierarchy, examples of actions with accounts, and a list of account attributes are given. ## General information { #common } All types of data storage quotas are linked to the project **account**. An account is a service object in [Cypress](https://ytsaurus.tech/docs/en/cypress.md). All accounts have unique names. However, they are located in a separate namespace, so the username and account name may be the same. The account name is stored in the `name` attribute which is both readable and writable. All accounts known to the system can be found at `//sys/accounts`. You can create and remove accounts using the `create` and `remove` commands. ## Node attributes { #attributes } Each Cypress node has the readable and writable `account` attribute. This attribute stores the name of the account to which this node is linked. In the case of files and tables, assigning an account causes its disk quota to be consumed. This takes into account the space physically allocated on the cluster nodes for data storage, so it depends on the replication degree (the `replication_factor` attribute), erasure coding method (the `erasure_codec` attribute), and the compression utilized (the `compression_codec` attribute). Different Cypress nodes (files or tables) can refer to the same chunks. In addition, the same node can refer to a chunk multiple times. Such reuse can occur, for example, as a result of the `copy` command for tables and files, as well as the `merge` and `erase` commands for tables. Such chunks are counted in the quota once (with the maximum replication factor among the tables referring to them). Nodes of all other types (other than files and tables) do not require data storage on cluster nodes and therefore do not consume the disk quota of the account. In case of composite nodes, descendants automatically inherit the parent's account when created (unless otherwise specified). Setting the proper value of the `account` attribute on the user's home directory ensures that the tables and files created by the user are assigned to the proper account by default. To find out how many and which resources a given Cypress node consumes, you should use its `resource_usage` attribute. There is also a computed `recursive_resource_usage` attribute that measures resource usage taking descendants into account. These attributes have the `ClusterResources` structure, see below. To assign account `A` to a Cypress node, the user who initiated this change must have the `use` permission for account `A`. It must also have the `write` permission. An account for the node can likewise be specified at the time the node is created. In this case, you must also have the `use` permission. When you copy Cypress parts with the `copy` command or move them with the `move` command, the accounts and the owners in the copied subtree change — they become equal to the account associated with the destination folder, and to the user who is copying them, respectively. To prevent changes to an account, use the `preserve_account` option. If the user exceeds the account quota for certain resource types, the system starts denying the account further access to the exceeded resource type. This does not mean that an account never exceeds its limits when used. For example, the limits may have been forcibly set below usage. Besides that, disk and chunk quota consumption tracking works asynchronously, which can also result in exceeding allocated limits. ## Account hierarchy { #account_tree } Accounts can be nested in other accounts. This enables you to flexibly configure quotas and naturally reflect the organizational structure to which users and projects belong in them. For example, there may be a general organization unit quota and nested project quotas, or a service account and sub-accounts of its constituent groups. The main advantage of representing accounts as a tree is that users are able to manage the quotas assigned to them independently, splitting them into parts and merging them as needed. Further is a description of the hierarchical organization of accounts and the principles of working with this hierarchy — from the command line and the web interface. ### Quotas { #account_tree_algo } The quota given to any non-topmost account cannot exceed the quota of its parent (and, by induction, any ancestor). By default, the sum of sub-account quotas cannot exceed the parent's quota. But such oversubscription can sometimes be convenient, so it can be enabled: ```bash yt set //sys/account_tree/my_account/@allow_children_limit_overcommit %true ``` In this mode, the sum of children's limits can exceed the parent's limit. However, the limit of an individual sub-account must still be less than or equal to the limit of the parent account. The above refers to consumption limits (`limits`), not used resources (`usage`). Resources used in an account are counted in its quota, as well as in the quotas of all its ancestors. Validation works the same way: if an increase in consumption causes the account or any of its ancestors to run out of quota, an error is generated. **Is it possible to borrow resources in non-leaf accounts?** It is technically possible, but it will be more difficult for the user to evaluate where the quota is spent. **Is it true that an account cannot overflow if it does not have the enabled flag that allows overcommit of child accounts?** It is true if and only if resources are not used directly in the account. ### Viewing an account tree { #account_tree_viewing } Despite the hierarchical organization, accounts have globally unique names. This fact greatly simplifies working with them, because it enables you to refer to the account without specifying the full path to it. {% cut "In the YTsaurus web interface" %} The account hierarchy is represented on the **Accounts** page (as well as in the **Dashboard** section). Due to the global uniqueness of names, when editing the path to an account, you only need to specify its name. ![](https://ytsaurus.tech/_images/account_tree_view.png){ .center } {% endcut %} {% cut "In the command line" %} The account tree in Cypress is located at `//sys/account_tree`. The full flat list is available at `//sys/accounts` (account names are globally unique, despite the hierarchical organization). ```bash $ yt list //sys/account_tree/my_account my_subaccount1 my_subaccount2 $ yt list //sys/accounts my_account my_subaccount1 my_subaccount2 ``` {% endcut %} ### Creating an account { #account_tree_creating } The maximum allowable account tree height is 10. You can create nested accounts using the command line or the YTsaurus web interface. {% cut "In the YTsaurus web interface" %} To create a new account, you need to: 1. On the **Accounts** page, click **Create account**. 2. Specify the name of the new account, the name of the parent account into which the created account will be nested, and the users responsible for the new account. If necessary, you can also request the automatic creation of a home directory corresponding to the account. ![](https://ytsaurus.tech/_images/account_tree_create.png){ .center } {% endcut %} {% cut "In the command line" %} To create an account, specify its name in the `name` attribute and the parent name in the `parent_name` attribute. If no parent name is specified, a topmost account will be created. Only YTsaurus cluster administrators have the right to create topmost accounts. ```bash yt create account --attributes='{ name = "my_subaccount3"; parent_name = "my_account" }' ``` {% endcut %} ### Removing an account { #account_tree_removing } Accounts can be moved and renamed, so you should only remove an account when you really do not need it anymore. {% note warning "Attention" %} Before removing an account, make sure there is no data in it. Otherwise, the account will be put into a special pending state instead of being removed until all associated Cypress nodes are removed, after which it will be removed automatically. You can neither use the account in that state, nor return it to its normal state. {% endnote %} {% cut "In the YTsaurus web interface" %} To remove an account, you need to: 1. On the **Accounts** page, find the account in the list and click the pencil icon to edit the account. 2. In the dialog window that opens, go to the **Delete** section, click **Delete**, and confirm the action. {% endcut %} {% cut "In the command line" %} To remove an account, specify the path to it: ```bash yt remove //sys/accounts/my_subaccount3 # или yt remove //sys/account_tree/my_account/my_subaccount3 ``` {% endcut %} ### Moving or renaming an account { #account_tree_moving } {% cut "In the YTsaurus web interface" %} To move an account you need to: 1. On the **Accounts** page, find the account in the list and click the pencil icon to edit the account. 2. In the dialog window that opens, in the **General** section, in the **Parent** drop-down list, select where to move the account. {% endcut %} {% cut "In the command line" %} There are two ways to move an account: 1. Moving an account using the `parent_name` attribute. ```bash $ yt set //sys/account_tree/my_account/my_subaccount3/@parent_name my_subaccount2 $ yt exists //sys/account_tree/my_account/my_subaccount3 %false $ yt exists //sys/account_tree/my_account/my_subaccount2/my_subaccount3 %true ``` Due to the global uniqueness of account names, you only need to specify the name of the parent, not the full path to it. 2. Moving an account using the `move` command. ```bash yt move //sys/account_tree/my_account/my_subaccount3 //sys/account_tree/my_account/my_subaccount2/my_subaccount3 ``` Note that the full path to the account, including its name, is specified as the destination path. In particular, this enables you to rename an account at the same time as you move it. {% endcut %} ### Transferring resources from one account to another { #account_tree_transferring_resources } {% cut "In the YTsaurus web interface" %} To transfer resources, you need to: 1. On the **Accounts** page, find the account to which you want to transfer resources and click the pencil icon to edit the account. 2. In the dialog window that opens, open the section corresponding to the type of resource that you want to transfer. 3. Specify the account from which to reallocate resources (the parent account is suggested by default) and the new limit. 4. Click **Save**. ![](https://ytsaurus.tech/_images/account_tree_transfer_resources.png){ .center } {% endcut %} {% cut "In the command line" %} Transferring account resources using the `transfer-account-resources` command: ```bash # yt transfer-account-resources --resource-delta yt transfer-account-resources my_subaccount1 my_subaccount2 --resource-delta '{node_count=10}' ``` The `resource-delta` argument is a YSON structure of the `ClusterResources` type (see below). For example: `{node_count=5;disk_space_per_medium={default=1024}}`. You need to specify the amount of resources to be transferred, not the new limits. Missing quotas can be skipped. In case of the `argument command: invalid choice` error, update the `yt` utility. For more information, see [Try YTsaurus](https://ytsaurus.tech/overview/try-yt.md). {% endcut %} ## Built-in accounts { #builtin_accounts } These accounts appear in the system at the moment of initialization. They cannot be removed. 1. The `tmp` account. During initialization, it is assigned to the `//tmp` folder and therefore to all its descendants. By default, all authenticated users of the system (the `users` group) have the right to use this account. 2. The `sys` account. During initialization, it is assigned to all other nodes. By default, no explicit permissions to use it are given to anyone. The account is used for the internal needs of the YTsaurus system. ## Account attributes { #account_attributes } | **Attribute** | **Type** | **Description** | | ---------------------------------- | --------------------------------- | ------------------------------------------------------------ | | name | string | Account name (non-empty string) | | parent_name | string | Parent account name | | resource_limits | ClusterResources | Resource limits for the account | | allow_children_limit_overcommit | bool | Whether the sum of sub-account limits can exceed the limit of this account (see above) | | resource_usage | ClusterResources | Resources occupied directly by this account (including uncommitted transactions) | | committed_resource_usage | ClusterResources | Resources occupied directly by this account (without taking into account active transactions) | | recursive_resource_usage | ClusterResources | Resources occupied by this subtree of accounts (including uncommitted transactions) | | recursive_committed_resource_usage | ClusterResources | Resources occupied by this subtree of accounts (without taking into account active transactions) | | violated_resource_limits | ViolatedClusterResources | Resource excess flags for the account | | recursive_violated_resource_limits | RecursiveViolatedClusterResources | For each resource type — the number of accounts that exceeded the limit in this subtree | `ClusterResources` is the following structure: ### The ClusterResources structure | **Attribute** | **Type** | **Description** | | --------------------- | --------------- | ------------------------------------------------------- | | disk_space | integer | Total disk space in bytes | | disk_space_per_medium | string->integer | Disk space in bytes (for each medium) | | node_count | integer | Number of Cypress nodes | | master_memory | integer | Amount of consumed master server RAM | | chunk_count | integer | Number of chunks | | tablet_count | integer | Number of tablets | | tablet_static_memory | integer | Memory volume for dynamic tables loaded into memory | The `disk_space` field is read-only. To change the limits, change the values of the `disk_space_per_medium` dict. `ViolatedClusterResources` is the following structure: ### The ViolatedClusterResources structure | **Attribute** | **Type** | **Description** | | --------------------- | --------------- | ------------------------------------------------------------ | | disk_space | boolean | Whether the disk space limit is exceeded for at least one medium | | disk_space_per_medium | string->boolean | Whether the disk space limit is exceeded (for each medium) | | node_count | boolean | Whether the limit on the number of Cypress nodes is exceeded | | chunk_count | boolean | Whether the limit on the number of chunks is exceeded | | tablet_count | boolean | Whether the limit on the number of tablets is exceeded | | tablet_static_memory | boolean | Whether the limit on the memory volume occupied by dynamic tables loaded into memory is exceeded | `RecursiveViolatedClusterResources` is the following structure: ### The RecursiveViolatedClusterResources structure | **Attribute** | **Type** | **Description** | | --------------------- | --------------- | ------------------------------------------------------------ | | disk_space | integer | The number of accounts in the subtree that have exceeded the disk space limit for at least one medium | | disk_space_per_medium | string->integer | The number of accounts in the subtree that have exceeded the disk space limit (for each medium) | | node_count | integer | The number of accounts in the subtree that have exceeded the limit on the number of Cypress nodes | | chunk_count | integer | The number of accounts in the subtree that have exceeded the limit on the number of chunks | | tablet_count | integer | The number of accounts in the subtree that have exceeded the limit on the number of tablets | | tablet_static_memory | integer | The number of accounts in the subtree that have exceeded the limit on the memory volume occupied by dynamic tables loaded into memory | # Data schema ## General description { #schema_overview } The table schema describes the known columns and their types. The schema can be read from the `@schema` system attribute that each table has. To set the schema, set this attribute when creating a table. To change the schema of an existing table, use the `alter_table` command. For more information, see [Examples of working with a schema](https://ytsaurus.tech/docs/en/#examples). The `TableSchema` type is a list of `ColumnSchema` elements. Each such element is a dict with a set structure: | **Name** | **Type** | **Description** | Mandatory | | ------------ | --------- | ------------------------------------------------------------ | ------------------ | | `name` | `string` | Column name. | Yes | | `type` | `string` | Type of column elements. | Yes, if `type_v3` is not set | | `type_v3` | `any` | Alternative type representation. This field enables you to specify columns with [composite types](https://ytsaurus.tech/docs/en/data-types.md#schema). | Yes, if `type` is not set | | `sort_order` | `string` | The column sort order - either a missing value or the `ascending` value is allowed. If the value is set, the column is key. | No | | `lock` | `string` | Lock name. For non-key columns only. | No | | `expression` | `string` | An expression defining the value of the computed column field. For key columns only. For more information, see [Sharding](https://ytsaurus.tech/docs/dynamic-tables/resharding.md). | No | | `aggregate` | `string` | Aggregate function name. For non-key columns only. | No | | `required` | `boolean` | If set to `%true`, the value in the column must not be `null`. The default value is `required=%false`. A column of the `any` type cannot be `required=%true`. | No | | `group` | `string` | If two columns have the same group name, the data from those columns will get into the same blocks. You can change the `group` attribute at any time using the `alter-table` command, the changes will be applied to the new chunks. | No | Schema properties: - The column name must be a non-empty string. The maximum length is 256 characters. - The column name cannot begin with system prefix `@`. - Column names cannot be repeated within a table. - Key columns must form the schema prefix. - All key columns must be specified in the schema. - Up to 32,000 columns are supported, but we recommend limiting the number of columns to four figures. - A column must always have an explicitly specified type: in the `type` or `type_v3` attribute. - When reading a schema, both attributes are always returned: `type` and `type_v3` regardless of how the schema was created. - Computed columns can only be key. Computed columns can depend only on key non-computed columns. Schema attributes: - `strict` of the `boolean` type. The default value is `true`. If the table is strict, that is, the attribute has the `true` value, you cannot write data to the table column that is not in the schema. Otherwise, you are allowed to write to a missing column which will automatically appear, but only in a new row. - `unique_keys` of the `boolean` type. The default value is `false`. If the attribute has the `true` value and the table is sorted, all keys in the table must be unique. ### Supported column types { #types } YTsaurus supports the column types described in the [Data types](https://ytsaurus.tech/docs/en/data-types.md) section. Among them are both primitive and composite types. Values in the table cells always have the type specified in the schema. You can use one of the following methods to specify a type in the table schema: - Using the `type` and (optional) `required` keys, you can specify only primitive or optional primitive types. - Using the `type_v3` key. The `type` key expects a string. The `type_v3` key expects either a string (for primitive types) or a yson dict. A yson dict always has the `type_name` key that stores the type name. Other keys depend on the specific type and are described on the [Data types](https://ytsaurus.tech/docs/en/data-types.md) page. Standard order relations apply for elementary types other than `any` and `yson`. Order relations also apply for `optional` types where `T` is a primitive comparable type. It is guaranteed that missing values — values of the `null` type — are smaller than any value of any type. Comparing elements of comparable types is supported. The comparison is first made by type and then if the types match, values are compared. The order between different types is not specified. For example, it makes no sense to compare `100` and `3.14`, because the result is not specified. ### Schema examples { #schema_examples } {% cut "Schema of a static table after creation (unless explicitly stated otherwise)" %} ```yson [] ``` {% endcut %} {% cut "Automatically output schema for a table sorted by `key1; key2`." %} ```yson [ { name = "key1"; type = "any"; sort_order = "ascending" type_v3= { type_name=optional; item=yson; }; };{ name = "key2"; type = "any"; sort_order = "ascending" type_v3= { type_name=optional; item=yson; }; } ] ``` {% endcut %} {% cut "Example of a schema for a sorted table." %} ```yson [ { name = "key"; type = "string"; sort_order = "ascending" };{ name = "subkey"; type = "string"; sort_order = "ascending" };{ name = "value"; type = "string" } ] ``` {% endcut %} {% cut "Example of a schema with grouping for columns that are often used together." %} ```yson [ { name = "key"; type = "string"; sort_order = "ascending"; group = "g1" };{ name = "subkey"; type = "string"; sort_order = "ascending"; group = "g1" };{ name = "value"; type = "string"; sort_order = "ascending"; } ] ``` {% endcut %} {% cut "Example of a schema for a table with logs." %} ```yson [ { "name" = "id"; "type" = "int64"; };{ "name" = "class"; "type" = "int64"; };{ "name" = "uid"; "type" = "string"; };{ "name" = "ip"; "type" = "int64"; };{ "name" = "iso_eventtime"; "type" = "string"; };{ "name" = "error"; "type" = "string" };{ "name" = "ip6"; "type" = "string" };{ "name" = "port"; "type" = "int64" };{ "name" = "comment"; "type" = "string" } ] ``` {% endcut %} ## Weak and strong schema { #schema_mode } A table has the `schema_mode` attribute that defines the source of the schema origin. If the schema was not set explicitly by the user, the `@schema_mode` attribute will have the `weak` value. This means that the table schema was created automatically and is only needed to mark up the key columns. This schema automatically changes if the table loses its sorting property or changes the set of key columns. For example, an entry with the `` flag to a sorted table without specifying the `sorted_by` attribute causes the table to lose sorting property, which means that its schema changes — at least the `sort_order` attribute. If the table schema is set explicitly by the user, then `@schema_mode == strong`. The schema of such a table is saved and checked when writing, both in `append` and `overwrite` mode. Implicit transformations with a strong schema are impossible. ```bash # Create a table without a schema $ yt --proxy create table //tmp/table_1 213c-a01da-3fc0191-54a9a802 $ yt --proxy get //tmp/table_1/@schema_mode "weak" $ yt --proxy get //tmp/table_1/@schema < "unique_keys" = %false; "strict" = %false; > [] # Create a table with an explicitly specified schema $ yt --proxy create table //tmp/table_2 --attributes '{schema = [{name = a; type = int64}; {name = b; type = string}]}' 213c-b75ef-3fc0191-1e85fe66 $ yt --proxy get //tmp/table_2/@schema_mode "strong" $ yt --proxy get //tmp/table_2/@schema < "unique_keys" = %false; "strict" = %true; > [ { "name" = "a"; "type" = "int64"; }; { "name" = "b"; "type" = "string"; }; ] ``` ## Strict schematization advantages { #strict_schematization_advantages } - Performance: - If you additionally enable the columnar format of storing [chunks](https://ytsaurus.tech/docs/en/chunks.md) `optimize_for = scan`, data will be stored more compactly and reading a subset of columns will be faster. - Some table readers can work faster for tables with a schema. - Convenience and more options: - YQL is more convenient for working with schematized tables. - In [CHYT](https://ytsaurus.tech/docs/data-processing/chyt/about-chyt.md), you cannot work with unschematized data. ## Inheriting a schema in system operations { #schema_inference } System operations such as Sort, Erase, Merge, and Remote copy can independently output the output table schema. Schema output rules for operations are regulated by the `schema_inference_mode = (auto|from_input|from_output)` specification. Examples of using the options can be found in the [Examples](https://ytsaurus.tech/docs/en/#examples) section. ### Ordered merge, Erase | Option | Description | | ------------- | ------------------------------------------------------------ | | `from_input` | The schemas of all tables must match with an accuracy of the `sort_order` attribute and the column order. Then the output table schema matches the input table schema. If there is one input table, sorting is preserved. If input tables had a strong schema, the output table will have a strong schema as well. | | `from_output` | The output table schema is used, data is validated in relation to this schema. | | `auto` | If the output table has a strong schema, its compatibility by types with the schema of all highly schematized input tables is checked. If the output table has a weak schema, the schema is derived from the input table schemas similarly to the `from_input` option. | ### Unordered merge | Option | Description | | ------------- | ------------------------------------------------------------ | | `from_input` | The number and types of columns must match, then the output table schema matches the input table schema except for sorting. If input tables had a strong schema, the output table will have a strong schema as well. | | `from_output` | The output table schema is used, data is validated in relation to this schema. | | `auto` | If the output table has a strong schema, its compatibility by types with the schema of all highly schematized input tables is checked. If the output table has a weak schema, the schema is derived from the input table schemas (similarly to the `from_input` option). | ### Sorted merge | Option | Description | | ------------- | ------------------------------------------------------------ | | `from_input` | The number and types of columns must match, then the output table schema matches the input table schema, key columns are set according to the `merge_by` parameter of the specification. If input tables had a strong schema, the output table will have a strong schema as well. | | `from_output` | It is checked that key columns of the output table match the `merge_by` key columns from the specification. | | `auto` | If the output table has a strong schema, its compatibility by types with the schema of all highly schematized input tables is checked. It is checked that key columns coincide with the `merge_by` parameter. If the output table has a weak schema, the schema is derived from the input table schemas similarly to the `from_input` option. | ### Sort `Sort` is the only operation that can change a strong table schema. The column order is changed: key columns are moved to the beginning and the `sort_order` attribute is set. This is due to the common scenario of replacing a table with its sorted copy. | Option | Description | | ------------- | ------------------------------------------------------------ | | `from_input` | If all input tables have strong schemas and the schemas match with an accuracy of the `sort_order` attribute and column order, then the output table column types match the types in the input table schemas. The key columns are derived from the operation specification (`sort_by`). If all input tables have a weak schema, the schema will be derived from the key columns. | | `from_output` | The output table schema is saved, the key columns are changed according to the `sort_by` specification parameter. All key columns must be present in the schema. | | `auto` | If the output table has a strong schema, its compatibility by types with the schema of all highly schematized input tables is checked. The key columns change according to the specification. If the output table has a weak schema, the schema is derived from the input table schemas (similarly to the `from_input` option). | ## Changing a schema Changing a schema is an easy operation with metainformation that does not require reading data. To change a schema, use the `alter_table` command. The schema of an empty table can be changed in a random manner. For non-empty tables, there are several allowed schema change scenarios: - Adding a non-key column to a strict schema: `strict = %true`. - Adding a key column to the end of the key in a strict schema: `strict = %true`. - Deleting a column from a non-strict schema: `strict = %false`. - Converting a schema from strict (`strict = %true`) to non-strict (`strict = %false`). Other schema changes are prohibited, because they require data revalidation and such changes can be made through `Merge` or `Map` operations. There are a number of restrictions on changing the schema: - Changing the schema does not necessarily result in changing the contents of the table. For example, changing the `strict` attribute of the table from `false` to `true` requires checking that each row in the table contains only the values related to the columns mentioned in the schema and such a check is equivalent to reading the table completely. - You cannot delete columns if `strict = true`: there is no way to clear the corresponding value in each row. You cannot add columns if `strict = false`: when adding a column of a certain type, you cannot be sure that there are no rows in the table that already contain a value corresponding to this column, and this value may already belong to a different data type. - You cannot delete key columns from the middle, because this can potentially violate the sort order. - You cannot enter new computed columns or change the expression or aggregate function of existing columns, because this would require recomputing all rows in the table. But, for example, you can delete the last key columns, because this only weakens the sorting condition for future insertions without changing it for the existing set of rows. ## Examples of working with a schema { #examples } ### Reading a schema - CLI ```bash yt --proxy get //home/tutorial/links_sorted_schematized/@schema ``` - Python ```python yt.get("//home/tutorial/links_sorted_schematized/@schema") ``` ### Creating a table with a schema - CLI ```bash yt --proxy create table //tmp/table --attributes '{schema = [{name = a; type = int64}; {name = b; type = string}]}' ``` - Python ```python yt.create_table("//tmp/table", attributes={"schema" : [{"name" : "a", "type" : "int64"}, {"name" : "b", "type" : "string"}]}) ``` - C++ ```c++ auto schemaNode = TNode::CreateList() .Add(TNode()("name", "a")("type", "int64")) .Add(TNode()("name", "b")("type", "string")); client->Create( "//tmp/table", NYT::NT_TABLE, TCreateOptions().Attributes(TNode()("schema", schemaNode))); ``` ### The schema attribute in RichYPath You can use the `@schema` attribute of the path when writing data using the `write_table` command or at the output of MapReduce operations. - CLI ```bash cat data | yt write '//tmp/table' ``` ### Setting a schema and changing a format for typified and structured data CLI ```bash # Enable columnar storage format yt --proxy set //tmp/table/@optimize_for scan # Set a new schema and transfer data to the new storage format yt --proxy merge --mode ordered --src //tmp/table --dst '//tmp/table' --spec '{schema_inference_mode = from_output; force_transform = %true}' ``` There are more automatic solutions: - Run the query `INSERT INTO "path/to/somewhere" SELECT * FROM "source/table"` via YQL. YQL will try to infer the schema by reading the first row or the first few rows. # Data types This section describes the data types supported by YTsaurus and the way they are described in the schema and represented in the formats. ## Overview {#overview} YTsaurus supports a number of primitive types: - `string`; - `integer`; - `boolean`; - `float`; - `double`; - `date`; - `datetime`; - `timestamp`; - `interval`. As well as the following composite (complex) types: - `optional`; - `list`; - `struct`; - `tuple`; - `variant`; - `tagged`. You can use one of the following two methods to specify a type in a table schema: - Using the `type` and (optionally) the `required` keys: historically, the first method but it is only good for defining primitive or optional primitive types. - Using the `type_v3` key. The `type` key always expects a string. The `type_v3` key expects either a string for primitive types or a YSON dictionary. A YSON dict always has the `type_name` key that stores the type name. The remaining keys depend on the specific type and are described below. ## Describing types in a schema {#schema} ### Primitive types {#schema_primitive} You can define primitive types in a schema both using the `type` and the `type_v3` keys. If primitive type `T` is defined using `type`, YTsaurus will additionally check the `required` key in the schema: - `required=%true` — the column will have a strictly defined type. The `Null` value or a missing value is not allowed; - `required=%false` — the column will have the `optional` type, allowing all primitive type values and the `Null` value. The table lists the supported types and their representation in the `type`/`type_v3` keys. | **Description** | **Representation in `type`** | **Representation in `type_v3`** | |--------------|----------------------------|-------------------------------| | an integer belonging to the range `[-2^63, 2^63-1]` | `int64` | `int64` | | an integer belonging to the range `[-2^31, 2^31-1]` | `int32` | `int32` | | an integer belonging to the range `[-2^15, 2^15-1]` | `int16` | `int16` | | an integer belonging to the range `[-2^7, 2^7-1]` | `int8` | `int8` | | an integer belonging to the range `[0, 2^64-1]` | `uint64` | `uint64` | | an integer belonging to the range `[0, 2^32-1]` | `uint32` | `uint32` | | an integer belonging to the range `[0, 2^16-1]` | `uint16` | `uint16` | | an integer belonging to the range `[0, 2^8-1]` | `uint8` | `uint8` | | [an 8-byte floating-point number](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) according to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) | `double` | `double` | | [a 4-byte floating-point number](https://en.wikipedia.org/wiki/Single-precision_floating-point_format) according to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) | `float` | `float` | | standard Boolean type `true/false` | `boolean` | `bool` (different from `type`) | | a random sequence of bytes | `string` | `string` | | a proper UTF8 sequence | `utf8` | `utf8` | | a string that contains a valid [JSON](https://en.wikipedia.org/wiki/JSON) | `json` | `json` | | [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier), a random 16-byte sequence (stored in binary representation) | `uuid` | `uuid` | | an integer in the range `[-53375809, 53375808 - 1]`,
represents the number of days from the Unix epoch;
the representable time range is about 145,000 years into the past and into the future
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `date32` | `date32` | | an integer in the range `[-53375809 * 86400, 53375808 * 86400 - 1]`,
represents the number of seconds from the Unix epoch;
the representable time range is about 145,000 years into the past and into the future
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `datetime64` | `datetime64` | | an integer in the range `[-53375809 * 86400 * 10^6, 53375808 * 86400 * 10^6 - 1]`,
representing the number of microseconds elapsed since the Unix epoch;
the representable time range extends approximately 145,000 years into the past and future
see the section on [temporal types](https://ytsaurus.tech/docs/en/#temporal_types)| `timestamp64` | `timestamp64` | | an integer in the range `[-9223339708800000000, 9223339708800000000]`,
represents the number of microseconds between two `timestamp64` timestamps
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `interval64` | `interval64` | | an integer in the range `[0, 49673 - 1]`,
represents the number of days from the Unix epoch;
representable date range: `[1970-01-01, 2105-12-31]`
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `date` | `date` | | an integer in the range `[0, 49673 * 86400 - 1]`,
represents the number of seconds from the Unix epoch;
representable time range: `[1970-01-01T00:00:00Z, 2105-12-31T23:59:59Z]`
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `datetime` | `datetime` | | an integer in the range `[0, 49673 * 86400 * 10^6 - 1]`,
represents the number of microseconds from the Unix epoch;
representable time range: `[1970-01-01T00:00:00Z, 2105-12-31T23:59:59.999999Z]`
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `timestamp` | `timestamp` | | an integer in the range `[- 49673 * 86400 * 10^6 + 1, 49673 * 86400 * 10^6 - 1]`,
represents the number of microseconds between two `timestamp` timestamps
see the section about [temporal types](https://ytsaurus.tech/docs/en/#temporal_types) | `interval` | `interval` | | *(experimental, do not use in production)* type `date` with time zone information
see the section about [time zones](https://ytsaurus.tech/docs/en/#time_zones) | tz_date | tz_date | | *(experimental, do not use in production)* type `datetime` with time zone information
see the section about [time zones](https://ytsaurus.tech/docs/en/#time_zones) | tz_datetime | tz_datetime | | *(experimental, do not use in production)* type `timestamp` with time zone information
see the section about [time zones](https://ytsaurus.tech/docs/en/#time_zones) | tz_timestamp | tz_timestamp | | *(experimental, do not use in production)* type `date32` with time zone information
see the section about [time zones](https://ytsaurus.tech/docs/en/#time_zones) | tz_date32 | tz_date32 | | *(experimental, do not use in production)* type `datetime64` with time zone information
see the section about [time zones](https://ytsaurus.tech/docs/en/#time_zones) | tz_datetime64 | tz_datetime | | *(experimental, do not use in production)* type `timestamp64` with time zone information
see the section about [time zones](https://ytsaurus.tech/docs/en/#time_zones) | tz_timestamp64 | tz_timestamp64 | | an arbitrary YSON structure,
physically represented as a byte sequence,
cannot have the `required=%true` attribute | `any` | `yson` (different from `type`) | | a system singular type that can only contain `null`
(creating a separate column with this type makes no sense;
we don't expect to see this type in user tables,
but it's useful for YQL integration) | `null` | `null` | | a singular type whose only possible value is `null`; this type is distinct from `null`
(creating a separate column of this type is not particularly useful,
and we do not expect this type to appear in user tables,
but it is useful for YQL integration)| `void` | `void` | Schema example: ```yson type_v3=utf8 ``` ```yson type_v3=bool ``` ```yson type_v3=yson ``` ### Temporal types {#temporal_types} Temporal types in YTsaurus are categorized into two groups. Historically, the first ones to appear in the system were `date`, `datetime`, `timestamp`, and `interval`. They are used to represent times from the beginning of 1970 to the end of 2105. They were followed by `date32`, `datetime64`, `timestamp64`, and `interval64`. These types can be used to represent times over a wider range, about 145,000 years into the past and into the future. We recommend using the latter types, because they have a wider range of values. All dates and times assume the Gregorian calendar. When working with points in time in the distant past, keep in mind that YTsaurus does not account for the [adoption of the Gregorian calendar](https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar), which occurred at different times in different countries. YTsaurus assumes that the Gregorian calendar has always been used. ### Time zones {#time_zones} **Warning:** support for the data types described in this section is incomplete and will likely change in a backward-incompatible way in the future. These types **must not** be used in production workflows. Types `tz_timestamp64`, `tz_datetime64`, `tz_date32`, `tz_timestamp`, `tz_datetime`, and `tz_date` store time information incorporating time zone details. Logically, these types store the pair: - a timestamp represented by an integer of the corresponding "timezone-free" type; the number represents a point in time in the [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) - The time zone id, an unsigned 16-bit number (see the [list of time zones](https://github.com/ytsaurus/ytsaurus/blob/main/library/cpp/type_info/tz/tz_gen.h); numbering is zero-based: `"GMT"` = 0, `"Europe/Moscow"` = 1). The internal representation of values for these types is described below. Certain higher-level tools offer a convenient way to work with these types. #| || **tz type** | **corresponding "no time zone" type** | **underlying integer type** | **integer type value range** | **unit** || || tz_date | date | Uint16 | `[0, 49673 - 1]` | days || || tz_datetime | datetime | Uint32 | `[0, 49673 * 86400 - 1]` | seconds || || tz_timestamp | timestamp | Uint64 | `[0, 49673 * 86400 * 10^6 - 1]` | microseconds || || tz_date32 | date32 | Int32 | `[-53375809, 53375808 - 1]` | days || || tz_datetime64 | datetime64 | Int64 | `[-53375809 * 86400, 53375808 * 86400 - 1]` | seconds || || tz_timestamp64 | timestamp64 | Int64 | `[-53375809 * 86400 * 10^6, 53375808 * 86400 * 10^6 - 1]` | microseconds || |# This pair is serialized into a string as follows: - The timestamp is written in presorted representation (see below). - The time zone id is written in presorted representation (see below). The presorted representation of an integer is obtained as follows: 1. The number is written in big-endian format. 1. If the number has a signed type, the most significant (sign) bit is inverted. Otherwise, this step is skipped. **Example.** To store the point in time 2025-01-01T00:00:00 in the Moscow time zone using the `tz_datetime64` type, follow these steps: 1. Convert this point in time to UTC. The result is `2024-12-31T21:00:00Z`. 1. Convert the UTC time to a Unix timestamp. The result is `1735678800`. 1. Write this timestamp in big-endian representation. The result is `"\x00\x00\x00\x00\x67\x74\x5b\x50"`. 1. Because the `tz_datetime64` type is based on the signed `Int64` type, invert the most significant bit. The result is `"\x80\x00\x00\x00\x67\x74\x5b\x50"`. 1. Convert the `"Europe/Moscow"` time zone name to its id. The result is `1`. 1. Write the id in big-endian representation. The result is `"\x00\x01"`. 1. Append the time zone id to the timestamp. The result is `"\x80\x00\x00\x00\x67\x74\x5b\x50\x00\x01"`. ### Decimal {#schema_decimal} The values of type `decimal(p, s)` are real numbers with the specified precision. To define this type in a schema, specify the following keys: - `type_name`: value of `decimal`. - `precision`: total number of decimal digits in a numeric value, `precision` must be in the range `[1, 35]`. - `scale`: number of digits to the right of the decimal point in a numeric value, `scale` must be in the range `[0, precision]`. Schema example: ```yson type_v3={ type_name=decimal; precision=10; scale=2; } ``` The values `3.14`, `-2.71`, `9.99` may be of type `decimal(3, 2)` (`precision=3`, `scale=2`). The type supports a number of special values, such as `nan`, `+inf`, `-inf`. #### Description of binary representation {#schema_decimal_binary} `decimal` numbers have a special binary representation, which is used by default in many formats, such as `yson`. For the purposes of this representation, the values of `decimal(p, s)` types are maintained as binary strings. Binary string length depends on `precision`. | **Precision** | **Number of bits in the representation** | **Number of bytes in the representation** | |---------------|-------------------------------|--------------------------------| | 1-9 | 32 | 4 | | 10-18 | 64 | 8 | | 19-38 | 128 | 16 | | 39-76 | 256 | 32 | You need to perform the following steps to obtain a binary representation of a `decimal` number. These steps will be illustrated with the values `3.1415`, `-2.7182` of type `decimal(5, 4)`. 1. Take an integer made up of the value's digits. The number of bits is taken from `precision` in the table above. In this example, 32-bit numbers `31415`, `-27182`. 1. Write the number as a big-endian sequence. In this example, the strings are `\x00\x00\x7A\xB7`, `\xFF\xFF\x95\xD2`. 1. Invert the most significant bit. In this example, the strings are `\x80\x00\7A\xB7`, `\x7F\xFF\x95\xD2`. The integer representations of the special values of `nan`, `+inf`, `-inf` for the first step are shown in the table below: | **Special value** | **Integer representation** | |--------------------------|---------------------------------| | `nan` | `INT_MAX` | | `+inf` | `INT_MAX - 1` | | `-inf` | `- INT_MAX + 1` | {% note info "Please note" %} Currently, YQL supports only `decimal` values with a `precision` of 35 or less. {% endnote %} ### Optional type {#schema_optional} The `optional` type means that a value may be of type `T` or be empty. {% note info "Please note" %} Each use of `optional` for a type adds new values. For instance, `optional>` may take on the following values: - The external `optional` is empty. - The external `optional` is non-empty, and the internal one is empty. - all `optional` values are set, with the value `true` or `false`. {% endnote %} Legacy columns containing the `type=T;required=false` attributes correspond to type `optional` defined using `type_v3`. To define type `optional`, specify the keys below: - `type_name`: value of `optional`. - `item`: element type description. Schema example: ```yson type_v3={ type_name=optional; item=string; } ``` ```yson type_v3={ type_name=optional; item={ type_name=optional; item=bool; } } ``` ### List {#schema_list} Values of type `list` are lists of elements of type `T`. To define the type in the schema, specify the keys below: - `type_name`: value of `list`. - `item`: element type description. Schema example: ```yson type_v3={ type_name=list; item=string; } ``` ```yson type_v3={ type_name=list; item={ type_name=list; item=double; } } ``` ### Struct {#schema_struct} A collection of named fields with specified value types. To define this type in a schema, specify the following keys: - `type_name`: value of `struct`. - `members`: list of dictionaries with keys: - `name`: field name, must be a non-empty utf8 string. - `type`: field type. Schema example: ```yson type_v3={ type_name=struct; members=[ { name=foo; type=int32; }; { name=bar; type={ type_name=optional; item=string; } }; ] } ``` ### Tuple {#schema_tuple} A collection of unnamed fields of certain predefined types. To define this type, you need to specify the following keys in the schema: - `type_name`: value of `tuple`. - `elements`: list of dictionaries with keys: - `type`: element type. Schema example: ```yson type_v3={ type_name=tuple; elements=[ { type=double; }; { type=double; }; ] } ``` ### Variant {#schema_variant} Variant is strictly a single value from a defined collection of types. A variant may be of one of two types: - Variant over struct. Each type has a name (as in a struct), and each variant value is labeled with the name of the relevant variant element value. - Variant over a tuple. In this case, all elements are unnamed, and each value is identified by an index. To define this type, specify the following keys in a schema: - `type_name`: value of `variant`. - `elements` or `members` (not both): the keys have a structure similar to these keys in `tuple` / `struct`: * `elements`: for the option with unnamed elements with the key itself containing a list of dictionaries with keys: - `type`: element type. * `members`: for the option with named elements with the key itself containing a list of dictionaries with keys: - `name`: element name, must be a non-empty utf8 string. - `type`: description of element type. Schema example: ```yson type_v3={ type_name=variant; members=[ { name=int_field; type=int64; }; { name=string_field; type=string; }; ] } ``` ```yson type_v3={ type_name=variant; elements=[ { type=int32; }; { type=string; }; { type=double; }; ] } ``` ### Dict {#schema_dict} A dict is a sequence of key/value pairs. YTsaurus does not check the keys for uniqueness or order. However, most clients will upload data to an actual dictionary while processing, and the value for non-unique keys will be lost. To define this type, specify the following keys in a schema: - `type_name`: value of `dict`. - `key`: description of key type. - `value`: value type description. Schema example: ```yson type_v3={ type_name=dict; key=int64; value={ type_name=optional; item=string; }; } ``` ### Tagged {#schema_tagged} The `tagged` type helps annotate other types with a string. Any value of type `T` can serve as a value for type `tagged`, however, the types themselves will be considered different wherever YTsaurus compares schemas. For instance, when the possibility of merging two tables into one is being checked. To define this type, specify the following keys in a schema: - `type_name`: value of `tagged`. - `tag`: tag name, must be a non-empty utf8 string. - `item`: element type description. #### Media in the web UI {#schema_tagged_media} The web UI can display images and play audio when viewing a table or YQL query results in Query Tracker. To enable this, set the column type to `tagged` via `type_v3`, and specify the media tag in the `tag` field: ```yson type_v3={ type_name=tagged; tag="image/svg"; item="string"; } ``` [View the full example](https://ytsaurus.tech/docs/en/#schema_tagged_media_example) The `image/svg` value indicates that the column contains a Base64-encoded SVG image. For other supported media types, select a `tag` value from the table: | **Value of the `tag` field** | **Cell content** | **Notes** | | --- | --- | --- | | `image/svg` `image/svg+xml` `image/jpeg` `image/png` `image/gif` `image/webp` | Base64-encoded image | The value must contain a Base64 string without the Data URL prefix | | `imageurl` | Image URL | The web UI loads data only from addresses allowed by the administrator in the `uiSettings.reUnipikaAllowTaggedSources` parameter of the UI configuration. If the address is not allowed, the URL is displayed as text | | `audio/mpeg` `audio/webm` `audio/wav` | Base64-encoded audio | The value must contain a Base64 string without the Data URL prefix | | `audiourl` | Audio URL | The web UI loads data only from addresses allowed by the administrator in the `uiSettings.reUnipikaAllowTaggedSources` parameter of the UI configuration. If the address is not allowed, the URL is displayed as text | {% note warning "Current limitations" %} - The web UI does not display images or play audio from Cypress nodes of the `file` type. These features are available only for table columns. - The web UI currently cannot play video from either table columns or Cypress nodes of the `file` type. {% endnote %} #### Limits {#schema_tagged_media_limits} Two limits apply when viewing a table in the web UI: #| || **Limit** | **When it applies** | **Value** || || Cell data loading limit | When opening a table, as the UI loads the values of all cells on the page | 1 KiB by default, up to 64 KiB. Can be [changed](https://ytsaurus.tech/docs/en/*cell-size-limit-settings) || || Media preview limit | When clicking the preview button ![eye icon](https://ytsaurus.tech/images/table-cell-preview-icon.svg%20%22preview%22%20=16x), as the UI loads a single value in full | 16 MiB. Cannot be changed || |# **Cell data loading limit.** The web UI limits the amount of data downloaded from each table cell to avoid loading large values into memory along with the entire page. By default, this limit is 1 KiB and applies to all values, including Base64 strings. If the size of a Base64 string exceeds the current cell data loading limit, the UI receives only part of the value, marks it as incomplete, and displays a preview button ![eye icon](https://ytsaurus.tech/images/table-cell-preview-icon.svg%20%22preview%22%20=16x) in the cell. To view the image, click this button: ![Incomplete image and audio values and preview buttons](https://ytsaurus.tech/images/tagged-media-preview-button.png){.center} **Media preview limit.** A separate limit of 16 MiB applies to the size of the Base64 string used for previews. It applies to the encoded value in the cell, not to the size of the original file. If the string exceeds this limit, you cannot view the image or play the audio. This limit cannot be changed in the settings. {% note info %} If there is no preview button, the absence of the image or audio is not caused by incomplete loading. Verify that the Base64 value is valid and that the data matches the specified tag. {% endnote %} Thus, media data within the cell limit (1 KiB by default, up to 64 KiB) is displayed immediately; data above the cell limit and up to 16 MiB is displayed after you click the preview button ![eye icon](https://ytsaurus.tech/images/table-cell-preview-icon.svg%20%22preview%22%20=16x); and data larger than 16 MiB is not displayed. #### Example {#schema_tagged_media_example} In this example, you will create a table with a column for a PNG image. You will then write the YTsaurus logo to it in Base64 and verify the result in the web interface. 1. Save the [YTsaurus logo](https://raw.githubusercontent.com/ytsaurus/ytsaurus/main/yt/docs/images/logo.png) to the current directory as `logo.png`. 1. Create the `//tmp/tagged-media` table with a column for a PNG image: ```bash $ yt create table --attributes '{ schema=[ {name=image; type_v3={item=string; type_name=tagged; tag="image/png"}}; ] }' //tmp/tagged-media ``` 1. Encode the image contents in Base64 without the Data URL prefix and write the string to the table: ```bash $ IMAGE_BASE64=$(base64 < logo.png | tr -d '\r\n') $ printf '{"image":"%s"}\n' "$IMAGE_BASE64" \ | yt write-table --format json //tmp/tagged-media ``` 1. Open the `//tmp/tagged-media` table in the web interface. The `image` column should display the YTsaurus logo. ## Representing compound types in formats {#formats} Formats are used to read and write tables. Some formats do not support composite data, and some, such as dsv / schemaful_dsv, will return an error in response to an attempt to read a composite value. For example, `Values of type "any" are not supported by the chosen format`. ### YSON {#yson} There are two YSON representations of composite types. The representations of the `struct` and `variant` types differ: the default representation is more convenient to use, while the alternative representation is somewhat more efficient for storage and processing. You can switch between representations using the `complex_type_mode` flag. Possible values: `named` / `positional`. Type representation descriptions are provided below. Unless otherwise specified, the type representation does not depend on the `complex_type_mode` setting. The `dict` type with string keys has two representations. By default, the `positional` representation is used as a list (because this is how data is stored in YTsaurus). For readability, you can enable `named` mode using the `string_keyed_dict_mode` flag. #### Primitive types {#yson_primitive} Primitive types have a linear representation as a single YSON value. The table shows a mapping between the primitive and the YSON types. | **`type` / `type_v3`** | **YSON representation** | | ---------------------- | ----------------------------------- | | `int64` | signed number | | `int32` | signed number | | `int16` | signed number | | `int8` | signed number | | `uint64` | unsigned number | | `uint32` | unsigned number | | `uint16` | unsigned number | | `uint8` | unsigned number | | `double` | floating point number | | `boolean` / `bool` | boolean value | | `string` | string | | `utf8` | string | | `date` | unsigned number, see below | | `datetime` | unsigned number, see below | | `timestamp` | unsigned number, see below | | `interval` | signed number | | `uuid` | string with binary data, see below | | `any` / `yson` | value-dependent | | `null` | `#` | | `void` | `#` | ##### Time types {#yson_time_mode} The YSON format has a `time_mode` option that controls the representation of the `date`, `datetime`, and `timestamp` types. Its possible values are: - `binary` (default) — uses an unsigned number representing the number of days / seconds / milliseconds since the Unix epoch. - `text` — uses a string representation. The representations of `date`, `datetime`, and `timestamp` are as follows, respectively: `2022-01-02`, `2022-01-02T03:04:05Z`, `2022-01-02T03:04:05.123456Z`. ##### UUID {#yson_uuid_mode} The YSON format has a `uuid_mode` option that controls the representation of the `uuid` type. Its possible values are: - `binary` (default) — uses a 16-byte binary representation. - `text_yt` — uses a string representation in the YTsaurus format consisting of 4 groups, for example, `61626364-65666768-696a6b6c-6d6e6f70`; - `text_yql` — uses a string representation in the YQL format consisting of 5 groups and more closely resembling the RFC representation, for example, `64636261-6665-6867-696a-6b6c6d6e6f70`. #### Decimal {#yson_decimal} The YSON format has a `decimal_mode` option that controls the representation of the `decimal` type. Its possible values are: - `binary` (default) — encoded as a binary string containing the [binary representation](https://ytsaurus.tech/docs/en/#schema_decimal_binary) of a `decimal` number. - `text` — uses the textual representation of a `decimal` number. #### Optional {#yson_optional} The representation of the `optional` type depends on its inner type. This is required for backward compatibility with columns that have `required=%false` set. If `T` is an arbitrary type other than `optional`, then `optional` is represented as follows: - `Null` value of `optional` is represented as `#`; - otherwise, the standard representation of a value of type `T` is used. If `T` is an `optional` type, then `optional` is represented as follows: - `Null` value of the outer `optional` is represented as `#`; - otherwise, the `[ v ]` representation is used (a YSON list of length 1), where `v` is the YSON representation of a value of type `T`. Example values of type `optional`: ```yson # ``` ```yson -42 ``` Example values of type `optional>`: ```yson # ``` ```yson [ # ] ``` ```yson [ -42 ] ``` #### List {#yson_list} The `list` type is encoded as a YSON list whose elements are encoded representations of elements of type `T`. Example values of type `list`: ```yson [] ``` ```yson [42; -1;] ``` #### Struct {#yson_struct} The structure representation depends on the value of the `complex_type_mode` flag. ##### Named representation (default) {#yson_struct_named} The representation described here applies when the YSON format option `complex_type_mode` is not set or is set to `complex_type_mode=named`. The struct is represented by a YSON dictionary where field names serve as keys and the contents of these fields are the values. Example values for the `struct>` type: ```yson {Foo=42;Bar=#;} ``` ```yson {Foo=-5;Bar="minus five";} ``` ##### Positional representation {#yson_struct_positional} If the YSON format option `complex_type_mode=positional` is set, a different representation is used. A struct is encoded as a YSON list with the i-th position containing a YSON representation of the struct's i-th field. The list may be shorter than the number of fields in the structure. In this case, the remaining types must be of type `optional`, and the fields are considered to have an empty `optional` value. Example values for the `struct>` type: ```yson [42; #;] [42] ``` ```yson [-5;"minus five";] ``` #### Tuple {#yson_tuple} The `tuple` type is encoded as a fixed-length YSON list. The i-th position contains the i-th field's encoded value. Example values for the `tuple>` type: ```yson [42; #;] ``` ```yson [-5;"minus five";] ``` #### Variant {#yson_variant} ##### Unnamed variant {#yson_variant_tuple} The unnamed option is represented by a YSON list of length 2 that includes the following elements: - Alternative number (indexed at 0). - An encoded value for the relevant alternative. Example values for the `variant>` type: ```yson [0; 42] ``` ```yson [1; #] ``` ```yson [1; "foo bar";] ``` ##### Named variant option {#yson_variant_struct} ###### Named representation (default) {#yson_variant_struct_named} The representation described here applies when the YSON format option `complex_type_mode` is not set or is set to `complex_type_mode=named`. The named option is represented by a YSON list of length 2 that includes the following elements: - Alternative name. - An encoded value for the relevant alternative. Example values for the `variant>` type: ```yson [Foo; 42] ``` ```yson [Bar; #] ``` ```yson [Bar; "foo bar";] ``` ###### Positional representation {#yson_variant_struct_positional} If the format option `complex_type_mode=positional` is set. The named option is represented by a YSON list of length 2 that includes the following elements: - Index of alternative. - An encoded value for the relevant alternative. Example values for the `variant>` type: ```yson [0; 42] ``` ```yson [1; #] ``` ```yson [1; "foo bar";] ``` #### Dict {#yson_dict} By default, the `dict` type is represented as a YSON list in which each item is a two-item YSON list containing a key and a value. Example values of the `dict` type: ```yson [[1;"one"];[4;"four"]] ``` ```yson [] ``` `dict` can be represented as a YSON map. However, a YSON map supports only strings as keys, whereas `dict` also supports other key types. ##### Dict with string keys {#yson_string_keyed_dict} The representation of a dictionary with string keys depends on the value of the `string_keyed_dict_mode` flag. ###### Positional representation (default) {#yson_string_keyed_dict_positional} The representation described here applies when the YSON format option `string_keyed_dict_mode` is not set or is set to `string_keyed_dict_mode=positional`. See [above](https://ytsaurus.tech/docs/en/#yson_dict) Example values of the `dict` type: ```yson [["one";1];["four";4]] ``` ###### Named representation {#yson_string_keyed_dict_named} If the YSON format option `string_keyed_dict_mode=named` is set, a different representation is used. `dict` is encoded as a YSON map. Example values of the `dict` type: ```yson {one=1; four=4} ``` #### Tagged {#yson_tagged} The `tagged` type does not change the YSON representation of its element. [*cell-size-limit-settings]: To change the default limit, open **Settings** → **Table** → **Cell size limit**. To change the limit for the current table, click the gear icon above the table. # Binary data in tables This section contains information about how to use tables to store binary data. ## General information { #common } You sometimes need to save binary data from jobs. The obvious solution is to write a file from each job to Cypress. But this method has its drawbacks: - It creates a high load on the proxy. - A large number of objects appears in Cypress which makes working with them less efficient. Therefore, it is preferable to write the binary data in a single table: 1. The data is divided into parts and saved in separate table rows. This table must have a set of key columns that uniquely identify the file: name and path. Besides that, there must be a column responsible for the [BLOB](https://en.wikipedia.org/wiki/Object_storage) number with the data and a column responsible for the data itself. The table must be sorted by a set of key columns and by a column with the BLOB number so that point reads by the key that specifies the file name are possible. 2. For a file, all BLOBs, except for the last one, must be the same size. The recommended size is 4 MB, and some commands, such as `read_blob_table`, are applied to it by default. The columns with data and the BLOB number must be named `data` and `part_index`, respectively. Tables that meet the described conditions are BLOB tables. BLOB tables are used within YTsaurus to store stderr and core files generated by operation jobs. For example, the table with stderr operations has 3 columns: `job_id`, `part_index`, and `data`, the table is sorted by `job_id` and `part_index`. ## The read_blob_table command { #read_blob_table } The special `read_blob_table` command is implemented for convenient data reading from BLOB tables. The command takes the path to the table as input and outputs a binary data stream. The command also checks that the indexes of read BLOBs start from zero and go without gaps. The command has the `part_index_column_name` and `data_column_name` parameters that enable you to set the names of columns with the BLOB number and data, respectively. By default, these names are `part_index` and `data`. Example of running an operation and reading the stderr of a job from a BLOB table: ```bash # Starting a Map operation $ yt --proxy map --src '//tmp/table_in' --dst '//tmp/table_mapped' --format yson 'cat; echo something >&2' --spec='{stderr_table_path="//tmp/stderr_table";}' 2023-12-07 16:25:19,463 INFO Operation started: http:///?page=operation&mode=detail&id=11788844-52f37dd2-3ff03e8-ff3bf1e4&tab=details 2023-12-07 16:25:19,561 INFO ( 0 min) operation 11788844-52f37dd2-3ff03e8-ff3bf1e4 initializing 2023-12-07 16:25:22,961 INFO ( 0 min) Unrecognized spec: {'mapper': {'title': 'cat;'}} 2023-12-07 16:25:24,230 INFO ( 0 min) operation 11788844-52f37dd2-3ff03e8-ff3bf1e4: running=0 completed=0 pending=1 failed=0 aborted=0 lost=0 total=1 blocked=0 2023-12-07 16:25:25,353 INFO ( 0 min) operation 11788844-52f37dd2-3ff03e8-ff3bf1e4 completing 2023-12-07 16:25:27,527 INFO ( 0 min) operation 11788844-52f37dd2-3ff03e8-ff3bf1e4 completed # Finding out the job id $ yt --proxy list-jobs 11788844-52f37dd2-3ff03e8-ff3bf1e4 --format json | jq '.jobs[] | select(.type=="map").id' "4c3fc84d-884fbde5-3ff0384-94b1" # Reading the "errors" of this job only $ yt --proxy read-blob-table '//tmp/stderr_table["4c3fc84d-884fbde5-3ff0384-94b1"]' something ``` # Compression This section describes the compression algorithms supported by YTsaurus. By default, user data in YTsaurus is stored and transmitted in compressed form. The system performs compression: 1. When writing to a table or file, data is split into parts — [chunks](https://ytsaurus.tech/docs/en/chunks.md). 2. Each chunk is compressed using the algorithm specified for that file or table and written to the disk. 3. For network transmission, data is read from the disk already in compressed form. ## Viewing a compression algorithm { #get_compression } Chunks, files, and tables can be compressed in the YTsaurus system. The compression algorithm is specified in the `compression_codec` attribute: - For chunks, the `compression_codec` attribute specifies the compression algorithm for all chunk blocks. Different chunks within a file or table can be compressed using different algorithms. - For tables and files, `compression_codec` defines the default compression algorithm. It will be used if no algorithm is specified for individual file or table chunks. The default `compression_codec` value: for tables — `lz4`, for files — `none` (without compression). The degree of data compression is specified in the `compressed_data_size` and `uncompressed_data_size` attributes of a chunk, table, or file: - For chunks, the `uncompressed_data_size` attribute shows the size of the chunk before compression and the `compressed_data_size` attribute shows the size of the chunk after compression. - For tables and files, the `uncompressed_data_size` attribute shows the total uncompressed data size of all object chunks and the `compressed_data_size` shows the compressed data size. Statistics on the compression algorithms used in the table chunks are contained in the `compression_statistics` attribute of the table. ## Changing a compression algorithm { #set_compression } To change the compression algorithm of an existing static table, set a new value of the `compression_codec` attribute, then run the `merge` command to recompress the chunks. CLI ```bash yt set //path/to/table/@compression_codec zstd_3 yt merge --src //path/to/table --dst //path/to/table --spec '{force_transform = %true}' ``` To change the compression algorithm of a [dynamic table](https://ytsaurus.tech/docs/dynamic-tables/overview.md), set a new value for the `compression_codec` attribute. Then run the `remount-table` command. The old chunks will eventually be recompressed in the process of compaction. The compression rate depends on the table size, the write speed, and the background compaction settings. CLI ```bash yt set //path/to/table/@compression_codec zstd_3 yt remount-table //path/to/table ``` If the table is permanently written to, there is usually no need for [forced compression](https://ytsaurus.tech/docs/dynamic-tables/compaction.md#forced_compaction). If you need to force compression, use `forced compaction`. ## Supported compression algorithms { #compression_codecs } | Compression algorithms | Description | Compression/decompression rate | Compression degree | | ---------------------- | ---------------- | --------------------------- | --------------- | | `none` | Without compression. | - | - | | `snappy` | Learn more about the [snappy](https://google.github.io/snappy/) algorithm. | +++ | +- | | `zlib_[1..9]` | The YTsaurus system supports all 9 levels. The higher the level is, the more substantially and slowly data is compressed. Learn more about the [zlib](https://zlib.net) algorithm. | ++ | ++ | | `lz4` | The default compression algorithm for tables. Learn more about the [lz4](https://lz4.github.io/lz4/) algorithm. | +++ | + | | `lz4_high_compression` | The `lz4` algorithm with the enabled `high_compression` option. Compresses more efficiently, but is significantly slower. Its compression rate is inferior to `zlib`. | ++ | ++- | | `zstd_[1..21]` | Learn more about the [zstd](https://github.com/facebook/zstd) algorithm. | ++ | ++ | | `brotli_[1..11]` | We recommend using this algorithm for data that is not temporary. We recommend using levels 3, 5, and 8, because they have the best volume-to-speed ratio. Learn more about the [brotli](https://github.com/google/brotli) algorithm. | ++ | +++ | | `lzma_[0..9]` | Learn more about the [lzma](https://www.7-zip.org) algorithm. | + | +++ | | `bzip2_[1..9]` | Learn more about the [bzip2](http://www.bzip.org) algorithm. | ++ | ++ | ## Best practices { #best_practice } - Compression is usually not applied to files that are used as [symlinks](https://ytsaurus.tech/docs/en/links.md) to chunks in an operation. - `lz4` is often used for actual data. The algorithm provides high compression and decompression rates at an acceptable compression ratio. - When you need maximum compression and long runtime is acceptable, `brotli_8` is often used. - For operations consisting of a small number of jobs, for example, `final sort` or `sorted merge`, we recommend adding a separate processing stage — data compression in a merge operation with a large number of jobs. {% note warning "Attention" %} While strong compression algorithms (`zlib`, `zstd`, `brotli`) save disk space, they compress data significantly slower than the default algorithm (`lz4`). Using algorithms with substantial compression can lead to a significant increase in operation execution time. We recommend using them only for tables that take up a lot of space but rarely change. {% endnote %} ## Comparing compression algorithms { #benchmarks } The following method applies only to static tables. To determine which algorithm is best for a particular table, run `yt run-compression-benchmarks TABLE`. A sample that is 1 GB by default will be taken from the table. It will be compressed by all algorithms. After the operation is complete, you will see `codec/cpu/encode`, `codec/cpu/decode`, and `compression_ratio` for each algorithm. For more information, see [Job stats](https://ytsaurus.tech/docs/problems/jobstatistics.md#system_stats). For algorithms with multiple compression levels, the minimum, medium, and maximum levels are used by default. To obtain results for all levels, use the `--all-codecs` option. CLI ```bash yt run-compression-benchmarks //home/dev/tutorial/compression_benchmark_data ``` ```bash [ { "codec": "brotli_1", "codec/cpu/decode": 2103, "codec/cpu/encode": 2123, "compression_ratio": 0.10099302059669339 }, ... { "codec": "brotli_11", "codec/cpu/decode": "Not launched", "codec/cpu/encode": "Timed out", # compression did not finish within a standard --time-limit-sec (200) "compression_ratio": "Timed out" }, ... { "codec": "none", "codec/cpu/decode": 0, "codec/cpu/encode": 247, "compression_ratio": 1.0 }, ... { "codec": "zstd_11", "codec/cpu/decode": 713, "codec/cpu/encode": 15283, "compression_ratio": 0.07451278201257201 }, ... ] ``` # Replication and erasure coding This section provides information on replication and erasure coding as data storage redundancy mechanisms. These mechanisms protect distributed systems from data loss in the event of a hardware fault. [Replication](https://en.wikipedia.org/wiki/Replication_(computing)) requires a lot of disk space but not a lot of computational power (CPU). [Erasure coding](https://en.wikipedia.org/wiki/Erasure_code) requires a lot of computational power and less disk space. The `replication_factor` attribute defines the replication factor while the `erasure_codec` attribute controls the type of erasure codec used. ## Replication { #replication } For replication, the `erasure_codec` value is equal to `none` while the `replication_factor` determines the number of chunk replicas. To provide **rack awareness**, each replica occupies a separate **rack**. Chunks remain available for reading even if all replicas but one are lost. For most scenarios, we recommend a `replication_factor` of 3, as smaller values could lead to data losses. To change the replication factor, set the `replication_factor` attribute. The system will automatically create the required additional number of chunks or delete those no longer needed. CLI ```bash yt set //home/your_new_file/@replication_factor 6 ``` ## Erasure coding { #erasure } Erasure coding is a data storage method that saves space as compared to replication. Erasure coding also helps store data more reliably since it can survive the loss of any three cluster nodes whereas triple replication can only survive the loss of any two. {% note warning "Attention" %} - We strongly advise against using erasure coding to store foreign tables for `joint reduce` and `reduce with foreign tables` operations if these tables are much smaller than the primary ones. While these operations are running, jobs will overload cluster nodes hosting the foreign table chunks with requests. This is less of a problem with simple `reduce` that has many more running jobs than input table chunks. - For latency-sensitive processes, it may not be possible to use erasure coding. - There is no point in using erasure coding for small amounts of data since there will be no significant gain. {% endnote %} The `replication_factor` attribute does not affect erasure chunks. A single copy of the data is stored. There might be situations when a table stores both erasure and regular chunks. These tables may result from `merge` operations. The `erasure_codec` shows the type of erasure codec used. The types listed in the table are supported. If you edit the attribute, the method of writing new data will change. At the same time, old data will remain unchanged. | Codec | Algorithm | Description | Size on disk as compared to compressed data. | Recovery cost. | |-------|----------|----------|------------------------|--------------------------| | `isa_reed_solomon_6_3` | Uses the [Reed-Solomon codes](https://en.wikipedia.org/wiki/Reed-Solomon_error_correction). | When writing an erasure chunk, data are split into 6 data parts. Three control parts are generated for recovery. | 1.5x | The recovery cost is high in CPU resources and time. | | `isa_lrc_12_2_2` | Uses [Local Reconstruction Codes (LRC)](https://www.microsoft.com/en-us/research/publication/erasure-coding-in-windows-azure-storage/?from=http%3A%2F%2Fresearch.microsoft.com%2Fpubs%2F179583%2Flrc12-cheng%2520webpage.pdf) that are a variation on the Reed-Solomon codes. | When writing an erasure chunk, the data is split into 12 parts. Two xor parts are computed: for the first 6 parts containing data, and for the other 6 parts. Two further control parts are computed for recovery using an algorithm similar to Reed-Solomon (erasure parts). This is the recommended option for all cases where erasure coding is required. | 1.33x | The recovery cost is moderate in CPU resources and time. | In both cases, the system is able to survive the loss of any three cluster nodes. The system attempts to write each chunk part to a separate **rack** to provide **rack awareness**. Erasure coding has the following weaknesses: - Even after the loss of a single cluster node, the system will have to start background recovery to maintain failure tolerance. A different number of parts are required for recovery. Thus, `isa_reed_solomon_6_3` requires 6 separate parts. The recovery process is automatic but needs CPU and takes time: between 10 and 60 minutes. - Reading may be slow since the data has one physical replica. In fact, a read has nothing to choose from, and if the only cluster node hosting the data part being read is very busy for whatever reason, then reading will be slow. If a part cannot be read for a long time, the system will recover this part on the client (**read recovery**), which will require extra time and computational power. When using replication, the system selects the least busy replica for reading. - A data write is an operation that has a high cost in terms of RAM usage. For reference, by default, a table write that uses replication requires 100 MB of RAM while an erasure coding table write needs 1500 MB. ### Using erasure coding { #erasure_usage } To begin taking advantage of erasure coding, use the `transform` command. This command is a client-side wrapper for `merge`. The command automatically selects `data_weight_per_job` as well as other parameters and starts a `merge`. {% list tabs %} - CLI ```bash yt transform --src //path/to/table --dst //path/to/table --erasure-codec reed_solomon_6_3 --compression-codec brotli_6 ``` - Python ```python import yt.wrapper as yt yt.transform(src, dst, erasure_codec=erasure_codec, ...) ``` {% endlist %} To force the operation, run `merge` manually with `force_transform=%true` after configuring the required codes for the output table. `N` is specified in bytes. You must select its size to have the compressed chunk take up over 500 MB. CLI ```bash yt set //path/to/table/@erasure_codec isa_reed_solomon_6_3 yt set //path/to/table/@compression_codec brotli_6 yt merge --src //path/to/table --dst //path/to/table --spec '{force_transform = %true;data_weight_per_job=N} ``` Data encoding settings are contained in the table attributes. To view them, run the command below: CLI ```bash yt get //path/to/table/@erasure_codec yt get //path/to/table/@compression_codec ``` Changing the attribute affects the way new data will be written to the table. At the same time, old data will remain unchanged. To find out the table's data storage format, run the commands below: CLI ```bash yt get //path/to/table/@erasure_statistics yt get //path/to/table/@compression_statistics ``` # Media This section describes media types and provides example data storage settings for various media. ## General information Different media types (HDD, SDD, RAM) are logically combined in special entities referred to as media. Media types differ by the amount of available space and read and write performance. The higher the performance of a medium, the smaller the medium is. Each medium has a [chunk locator](https://ytsaurus.tech/docs/en/chunks.md). For a disk, the chunk locator is a file system partition. Media divide the entire set of chunk locations into non-intersecting subsets. At the same time, a single cluster node may have locations on different media. Each such subset does its own chunk balancing and handles its own quota. An account's disk quota is issued for a specific medium. A user may select the medium to use to store different types of data. YTsaurus clusters host the following media types: - default: set of HDDs in a cluster. - ssd_blobs: set of SATA SSDs in a cluster. - ssd_journals: set of SATA SSDs or NVMe SSDs for storing dynamic table logs. Retrieving a list of a cluster's media: CLI ```bash yt list //sys/media ``` ## Assigning a medium To configure the media store chunk owner (table, file, or log) chunks, set a value for the `media` attribute. The `media` attribute value is a dictionary where medium name is a key for every entry, and the value is a dictionary of two entries: - Key: `"replication_factor"`; value: replication factor for the specific medium. - Key: `"data_parts_only"`; value: a flag. On media with `"data_parts_only" = %true`, [erasure coded](https://ytsaurus.tech/docs/en/replication.md#erasure) object only store data parts and do not store erasure parts. For example: CLI ```bash yt get //path/to/my/table/@media ``` ```yson { "default" = { "replication_factor" = 3; "data_parts_only" = %false; }; "ssd_blobs" = { "replication_factor" = 1; "data_parts_only" = %true; }; } ``` To add replica tables to another medium, set the `media` attribute: CLI ```bash yt set //path/to/my/table/@media ' { "default" = { "replication_factor" = 3; "data_parts_only" = %false; }; "ssd_blobs" = { "replication_factor" = 1; "data_parts_only" = %true; }; }' ``` [Cypress](https://ytsaurus.tech/docs/en/cypress.md) enables you to read individual records from a dictionary without reading the entire dictionary. To retrieve the `default` medium replication factor, for instance, run the command below: CLI ```bash yt get //path/to/my/table/@media/default/replication_factor ``` ``` 3 ``` To remove a table from a medium, delete the corresponding entry in the `media` attribute: CLI ```bash yt remove //path/to/my/table/@media/default ``` ### Primary medium { #primary } Each chunk owner has a special medium specified that is considered primary. It is to this medium that chunks belonging to the owner in question will be written. Chunks find their way to other media through replication and balancing. To find out a table's primary medium, read its `primary_medium` attribute. You can change a primary medium by overwriting the attribute. The object will write its new chunks to the new primary medium while the actual movement of the old chunks occurs in the background and may take some time. By default, the `"default"` medium is specified as primary on create. The `replication_factor` attribute is a synonym for `@media//replication_factor`. The attribute has been kept for backward compatibility and ease of access. ### Inheritance Attributes `primary_medium` and `media` are [inheritable](https://ytsaurus.tech/docs/en/attributes.md#system_attr). This means they can be set on a directory, a then every newly created chunk owner in the subtree will receive corresponding attribute values automatically. Using the `primary_medium` attribute is preferred in most cases; using `media` this way is not recommended. ### Limitations Editing a table's `media` attribute is a potentially dangerous action. If you specify a medium that is out of space, for example, a table will become locked for writing. Editing `media` results in large amounts of data being moved, which may impact performance. Therefore, a number of limitations exist. - There must be at least one medium that holds all the data, that is the `replication_factor > 0 && !data_parts_only` condition must be satisfied. - A medium with `data_parts_only == true` cannot be primary. - An attempt to make primary a medium not being used by a table is considered a request to move the table from an old primary medium to a new one. ## Transient media RAM is a different type of medium from HDD and SDD in that storing data in RAM is unreliable. RAM is power-dependent and will lose all data in the event of an emergency loss of power. The YTsaurus system refers to such media as **transient**. To notify the system that a medium is transient, you need to set its `transient` attribute equal to `%true`. CLI ```bash yt set //sys/media/ram/@transient "%true" ``` This property being set does not affect replication or balancing. The system does not prevent chunk storage on transient media only. This may be required to hold intermediate computation output in memory for a time. Chunks replicated to transient media only are referred to as **unsteady**. You can retrieve their count as follows: CLI ```bash yt get //sys/precarious_chunks 42 ``` ``` yt get //sys/precarious_vital_chunks 0 ``` ## Attributes A medium object has the following attributes: | **Attribute** | **Type** | **Description** | | ----------- | --------- | ------------------------------------------------------------ | | `name` | `string` | The medium name is a non-empty unique string. | | `index` | `integer` | The index is a small integer that serves as a unique identifier for housekeeping purposes. | | `transient` | `bool` | Medium transient nature. | # Annotations This section describes the annotations of tables, directories, and other [Cypress](https://ytsaurus.tech/docs/en/cypress.md) nodes. ## Purpose of annotations { #rationale } Annotations are a way to provide a table, directory, or any other Cypress node with a short description. Formally, such a description can be random text up to 1 KB, consisting of non-control ASCII characters, but we recommend sticking to Markdown format. {% note info "Note" %} Annotations are stored in the memory of the YTsaurus master servers. We recommend limiting the content of the annotations to one or two sentences and links to more detailed documentation. {% endnote %} ## The annotation attribute { #annotation_attribute } All Cypress nodes have the `annotation` attribute with the [YSON string](https://ytsaurus.tech/docs/en/yson.md) value. You can set the value, as for the other attributes: ```bash yt set //home/project/path/table/@annotation '"This table contains valuable data."' ``` Removal is performed in the usual way: ```bash yt remove //home/project/path/table/@annotation ``` {% note info "Note" %} The `annotation` attribute is always present, but it is `null` by default. Removal resets the attribute to `null`, but formally it still exists. Thus, checking the existence of the `annotation` attribute always returns a positive result and is therefore useless. Instead, check that the attribute is `null`. This behavior peculiarity is related to inheritance. {% endnote %} ## Inheritance { #inheritance } The annotation can accompany both a table and an entire subtree. To add a subtree annotation, set it to the appropriate directory. Any node recursively contained in it that does not have its own annotation will then display it in its `annotation` attribute. In other words, the `annotation` attribute is inherited: it always displays the nearest non-zero annotation of the ancestor. The search starts with the node. To understand which ancestor the displayed annotation belongs to, use the `annotation_path` attribute. It is read-only and contains the full path to the corresponding ancestor or to the node if it is annotated. ## Support in the web interface { #web_interface } The web interface supports annotations and accepts the Markdown format — the [YFM](https://ydocs.tech/en/) dialect supported by the [@doc-tools/transform](https://www.npmjs.com/package/@doc-tools/transform) package. Annotations are displayed in the **Navigation** section, on the **Annotation** tab of the selected node. To edit an annotation, click **Edit metadata** and go to the **Description** tab. # Formats This section describes the data types and table data representation formats supported by YTsaurus. ## Data types { #data_types } The YTsaurus system stores data of the following types: - [Structured data](https://ytsaurus.tech/docs/en/#structured_data) - [Table data](https://ytsaurus.tech/docs/en/#table_data) - [Binary data](https://ytsaurus.tech/docs/en/#binary_data) Different formats enable users to work with different types of data. ### Structured data { #structured_data } Structured data is a node tree with attributes, where nodes correspond to [YSON types](https://ytsaurus.tech/docs/en/yson.md) (map, list, primitive types) with attributes. Structured data can be obtained using the [`get`](https://ytsaurus.tech/api/commands.md#get) command. Supported formats: `json`, `yson`, `yaml`. Example: [Cypress](https://ytsaurus.tech/docs/en/cypress.md) nodes, their attributes. ### Table data { #table_data } Table data is a sequence of rows where each row is logically a key-value pair, the key being a string, and the value is structured data. Supported formats: `json`, `yson`, `dsv`, `schemaful_dsv`, `protobuf`, `arrow`. Examples: Input and output data in operations, input data of the [`write_table`](https://ytsaurus.tech/api/commands.md#write_table) command, output data of the [`read_table`](https://ytsaurus.tech/api/commands.md#read_table) command. ### Binary data { #binary_data } Binary data is a sequence of bytes. For example, files store binary data. Supported formats: Cannot set a format. Representation only as a byte array. Examples: Input data of the [`upload`](https://ytsaurus.tech/api/commands.md#write_file) command and output data of the [`download`](https://ytsaurus.tech/api/commands.md#read_file) command. ## Table data representation formats { #table_formats } YTsaurus supports several data formats, which determine in what form data is transmitted to or received from the system. A format must be specified for all [`yt` utility](https://ytsaurus.tech/api/cli/cli.md) commands that return or accept data. Reading a table in DSV format: ```bash yt read //some/table --format dsv ``` YTsaurus supports the following data representation formats: * [YSON](https://ytsaurus.tech/docs/en/#yson) * [JSON](https://ytsaurus.tech/docs/en/#json) * [DSV (TSKV)](https://ytsaurus.tech/docs/en/#dsv) * [SCHEMAFUL_DSV](https://ytsaurus.tech/docs/en/#schemaful_dsv) ## Example { #example } The table used in the examples: | name | uid | | :----------| :---------------: | | Elena | 95792365232151958 | | Denis | 78086244452810046 | | Mikhail | 70609792906901286 | | Ilya | 15696008603902587 | | Oxana | 76840674253209974 | | Alexey | 15943558469181404 | | Roman | 37865805882228106 | | Anna | 35039450424270744 | | Nikolai | 45320538587295288 | | Karina | 20364947097122776 | ## YSON { #yson } The YSON format is the main format in YTsaurus: it is used to store table data, Cypress node attributes, and many other objects. YSON is largely similar to JSON, but the former has node attributes and supports binary data. For a detailed specification, see the [YSON](https://ytsaurus.tech/docs/en/yson.md) section. To read a table in YSON format: ```bash yt read --proxy --format 'yson' '//home/tutorial/staff_unsorted_sample' ``` Table representation in YSON format: ```yson { "name" = "Elena"; "uid" = 95792365232151958; }; { "name" = "Denis"; "uid" = 78086244452810046; }; { "name" = "Mikhail"; "uid" = 70609792906901286; }; { "name" = "Ilya"; "uid" = 15696008603902587; }; { "name" = "Oxana"; "uid" = 76840674253209974; }; { "name" = "Alexey"; "uid" = 15943558469181404; }; { "name" = "Roman"; "uid" = 37865805882228106; }; { "name" = "Anna"; "uid" = 35039450424270744; }; { "name" = "Nikolai"; "uid" = 45320538587295288; }; { "name" = "Karina"; "uid" = 20364947097122776; }; ``` **Parameters** The values are specified in parentheses by default. - **format** (`binary`) — describes the YSON variant to be used in generating data. Specifying the variant is only important when setting the output format in relation to the system, while for the input format the system reads all the YSON varieties homogeneously: - `binary` — binary - `text` — text without formatting - `pretty` — text with formatting - **skip_null_values** (`%false`) — enable skipping `null` values in schematized tables. By default, all values are written. - **enable_string_to_all_conversion** (`false`) — enable conversion of string values to numeric and boolean types. For example, `"42u"` is converted to `42u`, and `"false"` is converted to `%false`. - **enable_all_to_string_conversion** (`false`) — enable conversion of numeric and boolean values to string type. For example, `3.14` becomes `"3.14"`, and `%false` becomes `"false"`. - **enable_integral_type_conversion** (`true`) — enable conversion of `uint64` to `int64` and vice versa. This option is enabled by default. If an overflow occurs during conversion, you will see a corresponding error. - **enable_integral_to_double_conversion** (`false`) — enable conversion of `uint64` and `int64` to `double`. For example, integer `42` becomes decimal number `42.0`. - **enable_type_conversion** (`false`) — enable all of the above options. In most cases, this option will suffice. - **complex_type_mode** (`named`) — representation of composite types, structs, and variants. Possible values are `named` and `positional`. For more information, see [this section](https://ytsaurus.tech/docs/en/data-types.md#yson). - **string_keyed_dict_mode** (`positional`) — representation of dictionaries with string keys. Possible values are `named` and `positional`. For more information, see [this section](https://ytsaurus.tech/docs/en/data-types.md#yson). - **decimal_mode** (`binary`) — representation of the `decimal` type. Possible values are `text` and `binary`. For more information, see [this section](https://ytsaurus.tech/docs/en/data-types.md#yson). - **time_mode** (`binary`) — representation of `date`, `datetime`, and `timestamp` types. Possible values are `text` and `binary`. For more information, see [this section](https://ytsaurus.tech/docs/en/data-types.md#yson). - **uuid_mode** (`binary`) — representation of the `uuid` type. Possible values are `binary`, `text_yql`, and `text_yt`. For more information, see [this section](https://ytsaurus.tech/docs/en/data-types.md#yson). - **sort_keys** (`%false`) — output keys in dictionaries in a sorted order. Possible values are `%true` and `%false`. ## JSON { #json } [JSON](https://en.wikipedia.org/wiki/JSON) is a common format for displaying structured data. JSON is convenient to use thanks to popular tools such as [jq](https://stedolan.github.io/jq/). And there is no need to install additional libraries to support YSON. Detailed [JSON](https://www.json.org/json-en.html) specification. YSON is the main format of YTsaurus. Unlike JSON, YSON has attributes. Therefore, to convert data from YSON to JSON, you need to encode attributes in JSON. To do this, convert the node with attributes to map-node with two keys: - `\$value` — its value is taken as the entire current node without attributes. - `\$attributes` — its value is taken as the `map` of attributes. For example, `{x=y}` is represented as `{"$value": {"x": "y"}, "$attributes": {"attr": 10}}`. The strings in YTsaurus — YSON strings — are byte strings, while JSON uses Unicode encoding. The format has an `encode_utf8` setting that allows you to manage conversions. The `encode_utf8` defaults to `%true`. - `encode_utf8=%true` { #utf8-true } To convert a YSON string to a JSON string, convert each byte to a Unicode character with the corresponding number and encode it in UTF-8. This conversion occurs, for example, when reading table data using the [`read_table`](https://ytsaurus.tech/api/commands.md#read_table) command. When a JSON string is converted to a YSON string, the sequence of Unicode characters of the JSON string is checked to make sure each of them is in the 0–255 range. They are then converted to the appropriate bytes. This conversion is performed when writing table data using the [`write_table`](https://ytsaurus.tech/api/commands.md#write_table) command. {% note warning "Attention" %} With `encode_utf8=%true`, Unicode characters outside the 0..255 range in the JSON string are not allowed. {% endnote %} - `encode_utf8=%false` { #utf8-false } To convert a YSON string to a JSON string, the YSON string must contain a valid UTF-8 sequence. It will be converted to a Unicode JSON string. During JSON string to YSON string conversion, UTF-8 Unicode characters are encoded into byte sequences. This conversion is performed with the [`write_table`](https://ytsaurus.tech/api/commands.md#write_table) command. {% note warning "Attention" %} In general, byte sequences in tables are not always UTF-8 sequences. Therefore, with `encode_utf8=%false` not all YSON strings are readable. {% endnote %} UTF-8 strings: {% list tabs %} - Python ```python yt.write_table("//path/to/table", [{"key": "Ivan"}], format=yt.JsonFormat(attributes={"encode_utf8": False})) ``` - CLI ```bash echo '{"key": "Jane"}{"key":"Doe"}' | YT_PROXY= yt write --table "//path/to/table" --format="json" ``` {% endlist %} {% note info "Note" %} When the user reads the table in JSON format, each record is made in a separate line. The `pretty` format is not supported. {% endnote %} Reading a table in JSON format: ```bash yt read --proxy --format json '//home/tutorial/staff_unsorted_sample' ``` Table representation in JSON format: ```json {"name":"Elena","uid":95792365232151958} {"name":"Denis","uid":78086244452810046} {"name":"Mikhail","uid":70609792906901286} {"name":"Ilya","uid":15696008603902587} {"name":"Oxana","uid":76840674253209974} {"name":"Alexey","uid":15943558469181404} {"name":"Roman","uid":37865805882228106} {"name":"Anna","uid":35039450424270744} {"name":"Nikolai","uid":45320538587295288} {"name":"Karina","uid":20364947097122776} ``` **Parameters** The values are specified in parentheses by default. - **format** (`text`) — describes the JSON format in which data is to be generated. Matters only when the output format is specified relative to the system. For the input format, all JSON varieties are read by the system homogeneously: - `text` — text without formatting - `pretty` — text with formatting - **attributes_mode** (`on_demand`) — attribute mode: - `always` — each node turns into a map with `\$value` and `\$attributes`. - `never` — attributes are ignored. - `on_demand` — only nodes with non-empty attributes are converted to a map with `\$value` and `\$attributes`. - **encode_utf8** (`true`) — enable UTF-8 character interpretation in bytes with corresponding numbers. - **string_length_limit** — limit on the length of the string in bytes. If the limit is exceeded, the string is trimmed and the result is written as `{$incomplete: true, $value:...}`. - **stringify** (`false`) — enable conversion of all scalar types to strings. - **stringify_nan_and_infinity** (`false`) — enable conversion of `Nan` and `Infinity` to strings. You can't use this together with `support_infinity`. - **support_infinity** (`false`) — allow the `Nan` value for the `double` type. You can't use this together with `stringify_nan_and_infinity`. - **annotate_with_types** — enable type annotation `{$type: "uint64", $value: 100500}`. - **plain** — enable the JSON parser option, which disables accounting for the logic regarding special keys `\$attributes`, `\$value`, `\$type` and parses JSON as is. When the option is enabled, the parser works much faster. - **enable_string_to_all_conversion** (`false`) — enable conversion of string values to numeric and `boolean` types. For example, `"42u"` is converted to `42u`, and `"false"` is converted to `%false`. - **enable_all_to_string_conversion** (`false`) — enable conversion of numeric and `boolean` values to string type. For example, `3.14` becomes `"3.14"`, and `%false` becomes `"false"`. - **enable_integral_type_conversion** (`true`) — enable conversion of `uint64` to `int64` and vice versa. If an overflow occurs during conversion, you will see a corresponding error. - **enable_integral_to_double_conversion** (`false`) — enable conversion of `uint64` and `int64` to `double`. For example, integer `42` becomes decimal number `42.0`. - **enable_type_conversion** (`false`) — enable all of the above options. In most cases, this option will suffice. {% note warning "Attention" %} JSON represents bytes numbered 0–32 (namely \u00XX) in a non-compact way. When you try to write a long string with such data, an **Out of memory** error may occur in the Yajl library, because the memory amount limit is `2 * row_weight`. In that case, the JSON format is not recommended. {% endnote %} ## YAML { #yaml } [YAML](https://yaml.org/) is a human-friendly data serialization format that has become a standard for configuration files and for infrastructure as code. It offers more compact and readable syntax compared to JSON and supports comments and anchors. YAML is available in YTsaurus starting with version 24.2. Reading a Cypress node in YAML format: ```bash yt get //path/to/some_table/@ --format yaml ``` Output example: ```yaml id: 7b7-154e6-13440191-489067e7 type: table ref_counter: 1 foreign: false native_cell_tag: 4932 revision: 8482560616109 acl: [] owner: max path: null key: squirrels creation_time: '2023-12-06T22:03:43.498610Z' modification_time: '2023-12-06T22:20:05.142350Z' access_time: '2024-11-05T23:39:45.599145Z' access_counter: 1012 resource_usage: node_count: 1 chunk_count: 1 disk_space_per_medium: default: 560736 disk_space: 560736 chunk_host_cell_master_memory: 0 master_memory: 0 detailed_master_memory: nodes: 0 chunks: 0 attributes: 0 tablets: 0 schemas: 0 tablet_count: 0 tablet_static_memory: 0 recursive_resource_usage: null ... ``` ### Type system YAML format in YTsaurus uses a [YAML schema](https://yaml.org/spec/1.2.2/#schema) approach for type deduction. The implementation follows the [Core schema](https://yaml.org/spec/1.2.2/#core-schema), which is widely used by most YAML parsers: - When parsing, the system complies with specific tags and deduces non-specific tags using the Core schema regular expressions that detect integer/float/string/boolean/null values. - For example, `123`, `3.14`, `true`, and `null` are defined as an integer, float, boolean, and null value, respectively, and `foo`, `bar`, and `baz` are defined as strings. - When writing: - Non-string scalars are serialized in the plain (unquoted) style, for example: `123`, `3.14`, `true`, `false`, and `null`. - String scalars are serialized in the plain (unquoted) style if this is syntactically possible and if the unquoted representation doesn't match regular expressions for null/bool/int/float. Examples: `foo`, `bar`, and `baz`, but`"null"`, `"true"`, `"false"`, `"123"`, and `"3.14"` — otherwise they would be interpreted by most YAML parsers as types other than strings. ### Handling signed and unsigned integers To handle unsigned integers explicitly, the custom `!yt/uint` tag is introduced. The format has the `write_uint_tag` option (false by default), which forces the writer to use this tag for all uint64 scalars. When parsing: - If the `!yt/uint` tag is present, the value is interpreted as being a uint64 value. - If the tag is missing or is a standard YAML `!!int` tag: - For the decimal form, numbers within the [-2^63, 2^63) range are interpreted as int64 values, and numbers within the [2^63, 2^64) range are interpreted as uint64 values. - For the hexadecimal and octal forms, values are always interpreted as uint64 values. When writing: - If `write_uint_tag = %false`, all integers are inferred in the plain (unquoted) untagged form. - If `write_uint_tag = %true`, int64 values are inferred in the plain untagged decimal form, and uint64 values — in the plain decimal form with the `!yt/uint` tag. ### Attribute representation Nodes with attributes are represented as 2-element sequences with the `!yt/attrnode` tag so that you can distinguish them from regular lists: ``` YSON: { "foo" = < "x" = 1; "y" = 2; > { "bar" = 3; }; } YAML: foo: !yt/attrnode - x: 1 y: 2 - bar: 3 ``` ### Limitations - **Comments.** YAML comments are dropped when writing as they aren't part of the node structure. - **Anchors and aliases.** Only non-recursive anchors and aliases are supported. Aliases must refer to a node defined somewhere before them. Aliases aren't preserved when parsing as they are resolved immediately. - **Merge operator.** The YAML 1.1 merge operator ("<<") isn't supported and is treated as a regular scalar key. **Parameters** Default values are specified in parentheses. - **write_uint_tag** (`false`) — enable using the `!yt/uint` tag for all uint64 scalars when writing. ## DSV (TSKV) { #dsv } **DSV** — Delimiter-Separated Values. **TSKV** — Tab-Separated Key-Value. A format that is widely used for storing logs and working with them. DSV and TSKV are two names of the same format. This format only supports flat records with an arbitrary column set and string values. Records are separated by the line break character `\n`. Fields in a record are separated by the tab character `\t`. For more information, read below. Example record: `time=10\tday=monday\n`. {% note warning "Attention" %} The format does not support attributes and typed values. In the example, `10` will be recognized by the system as a string, not a number. {% endnote %} Reading a table in DSV format: ```bash yt read --proxy --format dsv '//home/tutorial/staff_unsorted_sample' ``` Table representation in DSV format: ``` name=Elena uid=95792365232151958 name=Denis uid=78086244452810046 name=Mikhail uid=70609792906901286 name=Ilya uid=15696008603902587 name=Oxana uid=76840674253209974 name=Alexey uid=15943558469181404 name=Roman uid=37865805882228106 name=Anna uid=35039450424270744 name=Nikolai uid=45320538587295288 name=Karina uid=20364947097122776 ``` Reading a table with some parameters: ```bash yt read --proxy --format 'dsv' '//home/tutorial/staff_unsorted_sample' ``` **Parameters:** The values are specified in parentheses by default. - **record_separator** (`\n`) — record separator. - **key_value_separator** (`=`) — key-value pair separator. - **field_separator** (`\t`) — separator of fields in a record. - **line_prefix** (no column by default) — mandatory column at the start of each record. - **enable_escaping** (`true`) — enable escaping for records. - **escape_carriage_return** (`false`) — enable escaping for the `\r` character. - **escaping_symbol** (`\`) — escape character. - **enable_table_index** (`false`) – enable output of the input table index. - **table_index_column** (`@table_index`) – name of the column with the table index. - **enable_string_to_all_conversion** (`false`) — enable conversion of string values to numeric and boolean types. For example, `"42u"` is converted to `42u`, and `"false"` is converted to `%false`. - **enable_all_to_string_conversion** (`false`) — enable conversion of numeric and boolean values to string type. For example, number `3.14` is converted to `"3.14"`, and `%false` to `"false"`. - **enable_integral_type_conversion** (`true`) — enable conversion of `uint64` to `int64` and vice versa. If an overflow occurs during conversion, you will see a corresponding error. - **enable_integral_to_double_conversion** (`false`) — enable conversion of `uint64` and `int64` to `double`. For example, integer `42` is converted to decimal number `42.0`. - **enable_type_conversion** (`false`) — enable all of the above options. In most cases, this option will suffice. {% note info "Note" %} In practice, an arbitrary string is a valid DSV record. If there is no key-value separator after the key, the field is ignored. {% endnote %} ## SCHEMAFUL_DSV { #schemaful_dsv } This format is a variation of DSV. With SCHEMAFUL_DSV, data is generated as a set of values separated by tabs. The format has a mandatory `columns` attribute, in which you specify the names of the columns you need, separated by a semicolon. If the value of one of the columns is missing from at least one of the records, an error occurs (see **missing_value_mode**). For example, if a table has two records `{a=10;b=11} {c=100}` and the format `schemaful_dsv` is specified, the following error occurs — `Column "a" is in schema but missing`. If only the first record is in the table, the job will receive record `10` as input. {% note info "Note" %} When working with YTsaurus CLI, attributes must be written in quotes. For example: `--format="schemaful_dsv"` {% endnote %} Reading a table in SCHEMAFUL_DSV format: ```bash yt read --proxy --format 'schemaful_dsv' '//home/tutorial/staff_unsorted_sample' ``` Table representation in SCHEMAFUL_DSV format: ```schemaful_dsv Elena 95792365232151958 Denis 78086244452810046 Mikhail 70609792906901286 Ilya 15696008603902587 Oxana 76840674253209974 Alexey 15943558469181404 Roman 37865805882228106 Anna 35039450424270744 Nikolai 45320538587295288 Karina 20364947097122776 ``` **Parameters:** The values are specified in parentheses by default. - **record_separator** (`\n`) — record separator. - **field_separator** (`\t`) — field separator. - **enable_table_index** (`false`) – enable printing of table switching in input data. - **enable_escaping** (`true`) – enable escaping for special characters `\n`, `\t`, and `\`. - **escaping_symbol** (`\`) – escape character. - **columns** – list of columns to be formatted, for example `schemaful_dsv`. - **missing_value_mode** (`fail`) — behavior in the absence of a value (`NULL`): - `skip_row` — skip a row. - `fail` — stop the operation if the specified column is missing in one of the rows. - `print_sentinel` — instead of missing values, set `missing_value_sentinel`, whose default is an empty string. - **missing_value_sentinel** (`empty line`) — value of the missing column for **print_sentinel** mode. - **enable_column_names_header** (`false`) — can only be used for generating data in SCHEMAFUL_DSV format, but not for parsing such data. If the value is `%true`, the first row will have column names instead of values. **Important!** Column names may be duplicated in the response body when parallel reading is enabled or when retrying. - **enable_string_to_all_conversion** (`false`) — enable conversion of string values to numeric and boolean types. For example, `"42u"` is converted to `42u`, and `"false"` is converted to `%false`. - **enable_all_to_string_conversion** (`false`) — enable conversion of numeric and boolean values to string type. For example, `3.14` becomes `"3.14"`, and `%false` becomes `"false"`. - **enable_integral_type_conversion** (`true`) — enable conversion of `uint64` to `int64` and vice versa. This option is enabled by default. If an overflow occurs during conversion, you will see a corresponding error. - **enable_integral_to_double_conversion** (`false`) — enable conversion of `uint64` and `int64` to `double`. For example, integer `42` becomes decimal number `42.0`. - **enable_type_conversion** (`false`) — enable all of the above options. In most cases, this option will suffice. ## ARROW { #Arrow } [Apache Arrow](https://arrow.apache.org/overview) is a binary format developed as a language-agnostic standard for columnar memory representation to facilitate interoperability. Format advantages: - Unlike row-based representation, where the data in each row is stored together, Arrow organizes data into columns. This improves the performance of some analytical queries. - Arrow supports composite data types, such as structs, lists, dictionaries, and others. **Parameters:** The values are specified in parentheses by default. - **enable_complex_types** (`false`) — enable converting YTsaurus complex types into Arrow complex types (see below). ### Arrow in YTsaurus Reading a table in Arrow format returns a concatenation of multiple [IPC Streaming Format](https://arrow.apache.org/docs/format/Columnar.html#ipc-streaming-format) streams. ``` ... ... ... ... ... ... ``` This returns a concatenation of multiple streams instead of a single stream, because YTsaurus can store the same column differently across different chunks. For example, column data can be stored either explicitly in its entirety or as a dictionary (similar to an [Arrow dictionary](https://arrow.apache.org/docs/format/Columnar.html#dictionary-encoded-layout)). When representing data in Arrow format, we store the encoding as a dictionary and return a concatenation of multiple streams if a column is encoded in its entirety in one chunk and as a dictionary in another. Limitations and notes: - Reading and writing is only available for schematized tables. - For more efficient reading, we recommend reading Arrow tables with columnar chunk storage (`optimize_for = scan`). {% note warning "Attention" %} Writing to Arrow isn't currently optimized for tables with columnar chunk storage and involves two-fold conversion of data: from columnar to row-based representation and back. {% endnote %} ### Types Table reads currently support the following types: - `string` is displayed as [arrow::binary](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type6BINARYE). - `integer` is displayed as one of the [arrow::integer](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type5UINT8E) types depending on its bit count and signedness. - `boolean` is displayed as [arrow::bool](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type4BOOLE). - `float` is displayed as [arrow::float](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type5FLOATE). - `double` is displayed as [arrow::double](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type6DOUBLEE). - `date` is displayed as [arrow::date32](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type6DATE32E). - `datetime` is displayed as [arrow::date64](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type6DATE64E). - `timestamp` is displayed as [arrow::timestamp](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type9TIMESTAMPE). - `interval` is displayed as [arrow::int64](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type5INT64E). - Representation of complex types depends on parameter **enable_complex_types**: - if `false`, complex types are represented as [arrow::binary](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type6BINARYE) with [YSON representation](https://ytsaurus.tech/docs/en/data-types.md#yson) of these types. - if `true`, complex types are represented as corresponding Arrow complex types: - `list` is displayed as [arrow::list](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type4LISTE) - `struct` is displayed as [arrow::struct](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type6STRUCTE) - `dict` is displayed as [arrow::map](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type3MAPE) {% note warning "Attention" %} Since YTsaurus stores the values of complex types as YSON-encoded strings, they have to be parsed when writing with **enable_complex_types** enabled, which may result in significant slowdown. {% endnote %} Writes support the same types as reads, including complex types. Other notes for working with types: - When reading a table, almost any type can be returned as [arrow::dictionary](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type10DICTIONARYE), where the dictionary elements are the column values, and the indexes specify the order of those values. For more information, see [this section](https://arrow.apache.org/docs/format/Columnar.html#dictionary-encoded-layout). - The Arrow format supports one level of nested `optional`, with missing values represented with unset bits in [*validity bitmap*](https://arrow.apache.org/docs/format/Columnar.html#validity-bitmaps). Representation of deeper nested `optional` depends on parameter **enable_complex_types**: - if `false`, the value is displayed as arrow::binary, same as for other complex data types. - if `true`, the value of `optional>` is displayed as nullable arrow::struct with a single field of type `optional` and metadata field `YtType: NestedOptional`. This behaviour is also supported for writes. - The Arrow format does not support empty structs, so they are displayed as arrow::struct with a single dummy field of type [Null/NA](https://arrow.apache.org/docs/cpp/api/datatype.html#_CPPv4N5arrow4Type4type2NAE) and metadata field `YtType: EmptyStruct`. This behaviour is also supported for writes. ### Examples of operations Reading an Arrow table: {% list tabs %} - Python ```python yt.read_table("//path/to/table", format=yt.format.ArrowFormat(), raw=True) ``` - CLI ```bash yt read --table "//path/to/table" --format arrow ``` {% endlist %} Arrow map operation: ```bash yt map --input-format arrow --output-format arrow --src "//path/to/input_table" "cat" --dst '//path/to/output_table' ``` ### Operations with multiple tables If you pass multiple tables as input to an Arrow operation, it will output a stream of multiple concatenated IPC Streaming Format streams, where each segment of the stream may belong to one of the tables. The table index is passed as [schema metadata](https://arrow.apache.org/docs/format/Columnar.html#schema-message) under the name `TableId`. Learn more about [Arrow metadata](https://arrow.apache.org/docs/format/Columnar.html#custom-application-metadata). ## BLOB { #BLOB } BLOB format offers a convenient method to deliver binary data from a BLOB table to a job (see [Binary data](https://yt.yandex-team.ru/docs/user-guide/storage/blobtables)). Data in BLOB format represents the concatenation of binary data from the `data_column_name` table column. When this format is used, the system verifies that BLOB numbers in the `part_index_column_name` column are sequentially ascending from a specific value. **Parameters:** The values are specified in parentheses by default. - **part_index_column_name** (`part_index`): Name of the column with BLOB numbers. - **data_column_name** (`data`): Name of the column with BLOB data. ### Example | part_index | data | | :--------- | :------: | | 0 | hel | | 1 | lo | | 2 | world | Reading in BLOB format: ```bash yt read --table "//path/to/table" --format blob ``` Representation in BLOB format: ``` helloworld ``` ## PARQUET { #PARQUET } You can work with [parquet](https://parquet.apache.org/docs) from the command line: - [dump-parquet](https://pydoc.ytsaurus.tech/yt.cli.html#yt.cli.yt_binary.dump_parquet): download a table locally as a parquet file. - [upload-parquet](https://pydoc.ytsaurus.tech/yt.cli.html#yt.cli.yt_binary.upload_parquet): upload a local parquet file to YT as a static schematized table. ## PROTOBUF { #PROTOBUF } Protobuf is a structured data transfer protocol for working with tables in the C++ API. Learn more about [Protobuf](https://ytsaurus.tech/api/cpp/protobuf.md). # Input/output settings This section lists operation input/output configuration options as well as table read and write command options. {% note info "Note" %} This section considers only important options that might help a user. In actuality, the options are much more numerous and provide fairly fine control of the various data read/write parameters. More often that not, this type of configuration is not required, and using non-default values might actually do harm. {% endnote %} You can control reading and writing via [table data formats](https://ytsaurus.tech/docs/en/formats.md), `TableReader/TableWriter`, and `ControlAttributes`. You control data reads via settings in two sections: `table_reader` and `control_attributes`. You can control data writes in the `table_writer` section. ## TableReader section { #table_reader } These settings can be specified when reading tables using the `table_reader` option or in the `table_reader` section under `job_io` in the operation specification. ### Sampling { #sampling } Under `table_reader`, you can specify sampling settings `sampling_seed` and `sampling_rate`: ```python ... spec = {"job_io": {"table_reader": {"sampling_seed": 42, "sampling_rate": 0.3}}} yt.run_map(map_function, input, output, spec=spec) ... yt.read_table(table_path, table_reader={"sampling_seed": 42, "sampling_rate": 0.3}) ... ``` The value of `"sampling_rate": 0.3` will result in the operation (`map_function`) receiving 30% of all the input table rows as input. The `sampling_seed` option controls a random number generator used to sample rows. It guarantees the same output for the same `sampling_seed`, a pure `map_function`, and the same collection of input chunks. If `sampling_seed` is not included in the specification, it will be random. When sampling is used all the data are read from disk, the specified probability notwithstanding. Nonetheless, this could be less costly than independent data sampling since the sampling itself will occur system-side immediately following the disk read. To obtain maximum sampling read performance in an operation at a reduced sampling quality, use the [`sampling`](https://ytsaurus.tech/docs/en/#sampling) option in the operation specification. ### Buffer size for data fed to a job { #window_size } You control the job data feed buffer via the `window_size` parameter expressed in bytes. The default value is 20 MB. Configuring the job feed data buffer: ```python ... spec = {"job_io": {"table_reader": {"window_size": 20971520}}} yt.run_map(map_function, input, output, spec=spec) ... yt.read_table(table_path, table_reader={"window_size": 20971520}) ... ``` ### Remote read and inter-cluster network bandwidth throttling { #remote_read } When reading input tables from other clusters (remote read), the read rate is limited by inter-cluster network bandwidth quotas. For more information, see [Inter-cluster Network Bandwidth Throttling](https://ytsaurus.tech/docs/data-processing/operations/cluster-throttlers.md). ## TableWriter section { #table_writer } These settings can be included in the `table_writer` section when writing table data or in the `table_writer` section under operation `job_io` settings. ### Limit on table row size { #max_row_weight } When writing table data, the YTsaurus system checks their size. If this size is over a certain limit, writing stops and the relevant job or `write` command fails. The default string size cap is 16 MB; however, it can be configured under `table_writer`. Updating the table row size constraint: ```python ... spec = {"job_io": {"table_writer": {"max_row_weight": 32 * 1024 * 1024}}} yt.run_map(map_function, input, output, spec=spec) ... yt.write_table(table_path, table_writer={"max_row_weight": 32 * 1024 * 1024}) ... ``` `max_row_weight` is specified in bytes. It cannot exceed 128 MB. `table_writer` controls the various storage settings at table creation time or when calling `merge`. ### Chunk size { #desired_chunk_size } The `desired_chunk_size` specification setting defines the desired chunk size in bytes. To modify a table to achieve a chunk size close to 1 KB, you can call the merge command. Modifying chunk size: ```bash yt merge --mode auto --spec '{"force_transform"=true; "job_io"={"table_writer"={"desired_chunk_size"=1024}}}' --src <> --dst <> ``` Data fed to a job cannot be smaller than block size. Block size is defined by `block_size` in bytes. The minimum value is 1 KB, and the default is 16 MB. ### Replication factor { #upload_replication_factor } The `upload_replication_factor` specification setting defines the number of table or file chunk replicas. The default is 2 and the maximum is 10. To modify the number of replicas, simply call the merge command. Modifying the replication factor: ```bash yt merge --mode auto --spec '{"force_transform"=true; "job_io"={"table_writer"={"upload_replication_factor"=10}}}' --src <> --dst <> ``` For the modification to take place in the background at a later time, call `set` on the table `replication_factor`. The same is true for a table write with a pre-defined parameter. ### Sampling rate { #sample_rate } The `sample_rate` specification setting defines the percentage of the rows to be selected and placed in chunk metadata. This row selection affects the construction of buckets for the sort's `partition` stage. Increasing the `sample_rate` value helps the system compute a key distribution more precisely but increases system load by increasing the amount of metadata and reducing read and write performance. To avoid skewed bucket sizes when putting values in a single partition for a column with few values, increase `sample_rate` prior to the `sort` or the `reduce stage`. The default value is 1/10,000. If a table has, say, 100,000 rows, the sample will include 10. Consequently, the sort will have no more than 10 partitions. To obtain a sample of 0.1% of the table (maximum sample size), run a merge. Modifying the sampling rate: ```bash yt merge --mode auto --spec '{"force_transform"=true; "job_io"={"table_writer"={"sample_rate"=0.001}}}' --src <> --dst <> ``` ## ControlAttributes section { #control_attributes } When reading several tables or several ranges of the same table, it is useful to have housekeeping information to provide insight which table or read range data is coming from. To support this information in jobs or when reading tables, we introduced the notion of system management (control) entries containing such information. This concept is internal, and its representation in a job's input data stream depends on the requested format. Let's look at control attributes using the YSON format. For this format, the stream might include special records of the Yson Entity type rather than regular data records that are of the Yson Map type. Their attributes contain housekeeping information. For instance, if record `#` comes in, it would mean that all subsequent records in the stream will be from the second input table until a new control record is received with a different `table_index`. The `control_attributes` section supports the following options: The values are specified in parentheses by default. - **`enable_table_index`** (`false`): send control records with an input table index (`table_index` field of type `int64` in the control record attributes). - **`enable_row_index`** (`false`): send control records with an input table record index (`row_index` field of type `int64` in the control record attributes). - **`enable_range_index`** (`false`): send control records with a range number from among the requested input table ranges. Please see ranges (`range_index` field of type `int64` in the control record attributes). - **`enable_key_switch`** (`false`): send records on input data key switch (`key_switch` field of type `bool` in the control record attributes). The setting is only valid for the `reduce` phase of the Map-Reduce and Reduce operations. {% note info "Note" %} The processing of control attributes through the Python API and the C++ API is transparent for the user. That is to say that their analysis and conversion to a more convenient representation are handled by the API. {% endnote %} Options that control the transmission of table indexes in the input stream are available at different system layers. The `enable_input_table_index` option is found in the operation spec section describing the user process. The value of this option overwrites the value in `enable_table_index` under `control_attributes`. The `enable_table_index` option enables and disables `table index` at the format level. Many formats have these indexes disabled by default; therefore, they must be enabled. Many formats have options responsible for representing `table_index` in this one. For more information on `table_index` in various formats, see [Table switching](https://ytsaurus.tech/docs/data-processing/operations/table-switch.md). The `row_index`, `range_index`, and `key_switch`representations are only supported by the YSON and the JSON formats. Input data stream showing all the control attributes in a YSON format reduce job: ```yson <"table_index"=0;>#; <"range_index"=0;>#; <"row_index"=2;>#; {"a"="2";}; <"key_switch"=%true;>#; {"a"="3";}; <"key_switch"=%true;>#; <"row_index"=0;>#; {"a"="1";}; ``` Enabling control attributes for reading in an operation: {% list tabs %} - CLI ```bash yt read --control-attributes="{enable_row_index=true;}" "//path/to/table[#10:#20]" {"$value": null, "$attributes": {"row_index": 10}} ... ``` - Python ```python ... spec = {"job_io": {"control_attributes": {"enable_row_index": True}}} yt.run_map(map_function, input, output, spec=spec) # will place "@row_index" key to input records ... yt.read_table(path, control_attributes={"enable_row_index": True}, format=yt.YsonFormat()) ... ``` {% endlist %} # Skiff format The first part of this section describes the skiff serialization library (`library/cpp/skiff`), which helps encode structured data, including data that is not YTsaurus tables. The second part describes the use of the library efficiently to exchange table data between job_proxy and user code. Skiff (from schemaful format) is an interchange [format](https://ytsaurus.tech/docs/en/formats.md) to communicate schematized data between job_proxy and a job. Format objectives: - Optimize data serialization and deserialization performance for schematized data. - Support all the types supported by YTsaurus. - Support the reading of tables even if not strongly typed. ## Comparison with other formats {% cut "protobuf" %} Protobuf has the following drawbacks: - For some clients (YQL), the message structure is defined at runtime. Protobuf is inconvenient to parse and write for an unknown type. - Protobuf supports backward compatibility. This results in tags that have to be maintained current. - Protobuf field order is not specified. This produces extra branches for tags on each field. - Protobuf frequently uses type varint. For job_proxy that talks to a job via a pipe, it is less costly to write a uint64 number than serialize and deserialize a varint. - Because of varint, strings and nested messages are prefixed with length. To serialize a message, you need to walk a structure twice. - In protobuf, repeated fields do not have to be contained in one part of a message: the start of a list can be at the top of a message, and the end at the bottom. This complicates the writing of proper parsers. {% endcut %} {% cut "flatbuffers" %} - For some clients (YQL), the message structure is defined at runtime. Flatbuffers is inconvenient to parse and write for an unknown type because there is no reflection. {% endcut %} ## Skiff serialization library The skiff serialization format is schematized. To be able to read and interpret data, you need to know the data schema. ### Skiff Schema A schema describes the structure of an encoded value and the encoding method. It is a tree-like structure. A single-node tree could be a valid schema. The order or a node's children in a tree is important. Each node has an encoding type (`wire_type`): leaves have simple types, and other tree nodes have compound types. Nodes of a skiff schema may have names, which do not affect data encoding and serve to map a skiff schema to that of a table. #### Simple types ##### Nothing `nothing`: No value, special type that can only be used as a child of the `Variant*` / `RepeatedVariant*` compound types. Encoded as an empty string. ##### Boolean `boolean`: Boolean type. Encoded with a single byte: `001` for `true`, `000` for `false`. Examples: `\x01`, `\x00`. ##### Int64 / Uint64 `uint64` / `int64`: Integers. Encoded with an 8-byte number in little-endian format. Examples: `\x2a\x00\x00\x00\x00\x00\x00\x00` (42), `\x94\x88\x01\x00\x00\x00\x00\x00` (100500). ##### Double `double`: 8-byte representation of a double. Example: `\x9B\x91\x04\x8B\x0A\xBF\x05\x40` (2.718281828). ##### String32 `string32`: String length encoded as a 32-bit uint in little-endian format, followed by the string itself. Example:`\x06\x00\x00\x00foobar` ##### Yson32 `yson32`: Value of an arbitrary type. The length of the encoded YSON is encoded as a 32-bit uint followed by the encoded YSON itself. Examples: `\x09\x00\x00\x00{foo=bar}`, `\x07\x00\x00\x00100500u` #### Compound types ##### Variant8 / variant16 `variant8` / `variant16`: Algebraic data types. Encoded by an 8-bit (for `variant8`) or a 16-bit (for `variant16`) unsigned number (`i` tag) followed by a value described by the child node type at `i`. - `variant8` / `variant16` can have type `nothing` as one of its nodes. Since type `nothing` is encoded by an empty string, string `\x00` is a valid value of type `variant8`. - Bit depths depend on practical considerations. The variant tag will indicate the presence or the absence of a value or a table index. ##### RepeatedVariant8 / RepeatedVariant16 `repeated_variant8` / `repeated_variant16`: Variant list. Encoded with an 8-bit (for `repeated_variant8`) or a 16-bit (for `repeated_variant16`) tag as a continuous sequence of variants ending with special tag `0xFF` (for `repeated_variant8`) or `0xFFFF` (for `repeated_variant16`) that indicates the sequence end. - Like the regular `variant8`, can have type `nothing` as one of its child nodes. - The type's bit depth depends on practical considerations based on the need to define a transmission format for table data (essentially, the variant tag will be used as a sparse column index). ##### Tuple `tuple`: Fixed-size tuple of values. Encoded as a series of values, the first of the same type as the first child node, the second the same type as the second child node, and so on. ## Using the skiff serialization library to transmit table data ### Skiff schema of a table A format's attributes for each table to be transmitted are used to send a table skiff schema (`table_skiff_schema`). This is a skiff schema with a number of constraints. Input and output format behavior is different. The input format concatenates all tables and transmits them through the same pipe whereas the output format reads each table from its own pipe. A job receives values with the general schema as its input. The general schema has a `Variant16` node at its root with all the table skiff schemas as children. From the output stream of the i-th output table, job_proxy attempts to read values described by the schema with the `Variant16` root node with the skiff schema of the i-th table as the only child. ### Limitations of a table's skiff schema. The following limitations exist on a schema for transmitting table data: - A node of type `tuple` must serve as the root node of a table's skiff schema. - All the children of the root node of a table's skiff schema must be named. YTsaurus uses them to determine which field to use for values from this or that column. - The names of the children of a table skiff schema root node may include special names that start with `$`. Those children whose names do not begin with this character make up the set of dense nodes. These are the names of columns whose values will be placed in these fields. - The children of the root node of a table's skiff schema may include a node named `$other_columns`. This node must be of type `yson32` and be the last one. - The children of the root node of a table's skiff schema may include a node named `$sparse_columns`. This node must be of type `RepeatedVariant16`. Its children make up the set of sparse nodes and must also be named. The `$sparse_columns` node must be the one before last if `$other_columns` exists or must come last otherwise. - The children of the root node of a table's skiff schema may include a node with a special name `$key_switch`, which must be of type `boolean`. - The children of the root node of a table's skiff schema may include a node with a special name `$remaining_row_bytes`, which must be of type `int32`. - The children of the root node of a table's skiff schema may include nodes with special names `$row_index / $range_index`. They must be of type `variant8` - All the dense nodes must have a schema that: - Either describes a simple type, such as `int64, uint64, boolean, double, string32, yson32`. - Or describes type `variant8`, where TYPE is a simple type, such as `int64, uint64, boolean, double, string32, yson32`. - All the sparse nodes must have a schema that describes a simple type, such as `int64, uint64, boolean, double, string32, yson32`. When you serialize a table row, for every value, you need to find a dense or a sparse node with the same name as the name of a column. Values described by dense nodes are always serialized as required by the skiff schema's root node (tuple). If there is no value for a dense node or if such a value is of type `NULL`, the corresponding dense node in the skiff schema must be of type `variant8`. Values described by the sparse part are only serialized if present and of type other than `NULL`. They find their way into the `$sparse_columns` field as a list. Those table row values that fail to be included in the dense or the sparse part, are sent to the `$other_columns` column. This column will store a yson map containing such values. Having no `$other_columns` column in the presence of a value not described in the dense or the sparse part will generate an error. For the `$remaining_row_bytes` node, the number of bytes taken up by the remaining part of the row in skiff format is returned. ### Format configuration You describe the skiff format with attributes, same as other formats. The attributes must include the `table_skiff_schemas` key with a list of table skiff schema descriptions. In addition, there may be a key called `skiff_schema_registry`. If it is there, it must include a YSON map storing a name representation in an auxiliary skiff schema. In `table_skiff_schemas` and `skiff_schema_registry`, a skiff schema may be described using one of the two methods below: - A string starting with `$`. Then, the =`$` character will be stripped, and the remaining string will be found in the `skiff_schema_registry` map. A schema is defined as a schema described with a value based on the relevant key. - Map with fields: - (required) `wire_type`: skiff node type in the tree. - (optional) `name`: Node name. - (optional) `children`: Node children. For compound types, this field must be included. A child may either reference the description in `skiff_schema_registry`, or be a map. `skiff_schema_registry` is required to enable the running of operations with a large number of input tables with identical schemas. Example: ```yson < "table_skiff_schemas" = [ "$table1" ]; "skiff_schema_registry" = { "table1" = { "children" = [ { "name" = "uint64_column"; "wire_type" = "uint64" }; { "name" = "int64_column"; "wire_type" = "int64" }; { "name" = "boolean_column"; "wire_type" = "boolean" }; { "name" = "string32_column"; "wire_type" = "string32" }; { "name" = "yson32_column"; "wire_type" = "yson32" } ]; "wire_type" = "tuple" } } > "skiff" ``` # File cache This section provides information on the file cache. ## General information { #common } The file cache enables you to upload a file to [Cypress](https://ytsaurus.tech/docs/en/cypress.md) once and then obtain a path to the file multiple times via a specified [MD5 hash](https://en.wikipedia.org/wiki/MD5). The file cache is required to run operations. When users run tasks, this uploads an executable and all its dependencies. Using Python, for instance, requires loading a large number of modules and dynamic link libraries. To avoid loading identical files multiple times, you only need to read them into the cache and request the path to these files in Cypress using an MD5 hash for any subsequent runs. ## Computing an MD5 hash for uploaded files { #compute_md5 } To request an MD5 hash computation for an uploaded file, send `compute_md5=True` to the `write_file` command. There are some constraints: 1. When using `append`, you cannot request an MD5 computation unless there was a previous write to the file with an MD5 computation. 2. A computed MD5 hash is reset if there is a call to the `concatenate` command and its output is written to the relevant file. ## API commands For working with the file cache, there are the `put_file_to_cache` and the `get_file_from_cache` commands. # Working with the meta information tree This section includes the most popular [Cypress](https://ytsaurus.tech/docs/en/cypress.md) command-line (CLI) code examples. For a more detailed description of the commands and their options, please see the Commands section. ## List { #list } This command displays a list of all keys for the `` node. The node must have type map_node. ### Description of the list command ```bash yt list [-l] [--format FORMAT] [--attribute ATTRIBUTES] [--max-size MAX_SIZE] [--absolute] ``` The `-l` option displays additional attributes (node type, account, amount of space used, most recent change date). ### Calling the list command ```bash yt list -l --max-size 3 //home/dev string_node dev 0 2018-12-22 09:30 some_key map_node dev 1248 2019-01-22 13:14 ifs map_node dev 69856453509 2019-02-26 09:05 zuci42 WARNING: list is incomplete, check --max-size option ``` The `--format` option defines the output data format. Supported formats are `json` and `yson`. For more information, see [Formats](https://ytsaurus.tech/docs/en/formats.md). The `--format` option is incompatible with `-l`. ### Calling list with the format option ```bash yt list --format json //home/dev ["some_key","ifs","zuci42","data_com","andozer","renadeen","is","babenko","ivanashevi","alximik","tsufiev","alxsev","t","psushin"] ``` You can use the `--attribute` option (which you can specify more than once) to request additional attributes for the nodes referenced by the listed keys. `--attribute account`, for instance, will add the account to which the node belongs to each key. You can only use `--attribute` together with `--format`. ### Calling list with the attribute option ```bash yt list --attribute account --attribute owner --format "json" --max-size 3 //home/dev { "$attributes": { "incomplete": true }, "$value": [ { "$attributes": { "account": "dev", "owner": "babenko" }, "$value": "some_key" }, { "$attributes": { "account": "dev", "owner": "ifsmirnov" }, "$value": "ifs" }, { "$attributes": { "account": "dev", "owner": "renadeen" }, "$value": "zuci42" } ] } ``` The `--max-size` option helps limit the number of elements returned. This will display arbitrary elements. ### Calling list with the max-size option ```bash yt list --max-size 3 //home/dev some_key ifs zuci42 WARNING: list is incomplete, check --max-size option ``` The `--absolute` option will display absolute paths. ### Calling list with the absolute option ```bash yt list --absolute --max-size 3 //home/dev //home/dev/some_key //home/dev/ifs //home/dev/zuci42 WARNING: list is incomplete, check --max-size option ``` ## Create { #create } This command creates a node of the type specified in `` in the path specified in ``. The command returns the identifier of the object created. Supported objects are listed in the [Objects](https://ytsaurus.tech/docs/en/objects.md) section. ### Description of create command ```bash yt create [-r, --recursive] [-i,--ignore-existing] [-l,--lock-existing] [-f, --force] [--attributes ATTRIBUTES] ``` The `-r, --recursive` option creates all the intermediate nodes of type `map_node`. ### Calling create with recursive option ```bash yt create --recursive map_node //home/dev/test1/test2/test3 127c1-388f86-3fe012f-6f994b03 ``` The `-i, --ignore-existing` option will not recreate a specified node from scratch if it already exists. In addition, the existing and the requested nodes must be of the same type, otherwise the request will return an error. ### Calling create without the ignore-existing option ```bash yt create map_node //home/dev/test1/test2/test3 Node //home/dev/test1/test2/test3 already exists ``` ### Calling create with the ignore-existing option ```bash yt create --ignore-existing map_node //home/dev/test1/test2/test3 127c1-388f86-3fe012f-6f994b03 ``` The `-l, --lock-existing` option is compatible with `--ignore-existing`. If indicated together, an [exclusive lock](https://ytsaurus.tech/docs/en/transactions.md#locks) will be taken out on the specified node even if it already exists. (In the event of a lock conflict, the command will return an error). Since actually creating a node always takes out an implicit (automatic) lock on the node, the option makes the outcome of the command more predictable when using `--ignore-existing`. The `-f, --force` option helps force-create an object. In the event that the specified node already exists, it is deleted and replaced with a new one. In this situation, the existing node can be of any type. Re-creation changes a node's ID. ### Calling create with the force option ```bash yt create --force map_node //home/dev/test1/test2/test3 127c1-4352be-3fe012f-6acab448 ``` You can use the `--attributes` option to set the attributes of the node being created. ### Calling create with the attributes option ```bash yt create --attributes "{test_attr=test1;}" map_node //home/dev/test1/test2/test3 127c2-1b78-3fe012f-288106fe ``` ### Checking the output of create with the attributes option ```bash yt get //home/dev/test1/test2/test3/@test_attr "test1" ``` ## Remove { #remove } This command deletes a node in the path specified in ``. If it is a composite non-empty node, the command will return an error. ### Description of remove command ```bash yt remove [-r, --recursive] [-f, --force] ``` The `-r, --recursive` option recursively deletes the entire subtree. ### Calling remove without recursive option ```bash yt remove //home/dev/test1 Cannot remove non-empty composite node ``` ### Calling remove with recursive option ```bash yt remove --recursive //home/dev/test1 ``` The `-f, --force` option enables you to ignore the fact that the specified node does not exist. ### Calling remove without the force option ```bash yt remove //home/dev/test1 Node //home/dev has no child with key "test1" ``` ### Calling remove with the force option ```bash yt remove --force //home/dev/test1 ``` ## Exists { #exists } This command helps check for the existence of the `` node passed in to it. ### Description of the exists command ```bash yt exists ``` ### Calling exists on an existing node ```bash yt exists //home/dev/test1/test2/test3 true ``` ### Calling exists on a non-existent node ```bash yt exists //home/dev/test1/test2/test4 false ``` ## Get { #get } This command displays the entire Cypress subtree found in ``. ### Description of the get command ```bash yt get [--max-size MAX_SIZE] [--format FORMAT] [--attribute ATTRIBUTES] ``` `` must exist. ### Calling get on a non-existent node ```bash yt get //home/dev/test1/test2/test4 Error resolving path //home/dev/test1/test2/test4 Node //home/dev/test1/test2 has no child with key "test4" ``` ### Calling get on an existing node ```bash yt get //home/dev/test1/test2 { "test3" = {}; } ``` ### Calling get on a node attribute ```bash yt get //home/dev/test1/test2/@account "dev" ``` The `--max-size` option helps limit the number of nodes returned in the case of virtual composite nodes. {% note warning "Attention!" %} For conventional map nodes, this option is useless. {% endnote %} ### Calling get without the max-size option ```bash yt get //sys/transactions/@type "transaction_map" ``` ### Calling get with the max-size option ```bash yt get --max-size 3 //sys/transactions < "incomplete" = %true; > { "12814-3b9ce8-3fe0001-3b10627" = #; "12817-1cc292-3fe0001-fcdca56f" = #; "1268e-42d32d-3fe0001-d0fc5c4f" = #; } ``` The `--format` option defines the output data format. Supported formats are `json` and `yson`. For more information, see [Formats](https://ytsaurus.tech/docs/en/formats.md). ### Calling get with the format option ```bash yt get --format "json" //home/dev/test1 { "test2": { "test3": { } } } ``` Special-type objects as well as **opaque** nodes (objects with `opaque` attribute set to `%true` ) are displayed as entity. No attributes associated with subtree nodes are displayed by default. But you can request that they be displayed using the `--attribute` option in a manner similar to the `list` command. ### Calling get with the attribute option ```bash yt get --attribute opaque //home/dev/test1/test2 < "opaque" = %false; > { "test3" = < "opaque" = %false; > {}; ``` Some nodes may return as opaque (such nodes will have no additional attributes) if the subtree being walked turns out to be too large. ## Set { #set } The `set` command assigns the specified `value` in [YSON](https://ytsaurus.tech/docs/en/yson.md) format using the specified ``. {% note warning "Attention!" %} One should be aware that if the `` ends with `/@`, the `set` command changes the *attribute dictionary* of the node and not just the attributes mentioned in `value`. In other words, the following two commands are not equivalent: ```bash $ yt set //home/dev/test1/@some_attr test ``` and ```bash $ yt set //home/dev/test1/@ '{"some_attr"="test";}' ``` The former assigns to a specific attribute while the latter modifies the whole attribute dictionary which sets the attributes listed in `value` but also **removes everything not mentioned there.** For this reason, the `set` command should never be used to set multiple attributes at once as doing so may lead to data loss. {% endnote %} ### Description of the set command ```bash yt set [--format FORMAT] [-r, --recursive] [-f, --force] ``` ### Calling set ```bash yt set //home/dev/test1/@some_attr test ``` ### Verifying a set call ```bash yt get //home/dev/test1/@some_attr "test" ``` The `--format` option defines the `value` format. Supported formats are `json` and `yson`. For more information, see [Formats](https://ytsaurus.tech/docs/en/formats.md). ### Calling set with the format option ```bash yt set --format json //home/dev/test1/test2/test4 '{"some_test_key":"some_test_value"}' ``` ### Calling set without the format option ```bash yt get //home/dev/test1/test2/test4 { "some_test_key" = "some_test_value"; } ``` The `-r, --recursive` option helps create all the non-existent intermediate nodes in a path. ### Calling set without the recursive option ```bash yt set //home/dev/test1/test2/test3 some_test_value Node //home/dev has no child with key "test1" ``` ### Calling set with the recursive option ```bash yt set --recursive //home/dev/test1/test2/test3 some_test_value ``` The `-f, --force` option modifies any Cypress nodes rather than just attributes and documents. ## Copy, Move { #copy_move } This command copies/moves a node from `` to ``. When copying files or tables, chunks are not physically copied, and all changes occur at the metadata level. {% note warning "Attention!" %} A move first copies and then deletes a source node, thereby changing the IDs of all the objects in the subtree. {% endnote %} ### Description of copy/move command ```bash yt copy/move [--preserve-account | --no-preserve-account] [--preserve-expiration-time] [--preserve-expiration-timeout] [--preserve-creation-time] [-r, --recursive] [-f, --force] ``` The `--preserve-account` option helps keep the account of the node being copied instead of using the account of the destination node's parent folder. The `--preserve-expiration-time` option helps keep the specified [delete time](https://ytsaurus.tech/docs/en/cypress.md) of the node being copied. The `--preserve-expiration-timeout` option helps keep the specified [delete interval](https://ytsaurus.tech/docs/en/cypress.md) of the node being copied. The `--preserve-creation-time` option helps keep the [create time](https://ytsaurus.tech/docs/en/cypress.md) of the node being copied. **For copying only.** ### Verifying an account ```bash yt get //home/dev/test1/test_file/@account "dev" ``` ### Calling the copy command ```bash yt copy //home/dev/test1/test_file //home/dev/test1/test2/test_file_new yt get //home/dev/test1/test2/test_file_new/@account "tmp" ``` ### Calling copy with the preserve-account option ```bash yt copy --preserve-account //home/dev/test1/test_file //home/dev/test1/test2/test_file_new_p yt get //home/dev/test1/test2/test_file_new_p/@account "dev" ``` The `-r, --recursive` option controls behavior if the intermediate nodes in `` do not exist. If this option is enabled, these nodes will be created recursively. Otherwise, the command will return an error. ### Calling copy without the recursive option ```bash yt copy //home/dev/test1/test_file //home/dev/test1/test2/test3/test_file_new Node //home/dev/test1/test2 has no child with key "test3" ``` ### Calling copy with the recursive option ```bash yt copy --recursive //home/dev/test1/test_file //home/dev/test1/test2/test3/test_file_new ``` The `-f, --force` option makes the command run even if the destination node exists. This deletes the existing node. ### Calling copy without the force option ```bash yt copy //home/dev/test1/test_file //home/dev/test1/test2/test_file_new Node //home/dev/test1/test2/test_file_new already exists ``` ### Calling copy with the force option ```bash yt copy --force //home/dev/test1/test_file //home/dev/test1/test2/test_file_new ``` ## Link { #link } This command creates a [symbolic link](https://ytsaurus.tech/docs/en/links.md) in `` to an object found in ``. ### Description of the link command ```bash yt link [-r, --recursive] [-i, --ignore-existing] [-l, --lock-existing] [-f, --force] ``` The `-r, --recursive` option controls behavior if the intermediate nodes in `` do not exist. If this option is enabled, these nodes will be created recursively. Otherwise, the command will return an error. ### Calling link without the recursive option ```bash yt link //home/dev/test1/test_file //home/dev/test1/test2/link_test_file Node //home/dev/test1 has no child with key "test2" ``` ### Calling link with the recursive option ```bash yt link --recursive //home/dev/test1/test_file //home/dev/test1/test2/link_test_file ``` The `-i, --ignore-existing` option will not recreate a specified link from scratch if it already exists. ### Calling link without the ignore-existing option ```bash yt link //home/dev/test1/test_file //home/dev/test1/test2/link_test_file Node //home/dev/test1/test2/link_test_file already exists ``` ### Calling link with the ignore-existing option ```bash yt link --ignore-existing //home/dev/test1/test_file //home/dev/test1/test2/link_test_file ``` The `-l, --lock-existing` option guarantees that an [exclusive lock](https://ytsaurus.tech/docs/en/transactions.md#locks) will be taken out on the link even if it already exists. (Only useful with `--ignore-existing`; if a lock cannot be set, the command will return an error). The `-f, --force` option makes the command run even if the destination node exists. This deletes the existing node. ## Concatenate { #concatenate } The `concatenate` command combines data from the nodes in `` into a node in ``. All the nodes must exist and belong to the same type by being either tables or files. {% note warning "Attention!" %} The data is combined at the meta information level, that is, only on the master-server. {% endnote %} ### Description of the concatenate command ```bash yt concatenate --src --dst ``` {% note warning "Attention!" %} The running time of this request is a function of the (total) number of chunks making up the source nodes. At the same time, many client libraries have a limitation on the maximum command running time, normally on the order of tens of seconds. Therefore, concatenate is only worth using if you are certain that there are few chunks, about 10,000, in the output paths. Otherwise, the Merge operation is to be used. It will not perform better (and may take many minutes for tables of millions of chunks) but a client will track its operation by periodically requesting its status which removes virtually all constraints on the time it takes to run. {% endnote %} # Examples of Cypress object processing This section provides examples of Cypress object processing. ## Static tables { #static_tables } ### Creating { #create_table } Use the `create` command to create a table. ```bash yt create table //home/dev/test_table 1282c-1ed72c-3fe0191-443cf2ee ``` ### Deleting { #remove_table } Use the `remove` command to delete a table. ```bash yt remove //home/dev/test_table ``` ### Reading { #read_table } To read an existing table named ``, use the `read-table` command. ```bash yt read-table [--format FORMAT] [--table-reader TABLE_READER]
``` The `--format` option defines the output data format. The supported formats are `json`, `yson`, `dsv`, and `schemaful_dsv`. For more information, see [Formats](https://ytsaurus.tech/docs/en/formats.md). ```bash yt read-table --format dsv //home/dev/test_table day=monday time=10 day=wednesday time=20 day=friday time=30 ``` The `--table-reader` option modifies the table read settings. #### Sampling { #sampling_rate } The `sampling_rate` attribute sets the percentage of data that must be read from an input table. ```bash yt read-table --format dsv --table-reader '{"sampling_rate"=0.3}' //home/dev/test_table day=monday time=10 day=friday time=30 ``` In this example, `read-table` will return 30% of all the rows in the input table. The `sampling_seed` controls a random number generator that selects the rows. It guarantees the same output for the same `sampling_speed` and collection of input chunks. If unspecified, the `sampling_seed` attribute will be random. ```bash yt read-table --format dsv --table-reader '{"sampling_seed"=42;"sampling_rate"=0.3}' //home/dev/test_table ``` To place sampling output in a different table, please run `map`: ```bash yt map cat --src //home/dev/input --dst //home/dev/output --spec '{job_io = {table_reader = {sampling_rate = 0.001}}}' --format yson ``` {% note info "Note" %} The specified `sampling_rate` attribute notwithstanding, sampling reads all data from disk. {% endnote %} ### Overwriting { #write_table } The `write-table` command overwrites an existing table called `
` with the data transmitted. ```bash yt write-table [--format FORMAT] [--table-writer TABLE_WRITER]
``` The `--format` option defines the input data format. The supported formats are `json`, `yson`, `dsv`, and `schemaful_dsv`. For more information, see [Formats](https://ytsaurus.tech/docs/en/formats.md). ```bash yt write-table --format dsv //home/dev/test_table time=10 day=monday time=20 day=wednesday time=30 day=friday ^D ``` To add records to a table, use the `` option before the table path. ```bash cat test_table.json {"time":"10","day":"monday"} {"time":"20","day":"wednesday"} {"time":"30","day":"friday"} cat test_table.json | yt write-table --format json ""//home/dev/test_table ``` The `--table-writer` option modifies the table write settings: * [Limit on table row size.](https://ytsaurus.tech/docs/en/#max_row_weight) * [Chunk size.](https://ytsaurus.tech/docs/en/#desired_chunk_size) * [Replication factor.](https://ytsaurus.tech/docs/en/#replication_factor) ##### Limit on table row size { #max_row_weight } When writing table data, the YTsaurus system checks its size. A write will return an error if the size is greater than the maximum legal value. By default, maximum row size is 16 MB. To modify this value, use the `--table-writer` option's `max_row_weight` parameter. Enter a value in bytes. ```bash cat test_table.json | yt write-table --format json --table-writer {"max_row_weight"=33554432} //home/dev/test_table ``` {% note info "Note" %} `max_row_weight` cannot be greater than 128 MB. {% endnote %} #### Chunk size { #desired_chunk_size } The `desired_chunk_size` defines chunk size in bytes. ```bash cat test_table.json | yt write-table --format json --table-writer {"desired_chunk_size"=1024} //home/dev/test_table ``` #### Replication factor { #replication_factor } You can control the replication factor for new table chunks through the `min_upload_replication_factor` and the `upload_replication_factor` attributes. `upload_replication_factor` sets the number of synchronous replicas created when writing new data to a table. `min_upload_replication_factor` sets the minimum number of successfully written chunks. Both attributes have a default value of 2 and a maximum value of 10. ```bash cat test_table.json | yt write-table --format json --table-writer '{"upload_replication_factor"=5;"min_upload_replication_factor"=3}' //home/dev/test_table ``` To increase the number of replicas of an existing table, use one of the methods below: - Start a Merge operation: ```bash yt merge --mode auto --spec '{"force_transform"=true; "job_io"={"table_writer"={"upload_replication_factor"=5}}}' --src <> --dst <> ``` - Increase the table's `replication_factor` attribute. This will perform the conversion in the background at a later point in time. New records added to the table will have a replication factor set by the `upload_replication_factor` attribute. Subsequently, a background process will asynchronously replicate chunks to achieve the replication factor defined for the specific table. ```bash yt set //home/dev/test_table/@replication_factor 5 ``` ### Medium { #medium } To move a table to a different medium, change the value of the `primary_medium` attribute. New data written to the table will be delivered directly to the new medium while old data will move in the background. To force-move data to a new medium, start a Merge: ```bash yt set //home/dev/test_table/@primary_medium ssd_blobs yt merge --mode auto --spec '{"force_transform"=true;}' --src //home/dev/test_table --dst //home/dev/test_table ``` To check whether a table's medium has changed, run the command below: ```bash $ yt get //home/dev/test_table/@chunk_media_statistics { "ssd_blobs" = { "chunk_count" = 2126; "uncompressed_data_size" = 9667220402266; "compressed_data_size" = 4954465956017; "data_weight" = 10764306825793; "max_block_size" = 6584787; }; } ``` The amount is shown in bytes. # Batch processing of requests This section contains information about batch processing of requests (batch requests), with usage examples for the [Python](https://ytsaurus.tech/docs/en/batch-requests.md#python_api) and [C++](https://ytsaurus.tech/docs/en/batch-requests.md#c_plus_plus_api). When working with the YTsaurus system, each command generates a separate request to the master server. A request has its own cost, often higher than the execution of a command. Therefore, combining several commands into a single request can significantly speed up processes that send many easy commands to [Cypress](https://ytsaurus.tech/docs/en/cypress.md) and are waiting for a response most of the time. This request is called a **batch request**. The Cypress master server will execute the commands from the batch request in random order and return all the obtained results. Errors that occur during the execution of individual commands do not affect other commands. ## Concurrency A batch request on the Cypress master server is executed as a set of independent commands, the specific command execution order is not guaranteed. All commands are counted in the user command quota. A batch request has the `concurrency` parameter which regulates the concurrency of commands. The parameter value limits the number of concurrently running commands, thus enabling you not to exceed the allowable number of concurrent requests to the master server for a user. Otherwise, the `User 'user' has exceeded its request queue size limit` error will be returned in response to the request. The default value of the parameter is 50 (concurrently running commands) and the user has a limit of 100, which helps avoid errors involving exceeding the limit. Exceeding the limit is not a fatal error. If it occurs, we recommend sending a request again. If you use high-level SDKs, the request will be sent again automatically. ## Python API { #python_api } In the Python API, there is a `create_batch_client()` method that enables you to create a client to combine multiple commands into a single batch request (`client.create_batch_client()`) if you have a simple client (`client = yt.YtClient()`). A usage example is shown in Listing 1: ```python import yt.wrapper as yt if __name__ == "__main__": client = yt.YtClient() batch_client = client.create_batch_client() list_rsp = batch_client.list("/") exists_rsp = batch_client.exists("/") batch_client.commit_batch() print list_rsp.get_result() print exists_rsp.get_result() #['cooked_logs', 'home', 'logs', 'projects', 'statbox', 'sys', 'tmp', 'user_sessions', 'userdata', 'userfeat', 'userstats'] #true ``` As a result of executing the request using batch_client, a special object of the `BatchResponse` type, which has the structure shown below, is returned. ```python class BatchResponse(object): ... def get_result(self): ... def get_error(self): ... def is_ok(self): ... ``` You cannot specify the [format](https://ytsaurus.tech/docs/en/formats.md) of the returned data by a separate command via `batch_client`. The reason is that the format of the returned data is the same for the entire batch request. ## C++ API { #c_plus_plus_api } In the C++ API, to execute a batch request, you need to: - Create an object of the `TBatchRequest` type. - Use it to specify commands (the `TBatchRequest` methods return `TFuture` to the appropriate type). - Pass it to the `ExecuteBatch` method of the client. - Get the results from the previously obtained `TFuture`. However, if some commands were completed with an error, you can find this out by calling `future.GetValue` or `future.HasException` (i.e. calling `ExecuteBatch` will not return an error). A usage example is given below. ```c++ #include #include using namespace NYT; int main() { auto client = CreateClient(); TBatchRequest batchRequest; auto listRsp = batchRequest.List("/", TListOptions().MaxSize(5)); auto existsRsp = batchRequest.Exists("//tmp"); client->ExecuteBatch(batchRequest); for (auto item : listRsp.GetValue()) { Cerr << NodeToYsonString(item) << Endl; } Cerr << NodeToYsonString(existsRsp.GetValue()) << Endl; return 0; } /* Output of program: "tmp" "projects" "logs" "userfeat" "cooked_logs" %true */ ``` # FAQ #### **Q: Can I put an arbitrary YSON structure in a table cell?** {#structure-in-cell} **A:** Yes. You can use columns of type `any`. ------ #### **Q: Can I modify a table's schema?** {#change-table-schema} **A:** Yes. You can find examples and limits in the [Table schema](https://ytsaurus.tech/docs/en/static-schema.md#create) section. ------ #### **Q: How large are the files the system supports?** {#file-size-limit} **A:** The formal limit on file length is just a 64-bit integer. In actual fact, it is the amount of free space on the cluster's nodes. Since a file is broken down into chunks, it does not have to be stored on a single node. Therefore, it can be larger than the size of a single hard drive. A single job cannot process files this large; therefore, the relevant activity will boil down to using the read_file command and reading by ranges. ------ #### **Q: How do I move a table from an HDD to an SSD?** {#move-table} **A:** You can change medium type for a table or a file by editing the table's primary medium via the `primary_medium` attribute. Before modifying this attribute, you need to unmount your table (and remount it when done to return things to the way they were). For example: ```bash yt unmount-table --sync //home/path/to/table yt set //home/path/to/table/@primary_medium ssd_blobs yt mount-table --sync //home/path/to/table ``` Immediately after you set the attribute, new data will start writing to an SSD while old data will be moved in the background. For more information about controlling this process and tracking its progress, please see the section on [static tables](https://ytsaurus.tech/docs/en/static-tables.md#medium). ------ #### **Q: What do I do if table reads are slow?** {#slow-table-read} **A:** There is a [dedicated page](https://ytsaurus.tech/docs/problems/slow-read.md) about this. ------ #### **Q: How do I reduce the number of chunks I am using in my quota?** {#reduce-chunk-usage} **A**: If these chunks are taken up by tables (which is the most common scenario), you need to run a [Merge](https://ytsaurus.tech/docs/data-processing/operations/merge.md) with `combine_chunks = %true`. This will rebuild your table from larger chunks, thereby reducing the use of chunks within your quota. You can use a command-line command to run the operation replacing `src table` and `dst table`: ``` yt merge --src table --dst table --spec "{combine_chunks=true}" ``` There is also a way of monitoring chunk usage without running a separate operation. For more information, see Merging chunks automatically on operation exit. Also, files may use many chunks in certain situations. For instance, when small fragments are being continuously appended to existing files. At the moment, there no ready-made method similar to Merge to combine file chunks. You can run `yt read-file //path/to/file | yt write-file //path/to/file` in combination. This will make the entire data stream go through the client. ------ #### **Q: I am getting the Format "YamredDsv" is disabled error. What should I do?** {#yamreddsv-disabled} **A:** The `YAMRED_DSV` format is no longer supported. Use a different [format](https://ytsaurus.tech/docs/en/formats.md#formaty-predstavleniya-tablichnyh-dannyh). # Dynamic tables Dynamic tables are a type of table in YTsaurus that implement an interface to read and write data by key. They support transactions and their own SQL dialect. Dynamic tables add the capabilities of typical key-value storage to the YTsaurus system. Elementary operations with such tables are writing, deleting, and reading rows by key. You can also execute SQL queries to work with row ranges. Dynamic tables support [MVCC](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) for transactions. Working with them does not require communication with the YTsaurus master server, which provides additional scalability. The closest analogs to dynamic tables are: * [Apache HBase](http://hbase.apache.org): In terms of data storage organization. * [Spanner](https://ai.google/research/pubs/pub39966): In terms of transaction implementation. * [Impala](http://impala.apache.org): In terms of the query calculation model. The similarities with these systems will be described below. ## Types of dynamic tables { #types } Dynamic tables can be [sorted](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md) and [ordered](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md). In addition, a replication mechanism is implemented for each table type. For more information about replication, see [replicated dynamic tables](https://ytsaurus.tech/docs/en/replicated-dynamic-tables.md). ## Architecture { #architecture } Architecturally, the way dynamic tables are stored in YTsaurus uses the [Log-structured merge tree](https://en.wikipedia.org/wiki/Log-structured_merge-tree) idea. This makes it similar to [BigTable](http://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf) and [HBase](https://hbase.apache.org/). To ensure fault tolerance and consistency, the service uses the Hydra state replication library ([Raft](https://raft.github.io/raft.pdf) analog). Similar to [Google Spanner](https://research.google/pubs/pub39966/), it implements [two-phase commits](https://en.wikipedia.org/wiki/Two-phase_commit_protocol) to support distributed transactions. For more information about the data architecture and model, see separate sections about [sorted](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md) and [ordered](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md) dynamic tables. ## Attributes { #attributes } Besides the attributes inherent to all tables, dynamic tables have a number of unique attributes. Some of them are detailed in the following table: | **Name** | **Type** | **Description** | | ---------- | -------- | ------------- | | chunk_row_count | Int | Total number of non-combined rows in all table chunks. | | tablets[*](https://ytsaurus.tech/docs/en/**) | TabletInfo | Tablet description. | | atomicity | Atomicity | [Atomicity mode](https://ytsaurus.tech/docs/en/transactions.md#atomicity). By default: `full`. | | in_memory_mode | InMemoryMode | [In-memory mode](https://ytsaurus.tech/docs/en/#in_memory) for tables. By default: `none`. | The `chunk_row_count` attribute can be used to evaluate the table size, but the evaluation is approximate for the following reasons: - It does not take into account the rows that were recently written and have not yet been flushed into chunks, residing only in nodes memory. - Different versions of a single row can be counted multiple times until corresponding chunks are combined. - Rows may exist but be marked as deleted or aged. Individual attributes of a dynamic table affect its behavior. In particular, you can use individual attributes to: - [Set up background compaction](https://ytsaurus.tech/docs/en/compaction.md#attributes). - [Set TTL](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md#remove_old_data): Time to live for individual values. - [Manage automatic sharding](https://ytsaurus.tech/docs/en/tablet-balancing.md). ## Limitations { #limitations } A number of size and content type limitations are imposed on rows and the schema of a dynamic table: - The number of columns in a dynamic table cannot exceed 1024, of which no more than 32 can be key columns. - The maximum length of the `String` value in a dynamic table is 16 megabytes. Besides that, dynamic tables inherit the [limitations](https://ytsaurus.tech/docs/storage/static-tables.md#limitations) of static tables. ## In-memory tables (in-memory) { #in_memory } Accessing data in RAM is much faster than accessing data on the disk. In the YTsaurus system, you can set up a table so that its data is permanently stored in memory. This drastically increases the speed of table operations. This method has certain limitations: - The RAM capacity on all cluster nodes is limited, so this option should only be used when really needed. - Any sorted dynamic table can be switched to in-memory mode, as well as taken out of this mode. Run `remount_table` to make the changes take effect. - The use of dynamic in-memory tables does not affect the levels of isolation, consistency, or fault tolerance. The data of such tables is still stored in chunks of the distributed storage: in-memory mode only means that in addition to being stored on the disk, the table data is also stored in RAM. - When in-memory mode is enabled, data is uploaded into the cluster node memory. This may take some time. In-memory mode is configured using the `in_memory_mode` attribute for the table. Possible attribute values: | **Mode** | **In-memory mode** | **Description** | | --------- | ------------------- | ------------ | | None | Disabled | Table data is written to chunks and their blocks, compressed and uncompressed, and cached on a common basis. This mode is suitable for large tables that cannot be fixed in memory. | | Compressed | Enabled | The compressed table data — the contents of the chunk blocks — is permanently present in memory, but the data blocks must be unpacked for read accesses. Unpacked blocks are cached on a common basis. This mode is a good combination in terms of speed-to-memory capacity ratio. | | Uncompressed | Enabled | Uncompressed table data is permanently present in memory, no disk accesses or data unpacking are needed for read accesses. This mode provides maximum performance at the cost of RAM. | ### Hash tables to read by key The read by key operation searches for the table data keys using a binary search. You can achieve some acceleration by building hash tables for the keys. To do this, set the `enable_lookup_hash_table = %true` attribute for the table. Hash tables only make sense in `uncompressed` mode and they are not allowed in other modes. ## Table storage formats { #formats } The YTsaurus system offers several ways of table data storage in chunks. Switching between them is managed by the `optimize_for` attribute on the table. This attribute can be changed on an existing table, but the effect will only be noticeable for newly created chunks. Taking into account compaction, old data can also be recompressed in background mode. If the table is already mounted, run the `remount_table` command to inform the tablets of the format change. Possible values of the `optimize_for` attribute: - `lookup`: The format, which enables rows to be efficiently read from a table by individual keys. - `scan`: The `scan` format uses column-by-column storage, as well as adaptive column-by-column compression algorithms. The vertical data partitioning method is less suitable for random reads by keys, because more blocks will need to be read and more CPU performance will be required. ## Access permissions { #acl } The access control system applies to dynamic tables: - To read data from a dynamic table (using the `select_rows` and `lookup_rows` queries), the user must have the `read` permission for the table. - To write data to a dynamic table (using the `insert_rows` and `delete_rows` commands), the user must have the `write` permission for the table. - To run the `mount_table`, `unmount_table`, `remount_table`, and `reshard_table` commands, the user must have the `mount` permission for the table. - Similar to accounts, in order to create tables linked to a bundle, the user must have the `use` permission for the bundle. The same rule is required to change the table bundle. ## Useful links { #links } Webinars: - [Dynamic tables overview, part 1](https://www.youtube.com/watch?v=vxKD0lLS4mM) (in Russian) - [Dynamic tables overview, part 2](https://www.youtube.com/watch?v=JZGTUjyJw38) (in Russian) [**]: The parameter appears in the answer several times. # Concepts ## Tablets { #tablets } For each table, the key space is divided by a set of boundary keys into non-overlapping ranges — tablets. Table tablets are listed in the `tablets` attribute. Each tablet is described by the `TabletInfo` structure: | **Name** | **Type** | **Description** | **Mandatory** | | -------- | --------- | -------------- | -----------------| | tablet_id | Guid | Tablet ID | Yes | | statistics | TabletStatistics | Tablet statistics | Yes | | state | String | State (mounted/unmounted/frozen/mounting/unmounting/freezing/unfreezing/frozen_mounting) | Yes | | pivot_key | List | Boundary key where the tablet starts (for sorted tables only) | Yes | | cell_id | Guid | ID of the cell to which the tablet is mounted | No | {% cut "Example of tablet statistics" %} ```yson { "chunk_count" = 1; "compressed_data_size" = 324; "disk_space" = 2502; "disk_space_per_medium" = { "default" = 2502; }; "dynamic_memory_pool_size" = 0; "hunk_compressed_data_size" = 0; "hunk_uncompressed_data_size" = 0; "memory_size" = 0; "overlapping_store_count" = 1; "partition_count" = 1; "preload_completed_store_count" = 0; "preload_failed_store_count" = 0; "preload_pending_store_count" = 0; "store_count" = 1; "tablet_count" = 1; "tablet_count_per_memory_mode" = { "none" = 1; "compressed" = 0; "uncompressed" = 0; }; "uncompressed_data_size" = 538; "unmerged_row_count" = 2; } ``` {% endcut %} Each tablet logically consists of a set of sorted data chunks and a special memory area called `dynamic store`. Chunks are stored in a replicated way in a blob repository ^[storage of large amounts of unstructured data] and overlap in key ranges. When data is written, it goes into the `dynamic store` and is stored there. When the data reaches the amounts specified in the configuration, it is written to the disk as a chunk. When the number of chunks within a tablet becomes large enough, some of them are combined — compaction is performed. At this moment, old and irrelevant data may be deleted. For more information, see [Compaction](https://ytsaurus.tech/docs/en/compaction.md). A list of all tablets of all dynamic system tables is available at `//sys/tablets`. ## Boundary keys { #keys } To change a table tablet set, use the `reshard_table` command. When changing the number of tablets of sorted dynamic tables, boundary keys must be specified. Tablet boundary keys are random sequences of values, not necessarily of the same length as the key columns. One of the options is to take a hash of some subsequent key components in order to distribute the data evenly across the key space. Then the boundary keys can be sequences of length 1, consisting of numbers that evenly divide the entire interval of hash function values. The boundary key of the first table tablet always equals the empty sequence — effective minus infinity — and cannot be changed. ## Partitions { #partitions } Tables in dynamic tables are divided into partitions to ensure parallel processing of queries within the cluster node. A typical partition size is several hundred megabytes. If the chunks become larger than the partition size, a background process is started that forces the chunks to be split into smaller chunks. ## Tablet cells { #tablet_cells } Tablets are maintained by entities called tablet cells. Each cell is a complex automaton whose state is replicated by the `Hydra` subsystem. Each cluster node has so-called tablet slots — virtual positions into which cells can fall. Cells in the cluster are created by the administrator during initial setup. The total number of cells must be close to the total number of tablet slots on the cluster nodes, but with a certain margin to account for the possibility of a certain number of cluster nodes failing. Each tablet cell belongs to a certain bundle. The name of the bundle in which the cell is created is specified in the `tablet_cell_bundle` attribute of a specific table or one of the parent directories. By default, tablet cells are created in the `default` bundle. A list of all cells is available at `//sys/tablet_cells`. Each cell maintains changelogs (mutation logs) and sometimes records state snapshots. The logs go into the `//sys/tablet_cells//changelogs` directory and the snapshots go into the `//sys/tablet_cells//snapshots` directory as files. The master server automatically selects a specific cluster node and an available tablet slot on that node to serve the tablet cell. If the cluster node serving the cell fails, after some time the master server will select a free slot on another node and start the cell on that node. Tablet cells have the attributes indicated in the table: | **Name** | **Type** | **Description** | | ---------- | ---------- | ------------------ | | id | Guid | Cell ID. | | peers | Dict | Dict containing the name of the cluster node serving the cell and the status of the cell. | | tablet_cell_bundle | String | Name of the tablet cell bundle to which the cell belongs. | | status | Dict | Cell state, decommission flag. | | total_statistics | Dict | Various tablet cell statistics. | {% cut "Cell state example" %} ```yson { "health" = "good"; "decommissioned" = %false; } ``` {% endcut %} ## Tablet cell bundles { #tablet_cell_bundles } Cells are usually created in groups with the same settings. Bundles are needed to combine cells on the basis of common settings. In the current implementation, bundles are the only reliable way to isolate the load, provided that the bundles are properly distributed across the cluster nodes. Bundles are identified by string names. The list of bundles in the system is available at `//sys/tablet_cell_bundles`. In the system, there is always a built-in and non-removable bundle with the `default` name. Bundle settings are specified in the `options` attribute when it is created and can also be changed later. Changing the settings in the `options` attribute requires unmounting all the bundle tables and recreating all the cells. Each table, including a static one, has the `tablet_cell_bundle` attribute where the name of the bundle to which cells this table will be mounted is stored. For more information about mounting, see [Table mounting](https://ytsaurus.tech/docs/en/operations.md#mount_table). Each bundle has the `node_tag_filter` attribute which is used to select the cluster nodes for tablet cells in the given bundle. The filter is a logical expression with the operators: `&` — logical "and", `|` — logical "or", `!` — logical "no" and round brackets. For example, `foo | (bar & !baz)`. The variables in the expression are the possible tag values. If the cluster node has the specified tag, the variable in the formula takes the `true` value; if not, it takes the `false` value. Tablet cells will only fall onto those cluster nodes for which the entire expression takes the `true` value. Tablet cell bundles have the attributes indicated in the table: | **Name** | **Type** | **Description** | | ------------------- | ------------------- | -------------------------------------- | | name | String | Bundle name. | | options | TabletCellOptions | Settings of all tablets of this bundle. | | tablet_cell_count | Int | Number of cells in a bundle. | | tablet_cell_ids[*](https://ytsaurus.tech/docs/en/**) | Guid | IDs of cells in a bundle. | The state of the bundle, the state of the tablet cells, and the list of cluster nodes included in the bundle are displayed on the Tablet cells tab. [**]: The parameter appears in the answer several times. # Operations with dynamic tables ## Mounting { #mount_table } When reading or writing to a table, the client uses the metadata cache to understand which tablets are affected. In order to be able to execute queries, the relevant tablets must be mounted. Once created, the tablet is not mounted and is in the `unmounted` state. To mount the tablet, use the `mount-table` command. The state of the tablets engaged will change from `unmounted` to `mounting` and then to `mounted`. When mounting a tablet, YTsaurus selects the tablet cell that will serve this tablet. You can specify such a cell in the arguments of the `mount-table` command or allow the system to do it. In the current implementation, the system selects a random functional cell, but future versions may improve the algorithm and balance it more accurately taking the load into account. You can unmount a tablet using the `unmount-table` command. The tablet will change its state to `unmounting` and then to `unmounted`. An unmounted tablet cannot handle write or read queries. To reshard tablets, you must first unmount them. To unmount a tablet: - Wait until all write transactions are complete and do not allow any new ones. - Wait for all data from dynamic_store memory to be written to chunk_store chunks. Unmounting may take some time. You can perform forced unmounting (the `force` option), which works instantly but may result in data loss and lags of some two-phase commits. ## Freezing { #freeze } In addition to the `mount_table` and `unmount_table` commands, YTsaurus has close analogs: the `freeze_table` and `unfreeze_table` commands. The `freeze_table` command converts the table tablets — all or part of them — into a special `frozen` state. You can read data from these tablets, but you cannot write to them. The `unfreeze_table` command returns "frozen" tablets to their normal `mounted` state. Transition from `mounted` to `frozen`, in addition to locking writes, also causes all data accumulated in memory to be written as chunks to the disk. This process is not instantaneous and may take minutes. While the data is being written to the disk, the state of the tablet will be `freezing`. Freezing a dynamic table is useful for making a copy of it, while the table remains readable. ## Copying dynamic tables { #copy } If all tablets of the dynamic table are unmounted or frozen, the table can be copied as an ordinary static table using the `copy` command. The copy of the table will use the same set of chunks as the original one. As the state of these tables starts changing, the number of common chunks will start decreasing in the process of compaction. The disk space consumed by these tables will be equal to the sum of their volumes. # Sorted dynamic tables This section describes the structure of sorted dynamic tables and the actions that can be performed with these tables. ## Data model { #model } Each sorted dynamic table is a set of rows ordered by key. As with static tables, the key can be composite: it can consist of several columns. Unlike static tables, the key in a sorted dynamic table is unique. Sorted dynamic tables must be strictly schematic: all column names and types must be specified in the schema in advance. ## Supported operations { #methods } ### Creating To create a sorted dynamic table, run the `create table` command and specify the schema and `dynamic=True` setting in the attributes. The schema must correspond to the schema of the sorted table. CLI ```bash yt create table //path/to/table --attributes \ '{dynamic=%true;schema=[{name=key;type=string;sort_order=ascending}; {name=value;type=string}]}' ``` {% note warning "Attention" %} You must specify at least one key column in the table schema. If this is not done, the table will be successfully created, but it will [ordered](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md) rather than sorted. Most types of select queries will work for the table. But all such queries will come down to a full scan of the data. {% endnote %} ### Changing a table schema and type You can change the schema of an existing dynamic table using the `alter-table` command. For the command to be successful, the table must be [unmounted](https://ytsaurus.tech/docs/en/overview.md#mount_table), and the new schema must be compatible with the old one. There are no changes to the data written to the disk, because the old data is suitable for the new schema. Use `alter-table` to convert a static table into a dynamic table. For more information, see [MapReduce for dynamic tables](https://ytsaurus.tech/docs/en/mapreduce.md#convert_table). ### Reading rows The client can read rows by a given key using the `lookup` method. To do this, you need to specify the table name, a timestamp defining the slice of data to be read (`<= t`), and the names of the columns that interest you. This is a point query: it requires all key components to be specified. There is a type of API call that allows multiple rows to be read in a single query by different keys. Reads within an atomic transaction will use the transaction start timestamp. You can also indicate a specific numeric value, such as an approximation of the physical moment in time, as well as one of two special values: - `sync_last_committed`: Read the latest version guaranteed to contain all modifications of already committed transactions. - `async_last_committed`: Read the latest version if possible, but returning data with an unspecified, small delay (typically tens of milliseconds) is allowed. The `async_last_committed` label can be used when read consistency is not required. This mode may work faster when there is competition between reads and two-phase commits. With a two-phase commit, the table rows are locked in a special way until the second stage starts for the transaction and the commit label is selected. Readers wanting the most recent data have to wait for the first phase to end, because before then it is not known whether the transaction will be successful. The `sync_last_committed` and `async_last_committed` labels do not guarantee a consistent data slice. The data seen by the query reader can correspond to different points in time, either at the level of entire rows or at the level of individual row columns. A specific timestamp must be indicated for consistent reading across the entire table or for a set of tables. For a table with non-atomic changes, `sync_last_committed` and `async_last_committed` modes are equivalent, since no two-phase commit occurs. ### Running a script The system understands an [SQL-like dialect](https://ytsaurus.tech/docs/en/dyn-query-language.md), which can be used to select and aggregate volumes of data in millions of rows in real time. As with the row read operation by key, a timestamp can be specified in the query, relative to which the query must be executed. Since the data in the system is actually sorted by a set of key columns, the system uses this property to reduce the amount of reading at runtime. Specifically, the user's query is analyzed and `key ranges`, ranges in key space whose combination covers the entire search area, are inferred from it. This enables you to execute `range scan` with a single select query. For more information, see [Query language](https://ytsaurus.tech/docs/en/dyn-query-language.md). When building the query, remember that if the system fails to infer non-trivial `key ranges`, `full scan` of the data will occur. `Full scan` occurs, for example, if you specify `key1, key2` as key columns and set filtering only by `key2`. ### Limiting the read time range { #retention-timestamp } When reading a sorted dynamic table, you can specify the `retention_timestamp` option — the inclusive lower bound of the time range. If the `timestamp` option is also specified, the result is built from value versions in the range `[retention_timestamp; timestamp]`. The `retention_timestamp` value can’t be greater than `timestamp`. You can’t combine the `retention_timestamp` option with versioned lookup. The bound is applied to each column separately. If one column was written before `retention_timestamp` and another after, the first will contain `null` and the second will contain the written value. If no values remain in a row after applying the bound, the row is considered absent. By default, `lookup_rows` doesn’t return such a row; with the `keep_missing_rows` option, `null` is returned instead. `retention_timestamp` affects only the read result and doesn’t delete old versions. Physical data deletion is controlled by [TTL settings](https://ytsaurus.tech/docs/en/#remove_old_data). {% note warning "Warning" %} For aggregating columns, the semantics of `retention_timestamp` are **undefined**. Values before the bound don’t participate in aggregation, and compaction may pre-merge versions on different sides of the bound. As a result, the read result for an aggregating column before and after compaction may differ. **Don’t use this option to read the tail of changes for an aggregating column.** {% endnote %} ### Inserting a row { #insert_rows } You can insert data using the `insert_rows` method within an active transaction. To do this, you must provide the rows to write. Each such row must include all key fields. Some data fields specified in the schema may be missing. Semantically, if a row with the specified key doesn’t exist in the table, it’s created. If a row with that key already exists, some columns are overwritten. When specifying only some fields, there are two modes: - `overwrite` (default) — all unspecified fields are updated to `null`. - `update` — enabled with the `update == true` option. In this case, the previous value is preserved. In this mode, you must provide all columns marked with the `required` attribute. {% note info "Note" %} When reading and executing an SQL query, the data is visible only when the transaction starts. Changes written within the same transaction are not available for reading. {% endnote %} ### Deleting rows Within the transaction, the client can delete a row or a set of rows by reporting the appropriate keys. Semantically, if a row with the specified key was in the table, it will be deleted. If there was no row, then no changes will occur. As with most MVCC systems, deletion in YTsaurus comes down to writing a row without data, but with a special `tombstone` marker that signals deletion. This means that freeing up disk space from deleted rows does not happen immediately, but in a delayed manner. Deleting rows also does not instantly increase reading speed. On the contrary, it causes a slowdown, because during read operations data is merged. ### Locking rows The client can lock rows within a transaction. Locking guarantees that the row will not be changed in other transactions during the current transaction. A single row can be locked from multiple transactions. You can specify individual `lock` column groups to be locked and a `weak` or `strong` lock mode. ### Deleting old data (TTL) { #remove_old_data } In the process of combining the tablet chunks, some of the data may be identified as obsolete and deleted. {% note info "Note" %} Deleting rows in a transaction actually only writes a special marker, but does not free up memory. {% endnote %} Values of the `min_data_versions`, `max_data_versions`, `min_data_ttl`, and `max_data_ttl` attributes indicate whether data can be deleted. The value can be deleted if two conditions are met at the same time: - There is no prohibition to delete this value. - There is at least one permission to delete this value. To understand which values have permissions and prohibitions and what types they are, you can mentally sort all the values in this table row and column : (`t1, v1`), (`t2, v2`), ..., (`tn, vn`) where `ti` are timestamps and `vi` are values themselves. Timestamps are considered ordered in descending order. In addition, the table row delete command generates a special value for all its columns: semantically, it does not equal `null`, because once `null` is written to the rows, they cannot be deleted. Then the deletion rules are as follows: - The first `min_data_versions` values cannot be deleted for version number reasons. - Values written less than `min_data_ttl` before the current moment cannot be deleted for time reasons. - Values following the first `max_data_versions` can be deleted for version number reasons. - Values written earlier than `max_data_ttl` from the current moment can be deleted for time reasons. Default settings: - min_data_versions = 1; - max_data_versions = 1; - min_data_ttl = 1800000 (30 min); - max_data_ttl = 1800000 (30 min). With default settings, at least one value (the last one) is always stored, along with all values written in the last 30 minutes. The time during which a transaction can remain consistent is limited: the system does not allow prolonged transactions. By using the listed parameters, flexible storage policies can be built. For example, `min_data_versions = 0`, `max_data_versions = 1`, `min_data_ttl = 0`, and `max_data_ttl = 86400000 (1 day)` allow any data older than one day to be deleted, saving only one version from the last day. {% note info "Attention" %} The `min_data_ttl` parameter defines an interval when the data existence is guaranteed. Make sure that all reads by timestamp fall within the interval when all data is available: `[now() - min_data_ttl, now()]`. This applies both to reads with an explicitly specified `timestamp` and reads within a transaction where the transaction start time is used as the timestamp. If the timestamp is **older** than `now() - min_data_ttl`, read success **is not guaranteed**. In particular, if a table has `min_data_ttl = 0`, you should read from it only with `sync_last_committed` or `async_last_committed`. {% endnote %} {% note info "Note" %} The specified parameters enable the system to delete data, but do not force it to. Combining chunks and deleting data is a background operation. {% endnote %} If you need to force data cleaning, use the `forced_compaction_revision` attribute: ```bash yt set //table/@forced_compaction_revision 1; yt remount-table //table ``` The given set of commands starts the compaction of all data written up to this moment. In this way, both redundant duplicate versions and logically deleted data will be deleted. This operation creates an immediate load on the cluster, which depends on the volume of multi-version data, so this operation is considered an administrative intervention. {% note warning "Attention" %} The `forced_compaction_revision` setting causes a heavy load on the cluster. We do not recommend using this attribute without a specific need and understanding of the consequences. {% endnote %} Another popular scenario involves adding fresh writes to a table by keys that are (on average) ascending, while simultaneously deleting old data by keys that are also, on average, ascending. In this case, the table is appended with partitions containing logically deleted data that is marked by tombstones. These partitions will not be compressed as long as the number of chunks in them is small. The size of the table on the disk keeps increasing, even though the amount of undeleted data in the table remains constant. A possible solution is to specify the `auto_compaction_period` parameter, which sets the periodicity at which partitions are forced to compact. #### Row-level TTL The primary use case for row-level TTL is when you need to store rows from the same table for different amounts of time depending on the data they contain. The task of determining the storage duration of each row lies with the user. To enable row-level TTL, add an optional `uint64` column named `$ttl` to your table. This is a system column, which is why it has such designation. However, the task of updating this column with valid and current values still lies with the user. Writes to the table must include a value in milliseconds indicating how long the row should not be deleted. Requirements for values written to the `$ttl` column: - `min_data_ttl <= ttl` How the `min_data_versions`, `max_data_versions`, `min_data_ttl`, and `max_data_ttl` attributes impact the `$ttl` column: - If `min_data_versions > 0`, rows aren't deleted based on their values from the `$ttl` column. - Rows with non-empty `$ttl` column values aren't deleted after exceeding `max_data_ttl`. When deleting data, the `$ttl` value for the corresponding key is derived from the most recent entry in this column, if possible. This doesn't apply if some data is outside the chunks being merged. The deletion rules are similar to those listed in the previous section, with the addition of one rule: - Values written earlier than TTL before the current point in time can't be deleted for time-related reasons. How to manage row-level TTL: - To correctly delete a value from the `$ttl` column, delete the entire row for the corresponding key, then insert it again without specifying a value in the `$ttl` column. - Each insert must include a `$ttl` value for the corresponding key. Furthermore, the value must always be the same, with the exception of the scenarios described below. Otherwise, we can't guarantee the desired behavior. - To extend the Time to Live of data for a given key, write the entire row with the updated `$ttl` column value to the table. - To reduce the Time to Live of data for a given key, delete the data for that key, then insert it again, specifying your new value in the `$ttl` column. - You can only specify TTL for previously written keys by deleting the row and inserting the new value in the `$ttl` column. #### Deleting rows using a watermark Dynamic tables support deleting rows based on user-defined timestamps. A user can set a dedicated column to store timestamps for each row, and update a special table attribute by shifting the current time boundary for data that can be deleted during compaction. {% note warning "Attention" %} Rows are only deleted during compaction, so enabling the option won't immediately delete old rows: they will remain visible when reading. {% endnote %} To enable deletion using a watermark, follow the steps below: - Set `@mount_config/row_merger_type = "watermark"` for a table and and remount this table. - Set the `@custom_runtime_data` attribute for the table. The `custom_runtime_data` attribute must be as follows: ```yson { "watermark" = { "column_name" = ""; "watermark" = ; "comparison_type" = "less"; # the other available types are less_equal, greater, and greater_equal }; } ``` As a result, for rows where the specified column value is less than (or less than or equal to, greater than, or greater than or equal to — depending on the `comparison_type` parameter value) the specified one, all versions will be deleted if their timestamp is less than or equal to the maximum timestamp of the value that satisfies the comparison. If a row has been inserted once or the specified column is part of the key, the entire row is deleted. After enabling this option, `min_data_versions` and `min_data_ttl` will continue to be taken into account, but the `max_data_versions` and `max_data_ttl` attributes will be ignored. ## Aggregation columns { #aggr_columns } If the data scenario involves the constant addition of deltas to values already written in the table, use aggregation columns. In the schema, specify the `aggregate=sum` attribute or another aggregate function for the column. You can then make a write to such a column with additive semantics to the value already written in the table. No old value is read, only the delta is written to the table. The actual summing up occurs when reading. The values in the tables are stored together with a timestamp. When reading from an aggregation column, values corresponding to the same key and the same column, but with different timestamps, are summed up. To perform optimization, the old data in the aggregation column is summed up at the compaction stage, and only one value corresponding to their sum remains in the table. The following aggregate functions are supported: `sum`, `min`, `max`, `first` and `xdelta`. The default setting is to overwrite what is in the table. To write a delta, specify the `aggregate=true` option in the write command. ## Row-by-row cache { #lookup_cache } For point reads of rows (reading specific rows by their full keys) from a dynamic table, there's a lookup rows API call. In a basic scenario, the table is stored on a disk (HDD, SSD, NVMe). Data read from disk or compressed by a codec (LZ, gzip, zstd) is measured in blocks. When reading blocks from disk, the data is cached at multiple levels: at the OS level and at the level of the YTsaurus node process. In the YTsaurus node process, blocks are cached in both compressed and uncompressed form. In general, if the caches do not contain a block with a given key, the block is read from disk, transferred over the network, uncompressed, and then read according to the format of the table chunks (unversioned/versioned, scan/lookup). These operations may take a fair bit of time. Furthermore, this creates additional load on the disk, network, and CPU. Usually, point reads only read a fairly small fraction of the data contained in a block (less than 1%). So if there are no blocks with your lookup keys, a large chunk of work in the cache of uncompressed blocks will go to waste. In latency-critical scenarios involving reads by full keys from a dynamic table, the only option used to be to load the entire data into memory in uncompressed form (table attribute `@in_memory_mode=uncompressed`). The downside of this approach is that it requires a lot of RAM when working with large tables. A fairly common scenario involves working with a table that contains a certain limited amount of frequently read data. In other words, the probability of reads by any given key at any given time is not equal for all keys. If your table contains such a working set with hot data, you can use a lookup cache. A lookup cache is an LRU cache that stores individual rows from the table. You could theoretically cache individual rows using a separate tool. For instance, memcached: you send a request to memcached, and if the data is not present in the cache, read it from YTsaurus, then add it to the cache. The problem is that the table data can change at any time: a table row that is stored in the cache may at any given point of time be modified or deleted by a currently running transaction. For this reason, to ensure data consistency and isolation during reads, the cache must be synchronized with any data modifications in the table. And that is why the cache is integrated with YTsaurus. To use the cache, make sure you do the following: - Request a quota for the `lookup_rows_cache` memory category (similar to the `tablet_static` quota for in-memory tables). - Set the `@lookup_cache_rows_ratio` attribute for the tables. This attribute specifies the fraction of rows to be cached for each tablet. You can start at fractions of a percent (0.0005) and gradually increase the value all the way up to a few percent (0.03). - Set the `@enable_lookup_cache_by_default` attribute for the table to `%true`. - Run the remount-table command. Once this is done, all lookup queries to the table will be processed using the cache. If you do not want to use the cache for some queries, you can specify the `use_lookup_cache` option in the lookup rows query itself. This can be particularly useful if there's a background process that periodically modifies data across the entire table (performs a lookup followed by an insert). Using the cache for these lookups could otherwise cause data to be flushed from it. On the other hand, if this process only updates a small fraction of the table data, using the cache could be useful for all lookup queries, as this would speed up the execution of the periodic process. By default, the `use_lookup_cache` option is set to `null`. If `use_lookup_cache` is `null`, the cache's usage is determined by the table's `@enable_lookup_cache_by_default` attribute value (cache size must also be configured). If the passed `use_lookup_cache` value is `true` or `false`, the cache's usage for the lookup query is determined by this option instead. The `@lookup_cache_rows_per_tablet` table attribute is deprecated, so we do not recommend using it. The problem with this attribute is that different tablets can contain different amounts of data. The cache's operation may not be efficient with fixed tablet sizes. Furthermore, a fixed cache size can often become suboptimal as sharding settings change and the table grows in size. If you are using hunks, it may be beneficial to enable the lookup cache and set the `@in_memory_mode=uncompressed` mode for the table. In this case, the in-memory data contains references to hunks. The amount of this data is typically not very large. The lookup cache allows storing hunks and reducing the frequency of disk reads and network consumption. ### Diagnostic metrics for lookup cache performance When the cache operates efficiently, the frequency of block reads from disk or from the block cache should gradually decrease. When cache is enabled, `unmerged_row_count` is expected to decrease; however, `unmerged_data_weight / unmerged_row_count` (data amount per row) might increase, because all versions and all columns are read to fill the cache. Reading all versions has practically no impact on performance (because the system reads the same number of data blocks and processes the same volume of data). Reading all columns increases the number of blocks being read. So when queries read only a small portion of the table's columns (either by number of columns or volume of data), the volume of blocks read or `unmerged_data_weight` may either decrease much less than `unmerged_row_count` or even increase, despite cache hits. Common lookup metrics include: Metrics for disk reads and network transfers: - `yt.tablet_node.lookup.chunk_reader_statistics.data_bytes_read_from_disk.rate` - `yt.tablet_node.lookup.chunk_reader_statistics.data_bytes_read_from_cache.rate` - `yt.tablet_node.lookup.chunk_reader_statistics.data_bytes_transmitted.rate` If hunks are used: - `yt.tablet_node.lookup.hunks.chunk_reader_statistics.data_bytes_read_from_disk.rate` - `yt.tablet_node.lookup.hunks.chunk_reader_statistics.data_bytes_transmitted.rate` Response time metric for lookup rows queries on a tablet node: - `yt.tablet_node.lookup.duration` Lookup cache–specific metrics: - `yt.tablet_node.lookup.cache_hits.rate`: Number of cache hits per second. - `yt.tablet_node.lookup.cache_misses.rate`: Number of cache misses per second. - `yt.tablet_node.lookup.cache_inserts.rate`: Number of cache inserts per second. This value may be different from "cache_misses" if the keys aren't anywhere in the table, or if the cache runs out of memory (`lookup_rows_cache` memory categories). For the number of queried keys that are absent from the table, use the metric "yt.tablet_node.lookup.missing_row_count.rate". If the table is not loaded into memory, any attempts to read missing keys always result in queries to the disk. To prevent this, enable the key xor filter. - `yt.tablet_node.lookup.cache_outdated.rate`: Number of keys per second that present in the cache but are outdated because the cache ran out of memory and could not update them in time. Once the cache reaches its memory quota ("lookup_rows_cache" memory category), it can no longer update existing rows. In this case, the old value remains cached but can no longer be read. As the cache is processed, the system performs garbage collection. The following metrics allow tracking the actual amount of memory being used. - `yt.tablet_node.row_cache.slab_allocator.lookup.alive_items`: Number of current allocations. - `yt.tablet_node.row_cache.slab_allocator.lookup.allocated_items.rate`: Number of allocations per second. - `yt.tablet_node.row_cache.slab_allocator.lookup.freed_items.rate`: Number of deallocations per second. - `yt.tablet_node.row_cache.slab_allocator.lookup.arena_size`: Sizes of arenas (sets of allocations of the same size) in bytes. These metrics have a "rank" tag, which represents the allocation size in bytes: 16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1022, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 22576, 32768, large. "Large" is used for allocation sizes that are greater than 32768 bytes. ## Filtering out non-existent keys { #key_filter } Often, tables have a load pattern where reads by non-existent keys make up a significant portion of all queries. To quickly identify that a key is absent from the table, dynamic tables implement a key filtering mechanism. This is achieved using a [xor filter](https://arxiv.org/abs/1912.08258), which functions similarly to the Bloom filter. This filter can be useful in two scenarios: - The lookup key is not present in the table. - The lookup key is rarely written to. In this case, we read all chunks within the partition, even though we already know that most of them don't have the key that we need. Filtering is supported for lookup queries as well as for select queries that include a key prefix condition. ### Enabling the filter To enable filtering: - Allocate memory for the key filter block cache within the bundle (recommended value is 500 MB). You can do this yourself by navigating to the Edit Bundle -> Resources menu. For bundles that are still using the legacy resource model, use the [dynamic node config](https://ytsaurus.tech/admin-guide/cluster-operations#ytdyncfgen) to set the "data_node/block_cache/xor_filter/capacity" field value. This action is available to the cluster administrator. - Enable saving filters during write operations. To do this, set the `@chunk_writer/key_filter/enable = %true` attribute for the table. If the table does not have the `@chunk_writer` attribute, set it to `{key_filter={enable=%true}}`. - If you need filtering in select queries by key prefix — that is, in queries formulated using the `IN` or `BETWEEN` keywords — use the `@chunk_writer/key_prefix_filter` attribute to enable writes and specify the "prefix_lengths" parameter for this attribute. - Set `@mount_config/enable_key_filter_for_lookup = %true`. This option enables filtering for both lookup and select queries. - Perform the remount-table command on the table. - To add the filter for all existing chunks, you can use [forced compaction](https://ytsaurus.tech/docs/en/compaction#forced_compaction). ### Filter parameters Filter write parameters are specified in `@chunk_writer/key_filter` for lookup queries and in `@chunk_writer/key_prefix_filter` for select queries: - enable: Whether a filter should be created for the chunk's blocks. - false_positive_rate: Percentage of incorrect filter responses (the filter did not filter out a key that was, in fact, missing). The default value is 1 / 256. The higher the precision, the more space the filter will take up. - bits_per_key: Number of bits that a single key takes up in the filter. - prefix_lengths: List of key prefix lengths for which the filter should be created. This parameter is only valid for `@chunk_writer/key_prefix_filter`. Not all prefix lengths need to have filters built — `select` queries for a prefix of length l will use the filter built for the longest prefix length not exceeding l. You cannot set both "false_positive_rate" and "bits_per_key" at the same time. If "false_positive_rate" is set, the "bits_per_key" value is derived from it. We recommend using the "false_positive_rate" parameter. ### Diagnostic metrics for filter performance When the filter is running efficiently, the number of block reads from disk or from the block cache should decrease. #### Metrics for lookup operations Metrics for disk or cache reads: - yt.tablet_node.lookup.chunk_reader_statistics.data_bytes_read_from_cache.rate: Number of bytes read by lookup queries from the block cache. - yt.tablet_node.lookup.chunk_reader_statistics.data_bytes_read_from_disk.rate: Number of bytes read by lookup queries from disk. Filter-specific metrics: - yt.tablet_node.lookup.key_filter.input_key_count.rate: Number of keys that the filter received as input. - yt.tablet_node.lookup.key_filter.filtered_out_key_count.rate: Number of keys filtered out by the filter. - yt.tablet_node.lookup.key_filter.false_positive_key_count.rate: Number of keys that were not filtered out by the filter, even though they did not actually exist. #### Metrics for select operations Metrics for disk or cache reads: - yt.tablet_node.select.chunk_reader_statistics.data_bytes_read_from_cache.rate: Number of bytes read by select queries from the block cache. - yt.tablet_node.select.chunk_reader_statistics.data_bytes_read_from_disk.rate: Number of bytes read by select queries from disk. Filter-specific metrics: - yt.tablet_node.select.range_filter.input_range_count.rate: Number of ranges that the filter received as input. - yt.tablet_node.select.range_filter.filtered_out_range_count.rate: Number of ranges filtered out by the filter. - yt.tablet_node.select.range_filter.false_positive_range_count.rate: Number of ranges that were not filtered out by the filter, even though they did not actually exist. #### Common metrics Also, to evaluate the filter's performance, we recommend considering the following ratio: - yt.tablet_node.lookup.unmerged_missing_row_count.rate / yt.tablet_node.lookup.row_count.rate In the numerator, this value corresponds to the number of (unmerged) rows that were requested when reading separately from each chunk but were not found in the chunk. In the denominator, it's the final number of (already merged) rows read. The `xor filter` usually works effectively if this ratio is greater than three. # Ordered dynamic tables This section reviews the structure of ordered dynamic tables and describes the operations that can be performed on them. ## Data model { #model } Ordered dynamic tables are a table type in YTsaurus that is a simple ordered sequence of rows. Each row of an ordered table consists of columns and is subordinate to the table schema specified in the process of creation. There are no key columns in such tables. Ordered dynamic tables support adding new rows to the end within a transaction, as well as reading rows by their indexes without transaction isolation. The closest analog of ordered dynamic tables is [Apache Kafka](https://kafka.apache.org). As with [sorted dynamic tables](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md), the key space of an ordered dynamic table is divided into [tablets](https://ytsaurus.tech/docs/en/overview.md#tablets). Dividing data into tablets is random. Each tablet contains an ordered sequence of table rows. When the rows are written to the tablet, they get to the end of this sequence. That way, orderliness is only guaranteed within the tablet. An ordered dynamic table initially consists of one tablet. Multiple tablets can be changed using the `reshard_table` command. It enables you to change the structure for a set of consecutive tablets. When [resharding](https://ytsaurus.tech/docs/en/#reshard) an ordered dynamic table, you need to specify a new number of tablets for replacing the original ones. Specifying `pivot_keys` is not required. Existing data is redistributed between the new tablets in an unspecified way. ## Supported operations { #methods } ### Creating To create an ordered dynamic table, run the `create table` command and specify the schema and `dynamic=True` setting in the attributes. The schema must match the schema of the ordered table: there must be no key columns among its columns. ```bash yt create table //path/to/table --attributes \ '{dynamic=%true;schema=[{name=first_name;type=string};{name=last_name;type=string}]}' ``` When creating a table, you can specify the starting row index for each tablet. For example, this is useful when recreating a queue if consumers rely on `row_index` values. To do this, use the `trimmed_row_counts` attribute. When passing this attribute, you also need to specify `tablet_count`, which must match the length of the `trimmed_row_counts` list. ```bash yt create table //path/to/table --attributes \ '{dynamic=%true;schema=[{name=first_name;type=string};{name=last_name;type=string}]}; \ tablet_count=5; \ trimmed_row_counts=[10;20;30;40;50]}' ``` Similarly, if the table schema includes the [`$cumulative_data_weight`](https://ytsaurus.tech/docs/en/#cumulative_data_weight_column) column, you can specify its initial value for each tablet using the `cumulative_data_weights` attribute. The requirements for it are the same as for `trimmed_row_counts`: if you pass it, you also need to specify the `tablet_count` attribute with the same length. ```bash yt create table //path/to/table --attributes \ '{dynamic=%true;schema=[{name=first_name;type=string};{name=last_name;type=string};{name="$cumulative_data_weight";type=int64}]}; \ tablet_count=5; \ trimmed_row_counts=[10;20;30;40;50]; \ cumulative_data_weights=[1000;2000;3000;4000;5000]}' ``` ### Writing rows The `insert_rows` command is used to write to an ordered dynamic table. By default, data is written to a random mounted tablet. To manage data distribution manually, use a special `$tablet_index` system column of the `int64` type in the rows to be written. The values in this column must be numbers from 0 to N − 1 where N is the number of tablets in the table. The relevant rows will be written strictly to the specified tablet. Rows for which there is no such annotation will be written to a random mounted tablet. The write within a transaction is transactional: if the transaction succeeds, the rows appear in the relevant tables; if not, they do not. You can handle both sorted and ordered tables in a single transaction. ### Reading rows You can read data from ordered tables using an [SQL-like query language](https://ytsaurus.tech/docs/en/dyn-query-language.md) and the `select_rows` command. Each ordered dynamic table appears as a sorted one with system key columns `($tablet_index, $row_index)` (both of the `int64` type) and all data columns specified in the table schema. For example, this is what a query that reads a range of rows from a fixed tablet of an ordered table looks like: ```sql * from [//path/to/table] where [$tablet_index] = 10 and [$row_index] between 100 and 200 ``` ### Changing a table schema and type You can change the schema of an existing dynamic table using the `alter-table` command. For the command to be successful, the table must be [unmounted](https://ytsaurus.tech/docs/en/overview.md#mount_table) and the new schema must be compatible with the old one. There are no changes to the data written to the disk, because the old data is suitable for the new schema. Use `alter-table` to convert an ordered dynamic table into a static table and vice versa. For more information, see [MapReduce for dynamic tables](https://ytsaurus.tech/docs/en/mapreduce.md#convert_table). ### Trim {% note warning "Attention!" %} Using reshard together with trim is prohibited because this can lead to unpredictable results. {% endnote %} In general, data cannot be deleted from an ordered dynamic table. But there is an exception: you can delete the starting row segment in each tablet. To do this, use the `trim_rows` command. The table path and tablet number are transmitted to it as arguments, as well as the `trimmed_row_count` argument showing how many rows in the table will be deleted after the command is executed. The row numbering is retained. For example, when first called with `trimmed_row_count = 10`, rows with numbers from 0 to 9 inclusive will be deleted. Then, when called with `trimmed_row_count = 30` — rows from 10 to 29 inclusive, etc. `trimmed_row_count` does not have a relative sense, but an absolute one and indicates not the number of rows that will be additionally deleted in case of the next call, but which initial rows will be deleted after the call. The `trim_rows` command is executed outside of transactions. Once it is complete, the deleted data can no longer be read by the `select_rows` command. As soon as it appears that so many rows were deleted in the tablet that they form an entire initial chunk, the cluster node serving the tablet sends a signal to the master server and the given chunk is deleted as a whole. This is the moment when disk space is freed up. You can find out the number of deleted rows in any tablet from its `trimmed_row_count` attribute. This parameter is updated asynchronously, i.e. some time may elapse between `trim_rows` execution and its change. When a tablet is unmounted and then re-mounted, the number of initial deleted rows is retained, ensuring that the numbering is unchanged. {% note warning %} When you convert a dynamic table into a static table using the `alter_table` command, information about which rows were deleted is lost. The order of rows in the table is lost as well. The order of rows within the chunks remains unchanged, but this order is hardly reliable. Indeed, some initial rows of some chunks may be marked as deleted in a dynamic table, and this information cannot be retained when the table is converted into a static table. As a result, when converting a dynamic table into a static one, some of the previously deleted rows may re-appear. {% endnote %} ### Resharding { #reshard } If the number of tablets increases during sharding, the existing tablets are not changed and new tablets are created empty. If the number of tablets needs to be reduced during resharding, the tablets at the end of the range are merged with the last of the resulting list of tablets that is not in the range. The system tries to maintain the invariant “what is deleted is no longer available”. During resharding, the system appends the chunks from the deleted tablets to the end of the last retained tablet; this complicates resharding if the tablets you're deleting contain previously deleted rows. For resharding to complete successfully, the deleted rows must form a chunk prefix in the tablets to be deleted. In real world scenarios, this limitation is difficult to observe, so if a tablet you want deleted contains deleted rows, you may as well consider it impossible to perform a resharding where the number of tablets will be reduced. To bypass this limitation, you can use two `alter_table` calls, first making the table static and then dynamic. But do remember that some of the deleted rows re-appear in the table when you use this method. If resharding causes the number of tablets to increase, you can specify the starting row index for each created tablet. To do this, pass the `trimmed_row_counts` list as a command argument. Its length must match the number of created tablets: `tablet_count - (last_tablet_index - first_tablet_index + 1)`. ```bash yt reshard-table //path/to/table --tablet-count 5 --first-tablet-index 1 --last-tablet-index 3 --trimmed-row-counts 10 20 # Resulting offsets, assuming table had 5 tablets: # (old tablet #0) (old tablet #1) (old tablet #2) (old tablet #3) 10 20 (old tablet #4) ``` You can also set the initial value of the [`$cumulative_data_weight`](https://ytsaurus.tech/docs/en/#cumulative_data_weight_column) column for each created tablet using the `cumulative_data_weights` list of the same length. ```bash yt reshard-table //path/to/table --tablet-count 5 --first-tablet-index 1 --last-tablet-index 3 --cumulative-data-weights 1000 2000 ``` ### Automatic deletion of old rows (TTL) { #remove_old_data } The same old data deletion settings apply to ordered tables as to sorted tables. For more information, see [Deleting old data](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md#remove_old_data). The significant difference is that a row always has only one version in ordered tables. The cleanup settings can then be interpreted as follows: - If `min_data_versions > 0` (the default value is 1), no automatic deletion occurs. - YTsaurus does not delete rows written less than `min_data_ttl` before the current moment. - If `max_data_versions = 0`, you can delete rows written later than `min_data_ttl`. - If `max_data_versions > 0`, you can delete rows written later than `max_data_ttl`. {% note info "Note" %} Automatic deletion (trimming) applies to the entire chunk. As long as there are rows in the chunk that cannot be deleted, all rows from the chunk will be available. {% endnote %} ### Data size limit { #max_data_weight } You can limit the amount of data in an ordered table by setting the maximum allowed tablet size. To do this, specify a limit in bytes in the table's `@mount_config/max_ordered_tablet_data_weight` attribute. ## $timestamp column A special `$timestamp` system column of the `uint64` type can be specified in the ordered dynamic table schema. The value in this column is automatically generated by the system during writes and is equal to the commit timestamp for the transaction in which the rows were added to the table. ## $cumulative_data_weight column { #cumulative_data_weight_column } A special `$cumulative_data_weight` system column of the `int64` type can be specified in the ordered dynamic table schema. The value in this column is generated automatically when you write. It is equal to the total logical weight in bytes of rows in the tablet, counting from the initial row with index zero to the current one, inclusive. The weight of the `$cumulative_data_weight` column itself is also counted in this value. When you add this column to the schema of an existing table (via unmounting and the `alter_table` query), the initial value of `$cumulative_data_weight` is taken from the table chunk metadata. By default, the count in each tablet starts from zero. You can explicitly set the initial value using the `cumulative_data_weights` attribute when [creating](https://ytsaurus.tech/docs/en/#methods) the table or the `cumulative_data_weights` argument of the [`reshard_table`](https://ytsaurus.tech/docs/en/#reshard) command. This is useful, for example, when adding a new queue as a [replica] of an already running replicated table. ## Change visibility, strong/weak commit ordering { #commit_ordering } The consistency level of ordered dynamic tables is fundamentally lower than that of sorted dynamic tables: committed data may not generally be visible immediately after a commit and may not be added in the order in which the commits occurred. When using a distributed commit that involves multiple tablet cells, data appears in the tablets of different tablet cells at different points in time. Indeed, the second phase of the protocol (commit) is performed by physically distributed participants. Moreover, the same tablet cell may participate in multiple distributed transactions, and the values of their `commit-ts`, as well as the actual sequence in which that tablet cell performs commits, are generally not guided by any conditions. The participant can perform the commit of the `A` transaction before the commit of the `B` transaction even when `commit-ts(A) > commit-ts(B)`. In the case of dynamic sorted tables, this implementation detail is hidden from the user by snapshot isolation: even though the data from transaction `A` has already been added to the table, it can only be read by requesting a `ts` that is greater than or equal to `commit-ts(A)`. In addition, the row locking system ensures that such inversion will not occur on the same key when the `[start-ts,commit-ts]` intervals for transactions overlap. The situation is different for ordered dynamic tables: they usually do not store commit timestamps, except in the case of the `$timestamp` field, and do not support snapshot isolation. Therefore, the order in which writes will be appended to the end of the ordered dynamic table has nothing to do with the order of commit timestamps. When an ordered dynamic table is involved in a two-phase commit, confirmation of a successful commit from the coordinator does not mean that all participants have also performed a commit and that data has indeed been appended to the end of the table (a two-phase commit is a commit that affects more than one tablet cell: for example, this includes all commits in which an ordered dynamic table is a synchronous replica). An attempt to find the committed rows immediately after a successful two-phase commit may fail, as these rows will become visible only some time later. You can obtain certain monotony guarantees for added rows. A table has a `commit_ordering` system attribute that manages the order of adding rows to it: - `weak`: The default mode. The rows get into the ordered table immediately at the time of the commit of the participant, which is late relative to the coordinator, and the order relative to commit timestamps is not guaranteed. - `strong`: It is guaranteed that the rows get into the table in the order of commit timestamps. In `strong` mode, the table is ordered by the `$timestamp` field, provided it exists. Note that this does not make it a key field. In `strong` mode, each tablet cell tracks a special `barrier-ts` value. This value constantly and monotonically increases, and no transaction can get a `commit-ts` that is smaller than the `barrier-ts`. When `barrier-ts` exceeds the `commit-ts` of a transaction, the rows written by this transaction to an ordered dynamic table in `strong` mode do not appear in the table immediately at the time of the commit. Thus, the system serializes all transactions by `commit-ts`, but only those for which `commit-ts < barrier-ts`. For transactions with `commit-ts > barrier-ts`, the system can define a relative order, but cannot guarantee that there will not be a new transaction in the future that violates the established order. For static tables, there is also a `commit_ordering` attribute, but it is always `weak`. # YTsaurusQueue API This section describes the YTsaurus queue ecosystem, specialized methods for working with queues, and how to configure and use them. ## Data model { #data_model } In YTsaurus, a *queue* refers to any ordered dynamic table. A queue partition is a tablet of a dynamic table, with its index matching the index of the tablet. In YTsaurus, a *consumer (queue_consumer)* refers to a sorted table that has a defined schema. It has a many-to-many relationship with queues and represents a consumer of one or more queues. A consumer's function is to store offsets across the partitions of readable queues. Communication between consumers and queues is enabled by *registration* objects. Table 1 — Consumer table schema | Name | Type | Description | |----------------------|--------|-----------------------------------------------------------------------------------------| | queue_cluster | string | Queue cluster name | | queue_path | string | Path to a dynamic queue table | | partition_index | uint64 | Partition index, same as `tablet_index` | | offset | uint64 | Index of the **first row that hasn't been processed** by the consumer within the specified partition of the specified queue | | meta | any | System meta information | In YTsaurus, a *producer (queue_producer)* also refers to a sorted table that has a defined schema. A producer stores the indexes of the last rows added during queue write sessions, which helps prevent row duplication in the queue. Table 2 — Producer table schema | Name | Type | Description | |----------------------|--------|-----------------------------------------------------------------------------------------| | queue_cluster | string | Queue cluster name | | queue_path | string | Path to a dynamic queue table | | session_id | string | Session ID | | sequence_number | int64 | Sequence number of the last written row | | epoch | int64 | Number of the current epoch | | user_meta | any | User meta information | | system_meta | any | System meta information | ## API { #api } ### Creating a queue Creating a queue is no different from creating a regular ordered dynamic table. To ensure that your graphs and statistics are comprehensive, we recommend adding the [timestamp](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md#timestamp_column) and [cumulative_data_weight](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md#cumulative_data_weight_column) columns to the table schema. You can verify that the system has recognized the queue by checking if the **Queue** tab has appeared on the page of the dynamic table object. ### Creating a consumer You can create a consumer using the alias `create queue_consumer` or by explicitly creating a sorted dynamic table using the schema [provided above](https://ytsaurus.tech/docs/en/#data_model) and setting the `@treat_as_queue_consumer = %true` attribute for it. You can verify that the system has recognized the consumer by checking if the **Consumers** tab has appeared on the page of the dynamic table object. ### Registering a consumer with a queue { #register_queue_consumer } To register a consumer with a queue, use the `register_queue_consumer` method. Make sure to set a value for the `vital` parameter. It's a required boolean parameter that determines some of the settings for automatic queue trimming (see the [section](https://ytsaurus.tech/docs/en/#automatic_trimming) on automatic trimming). To execute the command above, you need a `register_queue_consumer` permission with `vital=True/False` for the directory that contains the queue. You can request it the same way as any other permission, on the directory page in the YTsaurus UI. A `register_queue_consumer` permission with `vital=True` allows you to register both vital and non-vital consumers. To delete a registration, use the `unregister_queue_consumer` method. Executing this command requires write access to the queue or the consumer. Both Cypress arguments in both of these commands are of [rich YPath](https://ytsaurus.tech/docs/storage/ypath.md#rich_ypath) type and can take cluster names. Otherwise, the command uses the cluster on which it was executed. ### Reading data There are two similar methods for reading data from queues. With the `pull_queue` method, you can read some of the rows from the specified partition of the specified queue by limiting the row count (`max_row_count`) or the size of the partition in bytes (`max_data_weight`). Executing this query requires read access to the queue. The `pull_queue_consumer` method is similar but takes the path to the consumer table as its first argument. This query is executed under two conditions: the user must have read access to the consumer **and** the said consumer must be registered with the specified queue. The queue parameter in this method is a [rich YPath](https://ytsaurus.tech/docs/storage/ypath.md#rich_ypath) that can take the cluster on which the queue is located. Otherwise, the command uses the cluster on which it was executed. In addition, you can read queue data the usual way, using the `select_rows` method. {% note warning "Attention" %} The `pull_queue` and `pull_queue_consumer` methods may return fewer rows than specified in the `max_row_count` parameter, even if there are enough rows in the queue partition and their combined size doesn't exceed `max_data_weight`. Only the return of an empty set of rows means that the partition doesn't have any rows at the specified offset. {% endnote %} ### Working with a consumer You can work with a consumer by using the `advance_queue_consumer` method, which moves the consumer offset in the specified partition of the specified queue. Within the passed transaction, the method updates the corresponding consumer offset row. You can also perform other operations with dynamic tables as part of the same transaction. If you specify a non-null `old_offset` parameter value, the method first reads the current offset within the transaction and compares it with the passed value. If they don't match, the method throws an exception. YTsaurus queue offsets are interpreted as the index of the *first unread row*. ### Creating a producer You can create a producer using the alias `create queue_producer` or by explicitly creating a sorted dynamic table using the schema [provided above](https://ytsaurus.tech/docs/en/#data_model) and setting the `@treat_as_queue_producer = %true` attribute for it. ### Writing data To write data to a queue, you can use either the `insert_rows` method from the dynamic tables API or the producer API to prevent row duplication during writes. To make writes using a producer, you need `write` permissions for both the queue and the producer. Before initiating a write, you need to call the `create_queue_producer_session` method, which takes the queue path, the producer path, and the ID of the write session (`session_id`). For `session_id`, you can pass any string: for example, you can use the name of the host from which the write is being made. Then, if no such session exists, a new session with an `epoch` of `0` and a `sequence_number` of `-1` is created in the producer table. If a session with this ID has already been created before, its `epoch` value is incremented. The `create_queue_producer_session` method returns the updated epoch value as well as the index of the last message written, which should be the current write session status stored in the producer. You can then write data to the queue using the `push_queue_producer` method. This method takes the queue path, producer path, session ID, epoch, and the data to write. Each row must be passed with the `$sequence_number` value corresponding to its index. Alternatively, instead of passing the index for each row in the data itself, you can specify only the index corresponding to the first row in the method options. In this case, we assume that the index should be incremented by one for each subsequent row. You can use the session's `epoch` to prevent zombie processes. ## Queue Agent Queue Agent is a dedicated microservice for monitoring queues, consumers, and registrations. ### Automatic queue trimming policies { #automatic_trimming } You can configure an automatic queue cleanup (trimming) policy by setting the `@auto_trim_config` attribute with a configuration in the [appropriate format](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/client/queue_client/config.h?rev=11720161#L41). Available options: - `enable: True` — Enables trimming by vital consumers. If there is at least one vital consumer, Queue Agent calls Trim for the partition at single-digit intervals in seconds until it reaches the smallest offset among vital consumers.
**NB:** If there are no vital consumers, no trimming occurs. - `retained_rows: x` — If set, this number of rows is guaranteed to be kept in each partition. This option is intended to be used in combination with the previous option. - `retained_lifetime_duration: x` — Ensures that each partition contains only those rows that were written to the queue no more than this number of milliseconds ago. The specified number of milliseconds must be a multiple of 1000. This option is intended to be used with trimming enabled (`enable: True`). This setting doesn't conflict with the [existing](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md#remove_old_data) TTL settings for dynamic tables: you can configure trimming based on vital consumers and set `max_data_ttl=36h, min_data_versions=0` to trim data based on its offset as well as ensure that it won't be stored for more than three days. ### Graphs and statuses The `@queue_status` and `@queue_partitions` queue table attributes along with the `@queue_consumer_status` and `@queue_consumer_partitions` consumer table attributes provide the current status and meta information of queues and consumers from the perspective of the Queue Agent. You can get this data both for these objects as a whole as well as for their individual partitions. These attributes aren't intended for high-load services and should only be used for introspection. ### Queue exports to static tables With Queue Agent, you can configure regular data export from queues to static tables. Exporting does not involve physical data transfer from the queue to the static table, since export works at the metainformation level. The exported table references the same chunks as in the queue instead of creating new chunks with the same data. #### Export parameters - `export_directory` (required): The destination directory for the created static tables. - `export_period` (required): Export frequency. It must be a multiple of 1 second and is expressed in milliseconds. For example, `export_period=300000` triggers export every 300 seconds. - `export_ttl`: [`expiration_timeout`](https://ytsaurus.tech/docs/storage/cypress.md#TTL) for created tables. - `output_table_name_pattern`: Table name pattern. The default value is `%UNIX_TS-%PERIOD`. If you set a custom value, different export iterations must generate different table names to ensure proper export operation; otherwise, behavior is undefined. `output_table_name_pattern` supports the `strftime` format and: - `%UNIX_TS`: Unix timestamp of the export boundary; the exact boundary is determined by the `use_upper_bound_for_table_names` parameter. - `%PERIOD`: `export_period` in seconds. - `%ISO`: Export time in ISO 8601 format, such as `2015-11-21T23:30:27Z`. - `use_upper_bound_for_table_names`: Determines the time boundary used in the table name. The default value is `false`. When set to `false`, the lower boundary is used; `true` indicates the upper boundary. The complete queue export configuration appears as a mapping from export names to their parameters, for example: ```yson { default={ export_period=60000; export_directory="//path/to/export/dir"; }; another_export={ export_period=300000; export_directory="//path/to/another/export/dir"; }; } ``` This configuration is assigned to the `static_export_config` attribute of the queue or, for replicated and сhaos tables, to the corresponding attribute of the queue replica, for example: ```bash $ yt set //path/to/some/queue/@static_export_config '...' ``` #### Configuring the export directory To verify permissions for creating static tables in the export directory, set the `queue_static_export_destination` attribute for the directory, specifying the queue ID: ```bash $ yt get //path/to/your/queue/@id "18f-50544-3ea0191-fc6c51c5" # sample queue ID $ yt set //path/to/your/export/directory/@queue_static_export_destination \ '{originating_queue_id="18f-50544-3ea0191-fc6c51c5"}' # originating_queue_id must contain your queue ID ``` #### Export progress Queue Agent uses the `queue_static_export_progress` attribute on the export directory to store internal information about already exported data. The contents of this attribute are not part of the public API. In typical scenarios, users should neither change this attribute nor depend on its contents. If `originating_queue_id` of the export directory is changed, the above mentioned attribute should be deleted to ensure proper operation: ```bash $ yt remove //path/to/your/export/directory/@queue_static_export_progress ``` #### Automatic export trimming Export can be considered a virtual vital consumer of the queue. When automatic trimming is enabled, data is deleted only if it has been read by all vital consumers and successfully exported. #### Export structure Time boundaries for the export iteration: export data is segmented based on Unix timestamps multiple of `export_period`. The upper boundary is inclusive while the lower one is exclusive. Considering the above, all non-exported (for this export) flushed chunks with a `max_timestamp` less than the upper boundary of the export iteration are included in the export. Due to this, enabling export for a queue that contains a large amount of data will result in a first massive export, since all chunks are non-exported. Starting from the next iteration, Queue Agent will export flushed data written to the queue after the previous export. {% note info %} The data flush requirements imply that data can't be exported more often than it gets flushed. By default, dynamic store flush occurs every 15 minutes. Export operations with intervals less than 15 minutes make no sense without changing queue flush parameters and only create excessive load on the master server. See the [documentation](https://ytsaurus.tech/docs/en/compaction#flush_attributes) for configuring dynamic table flush. {% endnote %} Due to the export structure, Queue Agent cannot provide strict timestamp gurantees for exported data. Each export chunk is processed as a whole and may contain data that logically belongs to different export iterations. If an export iteration fails, the data is exported during the next iteration, resulting in the export table containing data from two export iterations. ## Usage example These examples use a hypothetical configuration with multiple clusters named `freud` and `pythia`. Listing 1 — Example of using the Queue API ```bash # Create queue on pythia. $ yt --proxy pythia create table //tmp/$USER-test-queue --attributes '{dynamic=true;schema=[{name=data;type=string};{name="$timestamp";type=uint64};{name="$cumulative_data_weight";type=int64}]}' 2826e-2b1e4-3f30191-dcd2013e # Create queue_consumer on freud. $ yt --proxy freud create queue_consumer //tmp/$USER-test-consumer # OR: Create queue_consumer on freud as table with explicit schema specification. $ yt --proxy freud create table //tmp/$USER-test-consumer --attributes '{dynamic=true;treat_as_queue_consumer=true;schema=[{name=queue_cluster;type=string;sort_order=ascending;required=true};{name=queue_path;type=string;sort_order=ascending;required=true};{name=partition_index;type=uint64;sort_order=ascending;required=true};{name=offset;type=uint64;required=true};{name=meta;type=any;required=false}]}' 18a5b-28931-3ff0191-35282540 # Register consumer for queue. $ yt --proxy pythia register-queue-consumer //tmp/$USER-test-queue "//tmp/$USER-test-consumer" --vital # Check registrations for queue. $ yt --proxy pythia list-queue-consumer-registrations --queue-path //tmp/$USER-test-queue [ { "queue_path" = < "cluster" = "pythia"; > "//tmp/bob-test-queue"; "consumer_path" = < "cluster" = "freud"; > "//tmp/bob-test-consumer"; "vital" = %true; "partitions" = #; }; ] # Check queue status provided by Queue Agent. $ yt --proxy pythia get //tmp/$USER-test-queue/@queue_status { "partition_count" = 1; "has_cumulative_data_weight_column" = %true; "family" = "ordered_dynamic_table"; "exports" = { "progress" = {}; }; "alerts" = {}; "queue_agent_host" = "yt-queue-agent-1.ytsaurus.tech"; "has_timestamp_column" = %true; "write_row_count_rate" = { "1m_raw" = 0.; "1h" = 0.; "current" = 0.; "1d" = 0.; "1m" = 0.; }; "registrations" = [ { "consumer" = "freud://tmp/bob-test-consumer"; "vital" = %true; "queue" = "pythia://tmp/bob-test-queue"; }; ]; "write_data_weight_rate" = { "1m_raw" = 0.; "1h" = 0.; "current" = 0.; "1d" = 0.; "1m" = 0.; }; } $ yt --proxy pythia get //tmp/$USER-test-queue/@queue_partitions [ { "error" = { "attributes" = { "trace_id" = "21503942-85d82a3a-2c9ea16d-e2149d9c"; "span_id" = 17862985281506116291u; "thread" = "Controller:4"; "datetime" = "2025-01-23T13:42:21.839124Z"; "tid" = 9166196934387883291u; "pid" = 481; "host" = "yt-queue-agent-1.ytsaurus.tech"; "state" = "unmounted"; "fid" = 18445202819181375616u; }; "code" = 1; "message" = "Tablet 3d3c-50e7d-7db02be-7e178361 is not mounted or frozen"; }; }; ] # Check queue consumer status provided by Queue Agent. $ yt --proxy freud get //tmp/$USER-test-consumer/@queue_consumer_status { "queues" = { "pythia://tmp/bob-test-queue" = { "error" = { "attributes" = { "trace_id" = "623ba99c-b2dce5fe-50174949-5f508824"; "span_id" = 14498308957160432715u; "thread" = "Controller:1"; "datetime" = "2025-01-23T13:42:55.747430Z"; "tid" = 627435960759374310u; "pid" = 481; "host" = "yt-queue-agent-1.ytsaurus.tech"; "fid" = 18442320640360156096u; }; "code" = 1; "message" = "Queue \"pythia://tmp/bob-test-queue\" snapshot is missing"; }; }; }; "registrations" = [ { "consumer" = "freud://tmp/bob-test-consumer"; "vital" = %true; "queue" = "pythia://tmp/bob-test-queue"; }; ]; "queue_agent_host" = "yt-queue-agent-1.ytsaurus.tech"; } # We can see some errors in the responses above, since both tables are unmounted. # Mount queue and consumer tables. $ yt --proxy pythia mount-table //tmp/$USER-test-queue $ yt --proxy freud mount-table //tmp/$USER-test-consumer # Check statuses again: $ yt --proxy pythia get //tmp/$USER-test-queue/@queue_partitions [ { "meta" = { "cell_id" = "2dd9a-d4f7-3f302bc-f21fc0c"; "host" = "node-1.ytsaurus.tech:9022"; }; "lower_row_index" = 0; "cumulative_data_weight" = #; "upper_row_index" = 0; "available_row_count" = 0; "write_row_count_rate" = { "1m_raw" = 0.; "1h" = 0.; "current" = 0.; "1d" = 0.; "1m" = 0.; }; "available_data_weight" = #; "trimmed_data_weight" = #; "last_row_commit_time" = "1970-01-01T00:00:00.000000Z"; "write_data_weight_rate" = { "1m_raw" = 0.; "1h" = 0.; "current" = 0.; "1d" = 0.; "1m" = 0.; }; "commit_idle_time" = 1737639851870; }; ] $ yt --proxy freud get //tmp/$USER-test-consumer/@queue_consumer_status { "queues" = { "pythia://tmp/bob-test-queue" = { "read_data_weight_rate" = { "1m_raw" = 0.; "1h" = 0.; "current" = 0.; "1d" = 0.; "1m" = 0.; }; "read_row_count_rate" = { "1m_raw" = 0.; "1h" = 0.; "current" = 0.; "1d" = 0.; "1m" = 0.; }; "partition_count" = 1; }; }; "registrations" = [ { "consumer" = "freud://tmp/bob-test-consumer"; "vital" = %true; "queue" = "pythia://tmp/bob-test-queue"; }; ]; } # Enable automatic trimming based on vital consumers for queue. $ yt --proxy pythia set //tmp/$USER-test-queue/@auto_trim_config '{enable=true}' # Write rows without exactly once semantics. $ for i in {1..20}; do echo '{data=foo}; {data=bar}; {data=foobar}; {data=megafoo}; {data=megabar}' | yt insert-rows --proxy pythia //tmp/$USER-test-queue --format yson; done; # Check that queue status reflects writes. $ yt --proxy pythia get //tmp/$USER-test-queue/@queue_status/write_row_count_rate { "1m_raw" = 2.6419539762457456; "current" = 5.995956327053036; "1h" = 2.6419539762457456; "1d" = 2.6419539762457456; "1m" = 2.6419539762457456; } # Read data via consumer. $ yt --proxy freud pull-queue-consumer //tmp/$USER-test-consumer "//tmp/$USER-test-queue" --partition-index 0 --offset 0 --max-row-count 5 --format "yson" {"$tablet_index"=0;"$row_index"=0;"data"="foo";"$timestamp"=1865777451725072279u;"$cumulative_data_weight"=20;}; {"$tablet_index"=0;"$row_index"=1;"data"="bar";"$timestamp"=1865777451725072279u;"$cumulative_data_weight"=40;}; {"$tablet_index"=0;"$row_index"=2;"data"="foobar";"$timestamp"=1865777451725072279u;"$cumulative_data_weight"=63;}; {"$tablet_index"=0;"$row_index"=3;"data"="megafoo";"$timestamp"=1865777451725072279u;"$cumulative_data_weight"=87;}; {"$tablet_index"=0;"$row_index"=4;"data"="megabar";"$timestamp"=1865777451725072279u;"$cumulative_data_weight"=111;}; # Advance queue consumer. $ yt --proxy freud advance-queue-consumer //tmp/$USER-test-consumer "//tmp/$USER-test-queue" --partition-index 0 --old-offset 0 --new-offset 42 # Since trimming is enabled and the consumer is the only vital consumer for the queue, soon the rows up to index 42 will be trimmed. # Calling pull-queue-consumer now returns the next available rows. $ yt --proxy freud pull-queue-consumer //tmp/$USER-test-consumer "//tmp/$USER-test-queue" --partition-index 0 --offset 0 --max-row-count 5 --format "yson" {"$tablet_index"=0;"$row_index"=42;"data"="foobar";"$timestamp"=1865777485011069884u;"$cumulative_data_weight"=951;}; {"$tablet_index"=0;"$row_index"=43;"data"="megafoo";"$timestamp"=1865777485011069884u;"$cumulative_data_weight"=975;}; {"$tablet_index"=0;"$row_index"=44;"data"="megabar";"$timestamp"=1865777485011069884u;"$cumulative_data_weight"=999;}; {"$tablet_index"=0;"$row_index"=45;"data"="foo";"$timestamp"=1865777486084785133u;"$cumulative_data_weight"=1019;}; {"$tablet_index"=0;"$row_index"=46;"data"="bar";"$timestamp"=1865777486084785133u;"$cumulative_data_weight"=1039;}; # Create queue producer on pythia. $ yt --proxy pythia create queue_producer //tmp/$USER-test-producer 309db-eb36-3f30191-f83f27c0 # Create queue producer session. $ yt --proxy pythia create-queue-producer-session --queue-path //tmp/$USER-test-queue --producer-path //tmp/$USER-test-producer --session-id session_123 { "epoch" = 0; "sequence_number" = -1; "user_meta" = #; } # Write rows via queue producer. $ echo '{data=value1;"$sequence_number"=1};{data=value2;"$sequence_number"=2}' | yt --proxy pythia push-queue-producer //tmp/$USER-test-producer //tmp/$USER-test-queue --session-id session_123 --epoch 0 --input-format yson { "last_sequence_number" = 2; "skipped_row_count" = 0; } # Check written rows. $ yt --proxy pythia pull-queue //tmp/$USER-test-queue --offset 100 --partition-index 0 --format "yson" { "$tablet_index" = 0; "$row_index" = 100; "data" = "value1"; "$timestamp" = 1865777698685609732u; "$cumulative_data_weight" = 2243; }; { "$tablet_index" = 0; "$row_index" = 101; "data" = "value2"; "$timestamp" = 1865777698685609732u; "$cumulative_data_weight" = 2266; }; # Write one more row batch with row duplicates. $ echo '{data=value2;"$sequence_number"=2};{data=value3;"$sequence_number"=10}' | yt --proxy pythia push-queue-producer //tmp/$USER-test-producer //tmp/$USER-test-queue --session-id session_123 --epoch 0 --input-format yson { "last_sequence_number" = 10; "skipped_row_count" = 1; } # Check that there is no row dublicates. $ yt --proxy pythia pull-queue //tmp/$USER-test-queue --offset 100 --partition-index 0 --format "yson" { "$tablet_index" = 0; "$row_index" = 100; "data" = "value1"; "$timestamp" = 1865777698685609732u; "$cumulative_data_weight" = 2243; }; { "$tablet_index" = 0; "$row_index" = 101; "data" = "value2"; "$timestamp" = 1865777698685609732u; "$cumulative_data_weight" = 2266; }; { "$tablet_index" = 0; "$row_index" = 102; "data" = "value3"; "$timestamp" = 1865777742709000317u; "$cumulative_data_weight" = 2289; }; ``` # Replicated tables ## General information { #common } Replicated dynamic tables are a type of tables in YTsaurus that does not store a full set of written data, but is a replication queue. As the data is successfully written to replicas, it is gradually deleted from the replicated dynamic table. Copies (replicas) of a replicated table can be located both on the same cluster as the source table (intra-cluster replication) and on other clusters often located in different DCs (inter-cluster replication). Replication between dynamic tables enables you to have multiple copies of the same table in independent clusters and organize automatic data replication between them. Using replicated dynamic tables enables you to: - Survive DC level failures, both read and write. - Increase fault tolerance of the user service by isolating table replicas. - Update YTsaurus clusters with less or no downtime for the user. Both [sorted](https://ytsaurus.tech/docs/en/overview.md) and [ordered](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md) dynamic tables can be replicated. Replicas can be **synchronous** and **asynchronous**. A successful write transaction commit ensures that the data is in synchronous replication and in the replication queue. Data will be written to asynchronous replicas in the background. For more information, see [Replication](https://ytsaurus.tech/docs/en/#replication). Each replicated dynamic table can have replicas of both types. Data consistency when writing to a replicated dynamic table is the same as when using an unreplicated dynamic table (sorted or ordered, respectively). Replication requires one **meta cluster** and one or more **replica clusters**. Setting up replication consists of several steps: 1. Creating a special `replicated_table` object on the meta cluster. This object stores information about replicas and replication status. 2. Create one dynamic table (sorted or ordered depending on the task) on each of the replica clusters. 3. Setting up replicas on the meta cluster and enabling replication. Once the above steps are complete, data can be written to or deleted from the replicated table on the meta cluster using the `insert-rows` and `delete-rows` commands. The changes will get into the replica clusters. A replicated dynamic table has a _schema_ and _tablets_ defined by the boundary keys. The schema of the replicated table must be no wider than that of the replicas (differences are possible during migration of all replicas to a wider schema). Transactions apply to a write to a replicated dynamic table: the actual writing of rows into it occurs when a write transaction is committed. This transaction must be performed exactly on the meta cluster. Other dynamic (not necessarily replicated) meta cluster tables can still be used within such a transaction. ## Writing data { #write } Writing from the client to the replicated dynamic table results in a distributed commit. As part of the commit, rows are added to the replication queue on the meta cluster. The client also writes changes to each synchronous replica (if any) as part of this transaction. The data flows from the client to the meta cluster, as well as to each cluster with a synchronous replica directly, i.e. the flows from the client are multiplied (at least doubled with one synchronous replica). This distributed transaction is coordinated by the meta cluster. If data cannot be written to at least one synchronous replica, the client will get an error. Writing to a replicated dynamic table with at least one synchronous replica semantically does not differ from writing to a regular dynamic table. You can write to a replicated dynamic table even if there are no synchronous replicas. {% note info "Note" %} When writing to a replicated dynamic table without synchronous replicas, the system is unable to check for conflicts between transactions, so semantically such a write differs from a regular one. {% endnote %} A successful write to a replicated dynamic table with only asynchronous replicas means that the changes are accepted by the system and are guaranteed (as quickly as possible) to be delivered to the replicas. The asynchronous replicas themselves are always lagging behind the source table. To allow a client to write to a replicated dynamic table that has no synchronous replica, the write commands (`insert-rows`, `delete-rows`) have a special `require_sync_replica` flag (the default value is `true`). Setting this flag to `false` enables a write to a replicated dynamic table without a synchronous replica. If data cannot be written to the asynchronous replica, it will be accumulated in the replicated table. Once the connection with the replica is re-established, all unreplicated data will be written to the replica. ## Reading data { #read } The main task of a replicated dynamic table is to write user data to the replication queue as quickly as possible. As was mentioned above, a replicated dynamic table does not store a full set of data. Therefore, the YTsaurus system seeks to reduce the read load from the replicated dynamic table and will redirect all read queries to the synchronous replica if there is one (defined by a subquery to the meta cluster). Such reading is guaranteed to be semantically indistinguishable from reading from a regular dynamic table. {% note warning "Attention!" %} The ability to read from a replicated dynamic table causes the additional overhead costs of interacting with the meta cluster. This feature cannot be used in a situation when low latency or high RPS is required. {% endnote %} {% note warning "Attention!" %} With asynchronous replicas only, the ability to read from the replicated dynamic table remains, but is undesirable as it creates additional load on the replicated dynamic table. Reading should be performed from replicas. {% endnote %} Reading from a replicated dynamic table can be performed using the `select-rows` and `lookup-rows` commands. When using `select-rows`, several meta cluster tables can participate in a single query. If at least one of the tables in a query is replicated, all other tables must also be replicated, and there must be a single cluster on which there are synchronous replicas of all the specified tables. In other words, inter-cluster queries are not supported. ## Replication { #replication } The asynchronous replication process works according to the push schema. There is a replicator for each tablet of the replicated dynamic table and each replica there, which reads fresh changes from the replication queue and sends the data to the asynchronous replicas. The sending process is a distributed (between clusters) transaction, in which data is written to the replica table and data portion replication is recorded in the replicated dynamic table metadata. ### Guarantees For asynchronous replicas, replication transactions (delivering data to replicas) are in no way coordinated with write transactions to the replicated table. Changes in a single write transaction to a replicated table can be broken down into multiple replication transactions. Guarantees: - There are no guarantees of atomicity for the replica reader - they may catch the replica in an intermediate state when only part of the transaction changes have been applied to it. - Exactly-once semantics is guaranteed (each write to the replicated table will be delivered to all replicas exactly once). - The correct order of change delivery to the replica tables is guaranteed — all transactions in the tablet of the replicated table are ordered relative to the commit timestamp. This is the order in which the changes are sent to the replicas. In case of synchronous mode, the replicator is not involved in the appearance of data in the replica. Instead, a client writing to a replicated table within the same transaction writes (automatically, implicitly) to the synchronous replica table as well. In this sense, the way the data gets into the synchronous replica is not much different from an ordinary table write. In particular, for a synchronous replica, the consistency of its state is guaranteed at exactly the same level as for a regular dynamic table. However, it is important to realize that what is written is only true if the replica remains synchronous at all times. When replication mode switches to asynchronous, the data starts getting into the table with a lag, and when communicating directly with the replica, there is not much chance for the client to realize that such a switch occurred. The replica tables can be accessed directly through the clusters where these replicas are physically located. The replica tables are special. When replicated, the replica tables contain data with the timestamp generated on the meta cluster. You cannot write data to replica tables directly (bypassing the meta cluster), the system makes every effort to prohibit such writing (such writing can be performed by setting `upstream_replica_id` to zero. The risks of this action are described below). You can read data from replica tables. This is a welcome use case when the replica tables serve a realtime service hosted in the same DC, which gives data to an external user. Limitations on the consistency of the data that can be read: - If the replica is synchronous, reading without specifying a timestamp will give the most recent state (specifically, there is no snapshot isolation at the level of individual values of individual rows: different rows and different columns of the same row may correspond to different points in time; visibility of data from all committed transactions is guaranteed). - Reading from a replica with a timestamp is possible, but we recommend getting the timestamp from the meta cluster, because this timestamp is actually invalid for the replica cluster (because it came from the meta cluster). - For asynchronous replicas, a lag is always possible (and it exists), so any reading will return lagged data; consistency is not guaranteed in replication (a transaction in the replicated table can atomically change `A` and `B` keys, read the new value for `A` from the replica one, and the old value for `B` after that). ### Replicas Each replicated table is associated with a set of replicas (`table_replica` objects) created as follows: Creating a replica: ```bash yt create replicated_table //tmp/replicated --attr '{dynamic=%true; schema=[{name=k;type=string;sort_order=ascending};{name=v;type=string}]}' yt create table_replica --attr '{table_path="//tmp/replicated"; cluster_name="replica_cluster";replica_path="//tmp/replica"}' ``` This set of commands creates a replicated table on the meta cluster `//tmp/replicated` and configures an (asynchronous) replica for it on the `replica_cluster` cluster at `//tmp/replica`. The specified commands **do not create** the table itself `//tmp/replica` on the replica cluster, but only configure the replica for `//tmp/replicated`. Replica tables must be created individually. Note that after a replica is created, it is by default in a **disabled state** (`state = "disabled"`), in which replication is disabled. To run a replica, use the `alter-table-replica --enable` command. For more information, read [below](https://ytsaurus.tech/docs/en/#replica-state). Each created replica (like any master server object) has an ID. The following commands can be used to view replica information: Viewing a replica: ```bash yt get //tmp/replicated/@replicas yt get '#replica-id/@' ``` You can delete a replica like other objects using the `remove` command: Deleting a replica: ```bash yt remove '#replica-id' ``` #### Adding a replica to an existing replicated table {#add-new-replica} If you want to add a replica by fully copying the data of an existing replica, use the replica copy tool. If you want to add a new replica or you already have a replica table that stores the required data state, use one of the two attributes (`start_replication_timestamp` or `start_replication_row_indexes`) when creating a `table_replica` object. With `start_replication_timestamp`, all changes that have a commit ts strictly greater than the specified value (meaning the meta cluster timestamp) will be replicated. If you need to initiate replication from a specific moment, the easiest way to do that is to specify a timestamp obtained by calling generate-timestamp for the meta cluster. With `start_replication_row_indexes`, you need to specify the row index (and do so for each tablet of the replicated table) from where you want data to replicate. You can obtain relevant row indexes from the get-tablet-infos query. If the new replica table is [ordered](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md) and its schema includes the [`$cumulative_data_weight`](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md#cumulative_data_weight_column) column, you should also specify the `cumulative_data_weights` attribute when creating it — this is the initial value of that column in each tablet. Otherwise, the count in the new replica will start from zero, and its `$cumulative_data_weight` values will diverge from those of the other replicas. You can get suitable values from the `$cumulative_data_weight` column of the last already replicated rows of any existing replica, or use the replica copy tool. ### Creating replica tables A replica table is a regular (dynamic) table on the target cluster. But when creating it, you must specify the `upstream_replica_id` attribute and the ID of the replica object on the meta cluster. Specifying `upstream_replica_id` allows the replicator to write from the metacluster to the replica cluster and prohibits direct writing to the replica table (which would bypass the replicator). Specifying this ID protects against configuration errors when the same table appears as a replica for multiple replicated tables. Creating a replica table: ```bash yt create table //tmp/replica --attr '{dynamic=%true; upstream_replica_id=replica-id;schema=[{name=k;type=string;sort_order=ascending};{name=v;type=string}]}' ``` The `upstream_replica_id` table attribute can be changed after the table is created using the `alter-table` command. All table tablets must be unmounted. ### Replica settings {#replica-state} Immediately after a replica is created, it is in a `disabled` state (the `state` attribute). No replication is performed in this state. To enable a replica, use the `alter-table-replica` command: Enabling a replica: ```bash yt alter-table-replica replica-id --enable ``` The replica will switch to `enabling` state and after it is successfully enabled, it will switch to `enabled` state. The replica can be disabled, thereby interrupting replication. To disable a replica, use the same `alter-table-replica` command. Disabling is also asynchronous, the replica will switch to `disabling` and then to `disabled` state: Disabling a replica: ```bash yt alter-table-replica replica-id --disable ``` ### Manual synchronization In some cases it is sufficient to have only asynchronous replicas (they provide the maximum possible write speed to the replicated table, since all actions are performed in the background), but still be able to explicitly wait for replication to finish in any replica. The `get-in-sync-replicas` command is used for that. For it to work, you need to specify a timestamp and the system will find all replicas that are guaranteed to be reached by all changes with a timestamp no greater than the specified one. For example, you can generate the current timestamp and then wait in the cycle for the specified replica to be in the list returned by the command. This replica can then be queried and timestamped. This will guarantee read consistency. ### Replication modes By default, replicas are created in asynchronous mode. The user can specify a synchronous mode at creation (via the `mode` attribute) and change the mode on the fly (using the `alter_table_replica` command). This switch can result in write downtime: if a replica is switched from asynchronous to synchronous state, queue tail replication must be fully complete before the client can write to that replica. Therefore, it is not a good idea to consider a replica with a large lag to be synchronous (the replicated table cannot be written to until the lag disappears). Asynchronous replication is possible without saving the start `timestamps`. To use this mode, create a replica with the `{"mode": "async", "preserve_timestamps": "false"}` attributes, and the replica table must be created without specifying the `upstream_replica_id` attribute. This feature can be useful in a number of cases: 1. You want to replicate data from multiple sources into one replica table. 2. You want to manually change data in the replica table (patch, insert new data), bypassing the replication mechanism. ### Automatic switching of the synchronous replica {#sync-replica-switch} If the cluster on which the synchronous replica is located is unavailable, writing to the replicated dynamic table is stopped. To continue writing, the synchronous replica must be switched to another cluster. The YTsaurus system can automatically switch a synchronous replica. To activate this feature, either create a replicated dynamic table with the `replicated_table_options={enable_replicated_table_tracker=%true}` attribute or add the specified attribute later using the `yt set` call. The YTsaurus system will then monitor the table replicas and switch the synchronous replica to another cluster if necessary. There are two settings to specify the number of synchronous replicas: `min_sync_replica_count` and `max_sync_replica_count`. The automated system switches available replicas to synchronous mode, ensuring that there are no more than `max_sync_replica_count` synchronous replicas. The automated system also monitors that the number of synchronous replicas does not fall below `min_sync_replica_count`. If `min_sync_replica_count` and `max_sync_replica_count` are not specified, then both fields are equal to 1 by default. If only `min_sync_replica_count` is not specified, it will be equal to `max_sync_replica_count`. If only `max_sync_replica_count` is not specified, it will be equal to the total number of replicas. Thus, by default, the automated system supports exactly one synchronous replica. In case of chaos replication, there are special parameters (`min_sync_queue_replica_count` and `max_sync_queue_replica_count`) for replicas of type `queue`. The `sync_replica_lag_threshold` threshold determines when a lagging synchronous replica is considered unavailable: if the replica's lag exceeds the threshold, the automated system stops treating it as synchronous and switches to another replica if necessary. By default, the threshold is 10 minutes; 1 minute is recommended. There is a mode when the YTsaurus system stops monitoring any table replica, but continues monitoring the remaining replicas. To enable this mode for a replica (`table_replica`), use the `yt set` call and set the `enable_replicated_table_tracker=%false` attribute. You can view the current mode for the replica table using the `replicated_table_tracker_enabled` attribute of the meta cluster replica object: ```bash $ yt --proxy meta-cluster get //home/some_meta_table/@replicas "table-replica-id" = { "cluster_name" = "some_name"; "replica_path" = "//home/some_table"; "state" = "enabled"; "mode" = "async"; "replicated_table_tracker_enabled" = %false; }; ``` #### Concurrent switching of synchronous table group replicas { #replication_collocation } You can combine replicated tables into groups (collocations) so that the system monitors that synchronous replicas of these tables are on the same clusters. To do this, create a `table_collocation` object that stores a list of collocated replicated tables. Working with replicated table collocations: ```bash # Create a collocation with a list of tables. yt create table_collocation --attributes '{collocation_type=replication; table_paths=["//tmp/replicated_table_1"; "//tmp/replicated_table_2"]}' # Find out the list of tables in a collocation. yt get "#collocation-id/@table_ids" yt get "#collocation-id/@table_paths" # Find out the ID of the collocation to which the table belongs. yt get //tmp/replicated_table_1/@replication_collocation_id # Find out the list of all tables in the collocation to which the table belongs. yt get //tmp/replicated_table_1/@replication_collocation_table_paths # Add a table to the collocation. yt set //tmp/replicated_table_3/@replication_collocation_id collocation-id # Delete a table from the collocation. yt remove //tmp/replicated_table_3/@replication_collocation_id ``` #### Specifying preferred clusters for synchronous replicas When a replicated table on different clusters has multiple replicas that can become synchronous, you can specify which of them should be given preference in case of automatic switching. To do this, the following attribute must be set on the replicated table: ```bash yt set //tmp/replicated_table/@replicated_table_options/preferred_sync_replica_clusters '["cluster-name"]' ``` This attribute respects [collocations](https://ytsaurus.tech/docs/en/#replication_collocation) of replicated tables. To specify preferred clusters for all tables in a collocation at once, just set this attribute in the collocation options once. ```bash yt set #collocation-id/@replicated_table_options/preferred_sync_replica_clusters '["cluster-name"]' ``` If this attribute is not set for the collocation, its value will be derived from the replicated tables' options. In this case, make sure that it is set **for all tables** of the collocation. ### Limiting replication speed Asynchronous replication can lead to a substantial load on the network connecting the data centers as well as on the replica table cluster, so we recommend setting up limits for write-intensive tables. The system supports two types of settings. In most scenarios, we recommend using relative throttling. #### Relative replication throttling Relative throttling can be configured via the `mount_config/relative_replication_throttler` attribute. This attribute is a dict that can contain the following values. | Name | Type | Description | Default value | | -------------------------- | ------------------------ | ------------------------------------------------------------ | ---| | enable | bool | Throttling flag | `%false` (may depend on the cluster) | | ratio | double | Ratio of the replication speed to the write speed | 2 | Relative throttling limits the speed of asynchronous replication relative to the write speed. For example, if there's a data set that took an hour to write, and the asynchronous replica was unavailable during that time, then with ratio = 3 that data will be replicated in 20 minutes once the replica becomes available. If the write speed is constant, the default value (ratio = 2) ensures that the catch-up time is roughly the same as the lag time. In this case, it is assumed that the replica cluster has sufficient resources to handle a doubled load for the duration of the catch-up period. By decreasing the ratio value, you can reduce the required resource guarantees at the cost of increasing the replica catch-up time. Setup example: ``` yt set //tmp/replicated_table/@mount_config/relative_replication_throttler '{ratio=1.5}' ``` #### Absolute replication throttling This type of throttling can be configured via the `mount_config/replication_throttler` attribute, which can accept two values: - limit: The limit for the average bandwidth value in bytes/sec. - period: The time interval in ms during which the average bandwidth value is measured. For example, the following command sets a replication limit of one megabyte per second: ```bash yt set //tmp/replicated_table/@mount_config/replication_throttler '{period=1000;limit=1000000}' ``` {% note info "Note" %} The limit you set does not apply to the entire table, but to each table tablet individually. {% endnote %} ### Maintaining the tablet index during ordered table replication By default, asynchronous replication of an ordered dynamic table writes rows to an arbitrary replica tablet. If you need to maintain the tablet index (this is important if you want the same tablet row to appear under the same index across different replicas), set the `preserve_tablet_index` attribute of the ordered dynamic table itself to `%true`. When using this attribute, you need to manually track that the replicated table and the replica tables have the exact same number of tablets. ## Changing schemas { #alter_schema } To perform a compatible schema change to both a replica table and the replicated table, run the `alter-table` command. This command must be called for each table individually. Ensure that the table is unmounted before executing the command. The replica table schemas do not have to match the replicated table schema, but must be no narrower than the replicated table schema for replication to work correctly. You need to start the schema change with the replicas and then move on to the replicated table. ## Resistance to downtimes and updates { #downtimes } Each replica cluster exists independently. When a replica cluster is disabled, there is no way to read data from it. Replicas can be updated independently. The replica table schemas do not have to match the replicated table schema, but must be no narrower than the replicated table schema for replication to work correctly. You need to start the schema change with the replicas and then move on to the replicated table. In order to write data to a replicated table, two conditions must be met, and are sufficient: 1. The meta cluster is available. 2. All synchronous replicas of the replicated table are available. It is assumed that the meta cluster is geo-distributed, updated infrequently and with a short downtime during which the table cannot be written to (but can still be read directly from replicas). If a replicated table has no synchronous replicas, it can be written to whenever the meta cluster is available. If a table has a synchronous replica, it needs to be kept available (which can be helped by automated synchronous replica switching). ## Attributes { #attributes } All replicated tables are tables and they have a corresponding set of attributes. Any replicated table is additionally characterized by these parameters: | Name | Type | Description | | -------------------------- | ------------------------ | ------------------------------------------------------------ | | replicas | Guid->ReplicaInfo | Description of replicas (keys are the replica IDs, and values are some of their important attributes) | | replicated_table_options | ReplicatedTableOptions | Automated replica switching settings | | preserve_tablet_index | bool | Whether to save the tablet index during replication of ordered dynamic tables. The default value is `%false` | `ReplicatedTableOptions` here have the following form: | Name | Type | Description | | --------------------------------- | --------- | ------------------------------------------------------------ | | enable_replicated_table_tracker | bool | Whether automated synchronous replica switching is enabled. The default value is `%false` | | max_sync_replica_count | integer | Maximum and preferred number of synchronous replicas to be supported by the automated system | | min_sync_replica_count | integer | Minimum number of synchronous replicas to be supported by the automated system | | max_sync_queue_replica_count | integer | Similar to `max_sync_replica_count`, but for `queue` replicas (used in chaos replication). | | min_sync_queue_replica_count | integer | Similar to `min_sync_replica_count`, but for `queue` replicas (used in chaos replication). | | | preferred_sync_replica_clusters | list | List of preferred clusters for synchronous replicas | | sync_replica_lag_threshold | Duration | Maximum lag value up to which a synchronous replica is considered available. The default value is 10 minutes; the recommended value is 1 minute. | | enable_preload_state_check | bool | Whether to check the preload state of tables mounted in memory to see if they are available. The default value is `%false`. | | incomplete_preload_grace_period | Duration | When checking replica availability in terms of the preload state, the replica is considered available if the last preload was successfully completed no later than the value specified in this parameter. The default value is 5 minutes. This parameter is used to ensure that the short-term preload during tablet rebalancing does not interfere with the algorithm. | Each replica is described by the `ReplicaInfo` dict with the following form: | Name | Type | Description | | ---------------------- | ------------------- | ------------------------------------------------------------ | | cluster_name | string | Replica cluster name | | replica_path | string | The path to the replica table on the replica cluster | | state | TableReplicaState | Replica state | | mode | TableReplicaMode | Replica mode: `async` or `sync` | | replication_lag_time | Duration | Replica lag estimate | | errors | Error[*](https://ytsaurus.tech/docs/en/*error) | List of replication errors for this replica | | preserve_timestamps | bool | Save start `timestamps`? The default value is `true`. Makes sense only for asynchronous replicas | | atomicity | EAtomicity | `full` or `none`. Makes sense only for asynchronous replicas | The `replication_lag_time` gives a rough estimate of the replication lag (to within tens of seconds). The replica object itself, in addition to the listed attributes (as well as those inherent in all objects), also has a number of additional attributes (all of which can be found out by running a query of the `yt get '#replica-id/@'` type): | Name | Type | Description | | --------------------------------- | -------------------- | ------------------------------------------------------------ | | mode | ReplicaMode | Replica mode: `sync` or `async` | | start_replication_timestamp | Timestamp | Starting timestamp for the replica | | table_path | string | Path to the replicated table on the meta cluster | | tablets | TabletReplicaInfo[*](https://ytsaurus.tech/docs/en/**) | Description of tablet states. The position in the list corresponds to the tablet index in the table itself | | enable_replicated_table_tracker | bool | Enables automated synchronous replica switching (see above). The default value is `%true` | The `TabletReplicaInfo` structure has the following form: | Name | Type | Description | | ------------------------------- | ------------------- | ------------------------------------------------------------ | | tablet_id | Guid | Tablet ID | | state | TableReplicaState | State of this tablet replica | | current_replication_row_index | integer | Replication boundary in the queue by write index (writes with index >= the specified index are subject to replication) | | current_replication_timestamp | Timestamp | Replication boundary in the queue by timestamp (writes with timestamp > the specified timestamp are subject to replication) | | replication_lag_time | Duration | Replica lag estimate for this tablet | | flushed_row_count | integer | The number of this tablet's rows written to the disk. If the replicated table is frozen, you can compare this value against `current_replication_row_index` to keep track of the replication | | trimmed_row_count` | integer | The number of initial rows in this tablet that have been successfully replicated and whose chunks were deleted (log truncation) | | replication_error | Error | Describes a replication error. If there is no error, this tag is also missing | The `current_replication_row_index` and `current_replication_timestamp` parameters increase monotonically as the replicator runs, marking the successfully replicated writes. ## Checking a replication queue { #queue_check } A replication queue check is useful for re-creating a replicated table without losing changes. The procedure is as follows: 1. Run the `freeze-table` command for the replicated table. 2. Wait (by querying the `tablet_state` table attribute) for all tablets to go into `frozen` state. 3. Get information about the state of the tablets of the table of interest by making a `get #replica-id/@tablets` query. Each response list item describes the replication state for a specified tablet of the replicated table to the specified replica. For each tablet, view the `flushed_row_count` and `current_replication_row_index` values and wait until they are the same. ## Use cases { #examples } ### Creating, setting up replicas The examples work from version ytsaurus-client >= 0.8.19. . Examples of using replicated tables: ```bash export YT_PROXY=cluster-name # Creating a replicated table. yt create replicated_table //tmp/replicated --attr '{ dynamic=%true; schema=[{name=k;type=int64;sort_order=ascending};{name=v;type=int64}] }' 730e-68d3e-3ff01a9-325bbdcd # Creating a replica associated with first-replica-cluster. yt create table_replica --attr '{ table_path="//tmp/replicated"; cluster_name="first-replica-cluster"; replica_path="//tmp/replica" }' 730e-7bcd8-3ff02c5-fd0b36ee # Creating a second replica on second-replica-cluster. yt create table_replica --attr '{ table_path="//tmp/replicated"; cluster_name="second-replica-cluster"; replica_path="//tmp/replica" }' 730e-8611b-3ff02c5-f647333f # Creating a replica table on first-replica-cluster. YT_PROXY=first-replica-cluster yt create table //tmp/replica --attr '{ dynamic=%true; upstream_replica_id="730e-7bcd8-3ff02c5-fd0b36ee"; schema=[{name=k;type=int64;sort_order=ascending};{name=v;type=int64}] }' 6cd9-66770-3ee0191-980d9f6 # Creating a replica table on second-replica-cluster. YT_PROXY=second-replica-cluster yt create table //tmp/replica --attr '{ dynamic=%true; upstream_replica_id="730e-8611b-3ff02c5-f647333f"; schema=[{name=k;type=int64;sort_order=ascending};{name=v;type=int64}] }' 78b6-1dacd9-3f40191-2057d1df # Mounting a replica table and the replicated table. yt mount-table //tmp/replicated YT_PROXY=first-replica-cluster yt mount-table //tmp/replica YT_PROXY=second-replica-cluster yt mount-table //tmp/replica # Writing data to the replicated table. echo '{k=1;v=100}' | yt insert-rows //tmp/replicated --format yson Table //tmp/replicated has no synchronous replicas # Setting the no-require-sync-replica flag to force a write. echo '{k=1;v=100}' | yt insert-rows //tmp/replicated --format yson --no-require-sync-replica # Write was successful, but there is no data in the replica. YT_PROXY=first-replica-cluster yt select-rows '* from [//tmp/replica]' --format yson # Viewing replica state. yt get '#730e-7bcd8-3ff02c5-fd0b36ee/@state' "disabled" # Replica was not enabled. Enabling a replica. yt alter-table-replica 730e-8611b-3ff02c5-f647333f --enable yt alter-table-replica 730e-7bcd8-3ff02c5-fd0b36ee --enable # Checking replication progress. YT_PROXY=first-replica-cluster yt select-rows '* from [//tmp/replica]' --format json {"k":1,"v":100} # Attempting to read data directly from the replicated table. yt select-rows '* from [//tmp/replicated]' --format json No in-sync replicas found for table //tmp/replicated # Reading is not possible without synchronous replicas. # Disabling the second replica and rewriting. yt alter-table-replica 730e-8611b-3ff02c5-f647333f --disable echo '{k=2;v=200}' | yt insert-rows //tmp/replicated --format yson --no-require-sync-replica # Reading from replicas YT_PROXY=first-replica-cluster yt select-rows '* from [//tmp/replica]' --format json {"k":1,"v":100} {"k":2,"v":200} YT_PROXY=second-replica-cluster yt select-rows '* from [//tmp/replica]' --format json {"k":1,"v":100} # You can see that first-replica-cluster received the data, but second-replica-cluster did not. # Failed replication can be noticed by a growing lag. yt get '#730e-8611b-3ff02c5-f647333f/@replication_lag_time' 141000 # Changing the replication type on second-replica-cluster and enabling the replica. yt alter-table-replica 730e-8611b-3ff02c5-f647333f --enable --mode sync # Reading data from second-replica-cluster and checking if the data has appeared. YT_PROXY=second-replica-cluster yt select-rows '* from [//tmp/replica]' --format json {"k":1,"v":100} {"k":2,"v":200} # Having a synchronous replica allows us to read directly from the replicated table. yt select-rows '* from [//tmp/replicated]' --format json {"k":1,"v":100} {"k":2,"v":200} # Deleting the first row, this time in synchronous mode. echo '{k=1}' | yt delete-rows //tmp/replicated --format yson # Checking the command execution status. YT_PROXY=first-replica-cluster yt select-rows '* from [//tmp/replica]' --format json {"k":2,"v":200} YT_PROXY=second-replica-cluster yt select-rows '* from [//tmp/replica]' --format json {"k":2,"v":200} # Disabling writes to the replicated table. yt freeze-table //tmp/replicated # Checking if the tablet is frozen (if all the data has been written to the disk). yt get //tmp/replicated/@tablet_state "frozen" # Checking that all rows were successfully replicated in each of the replicas. # To do this, view the flushed_row_count and replicated_row_index attributes. yt get '#730e-7bcd8-3ff02c5-fd0b36ee/@tablets' [ { "flushed_row_count" = 5; "tablet_id" = "730e-68d3e-3ff02be-ee882e4a"; "trimmed_row_count" = 2; "replication_lag_time" = 0; "current_replication_timestamp" = 1610012496066773010u; "current_replication_row_index" = 5; "state" = "enabled"; }; ] yt get '#730e-8611b-3ff02c5-f647333f/@tablets' [ { "flushed_row_count" = 5; "tablet_id" = "730e-68d3e-3ff02be-ee882e4a"; "trimmed_row_count" = 2; "replication_lag_time" = 0; "current_replication_timestamp" = 1610012496066773010u; "current_replication_row_index" = 5; "state" = "enabled"; }; ] # You can see that the flushed_row_count and current_replication_row_index values are the same, which means that both replicas have been fully replicated. ``` [*error]: The parameter appears in the answer several times. ## Chaos replicated tables Chaos replicated dynamic tables in YTsaurus are functionally similar to regular [replicated tables](https://ytsaurus.tech/docs/en/replicated-dynamic-tables.md) but are based on the new *chaos replication* protocol. Much like regular replicated tables, they allow multiple copies of the same table to be stored across different clusters updated in real time. These copies are strictly consistent with each other and together represent a continuously available dynamic table with full transaction support. Compared to regular replicated tables, chaos dynamic tables are always available for both reading and writing, thanks to the chaos replication protocol. Chaos replication has the following distinct features: - Each cluster must contain a special type of replica: a replication queue. - Replication is performed using a pull model, where any available queue with sufficiently recent data can serve as a replication source. - Replica status data is stored in the form of a *replication card* on separate *chaos metadata* clusters. - To ensure compatibility with regular replicated tables, chaos replication relies on the special `chaos_replicated_table` object. Unlike a `replicated_table`, it doesn't contain any data: it's simply a proxy node for chaos metadata and replicas. - Chaos metadata is stored in a dedicated chaos cell bundle, and the `chaos_replicated_table` is linked to this bundle. - Differences in the replication topology requires a different approach to [capacity planning](https://ytsaurus.tech/docs/en/#capacity-planning). Chaos replicated dynamic tables can be both [sorted](https://ytsaurus.tech/docs/en/overview.md) and [ordered](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md). For ordered dynamic tables, each replica is used both to hold data and as a replication queue for other replicas. For sorted dynamic tables, replicas containing data and replicas serving as replication queues are fundamentally different and have distinct types. ## Creating a chaos replicated dynamic table This section describes the steps for quickly creating a chaos replicated dynamic table and highlights their main interface differences compared to regular replicated tables. For more in-depth information about chaos dynamic tables, read the sections that follow. 1. Creating a chaos dynamic table. First, create a `chaos_replicated_table` object. Even though it only functions as a proxy, it provides a convenient entry point that greatly simplifies working with chaos replicated dynamic tables. Furthermore, for any operation, `chaos_replicated_table` requires only read access to the cluster on which it resides. It remains available during updates performed in read access mode. When creating this object, make sure to specify the `chaos_cell_bundle` attribute, which defines the chaos bundle to link the table to and hold all necessary metadata. In addition, we recommend specifying the `schema` attribute: when it's present, you can specify the `chaos_replicated_table` directly for selects and lookups. ```bash yt create chaos_replicated_table //path/to/replicated_table --attr '{ chaos_cell_bundle=...; schema=[...]; }' ``` 2. Creating a replica with data. This replica stores the data in regular format. If you unlink this replica from the replication, it becomes a regular dynamic table and retains all its data. To link a replica table to a `chaos_replicated_table`, first create a `chaos_table_replica` object, which is part of the metadata describing their relationship. It's a complete analog of the `table_replica` object used for replicated dynamic tables. When creating a `chaos_table_replica`, make sure to specify the `chaos_replicated_table` it belongs to and the replica table it describes. Note the `"content_type" = "data"`: this indicates that the replica contains data and can't be used as a replication source. To create a replica of an ordered dynamic table, specify `"content_type" = "queue"`: this way, the replica will both hold data and serve as a replication source. ```bash $ yt create chaos_table_replica --attr ' { "table_path" = "//path/to/replicated_table"; "cluster_name" = replica_cluster; "replica_path" = "//path/to/data"; "content_type" = "data"; "mode" = "sync"; "enabled" = %true; }' 1-2-3-4 ``` The operation that creates the `chaos_table_replica` returns the ID of the newly created replica. You'll need it as the `upstream_replica_id` for the replica table. You can either specify the `upstream_replica_id` when creating the table or change it later using `alter_table`. ```bash $ yt --proxy replica_cluster create table //path/to/data --attr '{dynamic=%true; schema=[...]; upstream_replica_id="1-2-3-4"}' # Or $ yt --proxy replica_cluster alter-table --upstream-replica-id 1-2-3-4 //path/to/data ``` 3. Creating a replica with a replication queue (only for sorted chaos dynamic tables). This step is similar to creating a replica with data, but you need to specify `"content_type" = "queue"` when creating the `chaos_table_replica` and use the `replicated_log_table` type. When creating a queue, you can specify `pivot_keys` to split it into multiple tablets. Sharding can only be done once, when you're create the queue. If you end up needing more shards later, you'll have to create a new replication queue and delete the old one (chaos replication allows performing these operations on live tables). ```bash $ yt create chaos_table_replica --attr ' { "table_path" = "//path/to/replicated_table"; "cluster_name" = replica_cluster; "replica_path" = "//path/to/replication_queue"; "content_type" = "queue"; "mode" = "sync"; "enabled" = %true; }' 5-6-7-8 $ yt --proxy replica_cluster create replication_log_table //path/to/replication_queue --attr '{dynamic=%true; schema=[...]; upstream_replica_id="5-6-7-8"}' ``` ## Working with chaos replicated dynamic tables The simplest approach is to always use `chaos_replicated_table` for both writes and reads. This ensures that all transactional guarantees are maintained. However, chaos replication also supports accessing the entire chaos replicated table through any replica: - When writing to a `chaos_replicated_table` or any of its replicas, the changes apply across the entire chaos replicated table according to its current state. For example, when formally writing to an asynchronous replica, the data in the transaction is actually written only to synchronous replicas. - Reads from a replica retrieve data only from that replica. Note that this behavior may change in the future. - Reads from a replica with the `replica_consistency=sync` option retrieve data from a synchronous replica (or a sufficiently recent asynchronous replica, not necessarily the one specified in the operation). - Reads from a `chaos_replicated_table` are always performed in `replica_consistency=sync` mode, meaning they only return data from a sufficiently recent replica. ## Adding a new replica When working with a table over an extended period of time, you may occasionally need to add a new replica: for example, in order to move it to a new cluster or to simply recreate an existing replica. You can add a new replica using the familiar `chaos_table_replica` command. However, executing it on a table that has been running for a while will most likely lead to issues: replication to the newly created replica might fail, and the buildup of data in queues could potentially cause problems across the entire bundle. This happens because the replication history is regularly cleaned up, and the original data has long since been deleted. There are several ways to add a new replica that don't require replication of lifetime data: - Specify `catchup=%false` when creating the replica. In this case, it's assumed that the replica doesn't need the history, so it receives data only starting from a specific timestamp after the table's creation. For a replica with data, only the latest records are replicated, meaning its contents will differ from other replicas. However, `catchup=%false` mode may be a reasonable default approach for adding a new replication queue: typically, replication queues store only a small portion of recent data that hasn't yet been propagated to other replicas. - Specify a `replication_progress` when creating the replica. In this case, replication to the replica starts at the specified [progress point](https://ytsaurus.tech/docs/en/#progress-replikacii). With this method, you can explicitly control the starting point for data replication. For more information about replication progress, read below. - Specify the progress on the replica table. Similar to the previous approach, replication continues from the specified progress point. This is the recommended method because the progress is explicitly stored as an attribute of the table. **Note:** the progress on the table must be greater than or equal to the value specified in the `chaos_table_replica`. If the table's progress suddenly drops below this value, replication stops. When adding a new replica with an explicitly specified progress, make sure that your current replication queues contain the necessary data. You can usually achieve this by unmounting an existing replica with data. This ensures that data can no longer be replicated to this table and will start to accumulate in the queues. ### Copying an existing replica Let's take a closer look at a common scenario when you need to copy an existing replica with a data to a new cluster. In this case, we need the new replica to be completely identical to the existing one and to initiate replication to it. You can achieve this by following these steps: 1. Unmount the donor replica. We recommend switching it to asynchronous mode before doing this and wait till status becomes `unmounted`. Unmounting is necessary to record the replication progress. 2. Read the progress from the `@replication_progress` attribute of the donor replica table and remember it. 3. Create a new replica using the `create chaos_table_replica` command and specify the previous step progress. This will save the data in the replication queues until the replica starts working. You need to remember the resulting ID for later substitution into `upstream_replica_id`. 4. Copy the donor replica to a new cluster. This is a potentially time-consuming step and can be optimized: first, copy the replica locally (using the `yt copy` command) — this will quickly create the necessary data snapshot, then copy it in the background and move on to the next step. When copying to another cluster, you need to do a remote copy for the dynamic table — this will preserve all timestamps (if you convert the table to static during copying, the timestamp information will be lost, which may lead to data loss after replication is enabled). 5. Mount the donor replica. Replication will go to it, but the data in the copy will no longer change. 6. After copying is complete, set the correct `replication_progress` and `upstream_replica_id` on the new table. (Although a newly created replica with an empty progress will pull the progress from the card, it is always worth explicitly setting the progress on the replica table at this step — this way you can be sure that it will be exactly what is expected.) 7. Mount the new replica. It should update the progress in the replication card according to the specified value and begin replication. This is how you can copy a replica with data for a sorted chaos dynamic table. You cannot copy a queue replica, but you can simply create a new one by specifying `catchup=%false` — replication of subsequent records will use the new queue replica as well. ### Moving replicas You can move replicas to a new location while updating the path in the replication card. Since this operation is not atomic, it is recommended to switch the replica being moved to async mode in advance to avoid downtime. 1. `yt unmount-table ` 2. `yt move ` 3. `yt alter-table-replica --params '{replica_path=""}' ` 4. `yt mount-table ` ### Resharding of queue replicas Queue replicas are queues, so you cannot directly change the number of tablets in such replicas. Resharding of queue replicas is only possible through recreating them. This can be done in the following way: 1. Create new queue replicas with the desired number of shards in sync mode and specify `catchup=%false` and `replicated_table_tracker_enabled=%false` with a new path. The latter is necessary to prevent all new replicas from becoming async. 2. Make sure that writing has started in them: the replica lag in the interface should remain close to zero, and the `replication_lag_time` attribute should increase. 3. Generate a timestamp `yt generate-timestamp` and remember it. 4. Wait until the `replication_lag_timestamp` value of _all_ replicas is greater than the timestamp generated in the previous step. You can view the `replication_lag_timestamp` of table replicas using the command: ```bash $ yt get "/@replicas" | grep replication_lag_timestamp ``` 5. Delete the old queue replicas. 6. If necessary, move the new replicas to the place of the old replicas. 7. If necessary, enable automatic replica switching via `alter-table-replica`. ### Creating a new replica for an ordered dynamic table Replicas of an ordered dynamic table are queues, and the simplest way is to create a new replica by specifying `catchup=%false`, which will create a replica with working replication. However, in such a replica, row numbers will differ from those in other replicas, which may lead to unexpected results when reading, especially if `$row_index` is specified explicitly. To ensure that rows in the new replica have the same numbers as in previous replicas, you need to specify explicit [offsets](https://ytsaurus.tech/docs/en/ordered-dynamic-tables#creating) and progress corresponding to these offsets when creating the replica. Here is a way to do it: 1. Unmount the donor replica and wait for the `unmounted` status. 2. Save the replication progress (from the `@replication_progress` attribute of the replica table) and use it both when creating the replica (pass it in the attribute when creating `chaos_table_replica`) and set it explicitly on the new replica table (using `alter-table`). 3. For each tablet of the donor replica, get the number of rows in it — to do this, you need to iterate through the list in the `/@tablets` attribute and collect the `flushed_row_count` fields. Important: the donor replica must still be unmounted. 4. Create a new replica, specifying the saved progress for it. 5. After the new replica is created, you can mount the donor replica back. 6. Create a new replica table, specifying the number of tablets and the required offsets during creation using the `tablet_count` and `trimmed_row_counts` attributes. You also need to specify `upstream_replica_id` and `replication_progress`. 7. The new replica can be mounted — new rows will be replicated to it as well, and `$row_index` for them will be the same as for other replicas. This method allows you to create a new replica that is consistent with existing ones. It will only contain new data. We do not consider full copying for two reasons: 1) there is no remote copy for ordered dynamic tables, 2) the queue scenario generally assumes cleaning up old data, and their absence in the new replica is supposed to be a big problem. ## Required number of replication queues While the chaos replication protocol can work with any number of replication queues, in scenarios where queues are distributed across multiple data centers, you should have at least three queues, with at least two being synchronous at any given time. If, even temporarily, write are made only to one synchronous queue, replication will break if that queue suddenly becomes unavailable. This configuration may be acceptable only if the queue is located on a cross–data center cluster. ## Chaos cells Chaos tables achieve fault tolerance and continuous availability through a separate layer of chaos metadata stored in *chaos cells*. Chaos cells are located on cross–data center clusters. In addition, you can migrate information between chaos cells when temporarily shutting down some of them. For example, this can be useful during updates. Like tablet cells, chaos cells combine into chaos cell bundles. Bundles are required for isolation and help account for resources among different consumers. Unlike tablet cell bundles, chaos cell bundles are known to all clusters. You can view a full list of chaos cells directly in the UI, including those physically located on other clusters. To perform chaos replication, you need a chaos cell bundle and the appropriate permissions to use it. ## Replication progress During replication, it's important to know which data has been transmitted and which hasn't. In the transactional data model, updates to YTsaurus dynamic tables are uniquely identified by a key-timestamp pair, with timestamps strictly increasing. This way: - For each individual key, updates must be strictly ordered by timestamps. - Updates don't have to be ordered across different keys. Replication progress is represented as a step line chart in a key-time coordinate system: for each key range, the specified timestamp indicates the point where replication stopped for that range, inclusively. This approach allows separate data read requests for specific ranges, and these ranges don't necessarily have to match the sharding keys of the replication queues. Progress is represented by a sequence of segments (lower key, timestamp). The last key is set using a separate upper_key field. You can check a table's progress using the `@replication_progress` attribute. We recommend doing this only on unmounted tables, because the actual progress on mounted tables may be significantly greater. You can change the progress value using the `alter-table` command. This can be useful if you need to connect a new replica with prepared data to an existing table. Note that for a mounted replica, only the tablet node reliably knows the progress of its tablets. Neither the master that responds to reads of the `@replication_progress` attribute nor the chaos cell from which you can retrieve the replication card (more on that below) has reliable information about the actual progress. The information available to them lags behind, and that's by design. ## Replication card A replication card contains the basic metadata for the chaos replication protocol. While it isn't explicitly used in typical user scenarios, it can be helpful for troubleshooting. The card provides information about replicas, including their associated clusters and tables, replica modes, and (potentially lagging) progress. In addition, replication cards include the `history` and `era` fields. The era field tracks chaos replication epochs. A new epoch begins with each change in the cumulative state of replicas, such as additions, deletions, and mode switches. The `history` field records the epochs in which the state of a specific replica changed. The `history` is periodically cleaned up in the background. In order to find out the unique identifier of the card, you need to read the `@replication_card_id` attribute from the chaos replicated dynamic table or from the replica table. To get the card itself, it is enough to do a get by the identifier (don't forget to add `#`). The user never specifies `@replication_card_id` for replica tables. It is enough to specify `@upstream_replica_id` — the card identifier is unambiguously derived from the replica identifier. When creating a `chaos_replicated_table`, a new replication card is automatically created. To create a `chaos_replicated_table` that points to an existing replication card, you need to explicitly pass it as the `replication_card_id` attribute during creation. By default, the existence of a replication card is tied to a chaos replicated dynamic table — when the `chaos_replicated_table` object is deleted, the card will be automatically deleted as well (this is true for both new cards and attached existing ones). You can change this behavior using the `@owns_replication_card` attribute — if you set it to `false`, the card will continue to exist even after the `chaos_replicated_table` is deleted. Important: this feature should be used with extreme caution, and you should carefully monitor that there is always a chaos replicated dynamic table that owns the card. Otherwise, you can fill up the memory of chaos metadata with irrelevant cards — they will continue to exist. ## Switching replica modes To switch a replica's mode, use the `alter-table-replica` command, just like you would do with a regular ordered table. You can either toggle a replica using the enable/disable flags or change the mode from synchronous to asynchronous and vice versa using the `mode` option. Switching modes causes a brief shutdown of the replica while the system waits for any transactions writing to the old synchronous replicas to complete and starts new ones that write to the updated synchronous replicas. This ensures that the replication accurately reflects the data stored on the synchronous replicas. A similar barrier is implemented for replicated dynamic tables; in practice the downtime is usually about a few seconds for both replicated and chaos tables. ## Switching replica modes automatically Like regular replicated tables, chaos replicated tables support [automatic replica switching](https://ytsaurus.tech/docs/en/in%20practice%20the%20downtime%20is%20usually%20about%20a%20few%20seconds). To set up automatic switching, set the `ReplicatedTableOptions` for the replication card and specify for each replica whether its mode can change automatically. When creating a `chaos_replicated_table`, you can specify the `replicated_table_options` attribute where you want to pass the `ReplicatedTableOptions`. For more information about the `ReplicatedTableOptions`, see the dedicated [section](https://ytsaurus.tech/docs/en/replicated-dynamic-tables#attributes). For an existing chaos replicated dynamic table, you can change the options with the `alter_replication_card` command by passing it the replication card ID (you can find it in the `@replication_card_id` attribute of the chaos replicated table and its replicas): ```python yt.alter_replication_card(replication_card_id, replicated_table_options=replicated_table_options) yt.alter_replication_card(replication_card_id, enable_replicated_table_tracker=True) ``` To control automatic switching for individual replicas, each replica has the `enable_replicated_table_tracker` flag that you can either set when creating a new replica or change for an existing replica using the `alter-table-replica` command: ```bash $ yt alter-table-replica [--enable-replicated-table-tracker | --disable-replicated-table-tracker] replica_id ``` ## Capacity Planning When using chaos replication, the approach to capacity planning differs from the one you would take when working with regular replicated tables: - Instead of a single replicated dynamic table, there are now several replication queues, each requiring its own reserve for lag accumulation when other replicas are unavailable. - Replication queues result in more replicas, increasing the overall data stream. - Replication can potentially occur between any replication queue and replica, which requires reserving appropriate resources, particularly outgoing network bandwidth. - Unlike `replicated_table`, `chaos_replicated_table` objects don't store data themselves and don't require dedicated resources. In general, chaos replication is expected to consume resources more steadily than regular replication, which requires a dedicated cluster solely for handling replication, along with separate clusters for the replicas. ## Attributes `chaos_table_replica` attributes | Name | Type | Value | |----|-----|----------| | table_path | string | Path to the chaos_replicated_table. | | replication_card_id | TReplicationCardId | Explicitly specifies the replication card. You don't need to specify it if you already specified the table_path. | | cluster_name | string | Cluster with the replica table. | | replica_path | string | Path to the replica table. | | content_type | ETableReplicaContentType | `data` or `queue`, depending on whether it's data or a replication queue. For replicas of ordered dynamic tables, specify `queue`. | | mode | ETableReplicaMode | The replica's mode at creation time: `sync` or `async`. | | enabled | bool | Whether the replica should be enabled: `true` or `false`. | | catchup | bool | `true` if data should be copied to the replica; `false` if the replica should be blank. | | replication_progress | TReplicationProgress | Progress at the replica's creation time. You can omit this if the progress is already specified for the replica table. | | enable_replicated_table_tracker | bool | `true` if automatic mode switching should be enabled for the replica. | # Multi-versioning and transactions of dynamic tables This section describes transactions as they apply to dynamic tables. The transactional model of static tables is described in the [Transactions](https://ytsaurus.tech/docs/storage/transactions.md) section. Transactions are divided into master transactions and tablet transactions. Master transactions enable you to perform operations on the master meta-information. Tablet transactions enable you only to write data to dynamic tables. Data in sorted dynamic tables is read and modified using table transactions. The [MVCC model](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) is used to isolate transactions and resolve conflicts. The default isolation level is snapshot (not serializable snapshot). The system supports distributed transactions, i.e. it enables multiple rows of one or more dynamic tables to be changed within a single transaction. ## Restrictions { #restrictions } When using transactions, keep in mind: - Transactions must be short (the default limit is 1 minute). - Transactions must write a limited amount of data (the default limit is 100,000 rows). Calculating the moment of the last write is no simple task, because some of the data may have already been written to the disk. Attempting to read such data from the disk would cause a write by key to become a random read by key. The YTsaurus system design stipulates that all writes are made quickly. In terms of disk activity, access is sequential. Therefore, the following logic is implemented: - Dynamic stores (dynamic_store), which store freshly written data, are stored in the cluster node's memory for a guaranteed period of time even if they are already written to chunks on the disk. - These dynamic stores are checked quickly, because it does not require access to the disk. - The system prohibits commit of transactions whose lifetime exceeds a certain configurable limit. ## Timestamps { #timestamps } Each value stored in the sorted dynamic table is annotated with a timestamp. Timestamps are 64-bit integers generated by the cluster if necessary. When any transaction starts, a start timestamp is generated. All reads of dynamic tables within a transaction are made relative to this timestamp, thereby generating a snapshot of the data state in these tables. When a transaction is committed, another timestamp is generated — a commit timestamp. All changes to the tables made by this transaction are annotated with a commit timestamp. Timestamps within a cluster are generated centrally on the master servers. For better scalability, clients do not request timestamps directly, but through special proxy servers that buffer queries. In practice, it takes less than 10 ms to generate a new timestamp when the cluster and the client are within the same DC. The total capacity of their generation is practically unlimited. For timestamps the following is guaranteed: - Uniqueness: All obtained timestamps will be different. - Monotony: If `A` means an event of obtaining a `T(A)` timestamp and `B` means an event of ordering a `T(B)` timestamp, with `A` [occurring before ](https://en.wikipedia.org/wiki/Happened-before) `B`, then `T(A) < T(B)`). - Consistency: If an `A` transaction has a `Tc(A)` commit timestamp and a `B` transaction has a `Ts(B)` start timestamp, with `Tc(A) < Ts(B)`, then the `B` transaction will see all changes made by the `A` transaction (and only those). It is also true that any transaction in progress does not see its own changes (because the changes have not yet obtained any timestamp). In addition, the timestamps are arranged so that it is easy to calculate an approximate physical time of generation with an error of about a second. Working with transactions requires the client to maintain a certain state in memory. For example, you need to remember the transaction start timestamp. Changes made within a transaction are also buffered on the client and only sent out at the time of commit. In the current implementation, this state is maintained by the YTsaurus client library, but in theory it is possible to transfer this work to a suitable stateful proxy. ## Weakening guarantees { #guarantees } By default, when working with sorted dynamic tables, all user transactions are atomic and guarantee data integrity: written changes cannot be lost. A two-phase commit is used to ensure atomicity and the server waits until the data is written to the disk to ensure data integrity. In total, these guarantees contribute significantly to the latency of write operations (hundreds of milliseconds). However, there are user scenarios when less strict guarantees are acceptable but a markedly lower latency of write operations is required. To support such scenarios, the system has a number of parameters that enable you to set reduced levels of atomicity and preservation. ## Atomicity { #atomicity } Transaction atomicity means that if a user at time `t` observes an effect (written data) from the `T` transaction, then at any subsequent time `t'` they observe all effects from the `T` transaction. For example, if user 1 changes rows with `k_1` and ` k_2` keys in a single or different dynamic tables, it is impossible that user 2 reads the changed `k_1` row, but not the changed k_2 row. Tables have the `atomicity` attribute which defines the guarantees for the transactions to be applied. This attribute can only be changed for [unmounted](https://ytsaurus.tech/docs/en/overview.md#mount_table) tables. The attribute values can be the following: - `full`: Means that transactions are fully atomic (default mode). In `full` mode, changes become atomically visible at any time after commit timestamp (as stated earlier, the system provides a snapshot isolation level in this mode). - `none`: Means that the system does not give any guarantees about the atomicity of the changes. In `none` mode, there may be situations where commit fails (for example, because a tablet is not available), but some of the changes are committed and became visible to users. ### Non-atomic transactions { #non-atomicity } For non-atomic transactions, the isolation level can be considered read committed. Reading within non-atomic transactions is not performed relative to the start time (there is no start time as such). Instead, the most recently written data is read. Values written by non-atomic transactions receive timestamps, but the timestamps are generated by the clients based on system clock data. YTsaurus checks that the timestamp data does not differ from the server timestamps generated in a serialized way by more than the `client_timestamp_threshold` value (the default value is one minute) in the system configuration. If the difference between the client and server clocks exceeds the specified threshold, the client will get the `Transaction timestamp is off limits, check the local clock readings` error. Even with non-atomic modifications within a row, the uniqueness and monotony of the timestamps are guaranteed. For this, the YTsaurus system corrects the timestamps received from the client upwards if necessary. Non-atomic transactions do not take locks, so the transaction that is committed last (last write wins) is counted for concurrent writes. Successful completion of a non-atomic transaction means that all changes are applied on the server. In case of non-atomic transactions, outside observers may see only partially applied changes during commit time. For atomic transactions, visibility of a part of the changes at any time by anyone means visibility of all the changes. Once the commit is complete in any mode except `atomicity=none` + `durability=async` (you can read about `durability` [here](https://ytsaurus.tech/docs/en/#durability)), all changes are guaranteed to be visible to all. To use non-atomic transactions, you need to: - Specify the `atomicity` attribute equal to `none` for the table. - Specify the `atomicity=none` mode in the settings when starting a transaction. At the time of commit, the server checks the atomicity mode of the table and the atomicity mode specified by the client. If the modes do not match, an execution time error occurs. There are two reasons why it is mandatory to specify atomicity mode for a transaction: - Ideological: To ensure that places in the code where non-atomic commits occur are explicitly marked. - Technical: When an atomic transaction starts, there is an additional delay due to the calculation of the start timestamp. For non-atomic transactions, this timestamp does not make sense. This makes it impossible to start a non-atomic transaction for free and only find out that it is non-atomic at the time of commit. Many versions of the same value can be written to tables with `atomicity=none` at one time. For example, on the writing side, the repeated query goes into an infinite cycle, or a replicated table with an accumulated queue writes it all at to the replica at once. This causes the table data not to be able to be written to the disk and writing to it is stopped. Therefore, we recommend setting the following options for tables with `atomicity=none`: ```yson min_data_ttl = 0 merge_rows_on_flush = %true ``` In non-atomic transaction mode, the system behaves almost identically to [Apache HBase](https://hbase.apache.org/). By weakening guarantees, each write requires only one round-trip to the server and one disk write cycle (tens of milliseconds). ## Durability { #durability } {% note warning "Attention" %} The `durabilty` parameter has been deprecated. When removed, the behavior will be equivalent to the behavior with `durability=sync`. As an alternative way to obtain guarantees provided by the `durability=async` mode, we suggest not waiting for the response to CommitTransaction. {% endnote %} Transaction durability means that if the user at time `t` successfully commits a `T` transaction (receives confirmation from the server), then at any subsequent `t'` time, they observe all the effects of a `T` transaction. For non-atomic transactions, there is also a way to drastically reduce their latency to units of milliseconds at the cost of preservation. When starting a transaction, you can specify the `durability` parameter that has the following values: - async: The transaction was accepted by the server, saved to memory, and, if there are no failures, will at some point be written to the disk (for non-atomic transactions only). In this mode, the read-after-write guarantee is also lost. - sync: The transaction was accepted by the server, validated by the server quorum, and saved to the logs on the disk. If replication settings are correct and there are no serious problems, it survives failures (default mode). ## Supported operations { #methods } ### Creating a transaction { #create } There are two types of transactions in YTsaurus: - Master transactions: They are created and maintained on the master and enable you to work with Cypress only. - Tablet transactions: They are created by clients without the master and enable you to work with dynamic tables, but not Cypress. The transaction type is specified by the client when the transaction is created. When a transaction is started, the system generates a transaction start timestamp. In case of a master transaction, the timestamp is registered on the master and periodically polled by the client. In case of a tablet transaction, its state is handled by the client library. From YTsaurus's point of view, the transaction is not visible and does not cost anything. You can write of one or more dynamic tables within a single tablet transaction to a random set of rows. You cannot write to dynamic tables from master transactions. You can specify the `atomicity` and `durability` settings for tablet transactions. ### Writing rows { #insert_rows } The client can write data within the active transaction using the `insert_rows` method. To do this, the rows to be written must be reported. All key fields must be present in each such row. Some of the data fields from those specified in the schema may be missing. Semantically, if a row with the specified key is not in the table, it appears. If there is already a row with this key, some of the columns are overwritten. When part of the fields are specified, the following modes exist: - `overwrite`: All unspecified fields update their values to `null` (default mode). - `update`: Enabled by the `update == true` option. The previous value will then be retained. In this mode, all columns marked with the `required` attribute must be transmitted. {% note info "Note" %} When reading and executing an SQL query, the data is visible only when the transaction starts. Changes written within the same transaction are not available for reading. {% endnote %} ### Completing a transaction { #terminate } This call sends the changes accumulated during operation to the cluster and a commit attempt is made. It can be successful, and if it is, the client will know the transaction end timestamp. A commit may fail (for reasons ranging from network problems and temporary unavailability of individual cluster nodes to conflicts between transactions). There is no built-in reliable means to find out if a commit has been completed successfully (the relevant message may have gotten lost on the way to the client). ### Aborting a transaction { #abort } On the client, the memory occupied by the changes accumulated in the transaction is freed up. If this is a master transaction, it is aborted on the master server. If this is a tablet transaction, no further actions are required. ### Checking transaction conflicts { #conflicts } To check and prevent conflicts between atomic transactions that work (lock read or modify operations) with data overlapping by keys, there are locks on the table rows: - In case of read locks, shared-locks are used. - Exclusive-locks are used to modify data. In the simplest case without further configuration, there is one basic [shared-exclusive lock](https://en.wikipedia.org/wiki/Readers–writer_lock) associated with each key. This feature is implemented in the form of a shared-lock counter and an exclusive-lock flag. By default, locks apply immediately to entire rows. You can increase the granularity of locks in the system by marking the columns in the schema with a `lock` attribute and specifying the name of an additional lock. In this way, you can divide the columns into groups, each controlled by a different lock. If no `lock` attribute is specified for a column, it is considered to be controlled by the main lock. If the transaction writes by key, it takes the locks controlling all affected columns. You cannot take an additional lock if another transaction has already taken the main lock. When deleting a row, the transaction takes the main lock. #### Modification lock { #exclusive_lock } It is guaranteed that there cannot be two transactions modifying the same key at the same time. When modifying data by key, whether reading or writing,, the transaction takes an exclusive-lock by that key at the moment of commit. If the specified lock has already been taken by another modify transaction or by some number of read transactions that have taken shared-locks, a conflict occurs and the transaction is aborted. It is guaranteed that transactions with overlapping lifetimes cannot write data by the same key. Locks on rows are only taken and retained during commit. Until then, the data is on the client. Commit in the system is two-phase and its process starts with the `prepare` timestamp and ends with the `commit` timestamp. For example, `A` and `B` transactions were started at about the same time, then `A` performed a write by `k` key and was committed. A `B` transaction could now perform a write by `k` key and be committed, because there is no longer any lock on `k`. But this behavior is contrary to the snapshot isolation level provided by the system. Therefore, when a `B` transaction modifies a key, in addition to checking a lock on that key, the system also checks: - Whether there were any writes with timestamps greater than `Tstart(B)` on that key. - Whether this key was locked by another transaction before the time greater than `Tstart(B)`. If at least one of the conditions is not met, a conflict occurs. #### Read lock { #shared_lock } In case of a read lock, in addition to checking whether the shared-lock can be taken, only the lack of modifications to the given key with timestamps greater than `Tstart(B)` is checked. The structure for transaction conflict checking is defined by a shared-lock counter, an exclusive-lock flag, and two timestamps — the time before which the row was locked for reading and the time of the last modification of the row. When row modification is complete, the exclusive-lock is released and the timestamp of the last modification is written to the commit timestamp. When a transaction that takes read locks (shared-locks) is complete, the shared-lock is released at the end of the commit process, and in case of a strong-lock, the transaction commit timestamp is written. In case of a weak-lock, no timestamp is written. # Sharding This section describes ways of sharding dynamic tables. A description of the automatic sharding algorithm is given. Dynamic tables are divided into [tablets](https://ytsaurus.tech/docs/en/overview.md#tablets) (shards), which are the unit of concurrency. #### Sorted tables Each tablet of a [sorted table](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md) is responsible for a certain range of keys. Tablet boundary keys (also called pivot keys) are available in the `pivot_keys` table attribute. A tablet with the *k* index is responsible for the keys between the *k* boundary key (inclusive) and *k*+1 boundary key (not inclusive). Each boundary key consists of a certain prefix of the table's key columns. For example, for a table with three key columns of the types int64, string, and double, the acceptable boundary keys could be `[]`, `[10]`, `[50; foo]`, `[100; bar; 1.234]`. The boundary keys of tablets must be strictly ascending. The first boundary key must be empty. When resharding a sorted table, you can either specify the desired boundary keys explicitly or pass the `tablet_count` parameter. In the latter case, the system will attempt to split the table evenly into the required number of tablets. #### Ordered tables Each tablet of an [ordered table](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md) is an independent queue. To reshard an ordered table, you must specify the desired number of tablets. For more information, see [Sharding ordered tables](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md). #### Replicated tables Physically, each tablet of a [replicated table](https://ytsaurus.tech/docs/en/replicated-dynamic-tables.md) is a queue and stores the replication log. Despite this, the sharding logic is the same as for regular tables. - Each tablet of a sorted replicated table is responsible for a particular key range and stores the replication log of keys from that range. - Each tablet of an ordered replicated table is an independent queue. For the tablets of a replicated table and the replicas to match, use the [preserve_tablet_index](https://ytsaurus.tech/docs/en/replicated-dynamic-tables#sohranenie-indeksa-tableta-pri-replikacii-uporyadochennoj-tablicy) attribute. The sharding of a sorted replicated table doesn't have to match the sharding of replicas. Different replicas of the same table can also be sharded in different ways. You can't reshard a non-empty replicated table. To do that, you must recreate the table with the required boundary keys. ## Manual sharding Use the [reshard-table](https://ytsaurus.tech/api/commands.md#reshard_table) command to reshard a table. Before running the command, the table must be unmounted. {% list tabs %} - CLI ```bash yt unmount-table //path --sync # Reshard with tablet count yt reshard-table //path --tablet-count 10 --sync # --enable-slicing is used to pick pivots more precisely in case of small tables yt reshard-table //path --tablet-count 100 --enable-slicing --sync # Reshard with pivot keys yt reshard-table //path '[]' '[10;foo]' '[20]' --sync yt mount-table //path --sync ``` - Python ```python yt.unmount_table("//path", sync=True) # Reshard with tablet count yt.reshard_table("//path", tablet_count=10, sync=True) # enable_slicing is used to pick pivots more precisely in case of small tables yt.reshard_table("//path", tablet_count=10, enable_slicing=True, sync=True) # Reshard with pivot keys yt.reshard_table("//path", [[], [10, "foo"], [20]], sync=True) yt.mount_table("//path", sync=True) ``` {% endlist %} {% note info "About the "sync" parameter" %} The "unmount-table", "reshard-table", and "mount-table" commands are asynchronous, so we recommend waiting until execution is complete when using them. For that, you can use the `--sync` flag or the `sync=True` parameter. In practice, "unmount-table" can take a long time to execute, so waiting is required for this command. Typically, "reshard-table" and "mount-table" are completed instantly, so waiting can be omitted for manual runs, but with scripts it's recommended to use sync. {% endnote %} When the `tablet_count` parameter is specified, the system attempts to automatically select appropriate boundary keys. If the table is relatively small (less than 200 MB per tablet), there may be fewer total tablets than requested. In this case, we recommend using the [enable_slicing](https://ytsaurus.tech/api/commands.md#reshard_table) option to refine the boundary key search. You can find out the number of tablets via the `@tablet_count` attribute. ## Sharding by hash { #hash } To distribute the load evenly, you can use the standard approach: prepend an auxiliary key column that contains the hash of the part of the key to perform sharding for (for example, the hash from the first column). The resulting table will have its keys evenly distributed in the [0, 264 – 1] range. To split it into *k* tablets, simply split the [0, 264-1] range into *k* parts. To do this, you can use the "reshard-table" command with the `--uniform` flag: when `tablet_count` is specified, the system will set the boundary keys [], [264 / *k*], [264 * 2 / *k*], and so on. The value of the key column can be calculated on the client side and transmitted when writing to the table, but computed columns can be used as an alternative. ### Computed columns { #expression } The YTsaurus system supports the feature that automatically calculates the value of a key column using a formula. You must specify this formula in the schema of this column in the `expression` field. The key column can only depend on non-calculated key columns. When writing a row or searching for a row by key, the computed columns must be skipped. For even distribution, better specify `"expression" = "farm_hash(key)"` where `key` is the prefix of the source key (`farm_hash` is a built-in function that calculates [FarmHash](https://code.google.com/p/farmhash) from arguments). When using automatically computed columns, consider that the `select_rows` operation infers the range of affected keys out of the predicate to optimize performance. If some of the values of the computed columns are not specified explicitly in the predicate, YTsaurus will try to supplement the condition with the value of the computed columns. In the current implementation, the result will be successful if those columns on which the calculated one depends are defined in the predicate by an equality or using the `IN` operator. Calculation of the explicitly specified values of the computed columns in the range output does not take place. #### Example of using compucolumns Let's assume there is a table with `hash, key, value` columns, `hash` and `key` are key columns, and the `expression = "farm_hash(key)"` formula is specified in the schema for `hash`. Then for insert, delete, and read operations by key, you only need to specify `key` and `value`. In order for the `select_rows` operation to work efficiently, `key` must be exactly specified in the predicate so that the YTsaurus system can calculate which `hash` values to consider. For example, you can specify `WHERE key = key_value` or `WHERE key IN (key_value1, key_value2)` in the query. If you specify `WHERE key > key_lower_bound and key < key_upper_bound`, the range for `hash` cannot be inferred. In some cases, enumeration of the values of the computed columns is possible. Enumeration occurs in the following cases: 1. The expression of the computed column is `expression = "f(key / divisor)"` where `key` and `divisor` are integers. In this case, all such `key` values are enumerated and they generate different `expression` values. This behavior is generalized in case there are multiple computed columns and multiple `key` occurrences with different divisors. 2. The expression is `expression = "f(key) % mod"`. In this case, the `expression` values within the `mod` value are enumerated, and the `null` value is also included in the enumeration. If both enumeration methods can be applied, the one that generates the least number of values is chosen. The total number of keys generated by the enumeration is limited by the `range_expansion_limit` parameter. # Automatic sharding and dynamic table balancing Sharding and balancing help ensure even load distribution across the cluster. This includes: * Table sharding. * Redistributing tablets between [tablet cells](https://ytsaurus.tech/docs/en/concepts.md#tablet_cells). Sharding is needed for the table tablets to become approximately of the same size, while redistribution between tablet cells helps ensure that tablet cells have an approximately even amount of data and/or load. Even data distribution is especially important for in-memory tables (with `@in_memory_mode` other than `none`), because cluster memory is a rather limited resource and a poor distribution can overload some cluster nodes. Load distribution, on the other hand, is essential for cases where the load on table keys is uneven, or if a certain type of load is a limited resource in your bundle. You can configure balancing both on a per-table basis and for each [tablet cell bundle](https://ytsaurus.tech/docs/en/concepts.md#tablet_cell_bundles). You can also configure balancing for groups of tables. Table configuration has a higher priority than group configuration, which in turn has a higher priority than bundle configuration. The system has two types of balancers: the legacy balancer, which lives in the master process and supports only balancing by tablet size and a limited number of options, and the standalone balancer that also supports balancing by load. The external balancer is not currently supported in opensource. ## Balancing strategies * **Resharding by size**. To evenly distribute table tablets across tablet cells, each tablet needs to be of a reasonable size. Large tablets are split into several smaller ones, and small tablets are combined with the neighboring ones. * **Resharding by parameter and size** (available only for clusters with a standalone balancer). To evenly balance the table by parameter, all table tablets need to be of approximately the same size and have approximately the same load. Tablets with a higher load or of a larger size are resharded, and smaller tablets with a smaller load are combined with the neighboring ones. * **Balancing between tablet cells** * **In-memory tables**. In-memory tables are balanced by minimizing the load on the most loaded cell (for tables with hunks, this is done on their non-hunk part). Only the total node memory usage across all tables is taken into account, the tablet distribution within each individual table is not optimized. * **Disk tables**. Disk tables are balanced by the number of table tablets by the bundle's tablet cells. * **Parameterized balancing between nodes** (aka balancing by load, available only for clusters with a standalone balancer). Minimizes the dispersion of the selected metric by node and, to a lesser extent, by cell. Per-table distribution is not taken into account, all tablets are considered equivalent. ## Quick start for parameterized balancing { #quick-start } > Available only for clusters with a standalone balancer. If the bundle doesn't have parameterized balancing enabled and dedicated balancing groups set up, all you need to do is create a group with the desired settings and put the tables into that group. 1. To verify whether these instructions apply to you, make sure that the bundle config doesn't contain a list of groups (the `//sys/tablet_cell_bundles//@tablet_balancer_config` map doesn't include the `groups` field). 2. Select a metric for balancing the tables. For the meta cluster, if writes to replicated tables are evenly distributed across their tablets, we recommend balancing the load based on the number of tablets (`metric = "1"`). The list of available metrics is provided in the [section on parameterized balancing configuration](https://ytsaurus.tech/docs/en/#parameterized). You can add metrics using the + operator in the formula. If the bundle contains groups of tables with varying types of load, you may want to balance them using different metrics. To do that, create multiple groups with custom settings. For more information, see the [section on table groups](https://ytsaurus.tech/docs/en/#group). 3. For large balancing groups with more than 50,000 tablets, you may need to adjust the `max_action_count` parameter, which specifies the number of movements per balancing iteration. For more information, see the section on [parameterized balancing configuration](https://ytsaurus.tech/docs/en/#parameterized). If you enable parameterized balancing for the entire bundle, you can view the total number of tablets across all tables on the bundle page. 4. Create a `default` [group](https://ytsaurus.tech/docs/en/#system-groups) with the desired config for parameterized balancing and enter the desired metric in the creation command. If needed, use a similar approach to set the `max_action_count` parameter. ```(bash) yt set //sys/tablet_cell_bundles//@tablet_balancer_config/groups '{default = {parameterized = {metric = ""}}}' ``` 5. Enable parameterized balancing for the desired tables. - To enable it for the entire bundle: `yt set //sys/tablet_cell_bundles//@tablet_balancer_config/enable_parameterized_by_default %true` - To enable it only for specific tables, enable parameterized balancing for each table individually: `yt set //path/to/table/@tablet_balancer_config/enable_parameterized %true` 6. You can monitor the balancer's actions on the bundle page: go to Monitoring → Maintenance. ## Balancing configuration ### Table groups { #group } > Available only for clusters with a standalone balancer. To make configuration easier, the service supports balancing groups. Their configs are located in the `groups` field in the bundle config. If this parameter doesn't exist (it isn't created automatically), you need to create it. Inside the group, all tables are balanced together. {% cut "Creating a balancing group with certain values" %} You need to create an empty map in the `groups` field along with configs for balancing groups, subconfigs, and so on `yt set //sys/tablet_cell_bundles//@tablet_balancer_config/groups '{}'` , or create a group with the desired configuration right away `yt set //sys/tablet_cell_bundles//@tablet_balancer_config/groups '{alpha={parameterized={max_action_count=5}; schedule="minutes % 45 == 0"}}'` {% endcut %} To put a table into a group named `alpha`, set the `group` field of the `@tablet_balancer_config` attribute to `alpha`. #### Group configuration parameters | **Name** | **Type** | **Default value** | **Description** | |--|--|--|--| |type| `legacy` / `parameterized` | `legacy` for `legacy` and `legacy_in_memory` groups, `parameterized` for other group types | Balancing type. `legacy` can't be set for new groups | |parameterized|yson||Parameterized balancing configuration. Learn more in the section on parameterized balancing configuration| |schedule|Arithmetic formula that includes `minutes`, `hours`, and numeric values|If the value is set, it is taken from the bundle's config. Otherwise, it depends on the cluster configuration|Balancing schedule. Once the formula value becomes true, this triggers the resharding of all the group's tables, and after that (usually within 10 minutes), movement between cells. See the examples in the [Schedule](https://ytsaurus.tech/docs/en/#schedule) section| |enable_move|boolean|%true|Enables movement between tablet cells. Balancing type depends on the group| |enable_reshard|boolean|%true|Enables automatic resharding. Balancing type depends on the group| {% cut "Sample group configuration" %} {% note warning "Attention" %} This is not a recommendation. The example only lists possible options with random values. {% endnote %} ```bash "in_memory_stable" = { "enable_move" = %true; "enable_reshard" = %true; "schedule" = "minutes % 30 == 5"; "parameterized" = { "max_action_count" = 100; "metric" = "double([/statistics/memory_size])"; }; }; ``` {% endcut %} #### System groups { #system-groups } From the balancer's point of view, system groups exist by default even if `@tablet_balancer_config/groups` doesn't explicitly list their configs. The default values for these groups are taken from the above table. All system groups are listed below: * `legacy`: Balancing between the tablet cells of disk tables. * `legacy_in_memory`: Balancing between the tablet cells of in-memory tables. * `default`: Includes all tables with enabled parameterized balancing where the group isn't explicitly indicated. Tables are put into the first and second groups automatically if the group type and parameterized balancing for them are not indicated. Tables are put into the `default` group if: * The `default` group is explicitly indicated for the table. * The group isn't explicitly indicated but `enable_parameterized=true` is set in the balancing config. * The group isn't explicitly indicated, and parameterized balancing is enabled for the bundle by default (`enable_parameterized_by_default=%true`). If the bundle's config indicates the default group name for in-memory table balancing (the `default_in_memory_group` attribute), the in-memory tables that were supposed to be put into the `default` category are put into the group specified in the config instead. ### Per-table settings A list of available table settings (`//path/to/table/@tablet_balancer_config`) is given in the table: | **Name** | **Type** | **Default value** | **Description** | | -- | -- | -- | -- | | enable_auto_reshard | boolean | %true | Enables/disables resharding | | enable_auto_tablet_move | boolean | %true | Enables/disables moving table tablets between cells | | min_tablet_size | int | - | Minimum tablet size for resharding by size | | desired_tablet_size | int | - | Desired tablet size for resharding by size | | max_tablet_size | int | - | Maximum tablet size for resharding by size | | desired_tablet_count | int | - | The desired number of tablets for resharding | | min_tablet_count | int | - | The minimum number of tablets for resharding by size, [see note in text](https://ytsaurus.tech/docs/en/#reshard-config)| | group* | str | - | Puts the table into a balancing group, see more in [Table groups](https://ytsaurus.tech/docs/en/#group)| | enable_parameterized* | boolean | - | Enables/disables moving tablets by load| \* — Available only for clusters with a standalone balancer. {% cut "Sample table configuration" %} {% note warning "Attention" %} This is not a recommendation. The example only lists possible options with random values. {% endnote %} ```bash { "enable_auto_reshard" = %true; "enable_auto_tablet_move" = %true; "group" = "writes"; # use the rest wisely "enable_verbose_logging" = %false; "enable_parameterized" = %true; "desired_tablet_count" = 100; "min_tablet_count" = 5; "min_tablet_size" = 1000; "desired_tablet_size" = 5000; "max_tablet_size" = 10000; } ``` {% endcut %} ### Per-bundle settings The settings are specified in `//sys/tablet_cell_bundles//@tablet_balancer_config`. If the bundle and the table have conflicting settings, the table settings have a higher priority and will be used for balancing. | **Name** | **Type** | **Default value** | **Description** | | -- | -- | -- | -- | | min_tablet_size | int | 128 MB | Minimum disk table tablet size | | desired_tablet_size | int | 10 GB | Desired disk table tablet size | | max_tablet_size | int | 20 GB | Maximum disk table tablet size | | min_in_memory_tablet_size | int | 512 MB | Minimum in-memory table tablet size | | desired_in_memory_tablet_size | int | 1 GB | Desired in-memory table tablet size | | max_in_memory_tablet_size | int | 2 GB | Maximum in-memory table tablet size | | enable_tablet_size_balancer | boolean | %true | Enables/disables resharding by size | | enable_in_memory_cell_balancer | boolean | %true | Enables/disables in-memory table tablet balancing between tablet cells | | enable_cell_balancer | boolean | %false | Enables/disables disk table tablet balancing between tablet cells | | tablet_to_cell_ratio | double | 5. | Resharding by size limits the number of tablets in a table to the number of tablet cells multiplied by the value of this parameter | | enable_parameterized_by_default* | boolean | %false | Puts tables with no explicit `group` indication into the `default` group for parameterized balancing | | groups* | dictionary | - | Balancing group configs | | default_in_memory_group* | str | - | Default group name for in-memory tables for parametrized balancing | \* — Available only for clusters with a standalone balancer. {% cut "Sample bundle configuration" %} {% note warning "Attention" %} This is not a recommendation. The example only lists possible options with random values. {% endnote %} ```bash { "enable_cell_balancer" = %true; "enable_in_memory_cell_balancer" = %true; "enable_tablet_size_balancer" = %true; "min_tablet_size" = 500; "desired_tablet_size" = 1000; "max_tablet_size" = 2000; "min_in_memory_tablet_size" = 50; "desired_in_memory_tablet_size" = 100; "max_in_memory_tablet_size" = 1000; "hard_in_memory_cell_balance_threshold" = 0.15; "soft_in_memory_cell_balance_threshold" = 0.05; "tablet_balancer_schedule" = "(hours * 60 + minutes) % 80 == 10"; "tablet_to_cell_ratio" = 5.; "enable_verbose_logging" = %false; "enable_parameterized_by_default" = %true; "groups" = { "in_memory_stable" = { # ... parameterized balancing only }; "legacy" = { # ... }; "default" = { # ... parameterized balancing only }; "legacy_in_memory" = { # ... }; }; } ``` {% endcut %} ### Parameterized balancing configuration { #parameterized } > Available only for clusters with a standalone balancer. | **Name** | **Type** | **Default value** | **Description** | | -- | -- | -- | -- | | metric | str | write data weight | Balancing metric (parameter) | | max_action_count | int | - | Maximum number of tablets to be moved in one iteration | | enable_reshard | boolean | - | Enables the resharding algorithm by parameter and size instead of resharding by size | | per_table_uniform | boolean | - | Enables the movement balancing algorithm, which among other functions aims to ensure an even distribution of tables across nodes and cells. Recommended for groups of no more than 50 tables | The metric for parameterized balancing is set by an arithmetic formula using per-table metrics. The metric must be positive for any given tablet. For the correct operation of the balancing algorithm, the metric size must be in direct ratio to the tablet load. The formula may include tablet size metrics. Each of the load metrics can have one of the following two suffixes: `_10m_rate` or `_1h_rate`, which are calculated as the exponential decay of requests within 10 minutes and 1 hour, respectively. In the table, all metrics are given for the 10-minute window. Below you will find per-table metrics that can prove useful. - Write data weight `write_10m` - Amount of data read by lookup requests, including reads from hunk chunks `lookup_10m` - Amount of data read by select requests, including reads from hunk chunks `read_10m` - Amount of data read by lookup requests, excluding reads from hunk chunks `double([/performance_counters/dynamic_row_lookup_data_weight_10m_rate]) + double([/performance_counters/static_chunk_row_lookup_data_weight_10m_rate])` - Amount of data read by select requests, excluding reads from hunk chunks `double([/performance_counters/dynamic_row_read_data_weight_10m_rate]) + double([/performance_counters/static_chunk_row_read_data_weight_10m_rate])` - Uncompressed size `double([/statistics/uncompressed_data_size])` - Compressed size `double([/statistics/compressed_data_size])` - Memory size (the amount of data occupied by an in-memory table, equal to either compressed or uncompressed size depending on `in_memory_mode`) `double([/statistics/memory_size])` - CPU time consumed by lookup requests `lookup_cpu_10m` - CPU time consumed by select requests `select_cpu_10m` - Number of tablets (can be used if tablets need to be distributed equally across nodes without taking the load into account) `1` You may need to set the `max_action_count` parameter, which represents the number of movements per balancing iteration. This parameter determines how many tablets will be inaccessible during balancing. If your bundle is fairly large or, for some reason, the default values don't suit your needs, you can apply the following principle to estimate an initial value for this parameter: - For bundles of up to 100 tablet nodes: No less than the number of nodes and no less than 10. - For groups of up to 100 tablets: 10. - For groups of up to 1000 tablets: 50. - For groups of up to 10,000 tablets: 100. - For groups of up to 50,000 tablets: 250. - For groups of up to 100,000 tablets: 500. If all the bundle tables use parameterized balancing and there is only one parameterized balancing group, then the total number of tablets across all tables is displayed on the bundle page. {% cut "Full list of balancing metric aliases" %} - `write_10m`, `write_1h` - `lookup_10m`, `lookup_1h` - `read_10m`, `read_1h` - `lookup_cpu_10m`, `lookup_cpu_1h` - `select_cpu_10m`, `select_cpu_1h` {% endcut %} ### Resharding configuration #### Resharding by size { #reshard-config } - If the `desired_tablet_count` parameter is specified in the table settings, the balancer will attempt to shard the table by the specified number of tablets. - Otherwise, if all three parameters (`min_tablet_size`, `desired_tablet_size`, `max_tablet_size`) are specified in the table settings and their values are valid (i.e. `min_tablet_size < desired_tablet_size < max_tablet_size` is true), the specified parameter values will be used. - Otherwise, the bundle's tablet cell settings will be used. {% note warning "Note" %} If all three tablet size parameters (min, desired, and max) are explicitly indicated, the following recommendation needs to be observed for the algorithm to work correctly: `max` / `min` must be equal to at least 2.5 or, better yet, 3. With smaller ratios, the algorithm won't be able to reshard the table to the optimal size and will continually change the size of tablets, interfering with your work. {% endnote %} The automatic sharding algorithm is as follows: the background process monitors the mounted tablets and as soon as it detects a tablet smaller than `min_tablet_size` or larger than `max_tablet_size`, it tries to bring it to `desired_tablet_size`, possibly affecting adjacent tablets. If the `desired_tablet_count` parameter is specified, the custom table size settings will be ignored and values will be calculated based on table size and `desired_tablet_count`. If the `min_tablet_count` parameter is set, the balancer won't combine tablets if the resulting number of tablets is below the limit. However, this option doesn't guarantee that the balancer will reduce the size of tablets if their current number is too little: if you use it, you need to manually pre-shard the table to the desired number of tablets. #### Resharding by size and parameter { #parameterized-reshard-config } This type of resharding is intended for collaborative work with parameterized balancing. Both algorithms use the same metric (parameter) that is specified in the balancing group's config. To enable this type of resharding, you need to specify the desired number of tablets (`desired_tablet_count`) in the table's config. Without this value, the algorithm can't predict the desired parameter values for each tablet's metric. You also need to set the `enable_reshard` flag value to `%true` in the group's parameterized balancing config. The algorithm is similar to that for the resharding by size, except for the conditions at which the tablets are split and combined. When the algorithm detects a tablet that doesn't meet at least one of the min/max size or parameter requirements, this tablet is either split or combined with the neighboring tablets to bring it to the required size and parameter values if possible. The above-mentioned limits are calculated based on the desired number of table tablets. {% note info "Note" %} Unlike resharding by size, this type of resharding doesn't take the `min_tablet_count` parameter into account, which means that the number of tablets in the table can drop down below the specified value. {% endnote %} ### Disabling { #shutdown } To disable a certain type of balancing, set the value of the corresponding attribute to `%false` (see the table below). You need to specify this value in the following configs: - For bundles: `//sys/tablet_cell_bundles//@tablet_balancer_config/` - For groups: `//sys/tablet_cell_bundles//@tablet_balancer_config/groups//` - For tables: `//path/to/table/@tablet_balancer_config/` | **Balancing type** | **Bundle** | **Group** | **Table** | | --- | --- | --- | --- | | [in-memory move](https://ytsaurus.tech/docs/en/*in-memory-move) | enable_in_memory_cell_balancer | enable_move | enable_auto_tablet_move | | [ordinary move](https://ytsaurus.tech/docs/en/*ordinary-move) | enable_cell_balancer | enable_move | enable_auto_tablet_move | | [parameterized move](https://ytsaurus.tech/docs/en/*parameterized-move) | - | enable_move | enable_auto_tablet_move | | [reshard](https://ytsaurus.tech/docs/en/*reshard) | enable_tablet_size_balancer | enable_reshard | enable_auto_reshard | | [parameterized reshard](https://ytsaurus.tech/docs/en/*parameterized-reshard) | - | enable_reshard | enable_auto_reshard | ### Automatic sharding schedule { #schedule } Sharding will inevitably unmount some of the tablets. To make the process more predictable, you can set up the balancer schedule. The setting can be per-cluster, per-bundle, and per-group. The bundle schedule is located in the `//sys/tablet_cell_bundles//@tablet_balancer_config/tablet_balancer_schedule` attribute. The group schedule is located in the `schedule` attribute of the [group's](https://ytsaurus.tech/docs/en/#group) config. Any arithmetic formula from the `hours` and `minutes` integer variables can be specified as a format. The balancing of the bundle's tables will only occur when the formula value is true (i.e. different from zero). The background balancing process runs once every few minutes, so you should expect that the tablets can be in the unmounted state for 10 minutes after the formula becomes true. Examples: - `minutes % 20 == 0`: Balancing at the 0th, 20th, and 40th minute of each hour. - `hours % 2 == 0 && minutes == 30`: Balancing at 00:30, 02:30, ... If no attribute value is specified for the bundle, the default value for the cluster is used. Example of a cluster-level balancing setup is given in the table below: | **Cluster** | **Schedule** | | ---------- | ------------------------------------ | | first-cluster | `(hours * 60 + minutes) % 40 == 0` | | second-cluster | `(hours * 60 + minutes) % 40 == 10` | | third-cluster | `(hours * 60 + minutes) % 40 == 20` | ## Implementation details { #details } ### Movement balancing #### Parameterized balancing {% note info "Attention" %} Per-table distribution is not taken into account, all the group's tablets are considered equivalent. {% endnote %} Balancing is applied to tables in [groups](https://ytsaurus.tech/docs/en/#system-groups) **other than** `legacy_in_memory` and `legacy`. ##### Algorithm overview The algorithm calculates the `metric` for each tablet and then, depending on the tablet distribution by cells and nodes, the metrics for cells and nodes are calculated as the sum of metrics of the tablets that they host, followed by step-by-step minimization $\sum_{node}{NodeMetric^2} + \sum_{cell}{CellMetric^2}$ In one parameterized balancing step, a tablet can be moved from one cell to another. The algorithm won't run if tablet distribution by cells and nodes is adequate. #### Balancing between disk tablet cells {% note info "Attention" %} The tablet size isn't taken into account, all the table's tablets are considered equivalent. {% endnote %} Balancing [group](https://ytsaurus.tech/docs/en/#system-groups): `legacy` ##### Algorithm overview Tablets are evenly distributed across bundle cells independently for each table. If the bundle has empty cells, the tablets are distributed so that each cell contains approximately the same number of tablets. #### Balancing between in-memory tablet cells {% note info "Attention" %} Per-table distribution is not taken into account, all the group's tablets are considered equivalent. {% endnote %} Balancing [group](https://ytsaurus.tech/docs/en/#system-groups): `legacy_in_memory` ##### Algorithm overview Tablets are evenly distributed across bundle cells by `memory_size`. Per-table distribution is not taken into account. ### Sharding #### Resharding by size Performed by splitting and combining neighboring tablets. Each table is resharded independently. ##### Algorithm overview The algorithm splits large tablets and combines neighboring tablets independently in each table. A tablet is considered too large if its size exceeds `max_tablet_size`. In this case, it can be split into several smaller tablets separately or together with the neighboring tablets so that the size of new tablets is approximately equal to `desired_tablet_size`. Tablets smaller than `min_tablet_size` are combined. If `min_tablet_count` is specified for the table, and the number of tablets is less than the specified value, they won't be combined even if they are smaller than `min_tablet_size`. The parameter doesn't affect `min/desired/max tablet_size`. If `desired_tablet_count` is specified for the table, the limits are calculated as follows: - `desired_tablet_size = table_size / desired_tablet_count` - `min_tablet_size = desired_tablet_size / 1.9` - `max_tablet_size = desired_tablet_size * 1.9` For the algorithm to work properly, **max_tablet_size / min_tablet_size > 2** needs to be true. Otherwise, the balancer will repeatedly perform useless actions that will hamper the table operation. Other configuration recommendations can be found in the [relevant section](https://ytsaurus.tech/docs/en/#reshard-config). #### Resharding by size and metric Resharding by size and metric is performed by splitting and combining neighboring tablets. Tables are resharded independently, except for the stage when planned actions are sorted by relevance. ##### Algorithm overview The algorithm requires `desired_tablet_count`. This value is used in each table to calculate the following values: - `desired_tablet_size = table_size / desired_tablet_count` - `min_tablet_size = desired_tablet_size / 1.9` - `max_tablet_size = desired_tablet_size * 1.9` - `desired_tablet_metric = table_metric / desired_tablet_count` - `min_tablet_metric = desired_tablet_metric / 1.9` - `max_tablet_metric = desired_tablet_metric * 1.9` The algorithm splits large tablets and combines neighboring tablets. A tablet is considered too large if its size exceeds `max_tablet_size` or its metric exceeds `max_tablet_metric`. In this case, the tablet can be split into several smaller tablets so that the size of new tablets fits the specified limits. The algorithm will split a large tablet even if its value for the remaining parameter is smaller than the desired one. Tablets are combined if they are smaller than `min_tablet_size` or `min_tablet_metric`. If table resharding creates too many actions, exceeding the maximum number of actions per iteration, all planned reshards of this group are sorted by revelance, and only the most useful ones are performed. Other configuration recommendations can be found in the [relevant section](https://ytsaurus.tech/docs/en/#parameterized-reshard-config). [*in-memory-move]: In-memory table balancing between tablet cells. [*ordinary-move]: Disk table balancing between tablet cells. [*parameterized-move]: Table balancing by load. [*reshard]: Resharding by size. [*parameterized-reshard]: Resharding by size and load. # Background compaction A tablet of a sorted dynamic table is an [LSM tree](https://en.wikipedia.org/wiki/Log-structured_merge-tree). When a row is written, it enters the dynamic store. As the dynamic store gets filled with data, it is flushed to the chunks on the disk. A background compaction process periodically combines several chunks. It is needed to: - Apply [delete tombstones](https://ytsaurus.tech/docs/en/sorted-dynamic-tables#udalenie-stroki) and physically delete rows. - Delete old versions. - Reduce overlapping store count, the number of chunks in which a fixed key may need to be searched. Compaction relies mainly on heuristics. However, depending on the table write scenario, the optimal approaches may differ. For example: - If a small max_data_ttl is set on the table, it makes sense to periodically compact the old chunks and free up space. - If rows are regularly deleted, we recommend examining the chunk that contains insertions and deletions during compaction in order to physically delete the rows. - If the table write stream is large, you can sacrifice read optimality and loosen the overlapping store count settings in favor of reducing write amplification — the ratio of the data amount processed by compaction to the amount of written data, and vice versa. ## Glossary { #glossary } - **Dynamic store**: A structure for storing freshly written rows; it is located in RAM. Analog of MemTable. - **Chunk**: An immutable structure for storing rows flushed to the disk. Analog of SSTable. - **Store**: A common name for chunks and dynamic stores. Technically speaking, chunk stores (and dynamic stores), not chunks are stored in the tablet, but the chunk and chunk store terms are usually interchangeable. - **Partition**: A part of the tablet's subdivision. Similarly to tables that are split into tablets bounded by pivot keys, tablets are split into partitions. Chunks within a partition do not cross its boundaries. - **Eden**: A special partition containing chunks that cannot be placed in any other partition because they cross their boundaries. Chunks typically do not live long in Eden, quickly undergoing partitioning. - **Overlapping store count**, **OSC**: Maximum overlap of chunks; maximum number of chunks covering a particular key. Limits fan-in from above, i.e. the number of chunks that actually have to be read in order to get the actual value by key. - **Flush**: The process of flushing data from a dynamic store to a chunk on the disk. - **Compaction**: The process of merging chunks that involves discarding old versions of data, removing delete tombstones, and combining small chunks. - **Partitioning**: A process that is similar to compaction, except its purpose is not to combine chunks, but to split chunks located in Eden into different partitions. - **Background processes**: A collective term for flush+compaction+partitioning. - **Write amplification**, **WA**: The ratio of the amount of data handled by background processes (compaction/partitioning) to the amount of data written to the table. It is an important effectiveness indicator of the selected parameters. - **Row version**: In the [MVCC](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) model, a single key can be associated with multiple values, each with its own timestamp. An individual value is called a version. Generally speaking, writes to each column can be versioned independently, so it makes sense to refer to versions of the *values*, not a *row* as a whole. In terms of setting up compaction, this is usually irrelevant. ## List of attributes { #attributes } All values with the `Duration` type are specified in milliseconds. All values indicating the amount of data are specified in bytes. Attributes for which there is no description have a complex semantics and we do not recommend changing them. {% note warning "Warning" %} Many attributes influence the behavior of background compaction processes. Careless setting of attributes can create an unexpected load on a bundle or cluster. Please use only those attributes whose meaning you understand. {% endnote %} {% note info "Note" %} Setting an attribute on a mounted table does not apply the settings. To apply settings, use the `remount-table` command: CLI ```bash yt set //path/to/table/@mount_config/merge_rows_on_flush %true yt remount-table //path/to/table ``` You can find out the tablet's current settings at `//sys/tablets/x-x-x-x/orchid/config`. {% endnote %} ### Flush { #flush_attributes } The following attributes regulate flush behavior. | Name | Type | Default | Description| |--|--|--|--| | dynamic_store_auto_flush_period | Duration* | 900,000 (15 min) | Frequency of forced flushes, when the dynamic store is flushed to the disk straight away, even if it hasn't reached its overflow threshold yet. | | dynamic_store_flush_period_splay | Duration | 60 000 (1 min) | Random shift for the period to avoid synchronization of different tablets. Real flush will come after `period + random(0, splay)`. | | merge_rows_on_flush | bool | false | Allows version merging and deletion of rows by TTL at flush. | | merge_deletions_on_flush | bool | false | Allows consecutive deletions to be merged into one at flush. | | max_dynamic_store_row_count | int | 1 000 000 | Maximum number of rows in the dynamic store. | | max_dynamic_store_pool_size | int | 1 073 741 824 (1 GB) | Maximum dynamic store size. | | dynamic_store_overflow_threshold | double | 0.7 | The share of filling of the dynamic store relative to max_dynamic_store_row_count and max_dynamic_store_pool_size at which flush starts. | ### Compaction { #compaction_attributes } #### Basic options | Name | Type | Default | Description | |--|--|--|--| | auto_compaction_period | int*| - | Frequency of periodic compaction in ms. Compaction will affect every table chunk at least once per auto_compaction_period. For more information, see [Periodic compaction](https://ytsaurus.tech/docs/en/#periodic_compaction). In most cases, its use isn't recommended. | | auto_compaction_period_splay_ratio | double | 0.3 | Random shift for the period to avoid synchronization. Compaction will come after `period * (1 + random(0, splay_ratio))`. | | periodic_compaction_mode | `store`, `partition` | `store` | For more information, see [Periodic compaction](https://ytsaurus.tech/docs/en/#periodic_compaction). | | forced_compaction_revision | - | - | For more information, see [Forced compaction](https://ytsaurus.tech/docs/en/#forced_compaction). | | max_overlapping_store_count | int | 30 | Maximum number of chunks that potentially contain a key. When the threshold is reached, writing to the table will be locked until compaction optimizes the structure. | | critical_overlapping_store_count | int* | - | Sets a threshold beyond which compaction ignores the size [limits](https://ytsaurus.tech/docs/en/#compaction_size_policy) for chunks within a single partition. By setting a small value (5–10) for this option, you can reduce the OSC and data access speed at the cost of a noticeable increase in write amplification. | | enable_compaction_and_partitioning | bool | true | Completely disables compaction. Writing to the table in this state will quickly lead to exceeding overlapping store count. If no writing is intended, then instead of this attribute,mount the table in a frozen state. | #### Sizes and constants | Name | Type | Default | Description | |--|--|--|--| | min_partition_data_size | int | 96 MB | Minimum, desired, and maximum partition size. | | desired_partition_data_size | int | 256 MB | ↑ | | max_partition_data_size | int | 320 MB | ↑ | | min_partitioning_data_size | int | 64 MB | Minimum and maximum data size for a single partitioning. Increasing it reduces write amplification at the cost of increasing the number of chunks in Eden and therefore increasing overlapping store count. | | max_partitioning_data_size | int | 1 GB | ↑ | | min_partitioning_store_count | int | 1 | Minimum and maximum number of chunks for a single partitioning. | | max_partitioning_store_count | int | 5 | ↑ | | min_compaction_store_count | int | 3 | Minimum and maximum number of chunks for a single compaction. Periodic and forced compactions ignore the lower estimate, but take into account the upper one. | | max_compaction_store_count | int | 5 | ↑ | | compaction_data_size_base | int | 16 MB | | | compaction_data_size_ratio | double | 2.0 | | ### Deleting old data { #ttl_attributes } For more information about these attributes, see [Deleting old data](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md#remove_old_data). | Name | Type | Default | |--|--|--| | min_data_ttl | int | 1 800 000 (30 min) | | max_data_ttl | int | 1 800 000 (30 min) | | min_data_versions | int | 1 | | max_data_versions | int | 1 | ## Forced compaction { #forced_compaction } To trigger the forced compaction of all table chunks, set the `forced_compaction_revision` attribute value to `1`. If you execute `remount-table`, the setting will be immediately applied to all table tablets. When working with tables of a terabyte or more, this can create a load surge both on the table bundle and on the entire cluster. Therefore, we recommend executing `remount` of different tablets at different times. To do this, run the following command: `yt remount-table --first-tablet-index X --last-tablet-index Y`. To estimate the recommended time for remounting the table, use the formula `table_size / bundle_node_count / (100 Mb/s)`. To abort forced compaction, delete the `forced_compaction_revision` attribute from the table and execute `remount-table`. {% note info "Note" %} If the table size is more than 20 TB or 100,000 chunks, get administrator permission before starting forced compaction. {% endnote %} ## Periodic compaction { #periodic_compaction } Compaction starts automatically when there are enough chunks in the partition. This usually happens during active writes to the table. If no writes are being made, no compaction will occur. To perform compaction at regular intervals, even when there are no active writes to individual partitions or tablets, enable periodic compaction. You should use periodic compaction if you need to delete data based on TTL or apply deletions triggered by delete-rows, and automatic compaction isn't enough to meet your needs. {% note warning "Warning" %} Most scenarios don't require periodic compaction. In fact, it often creates excessive loads and can be harmful. Avoid using periodic compaction solely to reduce the number of chunks. The normal size of dynamic table chunks is about 40–60 MB (compressed size). If the chunks of your table fall within this size range, there is no need to make them larger. {% endnote %} To enable periodic compaction, use the `auto_compaction_period` attribute. It determines the compaction period for each chunk. After setting the attribute, run the `remount-table` command to apply the settings. #### Modes { #periodic_compaction_mode } There are two periodic compaction modes. You can set the mode using the `periodic_compaction_mode` attribute. - `store` (default value): The decision to compact each chunk created earlier than `now - auto_compaction_period` is made independently. Use this mode if you need to delete data based on TTL. - `partition`: If the partition contains at least one chunk created earlier than `now - auto_compaction_period`, all partition chunks are compacted at the same time. Use this mode if you need to clear rows deleted via delete-rows. `Partition` mode is better suited for clearing rows deleted via delete-rows because, when using `store`, a write by key and its corresponding delete tombstone may end up in different chunks that are independently compacted one after another, in which case the tombstone will never be deleted. `Store` mode reviews chunks independently, and when the oldest chunk is reviewed, the obsolete data will be deleted. Setting only the `periodic_compaction_mode` attribute is not enough to enable periodic compaction — you need to explicitly set the `auto_compaction_period` attribute. #### Selecting a period { #periodic_compaction_period } We recommend setting a period that is close to the table's TTL. For example, if the TTL is seven days, you can set the `auto_compaction_period` to three days. This will guarantee that the rows are deleted within ten days. Setting a period that is too short may result in excessive loads on the bundle. Each bundle node is capable of compacting about 100-200 MB/s maximum. The load from periodic compaction usually does not exceed several units of MB/s. The period is often commensurate with `max_data_ttl` or is several times less than it. For example, there is a table of 500 GB and a compaction period of one day and two nodes in the bundle. The load per node would then be 500 GB/86,400 sec/2 ≃ 3.1 MB/s, which is allowable. {% note warning "Warning" %} Initially setting `auto_compaction_period` on the table with a lot of old chunks can cause all chunks to start being compacted at the same time. In this case, follow the same recommendations as for forced compaction. {% endnote %} To avoid synchronization and even out the load, a random shift is added to `auto_compaction_period` when calculating the compaction time of each specific chunk. This shift is defined by the `auto_compaction_period_splay_ratio` attribute. ## Scenarios { #scenarios } #### Deleting data by TTL { #scenario_ttl } **Scenario**: TTL is set for the table, but the size continues to increase as if no TTL is applied. **Solution**: Use [periodic compaction](https://ytsaurus.tech/docs/en/#periodic_compaction). #### Deleting short-lived keys { #write_delete } **Scenario**: The row is written and deleted after a while. It is required that delete tombstones do not pile up and space is cleared. **Solution**: Use [periodic compaction](https://ytsaurus.tech/docs/en/#periodic_compaction) in `partition` mode. #### Many writes by one key { #woodpecker } **Scenario**: There may be more than a few thousand writes by one key. **Problem**: There is a limit on the number of versions of one key in the system. It is about 30,000 versions. If this is exceeded, background processes will end up with the "Too many write timestamps in a versioned row" error. **Solution**: Set the `True` value of the `merge_rows_on_flush` attribute and reduce TTL via the `min_data_ttl` attribute so that the number of versions within TTL is not more that a few thousand. If many deletions are made per key, use `merge_deletions_on_flush`. ## Tablets and background processes under the hood { #tablet_inside } This section details the structure of tablets. It's not mandatory for reading, but it may be helpful to those who want to tweak some of the settings on their own. ### Structure { #tablet_description } A tablet is a part of the table that is responsible for the data between two pivot keys. Typically, tablet size ranges from 100 MB (for in-memory tables) to a few gigabytes, sometimes reaching tens of gigabytes for especially large tables. From the master server's point of view, a tablet is a set of chunks. All operations described below occur on the tablet node directly serving the tablet. Similarly to tables that are split into tablets bounded by pivot keys, tablets are split into partitions. The partition size is about 200 MB (compressed size). In addition, there is a special partition, Eden. Its boundaries coincide with those of the tablet. Each chunk belongs either to Eden or to one of the partitions. If a chunk falls entirely between the boundary keys of a particular partition, it belongs to it. Otherwise it belongs to Eden. ### Dynamic stores and flushing data to the disk { #flush } There is always at least one dynamic store in the table — an active one. When writing, the data first gets into the dynamic store. When the dynamic store becomes too large (hundreds of megabytes) or the node runs out of tablet dynamic memory, the store is *rotated*. That is, it becomes passive and is replaced by a new active store. This activates the flush process, and the passive store is flushed to the disk over time. If flush does not work for some reason, the data is accumulated in memory, and after overflow, writing starts failing with the "Node is out of tablet memory, all writes disabled" error. By default, all row versions are saved to the chunk during flush. To apply cleanup by TTL, use the `merge_rows_on_flush` attribute. It should be done when the TTL is less than the typical dynamic store lifetime (about 15 minutes), and there are many writes by one key. ### Partitioning { #partitioning } The chunk flushed to the disk ends up in Eden. It usually contains keys from the entire tablet range and can't be attributed to a particular partition. The partitioning process takes one or more chunks from Eden, merges them, splits the data into partitions, and places one chunk into each partition. If there are many small chunks in Eden, it will first run compaction and merge them. Small chunks make the system less efficient. The first reason for this is overhead costs. The second reason is write amplification. Suppose a node serves several tablets of a large table. Since there are many tablets and the memory is shared, the dynamic store size in each tablet will be tens of megabytes, which is rather small. If there are 100 partitions in a tablet, the chunk size in each of them will be less than one megabyte after partitioning. If you increase the minimum allowable Eden size at which partitioning starts, larger chunks will be flushed to partitions at the cost of the increased overlapping store count. ### Compaction { #compaction } The compaction process reads a batch of chunks in a single partition and merges them into one (or several, in rare cases), physically deleting rows and old versions. Compaction can be started for several reasons: - forced: The `forced_compaction_revision` attribute is set on the table. In this case, all tablet chunks will be compacted and the batch size will only be limited by the `max_compaction_store_count` value. - periodic: The `auto_compaction_period` attribute is set on the table. Chunks created earlier than `now - auto_compaction_period` will be compacted. - regular: Regular mode started without any external influence. #### Picking chunks based on size { #compaction_size_policy } In regular mode, the system selects chunks taking into account write amplification. If there is a 100 MB chunk in the partition and a 1 MB chunk periodically appears, then if you compact them together each time, you will get x100 amplification. The following rules are used: - There must be from `min_compaction_store_count` to `max_compaction_store_count` chunks in a batch, but the more the better. - Chunks must be sorted by size. Each successive chunk must be by no more than `compaction_data_size_ratio` times larger than the sum of the sizes of the previous chunks. - The previous rule does not apply as long as the total size of the chunks is less than `compaction_data_size_base`. For example, ratio = 2, base = 16 MB. Then: - A set of 1 KB, 1 MB, 10 MB chunks is allowed: the sum is not exceeding 16 MB. - A set of 10 MB, 20 MB, 50 MB, 150 MB chunks is allowed: 50 < 2 × (10 + 20), 150 < 2 × (10 + 20 + 50). - A set of 1 MB, 10 MB, 100 MB chunks is not allowed: 100 > 2 × (10 + 1). Reducing the base and ratio values improves the WA at the cost of increasing the OSC. In the limiting case where base = 1 but no data is deleted, it can be demonstrated that the write amplification is logarithmically dependent on the amount of data. Each time a row participates in compaction, the size of the chunk that contains it increases by at least (1 + 1 / ratio) times. Consequently, this row participates in a total of no more than log(tablet_size, 1 + 1 / ratio) compactions. In practice, this estimation is inaccurate not only because of the deletions, but also because the partition splits into two when the threshold size is reached. #### Mechanism for cleaning up old versions { #major_timestamp } When compaction merges another batch of chunks, it can remove old versions of some rows and apply deletions. However, this may not always be possible, because compaction considers only some of the chunks within a partition, not all of them. Let's consider a table with min/max_data_ttl = 0, min/max_data_versions = 1 (only the most recent version must be stored by each key). For example, there were two writes by a key: {value = 1; timestamp = 10}, {value = 2; timestamp = 20}, and deletion with timestamp = 30. Suppose these versions are stored in three different chunks. If compaction considers only the first and third chunks, the following data is processed: ``` delete tombstone {timestamp = 30} {value = 1; timestamp = 10} ``` If you apply deletions and remove the row completely without writing it to a new chunk, it will result in an incorrect read: further reads will read the {value = 2; timestamp = 20} value, although the row was deleted. To avoid this problem, we calculate the *major timestamp*, which is the earliest timestamp across all data in all the chunks of a given partition and Eden to not be included in the current compaction batch. Compaction then has the right to delete only those versions where timestamp < major timestamp. This logic can lead to obsolete versions not actually being deleted. First, if the tablet or partition is not written to, many chunks within the partition stabilize, and compaction no longer initiates since it has no way of detecting if any of the rows are aged. Second, if there is one large chunk in the partition and writing is not very intensive, then the newly appearing small chunks will be compacted with one another without affecting the large one. The large chunk imposes a limit on major timestamp, so even if ttl = 0, repeated versions in fresh chunks will not be deleted. You can deal with this using `auto_compaction_period`. # Running operations on dynamic tables This section describes how to run MapReduce operations on dynamic tables and tells you about data consistency. Examples of converting a static table into a dynamic table and vice versa are given. Everything described applies equally to all types of operations — map, merge, etc., and not only directly to map-reduce, and the read-table command. ## Overview YTsaurus can run MapReduce operations on top of [sorted and ordered](https://ytsaurus.tech/docs/en/overview.md#types) dynamic tables. You can also quickly (at the metadata level) convert a static table into a dynamic table. Since static and dynamic tables were originally created for different work scenarios, there are several fundamental differences between them (both sorted and ordered). 1. When inserted into a dynamic table, the data first gets into the log and a special in-memory storage (the dynamic store), and only after that it is written to chunks on the disks in the form of versioned rows (until the data is written to chunks, fault tolerance is guaranteed by the log). Data from static tables is always written to chunks on the disks. 2. All keys in a _sorted_ dynamic table are unique. Table rows are uniquely determined by their key. 3. All data in a _sorted_ dynamic table is versioned: when inserted into a sorted dynamic table, the commit timestamp is assigned to it. When reading, you can specify a timestamp to get the table row version for the specified time slice. 4. In _sorted_ dynamic tables, compaction occurs periodically: old data versions are deleted, chunks are overwritten. 5. Neither _sorted_ nor _ordered_ dynamic tables allow pass-through indexing by row number: in sorted dynamic tables, it is completely impossible, in ordered dynamic tables, it is possible within each tablet separately. ## Running operations on dynamic tables { #running_operations_over_dyn_tables } _This section applies to both sorted and ordered tables._ A dynamic table can be specified as an input one (or one of the input tables) for a MapReduce operation. The MapReduce operation interface is the same as static tables have, except that you specify the path to the dynamic table instead of the path to the static table. The dynamic table can be either mounted or unmounted. A [separate article](https://ytsaurus.tech/docs/en/bulk-insert.md) is devoted to the use of dynamic tables as output tables for the operation. Since ordinary MapReduce operations work with chunks and dynamic tables store some data in memory in dynamic stores, there is some specificity in running operations on dynamic tables. By default, data from dynamic stores is not visible from operations. There are several ways to bypass this restriction. A suitable method in most cases is to first unmount the table and then specify the `@enable_dynamic_store_read=%true` attribute on it, after which all data will be visible from operations. This is a popular approach if you want fresh data, but there are side effects: - The results of successive runs of the same operation on top of the same table (even under snapshot lock) may differ, because the second run will read more fresh rows. For this reason, caching in YQL is disabled for tables with`@enable_dynamic_store_read`. For idempotency, you must [explicitly specify a timestamp](https://ytsaurus.tech/docs/en/#dyntable_mr_timestamp) or [a set of row indexes](https://ytsaurus.tech/docs/en/#dyntable_mr_ordered). - If a table has a set `@merge_rows_on_flush` attribute, it's currently impossible to ensure consistent read by timestamp when `@enable_dynamic_store_read` is enabled. - If one tablet is read by hundreds of jobs, there is a risk of overloading the tablet nodes. If, for whatever reason, you do not want to use this mode, there are some other ways: 1. Unmount the table. When the dynamic table is unmounted, all of its data is stored in chunks. 2. Freeze the table using the `freeze-table` command and wait until all tablets are in `frozen` state. In this state all data is written to the chunks, no new data can be written, but the table can be read using the `lookup-rows` and `select-rows` commands. You can execute MapReduce operations. 3. Order the table state at a point in time in the past so that all data is already in the chunks. 4. Do nothing and get the data from the chunks as in clause 3, but with no guarantee that all keys will refer to the same table version (data is written to the disk at different times in different tablets, there is no synchronization between them). {% note info "Note" %} You can run operations that ignore dynamic stores and read only from chunks on top of tables with enabled `@enable_dynamic_store_read`. To do this, explicitly specify `enable_dynamic_store_read = %false` in the spec root. {% endnote %} ## Running operations on the state of a sorted dynamic table at a particular point in time { #dyntable_mr_timestamp } By default, the most recent row version contained in the chunks gets into the MapReduce operation. If the table is mounted, the versions in the chunks may be inconsistent: it may be for two different rows from the same transaction that one row is already in the chunk and the other is still in RAM. As a result, the MapReduce operation will only display some of the transaction rows. If `@enable_dynamic_store_read` is enabled, inconsistency is also possible because the jobs read different parts of the table at different times, and writing may have occurred between reads. In cases where it is important to maintain a consistent table state in the MapReduce operation, you must specify a timestamp in the special YPath attribute: `//path/to/dynamic/sorted/table`. There is a lower and upper limit on `timestamp`: - Since the data is periodically compacted, `timestamp` cannot be less than the value of the `@retained_timestamp` attribute. Compaction clears old data, but only the one preceding `@retained_timestamp`. - If the `@enable_dynamic_store_read` attribute is specified on the table, there is no upper limit. However, if you specify `timestamp` from the future, consistency is not ensured. - If `@enable_dynamic_store_read` is not specified, `timestamp` must be smaller than the timestamp of those rows that have not yet been written to the disk. You can find out the timestamp via the `unflushed_timestamp` table attribute. Thus, if reading from dynamic stores is enabled, we recommend taking the current timestamp (it can be obtained using the [generate_timestamp](https://ytsaurus.tech/api/commands#generate_timestamp) command). When reading from a replica table, `timestamp` must be generated on the metacluster. If reading from dynamic stores is not enabled, use `unflushed_timestamp - 1`. When specifying `timestamp`, the scheduler will check that the timestamp value falls within the permitted interval. The `unflushed_timestamp` and `retained_timestamp` attribute values of a mounted table are constantly changing, so you must use snapshot lock when working with the table. There are no guarantees that `retained_timestamp` is always less than `unflushed_timestamp`. The listed values depend on the compaction settings and the process of writing data to the chunks, and if this condition is violated too often, the table settings will have to be changed. Pay attention to the following attributes: - `dynamic_store_auto_flush_period`: The frequency with which data is flushed to the disk. It makes sense if `@enable_dynamic_store_read` is disabled. The default value is 15 minutes. - `min_data_ttl`: Determines to what extent old data must not be compacted. For more information, see [Sorted dynamic tables](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md#remove_old_data). {% note info "Note" %} The current implementation does not support operations with timestamp indication on tables mounted in non-atomic mode. {% endnote %} ## Running operations on ordered tables { #dyntable_mr_ordered } Ordered dynamic tables are quite similar to static tables. The differences in terms of operations are that indexing by row numbers is performed separately within each tablet (and indexing may not start at zero because of [trim](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md#trim)) and fresh data in each tablet is in the dynamic store. {% note info "Note" %} The effect of trim may not be immediately seen in the operation. The situation in which rows deleted via trim are included in the operation is considered allowable. {% endnote %} ### Indexing The `enable_tablet_index` [control attribute](https://ytsaurus.tech/docs/storage/io-configuration.md#control_attributes) is available in operations on ordered tables. The `enable_row_index` attribute has a different semantics than in static tables: the returned row index is counted relative to the beginning of the tablet. Row numbering within the tablet is logical, i.e. even if trim is performed, the existing row numbers will not change. Row and tablet indexes can also be specified in [ypath ranges](https://ytsaurus.tech/docs/storage/ypath.md#known_attributes). Each row in the ordered table is indexed in a pair `(tablet_index, row_index)`. The `lower_limit = {tablet_index = a; row_index = b}` lower limit allows rows from tablet `a` with indexes `b` and greater and all rows from tablets with indexes greater than `a`. The `upper_limit = {tablet_index = c; row_index = d}` upper limit allows rows from tablets with indexes less than `c` and rows from tablet `c` with indexes strictly less than `d`. You can specify only `tablet_index` within the limit. The lower limit of the `{tablet_index = e}` type allows all rows of tablets with numbers not less than `e` and the upper limit of this type allows all rows of tablets with numbers strictly less than `e`. {% note warning "Attention!" %} There is a limit on the number of ranges in YPath. If the number of tablets in an ordered table exceeds 100, and you plan to specify one range per each tablet of an ordered table in the operation, discuss it with the cluster administrators. {% endnote %} ### Allowable index limits As with timestamp for sorted tables, row index in the ordered table has an upper and lower limit within the tablet. The lower limit has to do with deleting rows via trim. The upper one is related to storing rows in the dynamic store and makes sense if [enable_dynamic_store_read](https://ytsaurus.tech/docs/en/#running_operations_over_dyn_tables) does not work for you and you want to run an operation on a mounted table. Limits can be obtained from the `@tablets` table attribute. The value corresponding to each tablet has the `trimmed_row_count` and `flushed_row_count` fields. Unlike timestamp, the correctness of indexes for ordered tables is __not validated__: if you specify indexes outside the specified limits, the operation will simply read less data than expected. ## Converting a static table into a dynamic table { #convert_table } To convert a static table into a dynamic table, run the `alter-table` command: {% list tabs %} - CLI ```bash yt alter-table --dynamic //tmp/t ``` - Python ```python yt.alter_table("//tmp/t", dynamic=True) ``` {% endlist %} There are a few conversion peculiarities that are worth paying attention to. - The `alter-table` command will be executed for an unsorted table as well. The result will be an [ordered dynamic table](https://ytsaurus.tech/docs/en/ordered-dynamic-tables.md). For such tables, the `insert-rows` command works differently than it does for sorted tables and the `select-rows` command will be much slower on the same queries. - All keys in a sorted dynamic table are unique, so a static table must also contain only unique keys. The system identifies this fact by the `@unique_keys` attribute in the schema. You cannot set this attribute to the schema of an existing non-empty table using the `alter-table` command. You need to specifically create a table with the specified `unique_keys=true` attribute in the schema. The uniqueness of keys will be validated when writing. If the schema of a created table has a set `unique_keys=false` attribute when all of the table keys are unique, you need to create a new table with a revised schema and copy the data into it using the [Merge](https://ytsaurus.tech/docs/data-processing/operations/merge.md) operation. - Typical chunk and block sizes for dynamic tables are smaller than for static tables. If the table is to be converted into a dynamic table, set certain parameters in the spec of the operation generating the table: ```python "job_io": {"table_writer": {"block_size": 256 * 2**10, "desired_chunk_size": 100 * 2**20}} ``` If an operation has more than one job type, the config only needs to be specified for `job_io` of the last job type in the operation (`merge_job_io` for the Sort operation, `reduce_job_io` for the MapReduce operation). If the operation uses [automatic merging of chunks](https://ytsaurus.tech/docs/data-processing/operations/automerge.md), a similar config must be specified in the `"auto_merge": {"job_io": {...}}` section. For more information about `job_io` for different types of operations, see [Operation options](https://ytsaurus.tech/docs/data-processing/operations/operations-options.md). - You can check the sizes of chunks and blocks of the resulting table using the following table attributes: - Chunks: `@compressed_data_size` / `@chunk_count` - Blocks: `@chunk_format_statistics/*/max_block_size` (attribute value is a dict containing the max_block_size value for each chunk type in the table). - For static tables, there is a [chunk auto-merge functionality](https://ytsaurus.tech/docs/data-processing/operations/chunk-merger.md). To prevent this from affecting the table converting, ensure that the `@chunk_merger_mode parameter` is set to `none` for both the static table itself and the directory where it resides. Otherwise, the system will automatically increase the chunk size, and even with the correct settings on the source table, the chunks in the dynamic table will end up being too large. The move operation does not preserve an explicitly set `@chunk_merger_mode` attribute. After the move, the table inherits this attribute from the root directory, ignoring the value that was previously set for it. Therefore, it is recommended to explicitly set `@chunk_merger_mode` = `none` for the directory where you are working with these tables. {% cut "Learn more about chunk and block sizes" %} Dynamic tables are usually used for a quick search by key. Data in chunks is split into blocks, and when read from the disk, the entire block is read at a time. By default, the table is compressed with a specific codec, decompression will also be applied to the entire block. To avoid applying too many calculations to search for a single row, reduce the block size. By default, static table blocks have a default size of 16 megabytes, while dynamic table blocks are 256 kilobytes. But if a static table is converted into a dynamic table, the block size remains the same as that of the original static table. Therefore, when creating a static table, you must force the block size to be reduced by specifying the `block_size` setting in the specification. {% note warning "Attention!" %} The current YTsaurus implementation checks that the maximum block size does not exceed 512 kilobytes when attempting to query a dynamic table. Otherwise the query will end with the "Maximum block size limit violated" error. This is done to avoid slow reads from tables. The same error will occur with a dynamic table loaded into memory when trying to load a chunk with large blocks. To solve this problem, you can either compact the table or increase the block size limit to 32 megabytes using the `max_unversioned_block_size` attribute (you must set the attribute on the table and then call `remount_table`). {% endnote %} Tablets in dynamic tables are divided into partitions. A typical partition size is several hundred megabytes. If the chunks become larger than the partition size, a background process is started that forces the chunks to be split into smaller chunks. It is also useful to specify the `desired_chunk_size` setting in the specification when creating a dynamic table. The chunks will then be quite small (100 megabytes each). Otherwise, the chunks will be large by default (512 MB or more). When a table is sharded into shards (tablets), these chunks can fall into multiple tables and will then be counted multiple times when calculating a table size. As a result, the dynamic table size may be several times larger than the size of the original static table. {% endcut %} If the table is intended to be stored in memory (managed by the `@in_memory_mode`attribute), we recommend performing forced compaction to achieve the best performance. The need for this is caused by the following: - The same chunk gets into different tablets, and the chunks of different tablets are loaded into memory independently (the tablets may be on different cluster nodes). As a result, more memory is consumed than necessary. - Readers for static table chunks are not optimized for cases when the data is already in memory. If the dynamic table is to be read-only, it must be mounted in "frozen" state (managed by the `--freeze` flag of the `mount-table` command). This will prohibit writing, disable compaction, and reduce the table maintenance overhead costs. Do it only if the the chunk and block sizes are as recommended. ### Example Below is a script example that demonstrates how to save a sorted dynamic table into a static one and then restore it back. This operation can be performed to back up dynamic tables. Please note that the script is not production-ready and is given for reference only. In particular, it saves only some table attributes and does not handle errors in any way. {% cut "Converting a dynamic table into a static table and vice versa" %} ```python #!/usr/bin/python3 import yt.wrapper as yt import argparse def dump(src, dst): with yt.Transaction(): # Take snapshot lock for source table. node_id = yt.lock(src, mode="snapshot")["node_id"] # Get timestamp of flushed data # (not required if @enable_dynamic_store_read is set). ts = yt.get(f"#{node_id}/@unflushed_timestamp") - 1 # Create table and save vital attributes. yt.create("table", dst, attributes={ "optimize_for": yt.get(f"#{node_id}/@optimize_for"), "schema": yt.get(f"#{node_id}/@schema"), "_yt_dump_restore_pivot_keys": yt.get(f"#{node_id}/@pivot_keys"), }) # Dump table contents. yt.run_merge(yt.TablePath(f"#{node_id}", attributes={"timestamp": ts}), dst, mode="ordered") def restore(src, dst): # Create destination table. yt.create("table", dst, attributes={ "optimize_for": yt.get(f"{src}/@optimize_for"), "schema": yt.get(f"{src}/@schema"), }) # Make blocks smaller (required for real-time lookups). yt.run_merge(src, dst, mode="ordered", spec={ "job_io": {"table_writer": {"block_size": 256 * 2**10, "desired_chunk_size": 100 * 2**20}}, "force_transform": True, }) # Make table dynamic. yt.alter_table(dst, dynamic=True) # Restore tablet structure. yt.reshard_table(dst, yt.get(f"{src}/@_yt_dump_restore_pivot_keys"), sync=True) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Dynamic tables dump-restore tool") mode = parser.add_mutually_exclusive_group(required=True) mode.add_argument("--dump", nargs=2, metavar=("SOURCE", "DESTINATION"), help="Dump dynamic table to static") mode.add_argument("--restore", nargs=2, metavar=("SOURCE", "DESTINATION"), help="Restore dynamic table from static") args = parser.parse_args() if args.dump: dump(*args.dump) if args.restore: restore(*args.restore) ``` {% endcut %} ## Using computed columns To distribute the load across the cluster more evenly, dynamic tables need to be sharded (split into tablets). To distribute the load evenly across the tablets, it may be convenient to add an extra column prior to all the key columns in which to write hash from the key part by which it makes sense to perform sharding - for example, hash from the first column. The result is a table whose keys are evenly distributed in the `[0, 2^64-1]` range. The column added in this way is a computed column. The calculation formula must be specified in the column schema in the `expression` field. Support for computed columns was added to all operations except [Sort](https://ytsaurus.tech/docs/data-processing/operations/sort.md). When writing to the table, the value of such columns will be computed. To prepare a static table to be converted into a dynamic table, you must first make the table in an unsorted schema (but with computed columns), then sort it using the [Sort](https://ytsaurus.tech/docs/data-processing/operations/sort.md) operation. Below is an example of creating a dynamic table with computed columns from a static table. Instead of `write_table`, the table could be written to using the Map, Reduce, MapReduce, or Merge operations. In the latter case, you must specify `{'schema_inference_mode': 'from_output'}` in the specification so that the data is validated using the output table schema. In addition, when specifying `schema_inference_mode`, you need to create the output table of the operation manually by explicitly specifying the required schema. If you do not do this, the used API will attempt to create the output table independently using the schema inferred from the input table, which will cause errors in this case. {% cut "Creating a dynamic table with computed columns" %} ```python import yt.wrapper as yt import yt.yson as yson import time # Unsorted schema. schema = [ {"name": "hash", "type": "uint64", "expression": "farm_hash(key)"}, {"name": "key", "type": "uint64"}, {"name": "value", "type": "string"}] # Sorted schema with unique keys attribute. sorted_schema = yson.YsonList([ {"name": "hash", "type": "uint64", "expression": "farm_hash(key)", "sort_order": "ascending"}, {"name": "key", "type": "uint64", "sort_order": "ascending"}, {"name": "value", "type": "string"}]) sorted_schema.attributes["unique_keys"] = True # Create table table = "//tmp/table" yt.remove(table, force=True) yt.create_table(table, attributes={"schema": schema}) # Write rows into table. Computed columns are omitted: YTsaurus will evaluate them. yt.write_table(table, [{"key": 2, "value": "2"}, {"key": 1, "value": "1"}]) # Sort table. Resulting table schema has unique_keys=True. yt.run_sort(table, yt.TablePath(table, attributes={"schema": sorted_schema}), sort_by=["hash", "key"], spec={ "partition_job_io": {"table_writer": {"block_size": 256 * 2**10}}, "merge_job_io": {"table_writer": {"block_size": 256 * 2**10}}, "sort_job_io": {"table_writer": {"block_size": 256 * 2**10}}}) # Alter table into dynamic. yt.alter_table(table, dynamic=True) # Mount table and wait until all tablets are mounted. yt.mount_table(table, sync=True) # Print rows. for row in yt.select_rows("* from [{}]".format(table)): print row ``` {% endcut %} ## Examples of running the operations Running a Map operation: ```bash yt map 'grep some_value' --src //path/to/dynamic/table --dst //tmp/output --spec '{pool = "project-pool"; job_count = 100; }' ``` Running a Reduce operation: ```bash yt reduce 'python my_reducer.py' --src //path/to/dynamic/table --dst ''//tmp/output ``` # Inserting from operations into dynamic tables YTsaurus enables you to specify [sorted dynamic tables](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md) as output tables of map-reduce operations (the so-called bulk insert). Application: - Data delivery to tables. - `DELETE WHERE` client implementation. - Background process of cleaning up rows with a non-trivial TTL. ## Features { #features } Operations that contain dynamic output tables have a number of features. * The operation cannot be run under a user transaction. * Only sorted dynamic tables can be output. * A dynamic table can be written to either in "append" or "overwrite" mode. This is regulated by the `` attribute on the path. Unlike sorted static tables, adding data to a dynamic table takes a shared lock on it, so multiple operations can write to the table at the same time. Note that writing in "append" mode is actually upsert, not append (data does not have to be written to the end of the table). * You can only write to a mounted or frozen dynamic table. Writing to a frozen table is strongly discouraged, because compaction does not work in frozen tables. Without it, reading quickly becomes inefficient, and writing is [blocked](https://ytsaurus.tech/docs/en/#compaction). * Only sorted data with `unique_keys=%true` can be written to a dynamic table. If user jobs do not have this feature, use an intermediate static table, which can then be sorted into a dynamic one using the sort operation. * An operation transaction is not like classic tablet transactions. In particular, you can write to the output table using `insert_rows` while the operation is running, and if the operation affects the same rows, they will be overwritten. * Data can be written from the operation in extended format that enables you to delete rows and specify "aggregate" mode. ## Transactional model { #trasactional_model } As with ordinary operations, the bulk insert commit is atomic: the changes will either fall into all output tables or none. While the operation is being committed, all dynamic tables in which the insertion takes place are locked. The lock applies to any writes, as well as reads with a timestamp greater than the lock start time and SyncLastCommittedTimestamp. Reads with AsyncLastCommittedTimestamp will not be locked. If the operation commit is completed successfully, the table is unlocked and the data appears in it. The timestamp of all inserted rows is equal to the timestamp of the operation commit. In particular, the timestamp of the inserted rows in all affected tables will be the same. Bulk insert only conflicts with those tablet transactions that affect the moment of commit (locking). If a tablet transaction started before locking, it will not be able to commit afterwards. There will either (most likely) be an abort, or the operation commit will wait for the transaction commit. Several bulk insert operations with `` can run and even be committed in parallel. There are no row-by-row locks: the system enables different operations to edit the same row. ## Deletions and extended write format The extended format enables you to delete rows from the table, set "aggregate" (`aggregate` in insert_rows) and "overwrite" mode (`update` in insert_rows). To use it, you need to specify the `schema_modification=unversioned_update` attribute on the path to the output table and write from the operation in the special schema: * Key columns remain unchanged. * A mandatory column with the `$change_type` name is added, indicating whether the row is a write or a deletion. * Each non-key column with the name `name` is replaced by two columns: `$value:name` and `$flags:name`. The first one will contain the value and the second one will manage "aggregate" and "overwrite" modes. You can take the service column names from the [api](https://github.com/ytsaurus/ytsaurus/blob/879946a82c766da78612ab4fe3637f7ea4da8292/yt/yt/client/tablet_client/public.h#L152). `$change_type` accepts values from the [ERowModificationType](https://github.com/ytsaurus/ytsaurus/blob/879946a82c766da78612ab4fe3637f7ea4da8292/yt/yt/client/api/public.h#L53) enum. Only `Write` and `Delete` values are allowed. Name | Value :- | :- `ERowModificationType::Write`| 0| `ERowModificationType::Delete`| 1| The value flags accept the bit mask of values of the [EUnversionedUpdateDataFlags](https://github.com/ytsaurus/ytsaurus/blob/879946a82c766da78612ab4fe3637f7ea4da8292/yt/yt/client/tablet_client/public.h#L142) enum. The `missing | aggregate` combination makes no sense, but it is acceptable. If no flags are specified for a column, they are considered equal to 0. Name | Value | Clarification :- | :- | :- `EUnversionedUpdateDataFlags::Missing`| 1| If the bit is set, the value will be ignored, otherwise it will be overwritten (if there is no value, Null will be written to the table). `EUnversionedUpdateDataFlags::Aggregate`| 2| If the bit is set, aggregate will be applied, otherwise the value will be updated. {% cut "Example" %} Let's assume that we have a table with the _user_name_ key column (string), the _age_ column (uint64), and the _balance_ aggregate column (int64). The extended schema will then look like this: ```yson {name="user_name"; type=string; sort_order=ascending} {name="$change_type"; type=uint64; required=%true} {name="$value:age"; type=uint64} {name="$flags:age"; type=uint64} {name="$value:balance"; type=int64} {name="$flags:balance"; type=uint64} ``` Then to delete the string, you need the following write ```yson { "user_name"="vasya"; "$change_type"=1; // delete } ``` And to update the balance (and maintain the age), you need the following write ```yson { "user_name"="vasya"; "$change_type"=0; // write "$flags:age"=1; // missing "$value:balance"=100500; "$flags:balance"=2; // aggregate } ``` {% endcut %} In extended format, you can also create intermediate static tables which will then be inserted into dynamic tables using sort or merge, but this is very situational. ### DELETE WHERE via input query With bulk insert, you can effectively delete multiple rows from a table by condition using [input query](https://ytsaurus.tech/docs/en/dyn-query-language). An SQL-like query language can be used to filter the rows supplied to the input of an operation. The same syntax as in select-rows is used. For example, you can delete all rows with an even key from a table this way. ```python input_query = "1u as [$change_type], key1, key2 where key1 % 2 = 0" yt.run_merge( table, yt.TablePath( table, append=True, attributes={"schema_modification": "unversioned_update"}), mode="ordered", spec={"input_query": input_query}) ``` The operation will read all rows from the table, leave those matching the condition, convert them into a deletion format, and write them back to the table. {% note warning %} As with delete_rows, deletion does not delete rows from the table, but writes tombstones that will later be compacted. If you delete much data from a table, queries can start slowing down in a random way, because for every useful row you have to read a lot of deleted rows. For regular deletions, in most scenarios compaction will run automatically, but in some cases you may need to configure the table. {% endnote %} ## Additional fields in the specification { #additional_fields } The operation specification may contain the same fields as usual, with a few exceptions. * To specify the [table writer](https://ytsaurus.tech/docs/storage/io-configuration#table_writer) config, use the `dynamic_table_writer` option indicated in the same place as the ordinary `table_writer` (in `*_job_io`). Chunks and blocks of dynamic tables must be smaller than chunks of static tables. The default value is `{desired_chunk_size=100MB; block_size=256KB}`. ## Compaction features { #compaction } _This section describes the implementation details. If you want to use bulk insert for large amounts of data, we recommend reading it._ Sorted dynamic tables resemble an [LSM tree](https://ru.wikipedia.org/wiki/LSM-%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE). The used data structure works efficiently with a steady, albeit quite large, stream of writes. Written data is compacted in background mode and stacked in chunks in a predictable manner. When inserted from an operation, many new chunks are added to the table, at least one per job. These chunks are not aligned to the table structure, but "piled up" in an unpredictable manner. After a few operations, access to the data will become inefficient, so it must be repaired. There are two issues with compaction. First, resources are needed. A single node can compact at the speed of about 100 Mb/s, with not only fresh data but also old data being processed. Therefore, if there is only one node in the bundle, bulk insert for tens of gigabytes per minute is unlikely to work, because the structure will not have time to recover. Second, compaction heuristics are not perfect. If there are a lot of very small chunks in the table, the algorithm will become less efficient. The same happens if there is a very large chunk with a wide range of keys in the table. In the worst case, the table will get stuck and can only be repaired manually. Bulk insert can lead to a number of unusual scenarios. We have prepared the proper heuristics for many of them, but we have hardly taken everything into account. Therefore, a test run on real volumes is an important next step. # Versioned format for dynamic table interaction Sorted dynamic tables store a timestamp for each written column value. These timestamps can be useful for implementing custom logic or data migrations where you need to preserve original timestamps for correct TTL-based data cleanup operations. The versioned interaction format incurs virtually no overhead costs, except when the column itself isn't requested but its timestamp is. In this case, the column data will be read. For all subsequent interaction types, it is assumed that the timestamp of the `value` column is denoted as `$timestamp:value`. ## Versioned lookup and select C++ and Python APIs support versioned lookup and select. To add columns with timestamps to the output, specify `versioned_read_options = {read_mode = latest_timestamp}`. In select queries, additionally escape column timestamps with square brackets: `[$timestamp:value]`. In the Python API, you can specify the `with_timestamps` option instead of `versioned_read_options`. ### Examples ```python yt.lookup_rows("//path/to/table", [{"key": 123}], with_timestamps=True) # Read all columns and their timestamps. yt.select_rows("* from [//path/to/table]", with_timestamps=True) # Read select columns and timestamps. yt.select_rows("col_a, [$timestamp:col_a] as ts_a, [$timestamp:col_b] as ts_b from [//path/to/table]", with_timestamps=True) # From CLI. yt select-rows 'col_a, [$timestamp:col_a] as ts_a, [$timestamp:col_b] as ts_b from [//path/to/table]' --params '{versioned_read_options = {read_mode = latest_timestamp}}' --format='yson' ``` ## Versioned map-reduce ### Reading To read timestamp columns in map-reduce operations, add the following attribute to [rich YPath](https://ytsaurus.tech/docs/storage/ypath#rich_ypath) of the input table: ``. ### Writing To write data in map-reduce operations in versioned format, add the following attribute to [rich YPath](https://ytsaurus.tech/docs/storage/ypath#rich_ypath) of the output table: ``. Timestamp will be written for all columns where it is specified. For columns without a specified timestamp, the commit timestamp of the bulk insert transaction will be written instead. {% note warning "Attention" %} The versioned writing format has a few limitations: - Sort operations don't support writing. - During the write process, there is no check for potential duplicate timestamps across different records in the same column for the same key. {% endnote %} # Query language This section describes an SQL language dialect that enables you to read and transform data on the system side. ## Overview This feature is supported only for dynamic tables. {% note info "Note" %} An SQL language dialect is not a full-fledged implementation of the SQL language. This query language should be considered an extended data-reading operation, allowing some of the data processing work to be done on the server side. {% endnote %} {% note warning "Attention!" %} The feature is not a replacement of MapReduce operations. The query execution engine has no scheduler, and specifying a heavy query with full scan can lead to cluster degradation. The YTsaurus service team is working to improve the reliability of processing large queries, but there are no plans to develop the described SQL language dialect as a replacement for MapReduce. {% endnote %} ## Examples Reading from the table: ```bash yt select-rows "* FROM [//mytable] LIMIT 10" --format json ``` Counting the number of rows: ```bash yt select-rows "SUM(1) FROM [//mytable] GROUP BY 1" --format json ``` Queries with grouping: ```bash yt select-rows "day, MIN(price), MAX(price) FROM [//mytable] WHERE is_ok = 1 GROUP BY timestamp / 86400 AS day" --format json yt select-rows "item_id FROM [//mytable] WHERE (user_id, order_id) IN ((1, 1), (1, 2), (10, 10), (10, 11))" --format json ``` ## Query syntax ```sql * | [AS ], [AS ], ... FROM [//path/to/table] [WITH INDEX [//path/to/index/table] [AS] ] [JOIN [//path/to/rhs1] USING , , ...] [WHERE ] [GROUP BY [AS ], [AS | HAVING WITH TOTALS | WITH TOTALS | HAVING ]] [ORDER BY [ASC|DESC], [ASC|DESC], ...] [OFFSET ] [LIMIT ] ``` In general, the row order in the query output is not defined; however such guarantees are provided for [ORDER BY](https://ytsaurus.tech/docs/en/#order_by) and [LIMIT](https://ytsaurus.tech/docs/en/#limit). ### SELECT section { #select } SELECT syntax: ```sql * | [AS ], [AS ], ... ``` Defines the query output columns. If there's an asterisk (`*`), all input columns of the main table will be used as output columns. Alternatively, you can specify random expressions for the calculation. For queries without grouping, all columns of the source tables can be in the expression. In case of queries with grouping, only the key columns used for grouping or aggregates can be in the expression. ### FROM section ... JOIN { #from_join } FROM syntax ... JOIN: ```sql FROM [//path/to/table] [[AS] ] [WITH INDEX [//path/to/index/table] [AS] ] [[LEFT] JOIN [//path/to/dict-1] [[AS] ] (ON = | USING , , ...) [AND ]] ... ``` Defines the query data sources. The table immediately following FROM is considered the primary table and is used when coordinating query execution (provided the query does not contain the WITH INDEX keyword). It is also allowable to specify auxiliary reference tables in the JOIN section. The reference tables are connected to the main table if the specified key matches exactly. Without specifying the LEFT keyword, INNER JOIN will be executed. You can additionally specify an expression for reference table filtering which will be executed when reading data before join with the main table is executed. When the query contains the WITH INDEX keyword, the index table will be used for coordination, and the index table following FROM will be connected to it by common columns. See also [secondary indexes](https://ytsaurus.tech/docs/en/secondary-indices). To combine tables across multiple columns, a tuple must be written in the ON condition. ```sql FROM [//x] AS x JOIN [//y] AS y ON (x.a, x.b) = (y.a, y.b) ``` It is allowed to write expressions in the JOIN condition: ```sql ON (if(is_null(x.a), 0, x.a), x.b) = (y.a, y.b) ``` For more information about JOINS, their features, and limitations, see the [JOINS](https://ytsaurus.tech/docs/en/#joins) section. ### WHERE section { #where } WHERE syntax: ```sql WHERE ``` Defines the primary data filter. ### GROUP BY section { #group_by } GROUP BY syntax: ```sql GROUP BY [AS ], [AS ``` Defines the filtering condition after grouping. ### WITH TOTALS modifier { #with_totals } Used in conjunction with grouping and adds an extra row to the result, with `NULL` values in the grouping keys and total aggregates by grouped data in the values. The row added by the summary aggregate is not processed in ORDER BY ... LIMIT. When the HAVING section is used, the final aggregates can be counted both before and after filtering. The order in which the rows with the resulting aggregates are arranged in the resulting row set is unspecified. The added row can be either the first or the last one. It can be distinguished by a `NULL` key. WITH TOTALS syntax: ```sql GROUP BY ... WITH TOTALS GROUP BY ... WITH TOTALS HAVING ... GROUP BY ... HAVING ... WITH TOTALS ``` ### ORDER BY section { #order_by } ORDER BY syntax: ```sql ORDER BY [ASC|DESC], [ASC|DESC], ... ``` Defines the ordering of the output row set by the specified expressions. The default sorting order is ascending (ASC). The DESC keyword is specified for descending sorting. Sorting is applied after grouping, so only the key grouping columns or the counted aggregates can be in the expressions. The sorting is partial, i.e. only N smallest rows are returned as a result (the N parameter is specified in the LIMIT section). ### OFFSET section { #offset } OFFSET syntax: ```sql OFFSET ``` Specifies the number of skipped rows from the output set of rows. There can be no effective implementation of OFFSET in the query language under the current data storage schema. The OFFSET N LIMIT M query will read N + M rows. To implement this effectively, there must be an index from the row number to the key. The proper way to perform paginated output is to make LIMIT queries, but instead of using OFFSET, remember the last key values ("last_key = (key_column1, key_column2, ...)") of the current query result and send the next query with the condition "where (key_column1, key_column2, ...) > last_key". ### LIMIT section { #limit } LIMIT syntax ```sql LIMIT ``` Defines the size of the output row set. If there is an ORDER BY section, it sets a limit on the size of the heap. Otherwise, it sets a limit on the order and amount of scanned data. If the query has a LIMIT section, the data is scanned sequentially by the primary key until the target size is reached. In this case, the order of output rows is defined unambiguously. {% note info "Note" %} When using grouping with a limit (without ORDER BY), the data will be read in the order of the primary table key. If there are no aggregate columns, the query will end once the required set of grouped rows is accumulated. If there are aggregate columns, extra grouping will be performed for the keys accumulated in the number specified in LIMIT. When using LIMIT without ORDER BY, please note that LIMIT will be applied to the previous operator. For example, if you use GROUP BY ... WITH TOTALS and LIMIT, then the final aggregates will be calculated from the data that fits within the limit. Data above the limit will not even be read. If ORDER BY is added, LIMIT will affect it and GROUP BY will process all read data. Respectively, TOTALS will be calculated from the larger data set. {% endnote %} #### Queries with pagination If you need to sequentially read a bigger part of the table, use pagination. Do not use queries with an ascending `OFFSET` for pagination — this results in a quadratic table scan algorithm. Queries with pagination should look like this: ```sql select k0, k1, k2 from `//t` where (k0, k1, k2) >= (1, 2, 3) order by k0, k1, k2 limit 10000 ``` Here, `k0, k1, k2` is the primary table key. The tuple `(1, 2, 3)` is used as a cursor, and you need to update its value. Use `LIMIT` to adjust the read volume. ## Subqueries { #subqueries } ### Subquery in the FROM section You can specify a subquery in the `FROM` section. In this case, the inner query will be executed in a distributed manner, and the outer query will be executed on the proxy. Syntax: ```sql FROM () ``` Example: ```sql SELECT b AS c FROM (SELECT a AS b FROM `//t`) ``` ### Subquery for hierarchical data You can run a subquery on list-type data. This can be useful if you need to filter a list or transform it in some way. To do this, in the `FROM` section of the subquery, you need to specify a list-type column and an alias through which will be provided access to the list elements. The column specification must be enclosed in parentheses. This is necessary to resolve the syntactic ambiguity between a query on a table and a query on a list-type column. You can also run a query on a set of lists. To do this, you need to list the list-type columns and aliases for them, separated by commas. In the case of a set of lists, the lists will be merged using the zip operation. The lists must have the same size. To use this functionality, you need to specify the `expression_builder_version=2` query option. Syntax of the `FROM` section of the subquery: ```sql FROM ( AS , AS , ...) ``` #### Examples Data: ```yson a=1; b=[1;2;3]; c=[x;y;z] ``` Query: ```sql SELECT t.a AS a, (SELECT b, c FROM (t.b AS b, t.c AS c) WHERE b > 1) AS nested FROM `//t` AS t ``` Result: ```yson a=1; nested=[[2;y];[3;z]] ``` Lists of data can occur during aggregation in an outer query. The `array_agg(value, ignore_nulls)` aggregate function is used to aggregate data into a list. In a subquery about hierarchical data, you can use columns, aliases, and expressions from the outer query. ```sql SELECT t.k + 1 AS a, (SELECT li * sum(t.s) AS x, li + a AS y, ls AS z FROM (array_agg(t.a, true) AS li, array_agg(t.b, true) AS ls) WHERE li < 4) AS nested FROM `//t` AS t GROUP BY a ``` In this example, in the expression `li + a`, the identifier `a` is expanded to the expression `t.k + 1` from the outer query. In a nested query, you can use aggregate functions on data from the grouping of the outer query — the expression `sum(t.s)`. Grouping can be used in both the outer and inner queries: ```sql SELECT t.k % 2 AS a, (SELECT ls, sum(t.s) AS x, sum(li) AS y, sum(1) AS z FROM (array_agg(t.a, true) AS li, array_agg(t.b, true) AS ls) GROUP BY ls) AS nested FROM `//t` AS t GROUP BY a ``` The expression `sum(t.s)` will refer to the outer grouping, since the `t.s` column is not a grouping key and must be used under an aggregate function. ```sql SELECT t.k % 2 AS a, (SELECT li + sum(t.s) AS x, sum(1) AS z FROM (array_agg(t.a, true) AS li) GROUP BY x) AS nested FROM `//t` AS t GROUP BY a ``` An expression from the inner query column and the aggregate expression of the outer query `ls + sum(t.s) AS x` is used as the grouping key of the inner query. ```sql SELECT t.k % 2 AS a, sum(2) AS b, (SELECT b, sum(1) AS z FROM (array_agg(t.a, true) AS li) GROUP BY li) AS nested FROM `//t` AS t GROUP BY a ``` In the inner query, the identifier `b` is expanded to use the aggregate expression `sum(2)` from the outer grouping. ## Expression syntax { #expression_syntax } ```sql ``` Arithmetic operators: ```sql + - * / % ``` Bitwise operators: ```sql | & >> << ``` Logical operators: ```sql AND OR NOT ``` Comparison operators: ```sql = != < <= > >= BETWEEN AND BETWEEN ( AND , ...) ``` IN operator: ```sql IN (, ...) ``` Functions: ```sql FN(, ...) ``` Priorities: 1. `unary + - ~` 2. `binary * / %` 3. `binary + -` 4. `binary >> <<` 5. `binary &` 6. `binary |` 7. `binary < <= > >= IN BETWEEN` 8. `binary =` 9. `unary NOT` 10. `binary AND` 11. `binary OR` Each expression has its type: `int64`, `uint64`, `boolean`, `double`, `string`, or `any`. The typification of expressions is strict. If a typification error occurs, query execution stops and an error is returned to the user. Arithmetic operators take numeric types (`int64`, `uint64`, `double`) as input and return a value of the same type. Logical operators take and return values of the `boolean` type. Comparison operators, IN and BETWEEN operators take values of the same type as input and return values of the `boolean` type. They also allow the use of tuples. Tuple usage example: ```sql (a, b) >= (valA, valB) (a, b) IN ((valA0, valB0), (valA1, valB1), ...) (a, b) BETWEEN ((16202224, 1) AND (16202224, 3), (2011740432, 6) AND (2011740432), (591141536) AND (591141536, 2), ...) ``` Functions in expressions can be of two types: scalar applied to a tuple of values and aggregate applied to a group of input data corresponding to a single key. Scalar functions can be used in any expression. Aggregation functions can only be used in the SELECT section if there is a GROUP BY section. ### IDs { #identifiers } Column names containing special characters must be wrapped in square brackets. **Examples**: `t.[a.b], t.[c-1], t.[[d]] from [//path] as t` where `a.b`, `c-1`, and `[d]` are column names. In the resulting schema, the square brackets in the column names will be omitted wherever possible. For the `[my.column-1], [my+column-2] from ...` query, the schema will be `"my.column-1", "my+column-2"`. If expressions without an alias (`AS name`) are specified in the query, and the columns contain special characters, square brackets will be added. For the `[my.column-1] + x, [my+column-2] from ...` query, the schema will be `"[my.column-1] + x", "my+column-2"`. ### Synonyms { #synonyms } Expressions can be named using the AS keyword. Synonym support is implemented similarly to [ClickHouse](https://clickhouse.yandex). Unlike standard SQL, synonyms can be declared not only at the top level of expressions: `((1 as x) + x) as y`. In addition, synonyms can be used in all query sections. **For example**: `a from [...] group by x % 2 as a`. This behavior is compatible with standard SQL as long as no synonyms are used that overlap the column names of the source tables. The table shows an example of using synonyms for a table with the `UpdateTime` column. Synonym usage example: | Expression | YTsaurus | In standard SQL | | --------- | ---- | ----------------- | | `UpdateTime / 1000 as UpdateTime, UpdateTime as b` | As a result, UpdateTime = b | As a result, UpdateTime != b | | `UpdateTime / 3600 % 24 as Hour from ... group by Hour` | Grouping will be by `UpdateTime / 3600 % 24` | Grouping will be by `UpdateTime / 3600 % 24` | | `UpdateTime / 3600 % 24 as Hour where Hour = ...` | Filtering will be by `UpdateTime / 3600 % 24` | Filtering will be by `UpdateTime / 3600 % 24` | | `UpdateTime / 1000 as UpdateTime where in (...)` | Filtering will be by `UpdateTime / 1000` | Filtering will be by the `UpdateTime` source field | | `UpdateTime / 1000 as UpdateTime from ... group by UpdateTime` | Grouping will be by `UpdateTime / 1000` | Grouping will be by the `UpdateTime` source field | | `UpdateTime from ... group by UpdateTime / 1000 as UpdateTime` | Grouping will be by `UpdateTime / 1000` | AS is not supported within group by | | `max(UpdateTime) as UpdateTime where UpdateTime between ...` | There will be an error because where cannot use an aggregate | Filtering will be by the `UpdateTime` source field | ### Literals { #literals } Expressions may contain literals that encode typified values. To set numbers with a sign, decimal numbers are used (for example, `100,500` or `42`). To set numbers without a sign, decimal numbers with the `u` suffix are used (for example, `100,500u`, `42u`). Floating point numbers use a C-like decimal notation (for example, `3.1415926`) with an integer and fractional part separator. C-like string literals (like `"hello"` or `"world"`) are used to specify strings. ### Scalar functions {% note info "Note" %} Some functions are polymorphic: they can take values of several types. The signatures of such functions are described either using type variables (denoted by capital letters: `A`, `B`, ...), or using an explicit enumeration of taken types (denoted: `int64 | uint64`). {% endnote %} #### General functions `if(cond, then, else) :: boolean -> A -> A -> A` Conditional operator. If the `cond` expression is true, the function result is the `then` value. Otherwise, it is `else`. `is_null(x) :: A -> boolean` Checks whether the specified value is `NULL`. `is_finite(x) :: double -> boolean` Checks whether the specified value is a finite floating point number (i.e. it isn't infinite or `NaN`). `is_nan(x) :: double -> boolean` Checks whether the specified value is `NaN`. `transform(a, (a1, a2, ...), (b1, b2, ...)) :: A -> List[A] -> List[B] -> B` `transform[a2), ((a11, a12), ...), (v1, ...](https://ytsaurus.tech/docs/en/a1,) :: Tuple -> List[Tuple] -> List[B] -> B` Convert a value (or tuple) according to an explicit display of some elements on other elements. If there is no corresponding value, `NULL` is returned. The complexity is constant. `greatest(a1, a2, ...) :: A* -> A` Returns the argument with the greatest value. #### Mathematical functions (available starting with version 25.2) `abs(x) :: (int64 | uint64 | double) -> (int64 | uint64 | double)` Returns the absolute value of a number `x`. The returned value type matches the argument type. `acos(x) :: double -> double` Returns the arccosine of a number `x` (the argument must be in radians). `asin(x) :: double -> double` Returns the arcsine of a number `x` (the argument must be in radians). `ceil(x) :: double -> int64` Rounds a number `x` up to the nearest integer. `cbrt(x) :: double -> double` Calculates the cube root of a number `x`. `cos(x) :: double -> double` Calculates the cosine of a number `x` (the argument must be in radians). `cot(x) :: double -> double` Calculates the cotangent of a number `x` (the argument must be in radians). `degrees(x) :: double -> double` Converts an angle `x` from radians to degrees. `even(x) :: double -> int64` Rounds a number `x` to the nearest even number. `exp(x) :: double -> double` Calculates the exponential of a number `x` (ex). `floor(x) :: double -> int64` Rounds a number `x` down to the nearest integer. `gamma(x) :: double -> double` Calculates the gamma function for a number `x`. `is_inf(x) :: double -> boolean` Checks whether a number `x` is infinite (`+∞` or `-∞`). `lgamma(x) :: double -> double` Calculates the logarithm of the gamma function for `x`. `ln(x) :: double -> double` Calculates the natural logarithm (to the base of the mathematical constant e) of a number `x`. `log(x) :: double -> double` Calculates the base-10 logarithm of a number `x`. `log10(x) :: double -> double` Calculates the base-10 logarithm of a number `x`. `log2(x) :: double -> double` Calculates the binary logarithm (to the base 2) of a number `x`. `radians(x) :: double -> double` Converts an angle `x` from degrees to radians. `sign(x) :: double -> int64` Determines the sign of a number `x`. Returns 1 if `x > 0`, returns -1 if `x < 0`, and otherwise returns 0. `signbit(x) :: double -> boolean` Determines whether the signbit of a number `x` is set. `sin(x) :: double -> double` Calculates the sine of a number `x` (the argument must be in radians). `sqrt(x) :: double -> double` Calculates the square root of a number `x`. `tan(x) :: double -> double` Calculates the tangent of a number `x` (the argument must be in radians). `trunc(x) :: double -> int64` Drops the fractional part of a number `x` and returns its integer part. `bit_count(x) :: uint64 -> int64` Returns the number of bits set to one in a number `x`. `atan2(y, x) :: double -> double -> double` Calculates the angle (in radians) between the positive X-axis direction and a point represented by the coordinates (`x`, `y`). `factorial(x) :: uint64 -> uint64` Calculates the factorial of a number `x`. `gcd(a, b) :: uint64 -> uint64 -> uint64` Calculates the greatest common divisor (GCD) of numbers `a` and `b`. `lcm(a, b) :: uint64 -> uint64 -> uint64` Calculates the least common multiple (LCM) of numbers `a` and `b`. `pow(x, y) :: double -> double -> double` Calculates the value of `x` raised to the power of `y`. `round(x) :: double -> int64` Rounds a number `x` to the nearest integer. `xor(a, b) :: uint64 -> uint64 -> uint64` Performs bitwise exclusion on numbers `a` and `b` using the bitwise exclusive OR (XOR) operator. #### Working with strings `length(s) :: string -> int64` Returns the `s` string length in bytes. `is_substr(s, t) :: string -> string -> boolean` Checks whether the `s` string is the `t` substring. `is_prefix(p, t) :: string -> string -> boolean` Checks whether the `p` string is the `t` prefix. `lower(s) :: string -> string` Converts the `s` string to the lower case. `concat(lhs, rhs) :: string -> string -> string` Concatenates two strings. #### Accessing containers If a table column has the `Composite` data type, you can use the following syntax to access that column's fields: - To access `struct` fields, use a dot. - To access `tuple` fields, use a dot. Indexing is **zero-based**. - To access `dict` fields, use square brackets. - To access `list` fields, use square brackets. Indexing is **zero-based**. The correctness of accessing struct and tuple fields is checked **before starting** the query's execution. The correctness of accessing map and list fields is checked **during** the query's execution. To enable this syntax, make sure to set `syntax_version=2` in the query parameters. Starting with this version of the syntax, strings are escaped with `backticks`. To access containers, you need to specify the full column name, including the table alias. Usage examples: ```sql t.struct.member, t.tuple.0, t.dict["key"], t.list[1+1] from `//tmp/test` as t; ``` ```bash $ yt create table '//tmp/test' --attributes '{dynamic=true; schema=<"unique_keys"=%true;"strict"=%true;>[{name=a;sort_order=ascending;type=int64;};{name=b;type_v3={type_name=struct;members=[{name=c;type={type_name=list;item=int64}}]}}]}' $ yt mount-table --path '//tmp/test' $ echo '{a=0;b={c=[]}}' | yt insert-rows --table '//tmp/test' --format yson $ echo '{a=1;b={c=[1;2;3]}}' | yt insert-rows --table '//tmp/test' --format yson $ echo '{a=2;b={c=[4]}}' | yt insert-rows --table '//tmp/test' --format yson $ yt select-rows 't.b.c from `//tmp/test` as t' --syntax-version 2 --format json {"t.b.c":[]} {"t.b.c":[1,2,3]} {"t.b.c":[4]} $ yt select-rows 't.b.c[0] from `//tmp/test` as t' --syntax-version 2 --format json {"t.b.c[0]":null} {"t.b.c[0]":1} {"t.b.c[0]":4} ``` #### Working with [YSON](https://ytsaurus.tech/docs/storage/formats.md#yson) {% note warning "Attention!" %} A fixed key order in dictionaries is not guaranteed, so comparisons and groupings may not behave as expected. {% endnote %} ##### Extracting data The query language provides a set of functions for extracting data: 1. `try_get_int64`, `get_int64`, `try_get_uint64`, `get_uint64`, `try_get_double`, `get_double`, `try_get_boolean`, `get_boolean`, `try_get_string`, `get_string`, `try_get_any`, `get_any` These functions take two arguments, `(yson, path)`, where: - `yson` is a value of the `any` type that contains YSON. - `path` is a string representing the path to the desired field in [YPATH](https://ytsaurus.tech/docs/storage/ypath.md) format. The version with the `try_` prefix returns `NULL` if there is no field of the desired type at the specified path. The version without `try_` returns an error if the field is missing. Example: For the table row `{column={key3=2;k={k2=3;k3=10};lst=[0;1;2]}}` `try_get_uint64(column, "/lst/@a/2/b")` returns `7u`. 2. `list_contains(list, value) :: any -> (string | int64 | uint64 | boolean) -> boolean` Searches for `value` in the `list` YSON list of the `any` type. The searched `value` must have a scalar type. The list does not have to be homogeneous, meaning that it can contain values of different types. The comparison is type-sensitive. 3. `list_has_intersection(list, list) :: any -> any -> boolean` Takes two YSON lists as input and returns `true` if they contain at least one common element. The lists must be homogeneous. This function is supported on clusters version 24.2 or higher. 4. `any_to_yson_string(yson) :: any -> string` Converts a value of the `any` type into a string containing its text [YSON](https://ytsaurus.tech/docs/storage/yson.md) representation. 5. `yson_length(yson) :: any -> int64` Calculates the number of elements in a list or map. ##### Generating YSON 1. `make_entity()`. Example: `make_entity() -> #` 2. `make_map(args...)`. Example: `make_map("a", 1, "b", 2) -> {a=1;b=2}` 3. `make_list(args...)`. Example: `make_list("a", 1, 2, 3) -> [a;1;2;3]` #### Working with regular expressions The [Google RE2](https://github.com/google/re2) library is used to work with regular expressions. The library operates in UTF-8 mode. The syntax of regular expressions is described on a separate [page](https://github.com/google/re2/wiki/Syntax). `regex_full_match(p, s) :: string -> string -> boolean` Checks that the `s` string matches the `p` regular expression. `regex_partial_match(p, s) :: string -> string -> boolean` Checks that the `s` string contains a substring that matches the `p` regular expression. `regex_replace_first(p, s, r) :: string -> string -> string -> string` Replaces the first occurrence of the `p` regular expression in the `s` string with the `r` string. `regex_replace_all(p, s, r) :: string -> string -> string -> string` Replaces all occurrences of the `p` regular expression in the `s` string with the `r` string. `regex_extract(p, s, r) :: string -> string -> string -> string` Enables the substrings that match the `s` regular expression to be extracted from the `s` string and replaced `r`. For example, the result of the `regex_extract("([a-z]*)@(.*)", "email foo@bar.com", "\\1 at \\2")` expression will be `foo at bar.com`. `regex_escape(p) :: string -> string` Shields special characters of the regular expression language in the `p` string. ##### LIKE operator The operator can be used as follows: ```sql [not] like [escape ] ``` The operator checks that the `string` string matches the `pattern` template. The underscore (`_`) in the template matches any character. The percent sign (`%`) matches any sequence of characters (including an empty sequence). To literally use `_` or `%` in the template, you need to escape them using `escape-symbol` (`\` by default). If there is filtering by the primary key's string prefix in the query, `is_prefix` should be used to make the range output work. The `ilike` construction is used for case-insensitive search: `'Abc' ilike 'abc'`. In `rlike` and `regex` constructions, the `pattern` template is set in [re2 syntax](https://github.com/google/re2/wiki/Syntax). Example: ```sql 'abcdef' like 'a_c_%f' 'q' not like 'x_' escape 'x' 'Abc' ilike 'abc' 'a b' rlike '\\w+ b' ``` #### Working with dates If the function does not have the _tz suffix, the timezone used in rounding defaults to UTC. Otherwise, the second argument should be the IANA timezone name. `timestamp_floor_year[_tz](https://ytsaurus.tech/docs/en/t[,%20tz]) :: int64 [-> string] -> int64` Get the timestamp of the year (as of 0:00 on January 1) for the specified timestamp. `timestamp_floor_quarter[_tz](https://ytsaurus.tech/docs/en/t[,%20tz]) :: int64 [-> string] -> int64` Get the timestamp of the quarter (as of 0:00 on January 1, April 1, July 1, or October 1) for the specified timestamp. `timestamp_floor_month[_tz](https://ytsaurus.tech/docs/en/t[,%20tz]) :: int64 [-> string] -> int64` Get the timestamp of the month (as of 0:00 on the first day of the month) for the specified timestamp. `timestamp_floor_week[_tz](https://ytsaurus.tech/docs/en/t[,%20tz]) :: int64 [-> string] -> int64` Get the timestamp of the week (as of 0:00 on Monday) for the specified timestamp. `timestamp_floor_day[_tz](https://ytsaurus.tech/docs/en/t[,%20tz]) :: int64 [-> string] -> int64` Get the timestamp of the day (as of 0:00 at the start of the day) for the specified timestamp. `timestamp_floor_hour[_tz](https://ytsaurus.tech/docs/en/t[,%20tz]) :: int64 [-> string] -> int64` Get the timestamp of the hour (0 min 0 sec at the start of the hour) for the specified timestamp. `format_timestamp[_tz](https://ytsaurus.tech/docs/en/t[,%20tz],%20format) :: int64 [-> string] -> string -> string` Formats the time specified in the timestamp according to the provided format string. Uses the same convention as `std::strftime`. #### Hashing `farm_hash(a1, a2, ...) :: (int64 | uint64 | boolean | string)* -> uint64` Calculates [FarmHash](https://code.google.com/p/farmhash/) from the specified set of arguments. #### Converting types `int64(x) :: any | int64 | uint64 | double -> int64` `uint64(x) :: any | int64 | uint64 | double -> uint64` `double(x) :: any | int64 | uint64 | double -> double` `boolean(x) :: any -> boolean` `string(x) :: any -> string` Converts the `x` numeric argument to the target type. Rounding and overflow rules are standard and C-like. Starting with version 25.2, attempting to convert a floating-point number into an integer type will throw an error if that number is outside the valid range of that integer type. To convert a numeric type into a string, use this function: `numeric_to_string :: int64 | uint64 | double -> string` To convert a string into a number: `parse_int64 :: string -> int64` `parse_uint64 :: string -> uint64` `parse_double :: string -> double` Any type can be cast to `any`: `to_any :: int64 | uint64 | double | boolean | string | any -> any` `yson_string_to_any :: string -> any` Parses, validates, and converts a string value into a value of the `any` type. #### NULL value In most operators, using `NULL` values in operands results in a `NULL` value. Comparison operators have a different behavior. The result in them is always `boolean`. This is to ensure that the behavior of comparison operations is the same both when ordering data in the table by key and when calculating expressions within a query. The need to compare with `NULL` is due to the desire to make the order relation on the keys complete. `NULL` is considered less than the other values. ### Aggregation functions { #aggregation_functions } The following aggregation functions are supported in the query language: - `sum(expr) :: A -> A`: Calculates the sum. - `min/max(expr) :: A -> A`: Calculates the minimum or maximum. - `avg(expr) :: int64 -> double`: Calculates the average. - `cardinality(expr) :: int64 | uint64 | double | boolean | string -> uint64`: Calculates the number of different elements using the [HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog) algorithm. - `argmin/argmax(arg, expr) :: A -> int64 | uint64 | double | string | boolean -> A`: Calculates the argument value at which the expression reaches the corresponding extreme value. - `first(expr) :: A -> A`: Returns the first value of the group. During a sequential scan (when sorting by primary key), the function calculates the value of the first row in the group. If all aggregate functions in the query are `first`, the engine stops the sequential scan as soon as it reaches the `LIMIT` on unique group keys. For queries without a sequential scan, `first` returns the value of any matching row. - `xdelta(expr) :: string -> string`: Calculates the target value saved as a sequence of patches obtained through delta encoding using the [xdelta3](https://github.com/xorgy/xdelta3/blob/master/README) algorithm. ## Executing a query { #query_execution } ![](https://ytsaurus.tech/_images/query_execution.png) The query is executed on the cluster in a distributed way. The query execution process is conventionally divided into two phases: coordination and execution. During coordination, multiple cluster nodes are defined to execute the query, and during execution, the data is processed directly. A typical scenario involves a user-defined query being decomposed into multiple _execution fragments_ that are processed by cluster nodes. The final result is generated on the client by merging intermediate results received from cluster nodes. There is an [`explain_query`](https://ytsaurus.tech/docs/en/#explain_query) command to display information about the query plan. ### Coordination The process of splitting a query into multiple parallel execution fragments in order to distribute the load is called _coordination_. Query coordination occurs both on the client and on the cluster nodes. The coordination process itself consists of the following steps: 1. Defining the ranges of the source data. 2. Splitting the query to process parts of the input data independently. The input data for the query and its splitting options are determined by the structure of the main table specified in the query and the filtering predicate. Execution of queries on top of **[dynamic](https://ytsaurus.tech/docs/en/overview.md)** (sorted and ordered) tables is currently supported. When working with sorted dynamic tables, the data splitting is two-level: the table is initially split into _tablets_ and the query is sent to the machines serving the selected tablets, and then on each machine the tablet is further split into partitions to ensure concurrent execution within each machine. For more information about tablets, see [Dynamic tables](https://ytsaurus.tech/docs/en/overview.md). In the process of coordination, the cluster nodes that contain the data needed to execute the query are selected. The initial query is split into execution fragments and sent to the selected nodes. On each node, query execution can be additionally paralleled if a large enough amount of data is affected. Concurrency of query execution on a single node is limited by the `max_subqueries` query parameter and the number of threads allocated to query execution in the cluster node configuration. When the query is split for parallel execution, the calculation of predicates (`WHERE`), groupings (`GROUP BY`), and sortings (`ORDER BY`) shifts to occur as close as possible to the data read operation. Only a minimal number of essential operators remain on the coordinator. For example, if the splitting of the input data results in exactly one part, then the entire filtering and grouping will be performed by the subordinate node and the coordinator will do nothing with the data. ### Cutting off input data { #input_data_cut } During coordination, the (`WHERE`) filtering predicate is analyzed. Based on a set of key table columns and predicate conditions, the data read ranges are output. Range output can account for logical connectives (`AND`, `OR`, `NOT`), comparison operators, `IN` operator, `BETWEEN` operator, and `is_prefix` function. Once the ranges are output, some of the conditions from the predicate can be discarded if the conditions appear to always be true on the data read from the ranges. The output ranges are a set of lower and upper read limits. The read limit is the prefix of the main table key. If there are no computed columns in the table schema, the ranges are output as follows. The key columns are considered in their order in the schema as long as there are upper-level conditions in the predicate (connected by the `AND`, `OR`, and `NOT` logical operators) that limit the values of these columns. Column conditions can be point (`=` operator, `IN` operator) and range ( `!=`, `<`, `<=`, `>`, `>=`, `is_prefix`). The resulting range is built as long as the point conditions are met and ends at the first range condition. **Examples:** Let the table have key columns: `a, b, c`. - For the `a = 1 and b in (3, 4)` predicate, the following ranges will be output: ``` [[1, 3] .. [1, 3]], [[1, 4] .. [1, 4]] ``` - For the `a = 1 and b between 20 and 30 and c between 40 and 60` predicate, the following range will be output: ``` [[1, 20] .. [1, 30]] ``` - For the `a = 1 and b = 2 and c between 40 and 60` predicate, the following range will be output: ``` [[1, 2, 40] .. [1, 2, 60]] ``` - For the `a in (1, 2, 3) and b in (5, 6) and c between 40 and 60` predicate, the following ranges will be output: ``` [[1, 5, 40] .. [1, 5, 60]], [[1, 6, 40] .. [1, 6, 60]], [[2, 5, 40] .. [2, 5, 60]], [[2, 6, 40] .. [2, 6, 60]], [[3, 5, 40] .. [3, 5, 60]], [[3, 6, 40] .. [3, 6, 60]] ``` - For the `a = 1 and b = 2 and c between 40 and 60 or a between 10 and 20` predicate, the following ranges will be output: ``` [[1, 2, 40] .. [1, 2, 60]], [[10] .. [20]] ``` An important feature of the output ranges is that the lower and upper limits have a _common prefix_ and differ only by the last component. If there are computed columns in the table schema, the ranges are first output without the computed columns and then are either supplemented with computed columns or trimmed to the first computed column that cannot be computed. For each range, the computed column can be computed when placing in the expression only the columns belonging to the common prefix of the lower and upper limit. **Examples:** Let the table have key columns: `h = hash(a, b), a, b, c`. - For the `a = 1 and b in (3, 4)` predicate, the following ranges will be output: ``` [[hash(1, 3), 1, 3] .. [hash(1, 3), 1, 3]], [[hash(1, 4), 1, 4] .. [hash(1, 4), 1, 4]] ``` - For the `a = in (1, 2)` predicate, the range cannot be output, because the computed column depends not only on the `a` column, but also on the `b` column. - Special consideration is given to the case when the computed column is an expression taken by the module: `h = hash(a, b) % 3, a, b, c`. In that case, there is no need to compute the column value, because the set of values is already limited by the module. When the ranges are output, all possible values of the computed column with the expression taken by the module are considered. For the `a = in (1, 2)` predicate, the following ranges will be output: ``` [[0, 1] .. [0, 1]], [[0, 2] .. [0, 2]], [[1, 1] .. [1, 1]], [[1, 2] .. [1, 2]], [[2, 1] .. [2, 1]], [[2, 2] .. [2, 2]] ``` - For the `a between 10 and 20` predicate: ``` [[0, 10] .. [0, 20]], [[1, 10] .. [1, 20]], [[2, 10] .. [2, 20]] ``` The number of output ranges is limited by the `range_expansion_limit` option. For efficient query execution, it is important to use the information from the primary table key as much as possible in order to cut off as much data as possible when coordinating the query. If the ranges could not be output, the table is read as a whole. Such reading is called _full scan_. A rather large table (triple digit gigabytes) cannot be read within the time limit (by default, `query_timeout` is one minute) allocated for query execution. To prohibit _full scan_, you can set the `allow_full_scan` query option to `false`. ### JOINS { #joins } You can also specify additional reference tables in the query to compute the final response or for additional filtering. **Only lookup joins** are currently supported, i.e. a JOIN execution schema where the master data handler explicitly queries the additional table by key. In the query itself, this means that the condition for JOIN must contain such a set of key columns of the reference table that the ranges for reading the data can be derived from the limit on these columns. If JOIN cannot be executed using the reference table index, the "Foreign table key is not used in the join clause" error will be returned. To disable such behavior, use the `allow_join_without_index` parameter. In general, JOIN is executed as follows. On each cluster node, the data is read from the main table shard, accumulated in memory, and multiple JOIN keys for the reference table are computed. This is followed by a query to the reference table with the read ranges and optional predicate obtained based on the JOIN keys. Then JOIN is executed in memory, and execution continues as usual. When using multiple JOINs in a query, it would take a lot of time to execute them consecutively. Individual JOINs are therefore grouped into sets that can be executed concurrently. So when a JOIN group is executed while reading data from the main table, a set of JOIN keys is built for each reference in the group. Next, queries to read data from references are sent concurrently, and, once the data is received, JOIN is executed, forming the resulting rows. The complete resulting set of rows is not materialized in memory, but as JOIN is executed, the rows are passed to the next operator (for example, grouping). JOIN is executed in a special way when the master table and the reference table share a common key prefix to within a JOIN condition. In that case, the ranges for reading the reference table are already known before reading the main table during the coordination phase of the query. Therefore, the query to read the reference table is sent immediately after coordination. ## Debugging a query { #query_debug } ### Execution statistics { #execution_stat } You can specify the `--print-statistics` option in the CLI. The statistics will be output as follows: ```yson { "incomplete_output" = %false; "async_time" = 20458u; "memory_usage" = 0u; "inner_statistics" = [ { "incomplete_output" = %false; "async_time" = 12098u; "memory_usage" = 0u; "inner_statistics" = [ { "incomplete_output" = %false; "async_time" = 4547u; "memory_usage" = 0u; "sync_time" = 23u; "codegen_time" = 8u; "rows_read" = 2164; "execute_time" = 0u; "read_time" = 13u; "incomplete_input" = %false; "write_time" = 0u; "rows_written" = 2164; "data_weight_read" = 77352; }; ... ]; "codegen_time" = 5u; "sync_time" = 13u; "rows_read" = 26908; "execute_time" = 5u; "read_time" = 0u; "incomplete_input" = %false; "write_time" = 0u; "rows_written" = 26908; "data_weight_read" = 1176824; }; ... ]; "codegen_time" = 11u; "sync_time" = 64u; "rows_read" = 188520; "execute_time" = 16u; "read_time" = 27u; "incomplete_input" = %false; "write_time" = 8u; "rows_written" = 188520; "data_weight_read" = 8245048; } ``` The statistics have a hierarchical structure and correspond to the query execution structure. - The first level is aggregation on the coordinator (http or rpc proxy). - The second level is aggregation on nodes. - The third level is node-level execution of a part of the subquery to the thread pool. - `rows_read`: Number of rows read. - `data_weight_read`: The amount of read data in bytes in uncompressed form. - `codegen_time`: Code generation time. After the first code generation, the query template (query without literals) is cached. - `execute_time`: Query execution time excluding read time (decoding data from the chunk format). - `rows_read`: Number of rows read. - `read_time`: Synchronous read time. - `wait_on_ready_event_time`: Reader data waiting time. - `async_time`: Total waiting time. If it differs significantly from "wait_on_ready_event_time", this means that the query is waiting for the CPU to allocate time in the fair share scheduler. ### The explain-query command { #explain_query } You can debug a query using the "explain" command. The command outputs the result in a structured form. Example: ```yson { "udf_registry_path" = "//sys/udfs"; "query" = { "where_expression" = "((((oh.ff_status = ?) AND (o.fake_order_type = ?)) AND (NOT is_null(o.external_id))) AND (NOT is_null(oh.order_update_time))) AND (oh.id > ?)"; "is_ordered_scan" = %false; "joins" = [ [ { "lookup_type" = "source ranges"; "common_key_prefix" = 0u; "foreign_key_prefix" = 1u; }; ]; ]; "common_prefix_with_primary_key" = ...; "key_trie" = "(key0, { (0#0:0#] })"; "ranges" = ...; }; "subqueries" = { // Part of the query to be executed on the nodes. }; "top_query" = { // Part of the query to be executed on the proxy. }; } ``` - `where_expression`: Syntactic representation of the "where" condition from which read ranges are derived (`ranges`). - `is_ordered_scan`: Defines whether the order of the table rows will be retained when reading the data. This mode may be needed when implementing pagination. If you need to read the entire set of rows that meet the WHERE condition (no LIMIT), it is faster to read concurrently without preserving the order of rows in the table. - `joins`: Contains a list of join groups. Join within a group is executed concurrently for multiple tables. - `lookup_type`: The way of specifying read ranges when executing join. Possible options: source ranges, prefix ranges, and IN clause. The source ranges mode is the most optimal one. In prefix ranges mode, join keys are copied. This occurs if the join condition contains expressions or table fields that are not a secondary table key prefix. In IN clause mode, join is executed as a subquery with an IN expression. This mode is used if the read ranges of the secondary table cannot be derived from the join condition. If there is an additional AND condition in join, an attempt is made to output the read ranges using both the join condition and the additional AND condition. - `common_key_prefix`: The size of the common key prefix of the primary table and the secondary table. - `foreign_key_prefix` The size of the secondary table's key prefix that is derived from the join condition. - `common_prefix_with_primary_key`: The size of the table's primary key prefix contained in the grouping condition. This enables you not to make a common hash table for all keys (grouping keys with different prefixes cannot match). When using group by and order by concurrently, a non-zero common prefix with the primary key enables order by + limit computation to be transferred to the nodes and reduces the amount of data transferred to the proxy (coordinator). - `ranges`: A list of table read ranges obtained from the "where" condition. - `key_trie`: An intermediate representation of the key column limits generated from the "where" condition. The read ranges (`ranges`) are then derived from the key trie. - `top_query`, `subqueries`: Parts of the query running on different cluster nodes. To learn more, see [Executing a query](https://ytsaurus.tech/docs/en/#query_execution). ## Extensions and settings ### Query complexity limits (Options) Option in the driver configuration: `query_timeout` Options of individual queries: - `input_row_limit`: The limit on the number of rows that are read on one cluster node in one thread. - `output_row_limit`: The limit on the number of rows that are output on one cluster node in one thread. This value also limits the number of rows in grouping on one cluster node. - `range_expansion_limit`: The maximum number of ranges that will be output from the WHERE condition. - `fail_on_incomplete_result`: Defines the behavior in case of exceeding `input_row_limit` or `output_row_limit`. - `memory_limit_per_node`: The limit on the amount of memory available to a query on one cluster node. ### Named parameters You can use parameter placeholders in the query. Example: ```sql a from `//t` where b = {first} and (c, d) > {second} ``` The `first` and `second` parameters are set using a YSON map that can be specified in the `placeholder_values` query parameter: ```python client.select_rows( "a from `//t` where b = {first} and (c, d) > {second}", placeholder_values={'first': 1, 'second': [2, 'b']}, ) ``` ### Distributing resources between queries Queries are executed using a fair share CPU scheduler. Query scheduling is handled independently for each node. By default, an equal amount of CPU resources is allocated to all queries that are executed on the same node. This ensures that a node executing multiple heavy queries can perform a light query in a short and predictable amount of time. However, this does not protect you from scenarios where a cluster node gets overloaded with queries. In this case, the execution speed decreases proportionally to the number of concurrently executed queries. For example, a system user might initiate a large number of queries that will consume the bulk of the CPU. To address this scenario, the system supports a two-level fair share mechanism: besides distributing CPU resources between individual queries, it allows you to group queries together and distribute resources between these groups. You can group queries (for example, by user) by specifying a compute pool in the `execution_pool` query parameter. Queries that share the same `execution_pool` are grouped together. Each group is allocated its own share of resources. A group's resources are allocated between all individual queries within that group. By default, each pool is assigned a weight of 1. To assign a different weight value, create a node with the pool's name in the `//sys/ql_pools` directory, add the `@weight` attribute for this node, and specify the `use` permission for the group (user) in the ACL. By default, queries are executed in the `default` pool. # Secondary indexes In database theory, a secondary index is a data structure that allows for faster table sampling by non-key columns. An indexed tuple of non-key columns is called a *secondary key*. In YTsaurus, these structures are implemented for sorted dynamic tables as separate tables, hereafter referred to as index tables, and special objects linking the indexed table to the index one. In YTsaurus terminology, these objects are called secondary indexes. They do not have their own paths in Cypress, are immutable, and exist as long as both the indexed and index tables exist. Secondary indexes are also supported for replicated tables. ## Usage { #usage } By default, indexing is performed by a tuple of one or more non-key table columns, and there is a bijective correspondence between table rows — this behavior is set by the `kind=full_sync` secondary index attribute. The index table schema determines which data can be sampled faster with the use of the index. For efficient read range selection, the secondary key must be a prefix of the index table's key. Fast access to data in an indexed table is achieved by storing its full key (called a *primary* key) in the index table. Since rows of sorted dynamic tables must have unique keys, the index table key must contain the primary key, representing a concatenation of the secondary and primary keys (there is an [exception](https://ytsaurus.tech/docs/en/secondary-indices#unique) to this rule). An index table may contain one or more non-key columns of the indexed table as its own non-key columns. It may also contain no significant non-key values, but in this case you will have to add to the index table schema a dummy column `$empty` of the `int64` type, which will always have a null value. Example of creating tables and a linking secondary index: ```bash yt create table //path/to/table --attributes '{dynamic=true; schema=[{name=key; type=int64; sort_order=ascending}; {name=value; type=string}]}' yt create table //path/to/index_table --attributes '{dynamic=true; schema=[{name=value; type=string; sort_order=ascending}; {name=key; type=int64; sort_order=ascending}; {name="$empty"; type=int64}]}' yt create secondary_index --attributes '{table_path="//path/to/table"; index_table_path="//path/to/index/table"; kind=full_sync; table_to_index_correspondence=bijective}' ``` The `table_to_index_correspondence` mutable attribute specifies the relationship YTsaurus should assume between the rows of the indexed and index tables. It does NOT describe the actual relationship between table rows and can be changed manually by the user. Possible values for this attribute include `bijective`, `injective`, `invalid`, and `unknown`. Using an index with an `invalid` relationship is prohibited and will result in an error. `injective` indicates the presence of index table rows that do not correspond to any rows in the indexed table. For example, this can occur when creating tables without downtime. `bijective` indicates that the rows of the tables fully match. Both the `bijective` and `injective` relationships represent healthy table states. For information about their differences, read below. You may see the `unknown` relationship in indexes created by older YTsaurus versions. {% note warning "Note" %} Note that the column names and types in the tables are exactly the same. If the index table contains columns that are not in the indexed table, or their types do not match, a secondary index creation error occurs (except for the dummy column `$empty`, which is not required in the indexed table). {% endnote %} A secondary index can be created only if both tables are unmounted. Once the secondary index is created and the tables are mounted, *further* writes to the indexed table are automatically displayed in the index table (learn more about building an index table on top of the existing data [here](https://ytsaurus.tech/docs/en/secondary-indices#building)). You can also execute efficient select queries with filtering by secondary key using the `WITH INDEX` keyword. A query with an index runs efficiently if the predicate can be used to [infer](https://ytsaurus.tech/docs/en/dyn-query-language#input_data_cut) relevant read ranges from the index table. ```bash yt select-rows "key, value FROM [//path/to/table] WITH INDEX [//path/to/index/table] AS IndexTable where value BETWEEN 0 and 10" ``` When executing a query with this syntax, the system first reads the rows matching the `where value BETWEEN 0 and 10` predicate from the index table and then performs an inner join with the indexed table rows by common columns for indexes with an `injective` relation or by primary key for `bijective` indexes. Such a query is equivalent to one of the following: ```bash yt select-rows "key, value FROM [//path/to/injective/index/table] AS IndexTable JOIN [//path/to/table] ON (IndexTable.key, IndexTable.value) = (key, value) WHERE IndexTable.value BETWEEN 0 and 10" yt select-rows "key, value FROM [//path/to/bijective/index/table] AS IndexTable JOIN [//path/to/table] ON IndexTable.key = key WHERE IndexTable.value BETWEEN 0 and 10" ``` A current list of secondary indexes connected to the table is specified in the `secondary_indices` table attribute. A secondary index for which this particular table is an index one is specified in the `index_to` attribute. You can also exclude some rows from the index, create an index over a column with a list, and create a special index that enforces uniqueness of the secondary keys in the table. ### Partial indexing { #predicate } An index table may contain not all rows, but only those that satisfy a certain predicate. This predicate is set by the `predicate` secondary index attribute. It must be a valid boolean expression over the indexed table schema (the predicate can use columns that don't exist in the index table). The `predicate` attribute is compatible with any of the secondary index types. This can be helpful if the queries the index is useful for are related only to a particular isolated portion of the data. For example, the table schema is `[{name=id; sort_order=ascending; type=uint64}; {name=name; type=string}; {name=age; type=int64}; {name=loyalty; type=boolean}; ...]`, and in queries you perform sampling by age only for the rows with a non-null `loyalty` value. In this case, a secondary index with the `not is_null(loyalty)` predicate and an index table with the schema `[{name=age; type=int64; sort_order=ascending}; {name=id; sort_order=ascending; type=uint64}; {name=$empty, type=int64}]` is an optimal solution: the rows with `loyalty=#` will not appear in the index table. ### Index over a list { #unfolding } A `kind=unfolding` index unfolds rows by a column with a list of elementary type values. With this index, you can perform sampling of rows where the indexed list contains specific values. For example, the row `{id=0; child_ids=[1, 4, 7]}` will correspond to the rows `[{child_ids=1; id=0}; {child_ids=4; id=0}; {child_ids=7; id=0}]` in the index table. When creating such an index, specify the `unfolded_column` attribute, which represents the name of a column of type `T` in the index table and a column of type `List` or `Optional>` in the indexed table. In select queries, you can set a filter on the indexed value using the `list_contains` function. Example: `id FROM [//path/to/table] WITH INDEX [//path/to/index/table] AS IndexTable where list_contains(child_ids, 1)`. {% note warning "Note" %} When multiple allowable "child_ids" values are specified in the predicate, such as `list_contains(child_ids, 1) or list_contains(child_ids, 4)`, the select query may return multiple rows corresponding to the same primary key in the indexed table if both values are contained in the indexed list. In order for uniqueness to be restored, result must be grouped by the table’s primary key. A `list_contains(child_ids, 1) OR list_contains(child_ids, 2) OR ...` query cannot contain a large number of conditions because of the limitation on the syntax tree depth in all query expressions. To mitigate this limitation, you can write the predicate in the following form: `child_ids IN (1, 2, ...)`. In this case, the system will interpret the reference to the "child_ids" column as a reference to a column in the index table. {% endnote %} For example, let the rows in a table with the schema `[{name=id; type=int64; sort_order=ascending}; {name=book_name; type=string}; {name=genres; type_v3={type_name=list; item=string}}; ...]` describe books. To perform quick sampling by genre, you can use the `unfolding` secondary index over the index table with the schema `[{name=genres; sort_order=ascending; type=string};{name=id; type=int64; sort_order=ascending}; {name=book_name; type=string}]` — the `book_name` column in the index table schema will enable you to use the index table without joining it to the indexed one for queries where only the `genres`, `book_name`, and `id` columns are needed, further reducing the number of reads and speeding up query execution. ### Unique index { #unique } A `kind=unique` index maintains the uniqueness of the secondary key in the indexed table. A key in an index table schema for a secondary index of this kind is the secondary key, and non-key values are the primary key. Should user attempt a write which would result in two rows with the same secondary key being present in the indexed table, the `UniqueIndexConflict` error will occur. Should two transactions with row writes that have different primary keys and identical secondary keys conflict, one of them will be aborted with the `TransactionLockConflict` error. ### Secondary index sharding { #sharding } A popular solution for data sharding is using [computed key columns](https://ytsaurus.tech/docs/en/resharding#expression), which are independent between the index table and the indexed one — such columns can have the same or different names, types, and expressions so that sharding can be controlled independently. Please note that, just as with regular tables, using a computed column in an index table will make queries with a range by indexed column inefficient. ### Copying and moving { #copy-move } In the current version of YTsaurus, you cannot copy or move an index or indexed table while maintaining a secondary index that links them. Attempting to do this will result in an error. In this case, the correct procedure is to unmount the tables, delete the secondary indexes, perform the desired operations in Cypress, recreate the secondary indexes with the same attributes, then re-mount the tables. Alternatively, you can specify the `allow_secondary_index_abandonment=%true` parameter for your operations. In this case, all affected secondary indexes are deleted automatically. ## Limitations { #limitations } * Since a select query using an index performs a table join, reads must be performed within a transaction to obtain a globally consistent slice (by default, select queries are executed with SyncLastCommittedTimestamp; in this mode, the system generally cannot guarantee consistency between two data structures). * Indexing by aggregate columns and writing to an indexed table with a `shared write` lock are prohibited. * Insertions to an indexed table via a [map-reduce operation](https://ytsaurus.tech/docs/en/bulk-insert) are not displayed in the index table. * Changing the schema of a table with a secondary index has its specifics as well. When adding a key column, it must first be added to the index tables, and only then to the indexed table. When adding a non-key column, the opposite is true (or it may be omitted from index tables). * All secondary key columns, as well as columns used in the predicate, must have the same `lock` [group](https://ytsaurus.tech/docs/en/transactions#conflicts) in the indexed table. ### Limitations imposed on replicated tables Consistent reads from replicated tables when using secondary indexes are possible only if the index and indexed tables have an overlapping set of clusters with synchronous replicas. Before creating a secondary index, make sure that the indexed table and all index tables are combined into a single [collocation](https://ytsaurus.tech/docs/en/replicated-dynamic-tables#replication_collocation). ## Performance { #performance } One disadvantage of using secondary indexes is the slowing down of writes to the indexed table. Writing to the indexed table will also entail transparent reads from it and transparent writes to all index tables for the user, and in case of unique indexes, transparent reads from the index table as well. These reads are performed at the end of the transaction. As a consequence, the write latency is increased by the read latency. ## Building an index { #building } There is a [script](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/scripts/dynamic_tables/build_secondary_index) that helps correctly create a secondary index over the existing data. It enables you to build a secondary index with minimal downtime required to take a snapshot of the indexed table and remount it (unless you are building a unique index; to maintain strict uniqueness, no writes must be made to the table during the entire building process). After the script acquires the necessary lock, it mounts the indexed table, which can then be used for reading and writing. Once the script finishes running, the index table can be used for queries. The user must ensure that no queries are made with the use of the index until it is fully built. The script supports replicated tables, but the downtime will be a bit longer: you will also have to wait for the replicas to synchronize. Its use stipulates that the indexed and index tables already exist. {% note warning "Note" %} After a secondary index is built using this script, the index table may contain extra rows that do not correspond to any entries in the indexed table (`table_to_index_correspondence=injective`). This may be caused by background compaction and does not affect correctness when using the secondary index in select queries, because these rows will be omitted when joining the tables. Building a strictly bijective (`table_to_index_correspondence=bijective`) index is possible only with a full downtime for both tables. There is a dedicated flag in the script for this mode. {% endnote %} # Hunks usage in dynamic tables ## Introduction Hunk chunks are a mechanism for storing large string values separately from table rows. If a string value in a chunk exceeds a set limit, it is replaced by a light link that leads to a new type of chunk — a hunk chunk. Unlike regular chunks, hunk chunks store raw unstructured data: to read them, you just need to know the offset relative to the beginning of the chunk and the length. This construct opens up additional opportunities to optimize the use of dynamic tables: - Enables you to store only a small part of the table in memory, preserving low lookup latency. - Reduces write amplification, because hunk chunks are compacted less often than regular chunks. - Reduces disk throughput for tables not stored in memory. - Allows erasure coding with low overhead costs for on-the-fly hunk value erasure repair. For more information about optimization, see [Use cases](https://ytsaurus.tech/docs/en/#use-scenarios). ## When should I use hunks? Hunks may prove useful for tables containing many large string values. However, they might not be the best choice if one of the following applies: - If your table resides on an HDD, reading chunks from the disk involves many concurrent, fine-grained accesses to the medium — HDDs aren't exactly optimized for this. - The table data has a good compression ratio (table attribute `compression_ratio` is quite low). Currently, blocks in hunk chunks are not compressed to allow granular hunk reads from the disk. Switching to hunks can therefore substantially increase the table's disk space. This can be partially offset by setting the hunk erasure codec. There is also a feature for dictionary compression of hunk values you can currently try in test mode, which should help you completely negate the issue. - Your table is subject to a versioned remote copy (or a script that adds replicas to a replicated table). At the moment, you cannot make a versioned remote copy of a table with hunk chunks. However, this can be done manually. For more information, see [FAQ](https://ytsaurus.tech/docs/en/#faq). If the size of a string value is not greater than `max_inline_hunk_size`, this value is called an `inline value`. If it is greater, it is called a `ref value`. Hunk chunks consist of `ref values`. To roughly estimate the share of `ref values` in your table accounting for a varying `max_inline_hunk_size`, you can use a script: `yt/yt/tools/hunk_chunk_advisor/hunk_chunk_advisor compute_weight`. {% cut "Example" %} ```bash ./hunk_chunk_advisor compute_weight --proxy zeno --table-path //home/table --sampling-rate 0.1 --result-file result_file --computation-path //home/hunk_chunk_advisor ``` The script runs operations on the cluster that will create a static table from a dynamic table and allows row sampling from it. Then, it will compute the ratio of data that constitute `ref values` for different `max_inline_hunk_size`. | Script parameter | Description | | --- | --- | | `proxy` | YTsaurus cluster name. | | `table-path` | Cypress path to the table. | | `sampling-rate` | The share of table rows for calculating statistics (required if the table is too large to run the operation on it). | | `result-file` | Local path to where you want to create a PNG file with detailed statistics. | | `computation-path` | Cypress path to the directory where the tables with intermediate calculations will be located. We recommend creating a new subdirectory within the project directory to avoid violating the ACL and cluttering the working directory. | {% endcut %} ## Enabling hunks To enable hunks, you need to change the table schema by setting the `max_inline_hunk_size` attribute on the desired string columns. To modify the schema, the table needs to be unmounted. {#slim_format} We also recommend using an alternative chunk format. The default chunk format (`@optimize_for=lookup`) can be inefficient in terms of uncompressed data size. This is especially true for tables with hunks, because they store many hunk refs physically represented as short rows. You should either select the slim chunk format (`@optimize_for=lookup` and `@chunk_format=table_versioned_slim`) or the scan format (`@optimize_for=scan` and `@chunk_format=table_versioned_columnar`). When using the scan format, you will probably want to set the same column group on all columns of the schema. For more information about column groups, see the [documentation](https://ytsaurus.tech/_includes/user-guide/storage/chunks#columns). To enable hunks on your table, you can use the script located at `yt/yt/experiments/public/hunkifier`. {% cut "More about hunkifier" %} Example: ```bash ./hunkifier --proxy zeno --table-path //home/replica_table --max-inline-hunk-size 128` ``` The script unmounts the table, alters its schema by adding the necessary attribute, sets additional settings, and then re-mounts the table. Basic parameters: | Script parameter** | Description | | --- | --- | | `proxy` | YTsaurus cluster name. | | `table-path` | Cypress path to the table. | | `max-inline-hunk-size` | Maximum size of a string value in bytes. | Additional parameters: | Script parameter** | Description | | --- | --- | | `primary-medium` | A medium for storing the chunks (regular and hunk). | | `enable-crp` | A flag for enabling [consistent replica placement](https://ytsaurus.tech/docs/en/#consistent-replica-placement) on the table. | | `max-hunk-compaction-garbage-ratio` | The [max_hunk_compaction_garbage_ratio](https://ytsaurus.tech/docs/en/#max_hunk_compaction_garbage_ratio) attribute. | | `hunk-erasure-codec` | The [erasure codec](https://ytsaurus.tech/docs/en/#hunk_erasure_codec) for hunk chunks. | | `fragment-read-hedging-delay` | The delay before sending a [hedged request](https://ytsaurus.tech/docs/en/#hunk_hedging) for fragment reads. | | `enable-slim-format` | A flag for enabling the [slim chunk format](https://ytsaurus.tech/docs/en/#slim_format) on the table. | | `enable-columnar-format` | Use the column-by-column chunk format on the table. | | `columns` | A list of columns the `max_inline_hunk_size` attribute will be set on. If the option is not specified, the attribute is set on all string columns of the table. | {% endcut %} Once hunks are enabled and the table is mounted, flush and compaction will also generate hunk chunks with `ref values` along with regular chunks. To quickly change the structure of all chunks in the table, you can perform a forced compaction taking into account all the recommendations described in the [documentation](https://ytsaurus.tech/_includes/user-guide/dynamic-tables/compaction#forced_compaction). You can also use the `forced_store_compaction_revision` and `forced_hunk_compaction_revision` options for forced compaction of regular-only or hunk-only chunks. {% note info %} The `forced_hunk_compaction_revision` option does not compact all of the hunk chunks: the compaction algorithm unconditionally compacts only those hunk chunks that are referenced by regular chunks falling under the compaction criteria. {% endnote %} ## Use cases {#use-scenarios} There are several use cases where various hunk table configurations may prove useful. Each configuration is set by a specific set of options with their own trade-offs. - An in-memory table takes up a lot of tablet static memory. In such cases, having a low latency for table reads is often critical. If you put a table with enabled hunks in memory, only non-hunk chunks will end up there. This can save you a lot of tablet static memory without significantly increasing the latency of read requests. If a user reads a row that does not have `ref values`, reading will still be performed from memory. If the row does have `ref values`, reading them will require additional resources (to reach the data node and read these values from the disk). Accordingly, decreasing the `max_inline_hunk_size` will also reduce the number of memory reads and the amount of tablet static memory required. {% note info %} High-load tables may require additional optimization efforts, so we recommend reading the [Further optimization](https://ytsaurus.tech/docs/en/#optimization) section below. {% endnote %} {#max_hunk_compaction_garbage_ratio} - Table writes cause high write amplification. As you may know, the LSM tree data organization of a dynamic table implies that the write throughput to the disk is higher than the initial user write throughput. High flow and write amplification can affect the number of disks required to maintain the table (with DWPD, the allowed write throughput to the disk ends up being relatively small). When regular chunks that reference hunk chunks are compacted, it's not mandatory to read and modify the latter. Because of that, after compaction hunk chunks may partially consist of outdated values if some of the `ref values` a left without references. This makes it possible to skip reading and overwriting `ref values`, thereby reducing the read and write throughput during compaction. The option that adjusts the maximum allowable percentage of garbage in hunk chunks is `max_hunk_compaction_garbage_ratio`. The higher this value, the lower the write amplification, but the higher the table's disk space (because hunk chunks will contain more outdated values). - Table reads cause high disk load. When reading regular chunks from the disk, you have to read entire blocks that usually take up a few hundred kilobytes. Large flows may overload the disks, and the block cache may be underutilized. Hunks are read with a granularity of 4 KB. - The table disk space is too high. If the data node is overloaded or if some parts of regular chunks are inaccessible, erasure recovery may be inefficient and resource-intensive. Optimizations in the read layer of hunk chunks make this process much more bearable. You can enable erasure coding for hunk chunks by setting the `@hunk_erasure_codec` attribute on the table. To enable on-the-fly part recovery, see the [Further optimization](https://ytsaurus.tech/docs/en/#optimization) section below. After the erasure codec is set on the table, you can find out how many chunks are written in each of the codecs using the `@erasure_statistics` table attribute. {#hunk_erasure_codec} ## Further optimization {#optimization} From a data node's point of view, `ref value` reads transform into fragment reads, so that's the terminology we will stick to in this section. {#hunk_hedging} - Hedged requests for fragment reads or on-the-fly erasure recovery. If a read request to the data node is taking too long, it should be hedged. To do this, use the `@hunk_chunk_reader/fragment_read_hedging_delay` option (value in milliseconds). This option is also responsible for the time after which the erasure repair of a data part starts if the data node is not responding. - Fragment read request batching. By default, three random data nodes are selected for each table chunk to store its replicas. Even a single key lookup tends to generate reads from multiple chunks, not to mention lookups of multiple keys. Since reading each `ref value` is reading from the disk from a remote data node, there is a high fan-out of requests to the storage subsystem. This can be performance-critical if the flow of requests to the table is rather high. In this case, you can set the `@enable_consistent_chunk_replica_placement` boolean attribute on the table. This will make it so that chunk replicas of a single tablet will be assigned to the same data nodes in best-effort mode. Fragment read requests to data nodes are batched. And disk requests are batched on data nodes. In some cases, this substantially reduces the fan-out. {% note info %} This option can significantly affect the storage layer due to a particular chunk replication load. Set it only after consulting with the YTsaurus team. {% endnote %} {#consistent-replica-placement} - More efficient disk reads. Fragment reads cause many fine-grained reads from the disk. To keep them efficient and low-latency, use a medium with io_uring to store table chunks. You can also use `direct I/O` to bypass the OS cache. To do this, set the `@hunk_chunk_reader/use_direct_io` boolean attribute on the table (on some media, reads with `direct I/O` are globally disabled and this attribute will have no effect). ## Diagnostics To figure out how many chunks are hunk chunks and how large they are, you can execute the following request: ```bash yt get /@chunk_format_statistics. ``` To get more detailed statistics on the structure of hunk chunks, you can execute the following request: ```bash yt get /@hunk_statistics. ``` Both of these requests are heavy from the master's point of view and should be run manually and only once. Chunks also have some useful attributes: - All chunks: `@consistent_replica_placement_hash` and `@consistent_replica_placement`. - Regular chunks: `@hunk_chunk_refs`. - Hunk chunks: `@hunk_count` and `@total_hunk_length`. For more information about hunk chunk refs, see the node's Orchid: `tablet_cells//tablets//hunk_chunks/` ## FAQ {#faq} #### After enabling hunks, my disk space has grown a lot. What do I do? You can adjust the `@max_hunk_compaction_garbage_ratio` table attribute. It represents the allowable percentage of garbage (outdated data) in hunk chunks. If a hunk chunk exceeds this limit, it will be compacted. The default value is `0.5`. For some tables, you may want to change it to `0.15` or `0.3` depending on the load. The downside is that decreasing this value also increases the compaction write amplification. #### After enabling hunks, my chunk count has grown a lot. What do I do? When the dynamic store gets flushed to disk, this can create many small chunks. In this case, you can adjust the `@max_hunk_compaction_size` table attribute. All hunk chunks below the specified value will be compacted. The default value is `8 MB`. For some tables, you may want to change it to `16 MB` or `32 MB` depending on the load. The downside is that increasing this value also increases the compaction write amplification. #### How do I find out the actual share of disk space taken up by garbage in hunk chunks? You can read the `@hunk_statistics` table attribute and compare values in the `total_hunk_length` and `total_referenced_hunk_length` fields. {% note warning %} Requesting the `@hunk_statistics` attribute is fairly compute-intensive. You should do it only manually and not too often. {% endnote %} # Dynamic table tutorials This section contains step-by-step scenarios for building services, using queues, and maintaining dynamic tables. - [Creating a web service](https://ytsaurus.tech/docs/en/dyn-tables-tutorial.md) - [Using a dynamic table as a queue](https://ytsaurus.tech/docs/en/dynamic-table-queue.md) - [Inserting and deleting data from the command line](https://ytsaurus.tech/docs/en/add-remove-line.md) - [Converting a dynamic table to a static table](https://ytsaurus.tech/docs/en/dynamic-to-static.md) - [Creating a backup](https://ytsaurus.tech/docs/en/backup-table.md) # Creating a web service This section describes an example of creating a simple web service using dynamic tables. Backend for a small comment storage and display service will be built within the example. You can consider it to be a simplified analog of [Reddit](https://www.reddit.com). For this purpose, an HTTP-based stateless service will be developed on top of a consistent, fault-tolerant, and scalable repository of topics, comments, and likes. Technologies and tools used: - Service source code in Python 3.4 and higher. - [Replicated dynamic YTsaurus tables](https://ytsaurus.tech/docs/replicated-dynamic-tables.md) in synchronous replication mode will be used as a repository. Work with dynamic tables is implemented via the RPC proxy. - The [Flask](http://flask.pocoo.org/) framework will be used as the basis for the HTTP API. {% note info "Note" %} This example does not stipulate the development of the following components: authorization, rate limiting, and service monitorings. The described example is intended primarily as an introduction to the use and capabilities of dynamic YTsaurus tables, not for the development of production high-load web services in Python. {% endnote %} Click [here](https://github.com/ytsaurus/ytsaurus/tree/main/yt/examples/comment_service) to download code examples. ## Designing { #designing } Service requirements: - All comments stored in the service must relate to one of the topics. - Comments within a topic must be organized as a tree. Each comment, except for the root comment of the topic, must have one parent. - Each comment must be characterized by the `parent_path` string. It is the path from the ancestor IDs of a given comment in the tree. - The username, number of views, and creation time must be stored for each comment. - The API must enable you to: - Create, edit, and delete comments. - Select all topic comments or a subtree of comments within a topic. - Select the most recent comments by a given user. - Select the most recently added topics. Expected characteristics: * Up to 50,000 comments within a topic. * A typical comment is no more than tens of thousands of characters. * The service must be horizontally scalable by load (RPS) and data volume. ### API methods { #api } In case of POST requests, the request parameters are passed in the request body as a JSON document. In GET requests — as an URL. In all cases, the request result or error comes in the response body as a JSON document. Possible HTTP response codes: | HTTP code | Description | | -------------- | --------------- | | 200 | The GET/POST request was successfully complete. | | 201 | The value was successfully added. | | 400 | Incorrect request. For example, some mandatory parameters were not set. | | 404 | The object or objects for a specified criterion were not found. For example, an attempt to add a comment to a non-existing topic. | | 409 | Conflict when making changes to a dynamic table. | | 503 | The service is currently unable to process the request. For example, the meta-cluster is updated and/or the dynamic tables were unmounted. | | 524 | Timeout exceeded when working with dynamic tables. | In case of any error with 4xx and 5xx codes, a JSON document in the `{"error" : "message"}` form comes in the response body. Writing data — adding, editing, deleting comments — will be implemented via `POST`, reading — via `GET`. `guid` is used as a comment ID: this approach will ensure an even load on dynamic table shards. #### Call signatures { #call_signatures } - `POST: /post_comment`: Adding a comment. | Parameter name | Type | Required parameter | Description | | -------- | ------ | -------------- | --------------- | | `topic_id` | `guid` | No | If the topic ID is not specified, a new topic is created. | | `parent_path` | `string` | No | Path to the parent comment. Can be specified if `topic_id` is not specified. | | `content` | `string` | Yes | Comment content. | | `user` | `string` | Yes | Username. | If successful, code 201 and a document in the `{"comment_id" : "", "new_topic" : True | False} ` form are returned. - `POST: /edit_comment`: Editing a comment. | Parameter name | Type | Required parameter | Description | | --- | --- | --- | --- | | `topic_id` | `guid` | Yes | Topic ID. | | `parent_path` | `string` | Yes | Comment path. | | `content` | `string` | Yes | New comment content. | If successful, code 200 and an empty document are returned. - `POST: /delete_comment`: Deleting a comment. The comment is not actually deleted from the database in order not not to violate the tree comment structure. Instead, a special flag is set in the comment parameters and it means that the comment was deleted, after which the comment can no longer be edited. | Parameter name | Type | Required parameter | Description | | --- | --- | --- | --- | | `topic_id` | `guid` | Yes | Topic ID. | | `parent_path` | `string` | Yes | Comment path. | If successful, code 200 and an empty document are returned. - `GET: /topic_comments`: Getting a comment in the topic. A view counter is stored for each comment and it is incremented by one each time a topic comment is requested. | Parameter name | Type | Required parameter | Description | | --- | --- | --- | --- | | `topic_id` | `guid` | Yes | Topic ID. | | `parent_path` | `string` | No | The path to the root comment can be specified if only a subtree of comments for a specified topic is to be obtained. | If successful, code 200 and a document with a list of the following comments are returned: ```json [ { "comment_id":"", "parent_id":"", "content":"", "user":"", "view_count":"", "update_time":"", "deleted":"True|False" } ] [{"comment_id" : "", "parent_id" : "", "content" : "", "user" : "", "view_count" : , "update_time" : "", "deleted" : True|False}] ``` Returned comments are sorted by `parent_path`, so the responses to the comment come immediately after it and are ordered by creation time. * `GET: /last_user_comments`: Get a list of user comments sorted in reverse chronological order. It is assumed that there will be a page in the service web interface where the user can view all their comments. | Parameter name | Type | Required parameter | Description | | --- | --- | --- | --- | | `user` | `string` | Yes | Username. | | `limit` | `int` | No | Limiting the number of returned comments. | | `from_time` | `UTC_time` | No | Return only those comments with `update_time` greater than this parameter. The `from_time` parameter is specifically used instead of `offset`, because the YTsaurus query language does not support the `OFFSET` keyword in queries. | If successful, code 200 and a document with a list of the following comments are returned: ```json [{"comment_id" : "", "topic_id" : "", "content" : "", "user" : , "view_count" : , "update_time" : }] ``` * `GET: /last_topics`: Get the topics in which comments have recently been updated. A topic is a root comment. It is assumed that this request will be used by the homepage of the service and the list will be shared by all users. Pagination support is required. | Parameter name | Type | Required parameter | Description | | --- | --- | --- | --- | | `limit` | `int` | No | Limit on the number of returned topics. | | `from_time` | `UTC_time` | No | Return only those comments with `update_time` greater than this parameter. The `from_time` parameter is specifically used instead of `offset`, because the YTsaurus query language does not support the `OFFSET` keyword in queries. | If successful, code 200 and a document with a list of the following comments are returned: ```json [{"topic_id" : "", "content" : "", "user" = , "view_count" : , "update_time" : }] ``` ### Description of data tables { #tables_description } You need to define a required set of tables and their schema. When designing the schema, consider the features and limitations of dynamic tables: - Each dynamic table has a unique primary key. You can use the primary key to obtain writes by calling the `lookup` command. - Dynamic tables support transactions, including those between different tables in the `snapshot isolation` model. - Dynamic tables do not support secondary indexes. Efficient searching or filtering is only possible by the prefix of the primary key using the range output mechanism. Otherwise the request may result in full scan of the table. - Horizontal scaling of dynamic tables is performed through sharding. A table shard in the system is called a `tablet`. It is specified by a continuous range of primary key values. For sharding to be effective, reading and writing must be evenly distributed across the tablets. - Secondary indexes can be emulated by creating additional tables by the corresponding primary key. In this case, the master table and the index table must be written to within the same transaction. The main table needed to store content — `topic_comments` — is needed. Read queries to such a table will be generally limited to a single topic, so we recommend making the topic ID the first key component. You can output consecutive integers as the topic ID or generate them based on the current time. However, this will mean that when new topics are created, they will be given very close numbers, and since new comments are more likely to appear in new topics, the entire writing load will be on one tablet. Therefore, to ensure that the tablets are loaded evenly when the table is scaled, a randomly generated guid is used as the topic ID. The numbers of comments in creation order are used as the IDs i the topic — this can create close keys, but if the size of the topics is not too large, sharding by topic guid should be sufficient. The `parent_path` key column also stores the path to the comment in the topic: for the root comment — its id, and `parent_path` of each subsequent comment is derived from `parent_path` of its parent by adding `/` and the parent ID to it. For example, `parent_path` of the comment with the `#id1` ID written in response to the root comment with the `0` ID is written as `0/#id1`. This path arrangement enables you to quickly filter comments in the subtree when calling the `topic_comments` method: if `parent_path` of the root comment in the subtree is the `S` string, `parent_path` of its child will have the `S/#id1/.../#idN` form. It will contain `S` as a prefix. Therefore, if you lexicographically order the comments in the topic by `parent_path`, any subtree will correspond to a continuous block of comments starting from the root comment of the subtree. Thus, assigning `parent_path` as a key column enables you to avoid performing full scan of the table to select comments in the subtree. The ID of the parent comment is additionally stored for each comment. This is needed to assemble comments into a tree when displaying them. Besides that, the `topic_comments` table stores a comment view counter, for which an aggregation column mechanism is used. This mechanism enables you to increment and decrement the value of a column without reading its previous version. #### The `topic_comments` table | Column name | Type | Key column | Description | | --- | --- | --- | --- | | `topic_id` | `guid` | Yes | Topic guid. | | `parent_path` | `string` | Yes | Comment path. | | `comment_id` | `uint64` | No | Comment ID. Its sequence number when adding to the topic. | | `parent_id` | `uint64` | No | ID of the comment, in response to which this comment was written. Matches `comment_id` for root comments. | | `user` | `string` | No | Username of the user who left the comment. | | `create_time` | `uint64` | No | Comment creation time in POSIX Time. | | `update_time` | `uint64` | No | The time when the comment was last updated in POSIX Time. | | `content` | `string` | No | Text of the comment. | | `views_count` | `int64` | No | Number of views, aggregation column. | | `deleted` | `boolean` | No | The flag that the comment was deleted. | To select writes in the `/last_user_comments` call, the `topic_comments` table is not suitable. User comments may be located in different topics, which means that a full scan of the table is required to execute such a query. YTsaurus does not support secondary indexes, so they also cannot be used. Therefore, a second, auxiliary `user_comments` table is required where the username is used as the first primary key component. For reasons of even load balancing in future sharding of the table, add a computed column to the beginning of the key — hash `user_name`. The farm_hash function specified in the `expression` field in the table schema (see table creation code) is used for hashing. #### The `user_comments` table | Column name | Type | Key column | Description | | --- | --- | --- | --- | | `hash(user_name)` | `uint64` | Yes | Computed column. | | `user_name` | `string` | Yes | Username of the user who left the comment. | | `topic_id` | `string` | Yes | Topic guid. | | `parent_path` | `string` | Yes | Comment path. | | `update_time` | `uint64` | No | The time when the comment was last updated in POSIX Time. | You need to decide how to process the `/last_topics` call efficiently. A simple way is to query the `topic_comments` table with grouping by `topic_id`. But this approach requires re-running full scan of the largest table. Therefore, it makes sense to create another small `topics` table where `topic_id` will store the last update time of the topic, i.e. any of its comments. Although this entire table will be viewed, it is much smaller. This will enable you to apply a number of optimizations. For example, upload the entire table into memory or increase the query concurrency by increasing the number of tablets. Moreover, the response to such a query can be cached, because it will be the same for all users. This table will also store the number of comments in the topic so that when a new comment is added, its ID can be obtained. #### The `topic` table | Column name | Type | Key column | Description | | --- | --- | --- | --- | | `topic_id` | `string` | Yes | Topic guid. | | `update_time` | `uint64` | No | The time when the comment was last updated in the topic. | | `comment_count` | `uint64` | No | Number of comments in the topic, including deleted comments. | ## Creating and mounting tables { #tables_preparation } This example uses synchronous replication with one synchronous and one asynchronous replica and automatic replica mode switching. This mode provides the strictest consistency guarantees as with unreplicated dynamic tables and does not require manual replica mode switching. Creating a replicated table includes the following steps: 1. Creating a special `replicated_table` object on the meta-cluster. 2. Creating replica tables on other clusters and their corresponding `table_replica` objects on the meta-cluster. All tables must be created with the same schema. The `ssd_blobs` medium is used to store data. The choice of an SSD medium is driven by the required timings and meta cluster write flow. All replicas will first be created in`async` mode. After the tables are mounted, the synchronous replica will be selected automatically. The actions described above can be performed either via the CLI — via the `yt create` and `yt set` commands — or via Python wrapper. For this example, a script was written to create the required tables. This script reads the required parameters in the command line arguments. This enables you to use the script both production and testing environments. Script parameters: * `meta_cluster`: A cluster with a replicated table. * `replica_clusters`: Clusters with replicas. * `path`: Path to the project working directory. * `force`: A flag used to re-create tables if they already exist. Without it, no changes will be made. Example of running the script after building it via `ya make`: ```bash ./create_tables --path //path/to/directory/ --meta_cluster --replica_clusters --force ``` ## Service quotas Create a dedicated tablet_cell_bundle and an account with an SSD quota. To ensure fault-tolerance of the service, you should have 3 clusters: a meta-cluster and 2 replica clusters. But for training purposes, you can perform all operations on a single cluster. It will act as a meta-cluster and two replicas (synchronous and asynchronous). ## Developing service code { #development } ### Installing the required packages Install [Flask](http://flask.pocoo.org/) and [WTForms](https://wtforms.readthedocs.io). WTForms will be used to validate parameters. Installation commands: ```bash sudo pip install flask sudo pip install wtforms ``` ### Defining the required functions Environment variables will be used to pass parameters to the program. This approach will then enable the application to be easily moved from the testing environment to the production environment. The required environment variables: * `CLUSTER`: The name of the cluster on which replicated tables are stored. * `TABLE_PATH`: Path to the directory with tables. Setting the required environment variables: ```bash export CLUSTER= export TABLE_PATH=//path/to/directory ``` Create a function to add comment information to all tables. It can also be used when editing comments and when adding a comment for an example. ### Implementing the `find_comment` call T function to search for comments in the table takes the `topic_id` and `parent_path` values as arguments. This enables you to quickly find a comment using the `lookup_rows` method, because the values of all key columns of the `topic_comments` table are known. An example of getting information about the recently added comments: ```python # -*- coding: utf-8 -*- import yt.wrapper as yt import json import os def find_comment(topic_id, parent_path, client, table_path): # In lookup_rows you need to pass the values of all key columns return list(client.lookup_rows( "{}/topic_comments".format(table_path), [{"topic_id": topic_id, "parent_path": parent_path}], )) def main(): table_path = os.environ["TABLE_PATH"] = os.environ["CLUSTER"] client = yt.YtClient(cluster_name, config={"backend": "rpc"}) comment_info = find_comment( topic_id="1dd64501-4131025-562332a3-40507acc", parent_path="0", client=client, table_path=table_path, ) print(json.dumps(comment_info, indent=4)) if __name__ == "__main__": main() """ Program output: [ { "update_time": 100000, "views_count": 0, "parent_id": 0, "deleted": false, "comment_id": 0, "creation_time": 100000, "content": "Some comment text", "parent_path": "0", "user": "abc", "topic_id": "1dd64501-4131025-562332a3-40507acc" } ] """ ``` ### Implementing the `post_comment` call The first implementation of the `post_comment` call is a function that takes all parameters as arguments and returns a JSON string as a response. The dynamic table queries are gathered into a single transaction. Added exception processing when accessing the YTsaurus system. ```python # -*- coding: utf-8 -*- import yt.wrapper as yt import os import json import time from datetime import datetime # Auxiliary function to get the number of comments in a topic from the topic_id table # If the specified topic can't be found in the table, returns None def get_topic_size(client, table_path, topic_id): topic_info = list(client.lookup_rows( "{}/topics".format(table_path), [{"topic_id": topic_id}], )) if not topic_info: return None assert(len(topic_info) == 1) return topic_info[0]["comment_count"] def post_comment(client, table_path, user, content, topic_id=None, parent_id=None): # Handle the YtResponseError exception that occurs if the operation fails try: # Adding a comment involves executing several queries of different types, # so they need to be assembled into a single transaction to ensure atomicity with client.Transaction(type="tablet"): new_topic = not topic_id if new_topic: topic_id = yt.common.generate_uuid() comment_id = 0 parent_id = 0 parent_path = "0" else: # The comment_id field is assigned the comment's order in the topic as its value # This number matches the current size of the topic comment_id = get_topic_size(topic_id) if not comment_id: return json.dumps({"error": "There is no topic with id {}".format(topic_id)}) parent_info = find_comment(topic_id, parent_path, client, table_path) if not parent_info: return json.dumps({"error" : "There is no comment {} in topic {}".format(parent_id, topic_id)}) parent_id = parent_info[0]["comment_id"] parent_path = "{}/{}".format(parent_path, comment_id) creation_time = int(time.mktime(datetime.now().timetuple())) insert_comments([{ "topic_id": topic_id, "comment_id": comment_id, "parent_id": parent_id, "parent_path": parent_path, "user": user, "creation_time": creation_time, "update_time": creation_time, "content": content, "views_count": 0, "deleted": False, }], client, table_path) result = {"comment_id" : comment_id, "new_topic" : new_topic, "parent_path": parent_path} if new_topic: result["topic_id"] = topic_id return json.dumps(result) except yt.YtResponseError as error: # yt.YtResponseError defines the __str__ method, which returns a detailed error message json.dumps({"error" : str(error)}) ``` ### Implementing the `user_comments` call The function includes only one `select_rows` query. `user_comments` is used as the main table, because it is sorted by the `user` field and is most effective for retrieving comments of a specified user. An additional `topic_comments` table is connected to this table using `JOIN` to get full information about the comments. In the `ON` section, `topic_id` and `parent_path` must be specified explicitly so that the data from the additional table can be retrieved efficiently by key. The `WHERE` and `LIMIT` sections are used to set the required filters and the `ORDER BY` section is used to specify the order in which the most recent comments must return first. ```python # -*- coding: utf-8 -*- import yt.wrapper as yt import os import json def get_last_user_comments(client, table_path, user, limit=10, from_time=0): try: # user_comments is used as the main table that enables you to filter writes by the user field # An additional topic_comments table is connected via join # and it enables you to get full information about the omment comments_info = list(client.select_rows( """ topic_comments.topic_id as topic_id, topic_comments.comment_id as comment_id, topic_comments.content as content, topic_comments.user as user, topic_comments.views_count as views_count, topic_comments.update_time as update_time from [{0}/user_comments] as user_comments join [{0}/topic_comments] as topic_comments on (user_comments.topic_id, user_comments.parent_path) = (topic_comments.topic_id, topic_comments.parent_path) where user_comments.user = '{1}' and user_comments.update_time >= {2} order by user_comments.update_time desc limit {3}""".format(table_path, user, from_time, limit) )) return json.dumps(comments_info, indent=4) except yt.YtResponseError as error: return json.dumps({"error" : str(error)}) ``` Running the application. The application is created via [Blueprint application factory](http://flask.pocoo.org/docs/1.0/blueprints/). A special `Flask.g` object where the application context is configured is used to pass the `client` and `table_path` parameters. The connection settings are passed via the `HOST` and `PORT` environment variables. http://127.0.0.1:5000/ is used by default. Logging is also configured: one message is written to the `comment_service.log` file at the start of the query and another one at the end of the query. The YTsaurus system debug logs are redirected to the `driver.log` file. A separate YTsaurus client must be created for each query so that it is possible to process queries concurrently in the future. An individual driver version is created for each client, which wastes additional resources. To avoid this problem, one common driver object is created at application initialization and then this object is connected to each created client by specifying the corresponding option in the client. WTForms forms are used to validate query parameters. For each method, you need to create a separate class form that specifies the query parameters, their type, and other issues. It is assumed that the code is saved in the `run_application.py` file. ```bash export CLUSTER= export TABLE_PATH=//path/to/directory export HOST=127.0.0.1 export PORT=5000 python run_application.py ``` ## Query examples { #examples} Use `curl` to send queries, use the `jq` utility to format JSON output. {% cut "Listing 22 — Installing utilities" %} ```bash sudo apt-get install curl sudo apt-get install jq ``` {% endcut %} {% cut "Listing 23 — Creating topics with multiple comments" %} ```bash # First topic: curl - s - X POST - d 'user=abc&content=comment1' 'http://127.0.0.1:5000/post_comment/' | jq. { "comment_id": 0, "new_topic": true, "parent_path": "0", "topic_id": "d178dfb1-b721a596-4358abc9-ed93ae6b" } curl - s - X POST - d 'user=abc&content=comment2&topic_id=d178dfb1-b721a596-4358abc9-ed93ae6b&parent_path=0' 'http://127.0.0.1:5000/post_comment/' | jq. { "comment_id": 1, "new_topic": false, "parent_path": "0/1" } curl - s - X POST - d 'user=def&content=comment3&topic_id=d178dfb1-b721a596-4358abc9-ed93ae6b&parent_path=0' 'http://127.0.0.1:5000/post_comment/' | jq. { "comment_id": 2, "new_topic": false, "parent_path": "0/2" } # Second topic: curl - s - X POST - d 'user=def&content=comment4' 'http://127.0.0.1:5000/post_comment/' | jq. { "comment_id": 0, "new_topic": true, "parent_path": "0", "topic_id": "d9de3eac-fa020dab-4299d3b5-cb5fd5b8" } curl - s - X POST - d 'user=abc&content=comment5&topic_id=d9de3eac-fa020dab-4299d3b5-cb5fd5b8&parent_path=0' 'http://127.0.0.1:5000/post_comment/' | jq. { "comment_id": 1, "new_topic": false, "parent_path": "0/1" } ``` {% endcut %} {% cut "Listing 24 — Editing a comment" %} ```bash # Editing the second comment curl - s - X POST - d 'topic_id=d178dfb1-b721a596-4358abc9-ed93ae6b&content=new_comment2&parent_path=0/1' 'http://127.0.0.1:5000/edit_comment/' | jq. ``` {% endcut %} {% cut "Listing 25 — Deleting a comment" %} ```bash # Deleting the fourth comment (a root one in the second topic). The fifth comment will not be deleted. curl - s - X POST - d 'topic_id=d9de3eac-fa020dab-4299d3b5-cb5fd5b8&parent_path=0' 'http://127.0.0.1:5000/delete_comment/' | jq. ``` {% endcut %} {% cut "Listing 26 — Outputting comments" %} ```bash # Outputting the last two comments made by user abc curl - s - H @ headers 'http://127.0.0.1:5000/user_comments/?user=abc&limit=2' | jq. [ { "comment_id": 1, "content": "new_comment2", "topic_id": "d178dfb1-b721a596-4358abc9-ed93ae6b", "update_time": 1564581207, "user": "abc", "views_count": 0 }, { "comment_id": 1, "content": "comment5", "topic_id": "d9de3eac-fa020dab-4299d3b5-cb5fd5b8", "update_time": 1564581173, "user": "abc", "views_count": 0 } ] ``` {% endcut %} {% cut "Listing 27 — Outputting a comment subtree" %} ```bash # Outputting all comments in the first topic within the subtree of the second comment (which includes only this comment) curl - s - H @ headers 'http://127.0.0.1:5000/topic_comments/?topic_id=d178dfb1-b721a596-4358abc9-ed93ae6b&parent_path=0/1' | jq. [ { "comment_id": 1, "content": "new_comment2", "creation_time": 1564581113, "deleted": false, "parent_id": 0, "update_time": 1564581207, "user": "abc", "views_count": 1 } ] ``` {% endcut %} {% cut "Listing 28 — Outputting a list of topics" %} ```bash # Outputting all recent topics curl - s - H @ headers 'http://127.0.0.1:5000/last_topics/' | jq. [ { "content": "comment4", "topic_id": "d9de3eac-fa020dab-4299d3b5-cb5fd5b8", "update_time": 1564582660, "user": "abc", "views_count": 0 }, { "content": "comment1", "topic_id": "d178dfb1-b721a596-4358abc9-ed93ae6b", "update_time": 1564581207, "user": "abc", "views_count": 0 } ] ``` {% endcut %} {% cut "Listing 29 — Examples of bad queries" %} ```bash # Providing an incomplete set of arguments: topic_id is not specified in a query to topic_comments curl - s - H @ headers 'http://127.0.0.1:5000/topic_comments/?parent_path=0' | jq. { "error": "Parameter topic_id must be specified" } ``` {% endcut %} {% cut "Listing 30 — Example of an error message" %} ```bash # If YTsaurus cannot be queried, the following error message is returned: { "error": "Received driver response with error\n Internal RPC call failed\n Error getting mount info for _home/dev/username/comment_service/user_comments\n Error communicating with master\n Error resolving path #f0b5-5c916-3f401a9-dda0ef6f\n No such object f0b5-5c916-3f401a9-dda0ef6f\n\n***** Details:\nReceived driver response with error \n origin user_host.domain.com in 2018-09-28T10:33:17.618617Z\nInternal RPC call failed \n origin node001.cluster.domain.com in 2018-09-28T10:33:17.601953Z (pid 745355, tid 4359a68cbe5897dd, fid fffee7436fa6bd03) \n service ApiService \n request_id 3dc-764489c-69ebdf66-942f638f \n connection_id 7-e996d4e8-7b3a20cb-a9093d41 \n address node001.cluster.domain.com:9013 \n realm_id 0-0-0-0 \n method SelectRows\nError getting mount info for _home/dev/username/comment_service/user_comments \n origin node001.cluster.domain.com in 2018-09-28T10:33:17.601569Z (pid 745355, tid 372673d539e8466f, fid fffee7436e8d8609)\nError communicating with master \n origin node001.cluster.domain.com in 2018-09-28T10:33:17.601413Z (pid 745355, tid 372673d539e8466f, fid fffee7436e8d8609)\nError resolving path #f0b5-5c916-3f401a9-dda0ef6f \n code 500 \n origin m01.cluster.domain.com in 2018-09-28T10:33:17.602199Z (pid 471427, tid e8efa5c24fc65652, fid fffe806472536cdb) \n method GetMountInfo\nNo such object f0b5-5c916-3f401a9-dda0ef6f \n code 500 \n origin m01.cluster.domain.com in 2018-09-28T10:33:17.602147Z (pid 471427, tid e8efa5c24fc65652, fid fffe806472536cdb)\n" } # To output the error in a more readable form, you can replace "jq ." with "jq -r .error" Received driver response with error Internal RPC call failed Error getting mount info for //home/dev/username/comment_service/user_comments Error communicating with master Error resolving path #f0b5-5c916-3f401a9-dda0ef6f No such object f0b5-5c916-3f401a9-dda0ef6f ***** Details: Received driver response with error origin user_host.domain.com in 2018-09-28T10:33:31.449413Z Internal RPC call failed origin node001.cluster.domain.com in 2018-09-28T10:33:31.434137Z (pid 745355, tid 4359a68cbe5897dd, fid fffee743122257c3) service ApiService request_id 3df-add3dc38-3fd547ae-d9ab851f connection_id 7-e996d4e8-7b3a20cb-a9093d41 address node001.cluster.domain.com:9013 realm_id 0-0-0-0 method SelectRows Error getting mount info for //home/dev/username/comment_service/user_comments origin node001.cluster.domain.com in 2018-09-28T10:33:17.601569Z (pid 745355, tid 372673d539e8466f, fid fffee7436e8d8609) Error communicating with master origin node001.cluster.domain.com in 2018-09-28T10:33:17.601413Z (pid 745355, tid 372673d539e8466f, fid fffee7436e8d8609) Error resolving path #f0b5-5c916-3f401a9-dda0ef6f code 500 origin m01.cluster.domain.com in 2018-09-28T10:33:17.602199Z (pid 471427, tid e8efa5c24fc65652, fid fffe806472536cdb) method GetMountInfo No such object f0b5-5c916-3f401a9-dda0ef6f code 500 origin m01.cluster.domain.com in 2018-09-28T10:33:17.602147Z (pid 471427, tid e8efa5c24fc65652, fid fffe806472536cdb) ``` {% endcut %} # Using a dynamic table as a queue To implement a queue using dynamic tables, use ordered dynamic tables. Such tables are a sequence of ordered rows, with no key columns. {% list tabs %} - CLI 1. Create a table: ```bash yt create table //path/to/table --attributes \ '{dynamic=%true;schema=[{name=first_name;type=string};{name=last_name;type=string}]}' ``` 2. Mount a table: ```bash yt mount-table //path/to/table ``` 3. Write data: ```bash echo '{first_name=Ivan;last_name=Ivanov}' | yt insert-rows //path/to/table --format yson ``` 4. Read data: ```bash yt select-rows '* from [//path/to/table]' --format json {"first_name":"Ivan","last_name":"Ivanov"} ``` - Python 1. Create a table: ```python import yt.wrapper as yt schema = [ { 'name': 'first_name', 'type': 'string' }, { 'name': 'last_name', 'type': 'string' } ] yt.create('table', '//path/to/table', attributes = {'dynamic': True, 'schema': schema}) ``` 2. Mount a table: ```python yt.mount_table('//path/to/table') ``` 3. Write data: ```python data = [{ 'first_name': 'Ivan', 'last_name': 'Ivanov' }] client.insert_rows('//path/to/table', data) ``` 4. Read data: ```python for d in client.select_rows('* FROM [//path/to/table]'): print(d) {'$tablet_index': 0L, 'first_name': 'Ivan', '$row_index': 0L, 'last_name': 'Ivanov'} ``` {% endlist %} For more information about cleaning up and splitting ordered dynamic tables into tablets, see [Ordered dynamic tables](https://ytsaurus.tech/docs/ordered-dynamic-tables.md). # Inserting and deleting data from the console - Create a sorted dynamic table: ```bash yt create table //path/to/table --attributes \ '{dynamic=%true;schema=[{name=id;type=uint64;sort_order=ascending};{name=first_name;type=string};{name=last_name;type=string}]}' ``` - Mount a table: ```bash yt mount-table //path/to/table ``` - Write data: ```bash echo '{id=1;first_name=Ivan;last_name=Ivanov}; {id=2;first_name=Petr;last_name=Petrov};{id=3;first_name=Sid;last_name=Sidorov}' | yt insert-rows //path/to/table --format yson ``` - Read data: ```bash yt select-rows '* from [//path/to/table]' --format json {"id":1,"first_name":"Ivan","last_name":"Ivanov"} {"id":2,"first_name":"Petr","last_name":"Petrov"} {"id":3,"first_name":"Sid","last_name":"Sidorov"} ``` - Delete data by key: ```bash echo '{id=1};{id=3}' | yt delete-rows //path/to/table --format yson ``` - Read data: ```bash yt select-rows '* from [//path/to/table]' --format json {"id":2,"first_name":"Petr","last_name":"Petrov"} ``` # Converting a dynamic table into a static table 1. Create a static table with the same schema and settings as the original dynamic table. 2. Run the `Merge` operation on top of the dynamic table with output to the static table. To implement the above steps, use the script indicated in the [MapReduce for dynamic tables](https://ytsaurus.tech/docs/mapreduce.md) section. # Creating a dynamic table backup {% list tabs %} - CLI 1. Allow read access and prohibit data write: ```bash yt freeze-table //path/to/table ``` 2. Copy a table: ```bash yt copy //path/to/table //path/to/table_copy ``` 3. Allow data write access: ```bash yt unfreeze-table //path/to/table ``` - Web interface - To allow read access and prohibit data write, select `Freeze` in the table context menu. - To copy a table, select `Copy` in the context menu and specify the name of the new table. - To allow data write, select `Unfreeze`. - MapReduce If you need to give data read and write access when copying a table, dump the contents of the dynamic table into the static table and back again. To do this, use the script described in the [MapReduce for dynamic tables](https://ytsaurus.tech/docs/mapreduce.md) section. {% endlist %} # FAQ #### **Q: When working with dynamic tables from the Python API or C++ API, I get the "Sticky transaction 1935-3cb03-4040001-e8723e5a is not found" error. What should I do?** {#sticky-transaction} **A:** The answer depends on how the transactions are used. If a master transaction is used, then this is a pointless action and you must set the query outside the transaction. To do this, you can either create a separate client or explicitly specify that the query must be run under a null transaction (`with client.Transaction(transaction_id="0-0-0-0"): ...`). Full use of tablet transactions in Python API is only possible via RPC-proxy (`yt.config['backend'] = 'rpc'`). Use of tablet transactions over HTTP is currently not supported. ------ #### **Q: When writing to a dynamic table, the "Node is out of tablet memory; all writes disabled" or "Active store is overflown, all writes disabled" error occurs. What does it mean and how should I deal with it?** {#tablet-memory} **A:**The error occurs when the cluster node runs out of memory to store data not written to the disk. The input data stream is too large and the cluster node has no time to compress and write data to the disk. Queries with such errors must be repeated, possibly with an increasing delay. If this is a recurring error, this may mean (except in off-nominal situations) that individual tablets are overloaded with writes or that the cluster's capacity is not sufficient to cope with this load. Increasing the number of tablets can also help (the `reshard-table` command). ------ #### **Q: What does "Too many overlapping stores" mean? What should I do?** {#overlapping-stores} **A:** This error is an indication that tablet structure is such that the dynamic store coverage of the key range being serviced by the tablet is too dense. Dense coverage with stores leads to degradation of read performance, so in this situation a protection mechanism is activated that prevents new data from being written. The background compaction and partitioning processes should gradually normalize the tablet structure. If this does not happen, the cluster may be failing to cope with the load. ------ #### **Q: When querying a dynamic table, I get the "Maximum block size limit violated" error.** {#block-size-limit} **A:** The query involves a dynamic table once converted from a static table. The `block_size` parameter was not specified. If you receive an error like this, make sure you follow all the instructions from the [section](https://ytsaurus.tech/docs/en/mapreduce.md#convert_table) about converting a static table into a dynamic table. If block size is large, you need to increase `max_unversioned_block_size` to 32 MB and re-mount the table. This can happen, if the table's cells store large binary data that are stored in a single block in their entirety. ------ #### **Q: When mounting a dynamic table, I get the error "Cannot mount tablet ... since chunk ... has too large row or value size", "Cannot mount tablet ... since it has chunks with too large block size", "Cannot mount tablet ... since it has too large chunks"** {#cannot-mount-large-chunks} **A:** The error usually occurs due to incorrect settings during the converting of a static table to a dynamic one. Make sure you have followed all the instructions from the [conversion section](https://ytsaurus.tech/docs/en/mapreduce.md#convert_table). ------ #### **Q: When querying a dynamic table, I get the "Too many overlapping stores in tablet" error.** {#overlapping-stores-tablet} **A:** Most likely, the tablet can't cope with the write flow and new chunks don't have time to compact. Check that the table was sharded for a sufficient number of tablets. When writing data to an empty table, disable auto-sharding, because small tablets will be combined into one. ------ #### **Q: When querying a dynamic table, I get the "Active store is overflown, all writes disabled" error.** {#active-store-overflown} **A:** The tablet can't cope with the write flow — it doesn't have time to dump the data to the disk or it can't do it for some reason. Check for errors in the `@tablet_errors` table attribute and if there are none, check sharding as above. ------ #### **Q: When querying a dynamic table, I get the "Too many stores in tablet, all writes disabled" error.** {#too-many-stores} **A:** The tablet is too large. Get the table to have more tablets. Note that [auto-sharding](https://ytsaurus.tech/docs/en/tablet-balancing.md) limits the number of tablets to the number of cells multiplied by the value of the `tablet_balancer_config/tablet_to_cell_ratio` parameter. ------ #### **Q: When querying a dynamic table, I get the error "Tablet ... is not known".** {#tablet-not-known} **A:** The client sent a query to a cluster node no longer serving the tablet. This usually occurs as a result of automatic balancing of tablets or restarting of cluster nodes. You need to resend the query to make the error disappear after a cache update or to disable balancing. ------ #### **Q: When querying a dynamic table, I get the "Service is not known" error.** {#service-not-known} **A:** The client sent a query to a cluster node no longer serving the cell tablet. This usually happens when cells are rebalanced. You need to resend the query to make the error disappear after a cache update. ------ #### **Q: When querying a dynamic table, I get the error "Invalid mount revision of tablet"** {#invalid-mount-revision} **A:** The client sent a query to a cluster node that has an updated tablet revision. This can happen if the table was explicitly remounted or if the tablet was moved as a result of [automatic balancing](https://ytsaurus.tech/_includes/user-guide/dynamic-tables/tablet-balancing) (the tablet may have remained on the same node but changed its tablet cell). Send another query — the error will disappear once the cache is updated. Alternatively, you can reconfigure the balancing settings. ------ #### **Q: When querying a dynamic table, I get the "Chunk data is not preloaded yet" error.** {#chunk-data-not-preloaded} **A:** The message is specific to a table with the `in_memory_mode` parameter at other than `none`. Such a table is always in memory in a mounted state. In order to read from such a table, all data must be loaded into memory. If the table was recently mounted, the tablet was moved to a different cell, or the YTsaurus process restarted, the data is no longer in memory, which will generate this type of error. You need to wait for the background process to load data into memory. Tablets can move to other cells as a result of [automatic sharding](https://ytsaurus.tech/docs/en/tablet-balancing.md). You can see these moves on the "Tablet balancer moves" graph on the bundle's page. If you want, you can set a different schedule for automatic sharding. Your new schedule must allow the table to continue sustaining your load. ------ #### **Q: In tablet_errors, I see the "Too many write timestamps in a versioned row" or "Too many delete timestamps in a versioned row" error.** {#too-many-timestamps} **A:** Sorted dynamic tables store many versions of the same value at the same time. In lookup format, each key can have no more than 2^16 versions. A simple solution is to use a column format (`@optimize_for = scan`). In reality, such a large number of versions is not necessary and they occur as a result of misconfiguration or a programming error. For example, when specifying `atomicity=none`, you can update the same table key with great frequency (in this mode, there is no row locking and transactions with overlapping time ranges can update the same key). This is not recommended. If writing a large number of versions results from a product need, such as frequent delta writes in aggregation columns, set the `@merge_rows_on_flush=%true` table attribute and correctly [configure TTL deletion](https://ytsaurus.tech/docs/en/sorted-dynamic-tables.md#remove_old_data) so that only a small number of actually needed versions are written to the chunk in a flush. ------ #### **Q: When querying Select Rows, I get the "Query terminated prematurely due to excessive input; consider rewriting your query or changing input limit" error.** {#excessive-input} **A:** This error occurs if too much data is read during query execution. Check the following: 1. The primary key prefix of the table must be specified in the `WHERE` query section. If this hasn't been done, [input data cutting off](https://ytsaurus.tech/docs/en/dyn-query-language.md#input_data_cut) can't be performed, and the query can't be executed efficiently (it will run as a full scan). 2. If the query contains `JOIN`, make sure it's executed [using an index](https://ytsaurus.tech/docs/en/dyn-query-language.md#joins). You need to manually rewrite the query to make sure it runs efficiently. To debug the query, you can use the [explain](https://ytsaurus.tech/docs/en/dyn-query-language.md#explain_query) command. You can temporarily fix the error by increasing `input_row_limit`, but this won't fully resolve your issue. ------ #### **Q: When querying Select Rows, I get the "Maximum expression depth exceeded" error.** {#maximum-expression-depth} **A:** This error occurs if the expression tree depth is too large. This usually happens when writing expressions like ```FROM [...] WHERE (id1="a" AND id2="b") OR (id1="c" AND id2="d") OR ... ``` Instead, you need to set them in the form ```FROM [...] WHERE (id1, id2) IN (("a", "b"), ("c", "b"), ...)``` There are a few problems with the first option. The problem is that queries are compiled into machine code. Machine code for queries is cached so that query structure not including constants serves as the key. 1. In the first case, as the number of conditions increases, query structure will constantly change. There will be code generation for each query. 2. The first query will generate a very large amount of code. 3. Compiling the query will be very slow. 4. The first case will work simply by checking all the conditions. No transformation into a more complex algorithm is envisaged. 5. Besides that, if the columns are key, read ranges are displayed for them so that only the relevant data can be read. The read range output algorithm will work more optimally for the IN variant. 6. In case of IN, checking the condition will perform search in the hash table. ------ #### **Q: When working with a dynamic table, I get the "Value is too long" error.** {#value-too-long} **A:** There are quite strict limits on the size of values in dynamic tables. Single value (table cell) size must not exceed 16 MB, while the length of an entire row should stay under 128 and 512 MB taking into account all versions. There can be a maximum of 1024 values in a row, taking all versions into account. There is also a limit on the number of rows per query, which defaults to 100,000 rows per transaction when inserting, 1 million rows for selects, and 5 million for a lookup. Please note that operation may become unstable in the vicinity of the threshold values. If you are over limit, you need to change data storage in a dynamic table and not store such long rows. ------ #### **Q: How do I clear a replicated table using a CLI?** {#clear-replicated-table-cli} **A:** You can't clear a replicated table atomically, but you can clear individual replicas using the [Erase](https://ytsaurus.tech/docs/data-processing/operations/erase.md) operation. To do this, you need to [enable](https://ytsaurus.tech/docs/en/bulk-insert.md) bulk insert for the entire cluster by setting: - `//sys/@config/tablet_manager/enable_bulk_insert` to `%true` - `//sys/controller_agents/config/enable_bulk_insert_for_everyone` to `%true` (note the missing `@` symbol) If the last specified node doesn't exist, create it using the following command: ```bash $ yt create document //sys/controller_agents/config --attributes '{value={}}' ``` ------ #### **Q: When reading from a dynamic table, I get the "Timed out waiting on blocked row" error.** {#timed-out-blocked-row} **A:** This error occurs due to read-write conflicts – you attempt to read by a key used by a concurrent transaction. The system must wait for the result of that transaction to ensure the required level of isolation between transactions. If you don't require strict consistency, you can [weaken](https://ytsaurus.tech/docs/en/sorted-dynamic-tables#chtenie-stroki) the isolation level for the reading transaction. ------ #### **Q: When writing to a dynamic table, I get the "Row lock conflict due to concurrent ... write" error.** {#row-lock-conflict} **A:** This error occurs due to write conflicts – you're trying to write to a row that is locked by a concurrent transaction (this can be a write operation or an explicit lock). In some cases, the system may indicate the conflicting transaction. For example, for write-write conflicts, the error attributes include the transaction ID. In general, to eliminate conflicts caused by writing to the same key from multiple sources, you need to revise your logic for writing to dynamic tables. In some scenarios, weakening [guarantees](https://ytsaurus.tech/docs/en/transactions#conflicts) may help. For example, you can use `atomicity=none` mode or shared write lock mode. Before you weaken the guarantees, be sure to read the documentation to find out whether the side effects can impact you. ------ #### **Q: When reading from a dynamic table, I get the "No working in-sync replicas found for table" error.** {#no-working-replicas} **A:** This error occurs when reading from a replicated or chaotic table if there is no replica to read from. The replica must meet the following criteria: 1. It must contain fairly recent data. If a timestamp is specified when reading, the replica must contain data up to that timestamp. If a timestamp is not specified, reads must be made from an *actually synchronous* replica: a one that has `mode=sync` and definitely contains older data. For more information, see [guarantees of replicated dynamic tables](https://ytsaurus.tech/docs/en/replicated-dynamic-tables#garantii). 2. It must be a working replica. If the client has recently got an error from a replica, next time it will try to access a different replica. The replicas you cannot read data from are listed in the `banned_replicas` attribute of the error in question. --- #### **Q: When reading from a dynamic table, I get the error 'Timestamp ... is less than tablet ... retained timestamp'** {#retained-timestamp} **A:** The error occurs when requesting data that may have been deleted by compaction. Check that the `min_data_ttl` of the table is bigger than the expected lifespan of user transactions. For more information about compaction settings, see the section [Deleting old data](https://ytsaurus.tech/docs/en/sorted-dynamic-tables#remove_old_data). --- --- #### **Q: When attempting to execute RemoteCopy, I get the "Chunk view cannot be copied to remote cluster" error.** {#chunk-view-cannot-be-remote-copied} **A:** To ensure cost efficiency, some operations on a dynamic table, such as resharding, may create chunk views — wrappers for chunks. Currently, RemoteCopy can only copy regular chunks, not chunk views. To remove chunk views from a table, perform a special forced compaction by setting the `forced_chunk_view_compaction_revision` attribute to 1, and then execute `remount-table`. For more information, see [Forced compaction](https://ytsaurus.tech/docs/en/compaction#forced_compaction). ## Data processing This section contains information about running computations and data processing in a YTsaurus cluster. Data processing is performed in a distributed manner. The logical unit of computation is an **operation**. Most operations have a set of input and output tables. They process data from the input tables, writing the results to an output table. For example, an operation may sort table `A` and save the result to table `B`, or it may execute user code to process rows from table `A` and write the result to table `B`. YTsaurus also allows running operations that execute a custom bash command, such as running a user-defined executable, in a distributed manner. This feature is particularly useful for running distributed ML operations on a cluster. There are several [types of operations](https://ytsaurus.tech/docs/en/operations/overview#obzor). Among other things, they implement the [MapReduce](https://en.wikipedia.org/wiki/MapReduce) paradigm for distributed data processing. Each operation consists of a number of **jobs**, which are executed in the cluster in parallel and usually independently. Each job is an isolated process that handles a part of the overall computation and is executed on one of the cluster nodes. Before starting a job, an **allocation** is made on the node. An allocation is a logical entity characterized by a set of computational resources, such as RAM and CPU. After this, the job is executed within the created allocation. The general concept and design of the YTsaurus data processing system is similar to such systems as: * [Hadoop MapReduce](https://hadoop.apache.org/docs/r1.2.1/mapred_tutorial.html) and [Apache YARN](https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html). * [Apache Mesos](https://mesos.apache.org/). {% note info "Note" %} Please be aware that YTsaurus and Hadoop use different terminology: a YTsaurus operation would be called a job in Hadoop's terms, while a YTsaurus job would be called a task. {% endnote %} ## Architecture The components involved in running computations in a cluster include the scheduler, controller agents, and exec nodes. ![](https://ytsaurus.tech/_images/data_processing_overview.png) The **scheduler** is the key component that stores information about all operations, makes new allocations for operations, and implements hierarchical resource distribution. **Controller agents** are responsible for scheduling the jobs of individual operations. They contain the logic for the operation's execution plan, which breaks down the operation into individual jobs. The implementation correctly handles loss of individual jobs and ensures transactional execution of operations. An **exec node** is a process that runs on all compute nodes within the cluster. It handles the execution of individual jobs within allocations, prepares the environment for their execution, and ensures that the jobs are isolated from each other. At any moment, the cluster state consists of: - A single active scheduler that handles all allocation tasks. - Several independent controller agents that are responsible for scheduling operations. - A set of exec nodes that perform the computations. The scheduler and the controller agents store their state in Cypress, while exec nodes don't have a persistent state and can be easily added to and removed from the cluster. Let's make a short overview of the interaction between these components. The scheduler provides an API for starting new operations and managing operations that are already running. It also periodically reads its own configuration and the [pool](https://ytsaurus.tech/docs/en/scheduler/scheduler-and-pools.md) hierarchy from Cypress. In addition, the scheduler uses Cypress as a persistent storage for the operation's meta state. An operation's meta state is a small set of attributes that are necessary to manage transactional execution of the operation and ensure fault tolerance. When an operation is started, the scheduler initializes its state in Cypress and assigns it a controller agent, which will handle the job scheduling for that operation. Each exec node periodically sends a heartbeat to the scheduler, informing it about the status of current allocations and receiving new allocations. It also receives requests to preempt some of the existing allocations and abort any associated jobs. In addition, exec nodes send heartbeats to each controller agent, informing them of the status of currently running jobs and retrieving specifications for starting new ones. ## Features and capabilities Below is a description of the features and various capabilities provided by the YTsaurus data processing system. ### Fault tolerance YTsaurus guarantees reliability and availability of data processing. Even if individual components fail, the cluster can still recover and continue to function. In such situations, the progress of individual operations may be lost, but the operations' states are safely stored in Cypress and operations will be correctly recovered. In addition, the system aims to minimize loss of progress for running operations. For more information, see [this document](https://ytsaurus.tech/docs/en/reliability). ### Scalability The system offers a transparent way to add new exec nodes to the cluster in order to increase computational resources. It also supports the addition of extra controller agents, which is necessary for scaling up the number of operations that can be executed on the cluster. The largest YTsaurus cluster installations service more than 1M CPUs and can run tens of thousands of operations simultaneously. Note that the active scheduler is a single process, so the system's scalability is still limited. For more information about scheduler's scalability, see [this presentation](https://www.youtube.com/watch?v=Vy6rHf-BIG8) (in Russian). ### Resource management The scheduler provides a rich API for distributing cluster resources among different users and projects. The basic unit for managing resources is a **pool**. The pools form a hierarchy called a **pool tree**. For each pool, you can configure guaranteed resources, manage resource distribution weights, and set resource limits and various [preemption](https://ytsaurus.tech/docs/en/scheduler/preemption.md) settings. For more information about resource allocation, see [Scheduler and pools](https://ytsaurus.tech/docs/en/scheduler/scheduler-and-pools). ### Resource heterogeneity YTsaurus clusters can handle heterogeneous exec nodes and heterogeneous resource requirements of running jobs. For example, the same cluster can run both memory-intensive and CPU-intensive computations simultaneously. In addition, the system is designed to be reactive, which means that scheduler tries to start new jobs as quickly as possible using any available resources, while the efficient packing of jobs on the exec nodes is not a priority for the scheduling algorithm. Because of this, highly imbalanced loads may lead to fragmentation of resources in the cluster. ### GPU support Exec nodes are capable to start jobs that require GPU devices. In addition, the scheduler can be informed of the network topology on GPU hosts, and it will be taken into account for [gang operations](https://en.wikipedia.org/wiki/Gang_scheduling) scheduling. Internally, GPU resource is represented as a single integer indicating the number of devices, e.g. the total number of GPUs on a node or a number of GPUs required by a job. For this reason, it's recommended to split nodes with different GPU types into separate [pool trees](https://ytsaurus.tech/docs/en/scheduler/scheduler-and-pools#puly-i-derevya-pulov). ### Support for multiple data processing interfaces The YTsaurus data processing system not only implements the [MapReduce](https://ytsaurus.tech/docs/en/operations/overview.md) paradigm, but also supports running other data processing applications over shared computational resources. For example, you can run [YQL](https://ytsaurus.tech/yql) queries over YTsaurus, use [Flow](https://ytsaurus.tech/flow/about.md) for real-time streaming event processing, or create [ClickHouse cliques](https://ytsaurus.tech/docs/en/chyt/about-chyt) and [Spark clusters](https://ytsaurus.tech/docs/en/spyt/overview). ## Fault tolerance This article describes how the data processing system handles failure of individual cluster nodes and what guarantees are provided to YTsaurus cluster users. In terms of fault tolerance, the data processing system pursues the following objectives: - Ensure availability and reliability. - Preserve the progress of already completed jobs for running operations. ### Availability The availability of the data processing system is directly connected with liveness of the scheduler. Within the cluster, the scheduler is represented as several independent processes, which are typically executed on different nodes. Each process constantly tries to acquire a lock on the `//sys/scheduler/lock` node in Cypress until it succeeds. The succeeding process fetches meta information about current operations from Cypress, recovers its state, and becomes **active**. This means that it begins to handle user requests related to operations, process heartbeats from exec nodes, etc. If, for whatever reason, there is no active scheduler, the system can't schedule new jobs, make new allocations, or run new operations on the cluster. Essentially, the data processing system is unavailable. If the active scheduler fails, it takes some time for another process to acquire the lock and become active. This usually takes tens of seconds, the duration of such downtime depends on the following parameters: - The frequency of attempts to acquire the lock by the scheduler process. - The timeout of the transaction which is used for the lock acquiring. - The size of meta information about current operations. This value becomes significant if the cluster has thousands of running operations simultaneously, as it takes considerable time to fetch all of them from Cypress and recover the scheduler state. Usually users don't notice these periods of unavailability, since it is handled by retries in the YTsaurus SDKs. ### Reliability Remind that computations on the cluster are represented by operations. To be reliable, the data processing system must guarantee that running operations won't be lost or fail in case of failure of any data processing component in the cluster. The system guarantees that if the scheduler or any other component fails, all operations will be recovered and continue running. However, part of a running operation's progress may still be lost. The reliability of operations is ensured by Cypress, which safely stores meta information of current operations. When an operation is started, the system additionally creates transactions and acquires proper locks on input and output tables and files. This allows the operation to continue execution even if the input tables are removed or if the active scheduler has changed and operation revival happened. Note that these transactions have a timeout, which is typically a few hours. If the scheduler remains unavailable for a longer period then the new incarnation will attempt to restart operations from scratch. However, it may not have the necessary input or output tables or files. In this case, the operation aborts with an error. ### Preserving computation progress { #revival } Unlike with real-time cluster components, the execution of a single operation takes a significant amount of time: from minutes to days depending on the size of processed data, the available computational quotas, and the code executed within the jobs. Because of this, the requirements for the availability of the data processing system are rather relaxed: delays of a few minutes, and in most even a few dozens of minutes, are not supposed to affect the SLOs of systems that run their computations on YTsaurus clusters. To define exactly the guarantees for preserving the progress of computations, we first need to describe how the life cycle of operations and jobs is organized. The state of an operation can be divided into 3 parts: 1. The operation's meta information: its specification, set of Cypress transactions, input/output tables and files that are locked to run the operation, various runtime settings, and the current [operation state](https://ytsaurus.tech/docs/en/operations/overview#status). The scheduler essentially implements a finite-state machine that handles each operation, and the operation state is a node within it. 2. The state of the operation controller. An operation controller is an object that resides within the controller agent process and is responsible for the operation's execution. The controller manages information about which jobs are currently running within the operation, which ones have finished, which data has already been processed and which hasn't. Overall, this object encapsulates all the data processing logic for the operation as well as its state. 3. A set of all currently running and some of the finished jobs on the cluster's exec nodes. The scheduler *synchronously* writes the operation's meta information to the corresponding Cypress node, which resides in the `//sys/operations` subtree. The operation controller consistently writes its state to Cypress as a binary file. This file is called a **snapshot of the operation**. Each controller agent periodically generates snapshots of its current operations. Active jobs within each operation periodically report their state to the operation controller via heartbeats from exec nodes. Each exec node sends heartbeats with information about all relevant jobs to each controller agent. Let's consider the behavior of the data processing system in case of failures of individual components. * If the failed component is a cluster's exec node, after a small timeout (typically 1–2 minutes), the controller agents and the scheduler notice that the node has stopped sending heartbeats and as a result consider all allocations and corresponding jobs to be lost. These computations will be re-scheduled on the other cluster nodes and logically these computations will be represented as new jobs instead of the lost ones. * If the failed component is a controller agent, the current state of the operation controllers that were running on that agent is lost. In this case, the scheduler detects that the controller agent is unavailable and re-assigns all of its assigned operations to other controller agents. When re-assigned to a new agent, the operation controller is recovered from the last saved snapshot. While handling heartbeats from exec nodes, recovered operation controller gradually updates its state with the actual state of the cluster. * If the failed component is the active scheduler, each controller agent detects this situation, resets the inner state and starts waiting for the new active scheduler. The new scheduler reads the current meta information about operations, then re-distributes these operations across controller agents and starts handling user requests and node heartbeats. In this case, the operations are recovered from the latest snapshots written to Cypress. The process of recovering operations from a snapshot is called _reviving_. Note that if there's no snapshot, the operation is still revived but starts from scratch. Importantly, nodes also store information about completed jobs to report them to the controller if the operation needs to be revived — this helps avoid losing these jobs' progress. Let's consider an example of a situation where loss of information about a completed job can be avoided. Imagine there's a a job (`J`) within an operation (`O`) running on a node. The execution sequence can be as follows: #| || **Timestamp**| **Event** || || `X`| Job `J` is running on the node. The controller agent captures a snapshot of operation `O`. In this snapshot, the operation controller considers job `J` to be in progress. || || `X+1`| Job `J` completes, and the operation controller processes the completion event.|| || `X+2` | Operation `O` is revived (for example, after a restart of the controller agent). After recovery from the snapshot, the controller of operation `O` assumes that job `J` is still running.|| || `X+3` | While handling a heartbeat from the node on which job `J` was running, the operation controller notices that it isn't presented among currently running jobs. This prompts it to request information about all previously completed jobs from the node. In the next heartbeat, the node informs the controller that job `J` has completed. The controller processes this event and doesn't start a new job.|| |# For more information about the revival mechanism, see this [report](https://ytsaurus.tech/other/video-posts.md#2019-map-reduce-operaciya-dlinoyu-v-god). It should be noted, however, that this mechanism has undergone a number of changes since the report's publication. In summary: - If the scheduler or one of the controller agents fails, it takes a few minutes to recover the state. In this case, some of the progress since the last saved operation snapshot is lost. - If an exec node fails, progress is lost for all jobs that were running on that node. #### Consistency of scheduler and controller agent states The communication protocol between the scheduler and controller agents is fairly complex and extensive. It's important to understand how the system ensures consistency of their states and actions in the event of individual component failure or loss of network connectivity. This consistency is primarily achieved by *coupling* the controller agents with the active scheduler. Below is a detailed explanation of how the scheduler and agents become active and what this *coupling* means. Let's recall how the active scheduler is selected. Each scheduler process constantly creates a transaction and tries to acquire an exclusive lock on a Cypress node within that transaction. The scheduler that succeeds becomes active. It reads meta information about all operations from Cypress and recovers their state. Controller agents operate as follows: - First, an agent establishes a connection with the scheduler and receives the transaction in which the scheduler became active. - Then, the agent creates its own transaction, which is nested within the scheduler transaction, and attempts to acquire a lock on the `//sys/controller_agents/instances//lock` node. Typically, the agent doesn't have to compete with other processes to acquire the lock, but it may conflict with its previous incarnation. - When the agent succeeds in acquiring the lock, it also becomes *active*. Once the agent has become active, the scheduler starts assigning it operations, and the agent schedules the jobs for these operations. The agent transaction serves several functions: - It allows the scheduler to manage the agent and ensures that the agent is coupled with the currently active scheduler. With this transaction, the scheduler can disconnect the controller agent in various scenarios, such as loss of network connectivity or receiving outdated messages. To do this, it simply needs to abort this agent's transaction. In addition, if the scheduler transaction is aborted, all associated agent transactions are aborted as well. - It's used by the agent as a prerequisite for its interactions with Cypress. This ensures that the operation state in Cypress can be changed exclusively by the agent to which this operation is assigned. - It's used as a session ID for communication with the scheduler. Specifically, agent transaction is passed within each message from agent to scheduler. If the scheduler starts processing a message and detects that the current agent transaction differs from the one in the message, this means that the message came from a previous incarnation and should be ignored. All of this ensures that an operation is handled by only one controller agent at any given time. # Scheduler and pools This section describes the YTsaurus scheduler, introduces the concepts of a pool and pool tree (pool_tree), reviews the process of scheduling calculations, and provides an example of calculating the fair share ratio and a description of preemption. ## General information A scheduler is a part of the system responsible for allocating resources between operations and determining the order in which operations will be performed. The YTsaurus system uses a hierarchical [fair-share](http://en.wikipedia.org/wiki/Fair-share_scheduling) scheduler. This means that resources are dynamically allocated between system users according to their guarantees and priority. A pool is a container for the CPU and RAM resources that the scheduler uses. When users run tasks in pools, the scheduler allocates resources between the pools. A hierarchical resource allocation system can be built using the pool tree. ## Resources The scheduler uses the following resources: - `cpu`: 1 CPU corresponds to one HyperThreading core on the cluster node. By default, all custom jobs order a single core at start-up. If jobs are multi-threaded, you can increase the number of ordered cores in the operation settings. Otherwise, process jobs will be limited (throttling) so that the amount of actual CPU time consumed corresponds to a single core. - `memory`: The resource to control RAM consumption on the cluster node in bytes. - `user_slots`: The resource to control the number of jobs on the cluster node. Each job of custom operations always consumes exactly one user_slot. - `network`: The resource to control consumption of the internal cluster network by operations actively using the network. For example, sorting. The resource unit has no physical analog, and the automatic values set by the scheduler for sorting operations are selected so that jobs running on the same cluster node don't overload the network channel. For each pool and operation, there is a concept of a dominant resource. The dominant resource among the requested operation or pool resources is the resource type that has the highest share relative to the total volume of the corresponding resource of the entire cluster. For example: there is an operation that requires 1,000 `cpus` and 2 TiB of `memory`. The entire cluster has 50,000 `cpus` and 180 TiB of `memory`. 1,000 `cpus` are 0.02 (2%) of the total number of `cpus` in the cluster and 2 TiB are 0.011 (1.1%) of the total `memory` capacity in the cluster. Thus, the `cpu` share is larger than the `memory` share, so `cpu` is the dominant resource for a particular operation. ## Scheduler operation description { #scheduler } ### Pools and pool trees The YTsaurus scheduler stores multiple pool trees in memory. Each tree consists of operations and compute pools. Compute pools are tree nodes and operations are leaves. Each operation can belong to one or more trees. Logically, each tree can be regarded as a separate computing cluster — each tree belongs to a number of computing nodes in the cluster, and a cluster node can only belong to one tree. Pool trees are stored in Cypress and configured by the YTsaurus administrator. The pool tree is the `scheduler_pool_tree` node at the first level of the `//sys/pool_trees` directory in Cypress. The description of the pools for the `` tree is a set of nested `scheduler_pools` located in the `//sys/pool_trees/` node. The pool characteristics are described in the attributes of the appropriate nodes. The trees to which the operation will get at start-up are defined by the `pool_trees` parameter in the operation settings. If this parameter is not specified, one `default_pool_tree` will be used. For each of the listed trees, you can specify the pool from that tree in which the operation is to run (the `scheduling_options_per_pool_tree` option). Each pool tree has the `default_parent_pool` attribute, which specifies the default pool. This pool is used as follows: if the pool was not specified for the tree or for all trees in the operation settings, a temporary ephemeral pool will be created in the tree with the name of the user in YTsaurus who started the operation. The pools form a hierarchy `//sys/pool_trees///` in the end node of which the operation will be run. Example of the setting for running an operation in two trees with indication of the pool in one of them: ```yson pool_trees = [default; cloud]; scheduling_options_per_pool_tree = { default = { pool = cool_pool; } } ``` ## Job scheduling process Each operation consists of a set of jobs. A job is doing some useful work with data on one of the cluster nodes. A job is a scheduling unit. Job scheduling takes place on each cluster node separately: 1. The cluster node sends a special message (heartbeat) to the scheduler, indicating the type and amount of available resources, information about the completed and currently running jobs. 2. In response to this message, the scheduler decides whether to start new jobs and possibly abort those that are in progress. Scheduling takes place in the tree to which the cluster node belongs. 3. All aborted jobs will later be restarted. To understand which jobs should be started and which should be aborted on a particular cluster node, the scheduler analyzes the pool tree. Each pool in the tree has a number of characteristics that enable the scheduler to understand whether a given subtree needs to start new jobs, and, if it does, how much this subpool needs it. According to the pool characteristics, the scheduler goes down the tree from top to bottom until it reaches a leaf. After reaching the leaf, the scheduler suggests that the operation should start the job. The scheduler makes such bypasses as long as there are unscheduled jobs and the cluster node resources are not exhausted. There may be operations to which the scheduler allocated less resources than it was obliged to guarantee. Such operations are called [starving operations](https://ytsaurus.tech/docs/en/preemption.md) in the system. When the scheduler detects a starving operation, it selects the job with the minimum running time of the non-starving operation on the same cluster node and aborts it. {% note info "Note" %} The operation and pool characteristics considered during scheduling are global and do not depend on a particular cluster node. For example, an important characteristic is the share of cluster resources that are already taken up by running operation jobs (`usage_share`). {% endnote %} # Setting up pool trees This section describes how to set up compute pools and pool trees for allocating computing resources within the cluster. ## Pool tree configuration { #pool_trees } Trees are represented by `scheduler_pool_tree` objects at the first level of the `//sys/pool_trees` node in Cypress. The only child nodes these pools can have are compute pools. To specify the configuration of a tree, use the `config` attribute. The `//sys/pool_trees` node also has the `default_tree` attribute — you can use it to set the default tree for operations that don't have a set `pool_trees` option. The tree configuration has the following options: - `node_tag_filter` (previously `nodes_filter`): A filter for selecting cluster nodes that belong to the tree. For more information, see [Tagging cluster nodes](https://ytsaurus.tech/admin-guide/node-tags.md). - `main_resource`: The main tree resource. If the pool has guarantees, they must contain this resource. Guarantees for non-specified resources are computed proportionally to the main resource. - `max_operation_count`: The default value for the maximum number of concurrently started (running + pending) operations in the tree. - `max_operation_count_per_pool`: The default value for the maximum number of concurrently started operations in some pool if `max_operation_count` is not specified for this pool. - `max_running_operation_count`: The default value for the maximum number of concurrently running operations in the tree. - `max_running_operation_count_per_pool`: The default value for the maximum number of concurrently running operations in the pool if `max_running_operation_count` is not specified at the pool level. - `default_parent_pool`: The default pool for running operations that do not explicitly specify a pool in their configuration. - `forbid_immediate_operations_in_root`: Prohibits operations from running in the root pool of the tree. - `max_ephemeral_pool_per_user`: The maximum number of ephemeral pools for each user. An ephemeral pool is a pool that was indicated in the specification, but for which there is no explicit node in Cypress. - `infer_weight_from_guarantees_share_multiplier`: Specifying this multiplier enables inference of the pool weight proportionally to the pool guarantee based on the `main_resource` (more precisely, proportionally to the strong guarantee share). Inference applies only to those pools that do not have an explicitly set weight. - `integral_guarantees`: Settings for managing [integral guarantees](https://ytsaurus.tech/docs/en/integral-guarantees.md) in the given tree. - `pool_config_presets`: Description of pool configuration presets. - `preemptive_scheduling_backoff`: Limit on how often allocations can be scheduled with preemption on a single node. - `fair_share_starvation_timeout`: If an operation doesn't receive its resources before the timeout expires, it enters a starving state, and its allocations can be scheduled with preemption. - `fair_share_starvation_tolerance`: Threshold value indicating that an operation isn't receiving enough resources. An operation is considered starving (`below_fair_share` status) if `usage_share < fair_share * tolerance` for the dominant resource. { #fair-share-starvation-tolerance } - `preemption_satisfaction_threshold`: Threshold value that determines whether allocations are preemptible (including aggressively preemptible) or non‑preemptible. Allocations where the cumulative `usage_share` exceeds the `fair_share * threshold` are considered preemptible, while all others are either non‑preemptible or aggressively preemptible. - `non_preemptible_resource_usage_threshold`: The operation will not be subject to preemption if its resource usage is less than this value. - `allocation_preemption_timeout`: An allocation preemption timeout. If a job in a given allocation supports interruption, the system ensures that the job has enough time to complete successfully before termination. - `aggressive_preemption_satisfaction_threshold`: Threshold value that determines whether allocations are non-preemptible or aggressively preemptible (or just preemptible). Allocations where the cumulative `usage_share` exceeds the `fair_share * threshold` are considered either aggressively preemptible or just preemptible. This threshold must not exceed `preemption_satisfaction_threshold`. ## Pool configuration { #pools } A pool is an object of the `scheduler_pool` type. Each pool belongs to a specific pool tree. The pool hierarchy is an integral part of Cypress and is located at `//sys/pool_trees/`. When creating a pool, specify the pool tree and the parent pool within the tree. You can set the pool's options using attributes. Below you can find the options available in the pool configuration (default values are given in parentheses): * `weight` (none): A real non-negative number specifying the proportion of resources from the parent pool to be allocated to this subtree. For example, if a pool has two child pools with weights 2 and 1, the parent's resources will be split between them in a 2:1 ratio. * `strong_guarantee_resources`: A dictionary which lists guaranteed resources for the given pool (`user_slots`, `cpu`, `memory`, and `gpu`). {% note info "Note" %} For the guarantees to be fulfilled, the dominant resource of the operation has to match the guaranteed resource. {% endnote %} * `resource_limits`: A dictionary which describes the limits on different resources for the given pool (`user_slots`, `cpu`, and `memory`). * `mode` (`fair_share`): The scheduling mode that can take the `fair_share` or `fifo` values. In `fifo` mode, jobs are issued to child operations in lexicographic sorting order according to the values of the parameters specified in the `fifo_sort_parameters` attribute for the pool. For example, if the attribute value is `[start_time]`, jobs will be issued to the operations with the least start time. * `max_running_operation_count` (8): The limit on the number of concurrently running operations in the pool. Operations above this limit will be queued up and pending. * `max_operation_count` (50): The limit on the number of concurrently started (running + pending) operations in the pool. If the specified limit is reached, starting new operations in the pool will end with an error. * `fifo_sort_parameters` (`[weight, start_time]`): The order of starting operations in the FIFO pool. By default, operations are sorted first by weight and then by start time. This parameter enables you to change the order of operations in the queue. Supported values: `start_time`, `weight`, and `pending_job_count`. Sorting by `weight` is done in reverse order: operations with greater weight go first and have a higher priority. The `pending_job_count` value enables you to prioritize smaller operations (with few jobs). * `forbid_immediate_operations`: Prohibits the start of operations directly in the given pool. This restriction does not apply to starting operations in subpools. * `create_ephemeral_subpools` (false): Activates the mode in which an ephemeral subpool is created for operations in the given pool. This pool will have the name `poolname$username`, where `poolname` is the name of the current pool. The operation is started in the created ephemeral subpool. * `ephemeral_subpool_config`: A nested configuration of ephemeral subpools created in the current pool. Makes sense only if the `create_ephemeral_subpools` option is specified. You can specify the `mode`, `max_running_operation_count`, `max_operation_count`, and `resource_limits` in the configuration. * `offloading_settings`: A setting responsible for offloading some of the pool jobs to another tree along with the tree where the pool resides. Job offloading works for operations started after the setting was enabled. Example: `yt set //sys/pool_trees///@offloading_settings '{={pool=}}'`. * `config_presets`: A list of presets applied to the given pool. - `enable_aggressive_starvation` (false): Enables [aggressive preemption](https://ytsaurus.tech/docs/en/preemption.md#aggressive-preemption) for operations within a specific pool and its descendants. - `allow_aggressive_preemption` (true): Allows [aggressive preemption](https://ytsaurus.tech/docs/en/preemption.md#aggressive-preemption) of allocations for all operations within a specific pool and its descendants. Each pool has its _unique_ (in one given tree) name. The name of the pool selected to start the operation is shown in the operation settings. {% note warning "Attention" %} The `max_running_operation_count` and `max_operation_count` parameters are set (explicitly or implicitly) at all levels of the pool hierarchy and are also checked at all levels. To be specific, starting a new operation in pool P is possible if and only if both P and all of its parents (up to the root) have fewer started operations than a given limit. The same applies to the `max_running_operation_count` limit: an operation in pool P will remain in pending state as long as the number of running operations in that pool or in any of its parents is larger than or equal to the `max_running_operation_count` limit. You can oversubscribe these limits at each level of the hierarchy. You should keep this in mind when ordering or changing user pool limits. To guarantee a certain number of operations to the pool, make sure that there is no oversubscription at all levels up to the root. {% endnote %} ### Operation resources configuration { #operations } Some of the settings inherent to the pool are also available for operations and are specified in the operation specification root at start-up. These settings include `weight` and `resource_limits`. For example, you can specify `resource_limits={user_slots=200}` in the specification, meaning the operation will not run more than 200 jobs concurrently in each tree. The `resource_limits` setting and other parameters can be specified independently for different pool trees. ## Pool setup example { #example } Let's look at the pool structure in a project whose operations can be categorized into three groups: manual requests (ad hoc), batch processing, and backups. The project has a quota of 100 compute cores and standard limits on the [number of operations](https://ytsaurus.tech/docs/en/operations-limits.md). We need to set up pools so that: - Ad hoc operations are performed as quickly as possible and spend less time in the queue before running. - Batch processing requires the use of all available resources when there are few or no ad hoc tasks. - Backup operations require a small resource guarantee to make the process complete within the allotted time. To do this, we need to create separate pools for each load type and place them in the project's shared root pool (`project-root`). The pool settings are shown in the table. | pool_name | strong_guarantee_resources/cpu | weight | max_running_operation_count | max_operation_count | | ----------------------------- | ------------------------------- | ------ | --------------------------- | ------------------- | | `project-root` | 100 | 1 | 10 | 50 | | `project-root/project-adhoc` | 80 | 1 | 10 | 50 | | `project-root/project-batch` | 0 | 10 | 4 | 50 | | `project-root/project-backup` | 20 | 1 | 2 | 50 | Note that the sum of guaranteed resources allocated to descendants cannot exceed the parent pool’s guarantee. Note that there is no such restriction for limits on the number of operations; nevertheless, one basic requirement must be met: the `max_running_operation_count` value of each pool can't be greater than `max_operation_count`. If the sum of `max_running_operation_count` limits of the subpools exceeds the same limit for the parent pool, this may lead to an operation in a subpool not being able to be executed even if the number of running operations in the pool has not reached the limit. This may occur if there are too many running operations in other subpools and their number has reached the `max_running_operation_count` limit of the parent pool. In this case, even if a subpool has a guarantee, it will not preempt operations already running in other pools. Only jobs can be preempted (if the operation has more than one), but not operations themselves. In the pool structure shown in the table, all guaranteed computing resources go to `project-adhoc` and `project-backup` pools when they are at maximum capacity, and jobs of operations running in `project-batch` are preempted. However, if operations in at least one of the guarantee pools cannot fully utilize their pool, unclaimed resources can be allocated to the `project-batch` pool, which has a higher priority. ## Dynamic characteristics of operations and pools The scheduling algorithm allocates cluster resources across compute pools and operations. This information is available on the Scheduling page and on the operation page in the UI. For example, you'll find the following characteristics there: * `Fair share` is the share of cluster resources that is guaranteed for the operation (or pool) at this moment. * `Usage share` is the share of cluster resources that is currently consumed by the operation or pool: all allocations of the operation or all operations in the pool subtree. * `Demand share` is the share of cluster resources that the operation or pool needs to run all allocations. * `Starvation status` is a flag indicating whether the operation is starving. If there is a starving operation, the scheduler will attempt to abort the jobs of non-starving operations (perform [preemption](https://ytsaurus.tech/docs/en/preemption.md)) to run the allocations of the starving one. * `Dominant resource` is the dominant resource of an operation or pool — the resource whose share in the resources of the entire cluster is the largest. All the shares described above constitute a vector of shares for each cluster resource. If the share is expressed as a single number, this is the share for the dominant resource. Dynamic characteristics of operations are calculated for each tree separately. Consequently, an operation can have multiple values of `Fair share`, `Usage share`, and other parameters. # How the fair share ratio is calculated There is a pool tree, where the internal nodes are pools and the leaves are operations. Each operation in the pool tree has usage and demand — resource vectors that specify the current resource consumption of the operation and the maximum possible resource consumption. These characteristics (usage and demand) are naturally summed across the tree, making it possible to refer to usage and demand of the internal nodes of the tree. In addition, a resource vector can be turned into a non-negative number by taking the ratio of the dominant vector resource to the total amount of that resource on the cluster. This makes it possible to look at the usage ratio and demand ratio in each node of the tree. When calculating the fair share ratio for all nodes of the tree, a procedure is performed recursively to divide the fair share in a given node among its children. In the root, the division starts with a fair share equal to one. When distributing the fair share to the children, the following task is solved: Let `F` be the fair share value to be distributed to the children: `c_1, c_2, ... c_n`. The weight of the children is marked as `w_1, w_2, ... w_n` . The children also have an upper fair share limit denoted as `u_i`. This limit is calculated based on the demand, resource granularity, set resource limits, and so on. The lower fair share limit is denoted as `l_i` (set by the min share ratio). The algorithm that divides the fair share searches for an `x` to execute: ![](https://ytsaurus.tech/_images/fair_share_formula.png){ .center } If the total `min share ratio` of the children in the pool is greater than one, it is normalized to 1. In general, `min share ratio` is a complicated characteristic and we do not recommend setting it for nested pools. Otherwise, the task described above would have no solution. A few examples of calculating the fair share ratio: Let there be two pools in the root: `A` and `B`. The min share ratio of the `A` pool is 0.6 and the min share ratio of the `B` pool is 0.2. Let large operations with a very high demand run in both pools. 1. The weight of both pools is one. In this case, after calculating the fair share, the fair share ratio of the `A` pool is 0.6 and the fair share ratio of the `B` pool is 0.4. 2. The weight of the `A` pool is three and the weight of the `B` pool is one. Then the fair share ratio of the `A` pool is 0.75 and the fair share ratio of the `B` pool is 0.25. 3. The weight of both pools is one. Suppose that in addition to `A` and `B` pools there is a `C` pool that has no min share ratio and has a weight of 1. Then the fair share ratio of the `A` pool is 0.6, the fair share ratio of the `B` pool is 0.2, and the fair share ratio of the `C` pool is also 0.2. # Preemption When certain operations run for too long, they can take too many resources and block other operations from starting new jobs. As a result, the other operations are starved of resources. To solve this problem, the scheduler has a preemption mechanism. Based on its usage share and fair share, an operation can have one of two scheduling statuses: * `normal`: The operation gets enough resources. * `below_fair_share`: The operation receives less resources than its fair share (based on the configured [tolerance](https://ytsaurus.tech/docs/en/#fair-share-starvation-tolerance)). If an operation remains in the `below_fair_share` status for too long, it begins to starve. In this case, the scheduler can use preemption to allocate resources to it. In terms of starvation, each operation can be in one of three states: * `non_starving`: The operation doesn't need preemption to schedule its allocations. * `starving`: The operation is short on resources, so the standard [_preemption_](https://ytsaurus.tech/docs/en/#preemption) mechanism is applied when scheduling its allocations. * `aggressively_starving`: The operation is starving, so the [_aggressive preemption_](https://ytsaurus.tech/docs/en/#aggressive-preemption) mechanism is applied to schedule its allocations. At any time, the allocations of each operation are divided into three groups: * `non_preemptible`: Preemption can't be applied. * `aggressively_preemptible`: Preemption is applied only as part of aggressive preemption. * `preemptible`: Preemption is applied unconditionally. This division occurs as follows: all allocations of an operation are ordered by the start time of the last job. Within this order, non-preemptible allocations come first, followed by aggressively preemptible allocations, and then unconditionally preemptible ones. Allocations are divided into these groups based on the `aggressive_preemption_satisfaction_threshold` and `preemption_satisfaction_threshold`. These thresholds set the cutoff values for the ratio of an operation's usage share to its fair share, calculated from the prefix of the allocations. ## How preemption works { #preemption } The preemption mechanism is part of allocation scheduling. This means the decision to preempt an allocation is made when processing the heartbeat from the node where that allocation resides. Preemption can occur for three reasons: 1. To launch an allocation for a starving operation. 2. When the resources consumed by allocations on a node exceed the node's available resources. 3. When an operation or one of its parent pools exceeds the `resource_limits`. In practice, the vast majority of preemptions occur for the first reason. The preemption mechanism works according to the following algorithm. When processing a heartbeat, new allocations are scheduled in several stages: regular scheduling, preemptive scheduling, and aggressively preemptive scheduling. While other stages exist, they aren't discussed in this guide. Each stage is defined by a subset of operations involved in scheduling new allocations and the resources available to run them: * During the regular scheduling stage, the scheduler considers all operations, with the available resources being the free resources on the node. * During the preemptive scheduling stage, only starving and aggressively starving operations are considered. The available resources include both the free resources on the node and the resources of preemptible allocations on that node. * During the aggressive preemptive scheduling stage, the scheduler considers only aggressively starving operations. The available resources include the node's free resources plus the resources of preemptible and aggressively preemptible allocations on that node. Keep in mind that preemptive scheduling uses a configurable backoff, so this stage may not be triggered on every node heartbeat. In addition, only one allocation can be scheduled at the preemptive scheduling stage. If a new allocation is scheduled at this stage, the scheduler selects the smallest subset of preemptible jobs required to accommodate it, considering allocations in order from newest to oldest. ## Aggressive starvation and preemption { #aggressive-preemption } This mechanism is necessary to prevent fragmentation and schedule large allocations. Without this mechanism, most of the resources on each of the cluster nodes may be occupied by non‑preemptible allocations. In this case, an operation that requires a large allocation of resources (for example, half of a node's total resources) is unlikely to be scheduled. Suppose the aggressive preemption mechanism is enabled, and the `aggressive_preemption_satisfaction_threshold` is set to 0.5. This means at least half of the resources across all allocations on that cluster are preemptible. It also implies that there is a node with half of its total resources aggressively preemptible. This makes it possible to schedule large allocations for aggressively starving operations, provided they don't exceed half of a node's total resources. Note that the aggressive preemption mechanism has clear drawbacks: it can preempt allocations running within their fair share, which may be unacceptable for production operations with execution‑time SLAs. ## Preemption settings You can set the main preemption settings in the pool tree configuration. Some parameters can also be overridden in the pool options. - `preemptive_scheduling_backoff`: Limit on how often allocations can be scheduled with preemption on a single node. - `fair_share_starvation_timeout`: If an operation doesn't receive its resources before the timeout expires, it enters a starving state, and its allocations can be scheduled with preemption. - `fair_share_starvation_tolerance`: Threshold value indicating that an operation isn't receiving enough resources. An operation is considered starving (`below_fair_share` status) if `usage_share < fair_share * tolerance` for the dominant resource. { #fair-share-starvation-tolerance } - `preemption_satisfaction_threshold`: Threshold value that determines whether allocations are preemptible (including aggressively preemptible) or non‑preemptible. Allocations where the cumulative `usage_share` exceeds the `fair_share * threshold` are considered preemptible, while all others are either non‑preemptible or aggressively preemptible. - `non_preemptible_resource_usage_threshold`: The operation will not be subject to preemption if its resource usage is less than this value. - `allocation_preemption_timeout`: An allocation preemption timeout. If a job in a given allocation supports interruption, the system ensures that the job has enough time to complete successfully before termination. - `aggressive_preemption_satisfaction_threshold`: Threshold value that determines whether allocations are non-preemptible or aggressively preemptible (or just preemptible). Allocations where the cumulative `usage_share` exceeds the `fair_share * threshold` are considered either aggressively preemptible or just preemptible. This threshold must not exceed `preemption_satisfaction_threshold`. - `enable_aggressive_starvation` (false): Enables [aggressive preemption](https://ytsaurus.tech/docs/en/preemption.md#aggressive-preemption) for operations within a specific pool and its descendants. - `allow_aggressive_preemption` (true): Allows [aggressive preemption](https://ytsaurus.tech/docs/en/preemption.md#aggressive-preemption) of allocations for all operations within a specific pool and its descendants. # Integral guarantees The basic mechanism for providing guarantees for computing resources in the YTsaurus system stipulates issuing a constant amount of resources (strong guarantee), most often expressed in CPU cores. This guarantee type is well suited for tasks that can utilize resources constantly . In reality, there are other scenarios where more flexibility is required. For example, when a production process needs to perform computations in a relatively short period of time, but needs a lot of computational quota at its peak, whereas the consumption level can be much lower the rest of the time. With the basic mechanism for providing a guarantee, you need to allocate resources to the process at the maximum consumption value and order more resources when scheduling than you actually needed. There are processes for which it is not so important to get computational resources immediately, but it is important that the processes be completed within a certain extended period of time. If that's the case, you are looking at an average value of the computational quota over a long time: for example, research and analytical tasks - experiments. The mechanism of integral guarantees enables you to find a balance between the described scenarios. ## Integral pools In the YTsaurus system, you can enable the accumulation of resources for the pool. Such pools receive a certain amount of virtual resources at a constant rate — `accumulated_resource_ratio_volume`. The amount of resource is stored in the cluster share per second — `fair_share*sec`, but for simplicity we can assume that the value is given in cores per second — `cpu*sec`. The pool can consume a virtual resource to start operations and can accumulate it to a certain limit. Example: let there be `accumulated_resource_ratio_volume = 60 fair_share*sec` in the pool, 1000 cores on the cluster, and the dominant resource of the user process is CPU. This volume can be converted to `cores * seconds` — `60,000 cpu*sec`. An operation that consumes 100 CPU cores at a time will use that amount in 10 minutes; one that consumes 50 CPU cores — in 20 minutes. This example does not take into account that the volume of the virtual resource in the pool is replenished over time. The resource accumulation rate is configured in the `integral-guarantees/resource_flow` pool attribute. The attribute value must specify the resource type and its volume: ```bash $ yt set //sys/pools/root-pool/burst/@integral-guarantees/resource_flow "{cpu=100}" ``` Since `resource_flow` is the rate at which `accumulated_resource_ratio_volume` increases, which is the instantaneous amount of resource multiplied by time, `resource_flow/cpu` is measured in cores, simply put, `cpu*sec/sec = cpu`. For example, `resource_flow/cpu = 100` allows an operation consuming 100 cpu cores at a time to run indefinitely. Accumulation of `accumulated_resource_ratio_volume` occurs to a certain limit, which is equal to `k*resource_flow_ratio`, where `k` is a single scheduler parameter for all pools and `resource_flow_ratio` is `resource_flow` as a share of all cluster resources. The value for the `k` parameter is set by the cluster administrator. The described idea of accumulating and spending resources is much like the [Token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm (not to be mistaken for [Leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket)). ## Guarantee types There are three types of guarantees for computing resources in the YTsaurus system: - **strong_guarantee**: A strong guarantee of the share of cluster resources. Set in an absolute value (in cores), automatically recalculated into the cluster share. Enables you to get a guarantee at any time and use it indefinitely. - **burst_integral**: An immediate guarantee ("peak guarantee") in cores, enables you to get a fixed guarantee over a period of time. For example, get 2000 cores for two hours a day. - **relaxed_integral**: An integral guarantee (in cores) enables you to get a fixed average amount of resource per day. ### Types of integral pools To support the two scenarios described at the beginning of this section, two types of integral pools are implemented in the YTsaurus system: - **Burst pool**: The pool that has a priority when spending `accumulated_resource_ratio_volume`. Such pools must have `resource_flow` and `burst_guarantee_resources` specified. If a pool has resource-demanding operations, the scheduler must issue at least `burst_guarantee_resources` to the pool if the pool has enough `accumulated_resource_ratio_volume`. This pool type provides a `burst_integral` guarantee. A burst pool configuration example: ```bash yt set //sys/pools/root-pool/burst/@integral-guarantees " { guarantee_type=burst; resource_flow={cpu=1000}; burst_guarantee_resources={cpu=2000} }" ``` In the above example, the user can expect the pool to always be guaranteed 1000 cores, or guaranteed 2000 cores for "half" of the time (for example, 12 hours a day) if nothing is running during the second half. A burst guarantee is simultaneously a limit on the consumption rate of the accumulated resource. If the burst_gurantee_resources/cpu guarantee equals 2000, the scheduler will not allow the pool to consume more than 2000 cores at a time by default. This behavior protects against accidental rapid consumption of the accumulated integral resource. - **Relaxed pool**: The pool for which there is no guarantee to get all resources for `accumulated_resource_ratio_volume` at a given moment, but there is a guarantee to get them in the end (within a day). You can only set `resource_flow` for a relaxed pool. Such pools are expected to run operations that do not immediately require the resources they are entitled to. This pool type provides a `relaxed_integral` guarantee. A relaxed pool configuration example: Listing 3 ```bash yt set //sys/pools/root-pool/relaxed/@integral-guarantees " { guarantee_type=relaxed; resource_flow={cpu=1000} }" ``` Both relaxed and burst pools have a resource consumption limit. In the case of relaxed pools, the limit is three times the value of the flow guarantee. For example, with a resource_flow/cpu guarantee of 1000, the scheduler will limit pool consumption to 3000 cores at a time by default. The described pool types complement each other, allowing the use of the same resources by priority processes that require significant guarantees for limited periods of time as well as by less significant processes which do not require immediate resource allocation. ### Example Suppose there is a production process that requires 2000 CPUs 12 hours a day and a research process that needs only 1000 CPUs on average. With the mechanism of integral guarantees, burst and relaxed pools can be configured for such processes, and 2000 cores will be needed to provide guarantees. The specified pools must be located in the same pool tree, there are no other restrictions. With the basic mechanism of issuing guarantees (strong guarantee), 3000 cores would have to be ordered to enforce such guarantees. The figure shows a graph of CPU usage and demand for a burst pool with a `burst_integral` guarantee of 500 CPU cores and a `resource_flow` of 100 CPU cores. ![](https://ytsaurus.tech/_images/pool_consumption.png) The figure below shows a graph of the volume of accumulated resource for the same pool. You can see how the volume of virtual resource decreases when operations are running in the pool, and then the resource is again accumulated to the specified limit. ![](https://ytsaurus.tech/_images/pool_accumulated_resource_ratio_volume.png) ## Combination with strong guarantees Both types of integral pools are naturally compatible with strong guarantees. When issuing resources, the scheduler will first issue resources against strong guarantees. If they are enough to meet the current need of the pool, `accumulated_resource_ratio_volume` will not be spent. If the need for resources exceeds the strong guarantees, `accumulated_resource_ratio_volume` will be spent to meet the exceeding part. Integral pools along with neighboring pools participate in receiving resources in excess of their guarantees from the pools that are higher in the hierarchy in proportion to the set weights. ## Additional attributes of integral pools You can request the scheduler for the value of service attributes, which are available in a special subtree of Cypress called Orchid. - `accumulated_resource_ratio_volume`: Accumulated integral resources in terms of the cluster share * second. - `accumulated_resource_volume`: `accumulated_resource_ratio_volume` as a dict with all resources. For example, `accumulated_resource_volume/cpu` is a volume of the accumulated integral resource expressed in core * second. - `integral_pool_capacity`: The limit of accumulating `accumulated_resource_ratio_volume`. - `specified_burst_ratio`: `burst_guarantee_resources` converted into the cluster share. - `specified_resource_flow_ratio`: `resource_flow` converted into the cluster share. - `total_burst_ratio`: The sum of `specified_burst_ratio` for all descendants (including the current pool). - `total_resource_flow_ratio`: The sum of `specified_resource_flow_ratio` for all descendants (including the current pool). - `estimated_burst_usage_duration_seconds`: The estimated period of time that the accumulated resource will last (taking into account the continuing inflow of resources at the rate of `resource_flow`) when consuming `burst_guarantee_resources` (only available for burst pools). An example of requesting an attribute is shown in Listing 4. Listing 4 ```bash yt get //sys/scheduler/orchid/scheduler/pool_trees//pools//integral_pool_capacity ``` {% note warning "Attention" %} The scheduler Orchid is not part of the stable API and can be changed without announcement. {% endnote %} ## Limits on the number of operations YTsaurus has limits on the number of operations running in compute pools. These limits serve as a protective mechanism that controls the load on the scheduler. There are two types of limits on the number of operations: - `total_operation_count` limits the total number of operations. This reduces the load from maintaining the state of the pool tree and performing periodic recalculations of the fair share. - `running_operation_count` limits the number of running operations. This reduces the load from scheduling new allocations. If a pool has reached its limit on the total number of operations, attempting to run the `start_operation` command will result in an error. If an operation starts successfully, but the pool has already reached its maximum number of running operations, the operation enters the `pending` state, gets queued, and waits for the currently running operations to complete. In the pool hierarchy, operations count toward the limits of all their ancestor pools. The described rules apply if at least one ancestor pool reaches its limit. Generally, limits from different pools are independent of each other. There may be cases where the combined limits of all child pools exceed the limit of the parent pool. ### Lightweight operations The bulk of the scheduler's load comes from processing exec node heartbeats and scheduling new allocations. This load directly depends on the number of operations currently involved in the scheduling process. These are called `schedulable` operations. For example, a running operation may be non-`schedulable` if it already has all the necessary allocations. Since the `schedulable` status can change while an operation is in progress, it's difficult to enforce a limit on the number of such operations. For this reason, the system manages the load by implementing a conservative restriction on the number of currently running operations. Some operations may be non-`schedulable` for much of their duration. For example, an operation may consist of a single small job that requires minimal time for the scheduler to initiate. In this scenario, the operation continues to count toward the limit on running operations, which can be a valuable resource in large YTsaurus installations. To address cases like this, the service supports a special type of operations called *lightweight operations*. These operations don't count toward the `running_operation_count`, meaning the limit on the number of running operations doesn't apply to them. However, lightweight operations still count toward the `total_operation_count` and the special `lightweight_running_operation_count` counter, which doesn't have a limit. For an operation to be considered lightweight, it must meet several conditions: - The operation must be of the [Vanilla](https://ytsaurus.tech/docs/operations/vanilla.md) type. - The pool where the operation is running must be configured in `FIFO` mode. - The pool must allow lightweight operations. This is controlled by the `enable_lightweight_operations` setting, which can be set by the YTsaurus cluster administrator. #### Recommendations for use You may run non-lightweight operations in a pool that has lightweight operations enabled. These operations count toward the `running_operation_count` and are subject to the associated limit. However, mixing operations of different types is an anti-pattern and is strongly discouraged. The scheduler algorithm implements special logic to count lightweight operations and expects their jobs to start successfully within a short timeframe. For this reason, we don't recommend abusing this feature by running heavy `Vanilla` operations that consist of more than one job in lightweight pools. While these operations are technically considered lightweight, their launch time may be longer than usual. # Dynamic monitoring of CPU consumption If a job consumes significantly fewer cpu resources than ordered in the operation specification, the job CPU guarantee may be reduced. The freed up resources can be transferred to other jobs working in the operations of a particular pool. The `job-cpu-monitor` process monitors job CPU consumption and guarantee changes. The value of CPU consumption for the period (`check_period`) is regularly read from the job container. Exponential smoothing with the `smoothing_factor` parameter is applied to this value. The last `vote_window_size` smoothed values and the interval (`relative_lower_bound*current_cpu_limit`, `relative_upper_bound*current_cpu_limit`) where current_cpu_limit is a current limit set on the container are considered next. After that, each value is converted according to the rule: * `-1`: Value < `relative_lower_bound*current_cpu_limit`. * `1`: Value > `relative_upper_bound*current_cpu_limit`. * `0`: In other cases. The resulting values are summed into the `votes_sum` variable and the `current_cpu_limit` limit is recalculated: * `votes_sum > votes_decision_threshold => current_cpu_limit *= increase_coefficient` * `votes_sum < -votes_decision_threshold => current_cpu_limit *= decrease_coefficient` The `current_cpu_limit` value is limited at the bottom by the `min_cpu_limit` option from the `job-cpu-monitor` configuration and at the top by the `cpu_limit` option from the operation specification. If the value of the current_cpu_limit variable has changed, the new value is set on the container and sent to the scheduler to update resource consumption in the pool. `Job-cpu-monitor` aims to keep the current CPU consumption in the interval between `relative_lower_bound` and `relative_upper_bound` from that set on the container and shifts the specified interval up or down if the consumption exceeds its limits. The default values (the current values may be different): * `check_period = 1000` (ms); * `smoothing_factor = 0.1`; * `relative_upper_bound = 0.9`; * `relative_lower_bound = 0.6`; * `increase_coefficient = 1.45`; * `decrease_coefficient = 0.97`; * `vote_window_size = 5`; * `vote_decision_threshold = 3`; * `min_cpu_limit = 1`. The listed settings can be specified in the `job-cpu-monitor` section of the operation specification. In the section, you can specify the `enable_cpu_reclaim` option that enables/disables cpu limit changes. You can view the actual option values in the web interface on the operation page, on the `Specification` -> `Resulting specification` -> `job-cpu-monitor` tab. # Selecting memory for jobs Memory is the RAM consumed by operation jobs. For all operations, there is a default mechanism for selecting the actual memory reserve for the jobs to be run. The operation controller collects memory consumption information for all successfully completed jobs. This information is saved as a special digest, based on which the actual memory reserve for the newly run jobs is selected. The main aspects of the digest: 1. Collects actual values (typically in the range from 0.0 to 1.0) describing the share of `memory_limit` consumed by the job. 2. Only successfully completed jobs and jobs that were aborted because they exceeded the memory reserve are counted. 3. For jobs that were aborted because they exceeded the reserve, a point with the memory consumption share of that job multiplied by a certain constant slightly above one (the default value is 1.1) is added to the digest. 4. The `memory_limit * percentile(digest, P)` (by default, `P` is 0.95) is taken as a reserve for the newly run job. 5. The digest does not store all points internally, but uses a data structure that allows an approximate calculation of percentiles. The memory reserve allocated to the job is used by the scheduler to consider consumption the lower limit. If the reserve value is exceeded and there is no free memory on the cluster node, the job may be aborted due to the `resource_overdraft` reason. If there is extra free memory on the cluster node, the job may exceed its allocated reserve and be completed successfully. If the memory limit specified by the user in the operation specification is exceeded, the job will be aborted with the `Memory limit exceeded` error. Note that in addition to the user process, there is also a `job_proxy` process, which is a layer between the user process and YTsaurus. Depending on different circumstances, job proxy can consume a significant amount of RAM for compression, columnar read/write mechanism, erasure, and other needs. The operation controller supports individual digests for the user process (`user_job`) and the `job_proxy` process. In the case of the `job_proxy` process, the controller makes an assumption about the expected memory usage based on the number of input/output tables and their settings. Based on the assumption , the digest selects a reserve (ranging from 0.5 to 2.0 by default). Besides that, the memory allocated to the user process has a lower limit. By default, it is indicated in the specification and equals 0.05. However, if a job orders `tmpfs`, the memory reserve cannot be less than `tmpfs_size / memory_limit`, i.e. the system tends not to allow overcommit on `tmpfs`, because it can cause scheduler locking. ### Statistics There are a number of values in the operation statistics that enable you to analyze the reserved memory: * `user_job/memory_reserve` and `job_proxy/memory_reserve` (bytes): The memory reserve allocated to the appropriate job process. * `user_job/max_memory` and `job_proxy/max_memory` (bytes): The memory consumed by the appropriate job process. Maximum during the operation of a particular job. * `job_proxy/estimated_memory` (bytes) : The scheduler's evaluation of the `job_proxy` process memory of a given job. * `user_job/memory_limit` (bytes) : The job memory limit specified by the user. * `user_job/cumulative_memory_reserve`, `job_proxy/cumulative_memory_reserve`, `user_job/cumulative_max_memory`, `job_proxy/cumulative_max_memory`, `job_proxy/cumulative_estimated_memory` (bytes * seconds): The appropriate cumulative metrics taking into account the job duration. ### Digest settings The following options are available to the user in the operation specification: * `user_job_memory_digest_default_value`: Initial assumption for selecting the memory reserve (the default value is 0.5). * `user_job_memory_digest_lower_bound`: The limit below which the reserve must not fall (the default value is 0.05). We do not recommend changing the default value. * `memory_reserve_factor`: The alias for the `user_job_memory_digest_lower_bound` and `user_job_memory_digest_default_value` options concurrently. Using this option is not recommended. # Managing compute pools This section explains how to create pools and manage resources and settings of compute pools in the YTsaurus system using the CLI and web interface as examples. ## General information { #common } The YTsaurus system has a mechanism for managing compute pools, enabling pool actions such as: - Creating. - Renaming. - Moving. - Deleting. - Changing settings. You can change the compute pool settings by editing the relevant pool attributes. Attributes that can be edited by the user: * `weight`: Manages the share of the parent pool's resources that will go to the current pool. * `max_operation_count`: A limit on the number of running (`running`) operations and operations in the queue (`pending`). * `max_running_operation_count`: A limit on the number of running (`running`) operations. * `strong_guarantee_resources`: A dict describing the pool's guaranteed resources. * `forbid_immediate_operations`: Prohibits operations to be run directly in this pool. * `mode`: A way of allocating resources between operations in the pool. * `fifo_sort_parameters`: Managing the order in which operations are started in fifo pools. * `resource_limits`: A dict that sets the upper limits for the resources available to the pool. * `create_ephemeral_subpools`: Allows creating [ephemeral pools](https://ytsaurus.tech/docs/en/scheduler-and-pools.md#puly-i-derevya-pulov). * `ephemeral_subpool_config`: A dictionary describing the configuration of virtual pools. For more information about the listed attributes, see [Pool configuration](https://ytsaurus.tech/docs/en/pool-settings.md#pools). ### Validating When validating, YTsaurus checks attribute types, the `max_operation_count >= max_running_operation_count` limit, the number of tags in the `allowed_profiling_tags` list (no more than 200), consistency of resource guarantees, and whether there are subpools in fifo pools. Consistency of resource guarantees means that for each resource, the sum of the descendants' guarantees must not exceed the parent's guarantee. If the subpool has a non-zero guarantee for a specific resource, it must also be set for the parent. The pool is allowed to have a non-zero guarantee on more than one resource. ## Access rights To manage a pool, you need to request access to the pool. You can do this via the YTsaurus web interface. Pool access rights are managed by users who already have the appropriate access — by default, access is granted to the user who created the pool. To request access to a pool: 1. Navigate to the pool page from the `Scheduling` section. 2. Select the `ACL` tab. 3. Click `Request permissions`, fill in the form, and click **Confirm**. ![](https://ytsaurus.tech/_images/pool-request-permissions.jpg){ .center } ### List of allowed pool actions { #allowed_pool_actions } - To create or delete pool trees, you need the `write` permission on the root `//sys/pool_trees` (only for YTsaurus cluster administrators). - To create pools, you need the `write` or `modify_children` permission on the parent pool (granted together with **Use** access on the parent pool). - To delete a pool, you need the `remove` permission on it (granted together with **Use** access on the parent pool). - To change user-defined pool attributes, you need the `write` permission on the pool (granted together with **Use** access on the parent pool). - To change all other pool attributes, you need the `administer` permission (only for YTsaurus cluster administrators). - To manage operations in this pool and its descendants, you need the `manage` permission (managing operations means you can perform `suspend`, `resume`, and `update_operation_parameters` operations without being the operation's author). {% note warning "Limitation" %} The `manage` permission on a pool lets you change only the following parameters using the `update_operation_parameters` command: - `weight`; - `weight` and `resource_limits` in `scheduling_options_per_pool_tree`; - `scheduling_tag_filter`; - `controller_agent_tag`. To change other parameters, you need the `manage` permission on the operation itself; some parameters may require additional permissions. Specifically, the `manage` permission on a pool doesn't let you change the `pool` parameter and move an operation to another pool. {% endnote %} As an example, consider the `project-root` pool in the `default` pool tree. The required set of permissions for managing sub-pools is shown in the listing: ```bash $ yt get //sys/pool_trees/default/project-root/@acl [ { "permissions" = [ "modify_children"; ]; "action" = "allow"; "subjects" = [ "some_user"; ]; "inheritance_mode" = "object_and_descendants"; }; { "permissions" = [ "write"; "remove"; ]; "action" = "allow"; "subjects" = [ "some_user"; ]; "inheritance_mode" = "descendants_only"; }; ] ``` The user can manage sub-pools but not the project's root pool — they cannot change the guarantee of the project's root pool or other parameters. The permission applies only to the descendants of this pool. # Managing pools via the CLI Creating a subpool: ```bash yt create scheduler_pool --attributes='{pool_tree=default;name=project-subpool1;parent_name=project-root}' ``` In the attributes, you can additionally pass the pool attributes and they will be validated. If the validation fails, the object will not be created. Example of a weight change for the created pool: ```bash yt set //sys/pool_trees/default/project-root/project-subpool1/@weight 10 ``` Initial setting of a pool guarantee provided that the parent has an unallocated guarantee: ```bash yt set //sys/pool_trees/default/project-root/project-subpool1/@strong_guarantee_resources '{cpu=50}' ``` A specific parameter can be changed to alter a set guarantee: ```bash yt set //sys/pool_trees/default/project-root/project-subpool1/@strong_guarantee_resources/cpu 100 ``` Moving is performed in the standard way: ```bash yt move //sys/pool_trees/default/project-root/project-subpool1 //sys/pool_trees/default/new-project/new-subpool ``` Renaming via moving is supported. Validation occurs when moving. Attributes are set in the standard way: ```bash yt set //sys/pool_trees/default/project_pool/@max_operation_count 10 ``` Validation occurs when setting attributes. ## Managing pools via web interface { #ui } You can manage pools in the web interface in the `Scheduling` section. To create a sub-pool, navigate to the parent pool by clicking its name. Then click the `Create pool` button, fill in all required fields in the form, and click `Confirm`. An example of the pool creation form is shown in the figure. ![](https://ytsaurus.tech/_images/manage_pool_01.png) To edit pool settings, click the pencil icon in the row with the pool name on the right side of the screen, as shown in the figure. ![](https://ytsaurus.tech/_images/manage_pool_04.png) Editable pool settings are divided into groups. The figures show examples of general settings and guaranteed resources, respectively. ![](https://ytsaurus.tech/_images/manage_pool_02.png) ![](https://ytsaurus.tech/_images/manage_pool_03.png) In the `Resource Limits` section, you can set an upper limit for the pool, for example, to prevent the pool from using more than 100 CPU cores. By default, pools have no upper limit; the resources available to a pool are limited by the cluster's capacity. The `Other Settings` section contains additional settings, including the option to prohibit running operations in the pool. {% note info "Note" %} We recommend that you run operations in the leaves of the pool tree — that is, in pools that have no sub-pools. In such cases, it's useful to explicitly set a prohibition on running operations directly in a specific pool. Avoid running operations in the nodes of the pool tree (pools that have sub-pools). Otherwise, it becomes more difficult to investigate various issues related to resource distribution between pools and operations. {% endnote %} # Operation types This section defines the data processing operations available in the YTsaurus system. It also introduces the concepts of a user script and a job, which make up an operation, and describes the possible states of operations and jobs. ## Overview YTsaurus supports the following data processing operations: - **[Map](https://ytsaurus.tech/docs/en/map.md), [Reduce](https://ytsaurus.tech/docs/en/reduce.md), and [MapReduce](https://ytsaurus.tech/docs/en/mapreduce.md)** execute user-provided code against input data. - **[Sort](https://ytsaurus.tech/docs/en/sort.md)** sorts input tables. - **[Merge](https://ytsaurus.tech/docs/en/merge.md)** merges tables. - **[Erase](https://ytsaurus.tech/docs/en/erase.md)** deletes specified data from a table. - **[RemoteCopy](https://ytsaurus.tech/docs/en/remote-copy.md)** copies data between clusters. - **[Vanilla](https://ytsaurus.tech/docs/en/vanilla.md)** launches an appropriate number of **user scripts** on cluster nodes and keeps them running. Each operation has a `spec` parameter, which, as the name implies, conveys the operation's **specification** (aka its options). The specification is a hierarchical structure that exists in the [YSON format](https://ytsaurus.tech/storage/formats.md#yson). Each operation type has its own specification structure, whose detailed description can be found in the subsections dedicated to specific operation types. **User script** is a user-defined string that is executed via a `bash -c` call, plus the parameters detailing the execution. User scripts run in a `sandbox` environment, where they are executed by a service user that is not formally present in the system (referring to Linux system users) and that only has read/write rights for the current directory and for the `tmp` directory. To learn more about user script parameters, see [Operation options](https://ytsaurus.tech/docs/en/operations-options.md#user_script_options). Operations are run in **compute pools**, which are structured as a tree and serve to allocate the cluster's compute resources. For more information, see [Scheduler and pools](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md). Operations are comprised of **jobs**. Jobs as units enable the parallelism of an operation. Input data fed to an operation is divided into parts, with each such part processed by one job. ## Parameters of a running operation {% note info "Note" %} You can use the **Operations** screen of the web interface to keep track of operation progress and results. {% endnote %} Another way to get the info you need is the get_operation call, which returns a set of operation attributes, the following being especially useful: - `state` — current state of the operation. - `progress` — operation progress broken down by job. - `spec` — specification of the running operation. - `result` — execution result (error report in case the operation ended with the `failed` status). By calling list_jobs, you can get the list of the operation's jobs (both running and completed). For use cases of Python's `get_operation` and `list_jobs`, see "Python Wrapper". ### Operation statuses { #status } In the course of execution, an operation can have one of the following statuses: - `running` — the operation is executing. - `pending` — the operation is queued for launch. Each [pool](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md) has a limit on the number of simultaneously running operations, and, if it is exceeded, operations are sent to a queue. - `completed` — the operation was successfully completed. - `failed` — the operation encountered an error. The main possible reasons are failure to start due to incorrect parameters, or the threshold of jobs stopped due to an error being exceeded. - `aborted` — the operation was aborted. The main possible reasons are it being interrupted by the user, or the transaction housing the operation being interrupted. - `reviving` — the operation is being recovered following the restart of the [scheduler](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md). - `initializing`, `preparing`, `materializing`, `completing`, `aborting`, `failing` — intermediate states of an operation that facilitate the step-by-step change of meta-information on the YTsaurus master server. ### Job states Each operation comprises a number of jobs. Like operations, jobs can have various states: - `running` — the job is executing. - `completed` — the job completed successfully. - `aborted` — the job was interrupted. This normally occurs as a result of **preemption** or network errors. To learn more about preemption, see [Scheduler and pools](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md). - `failed` — the job encountered an error. This state usually results from errors in the user script. An error log (stderr) for the failed job has been generated and can be viewed. Such cases may also occur if the [account](https://ytsaurus.tech/storage/cypress.md) in use has run out of space, meaning the job cannot write the result of its work. - `waiting` — the job was sent to a cluster node, but is awaiting resources in a queue. - `aborting` — the scheduler decided to interrupt the job, but the process on the cluster node has not yet ended. The operation page in the web interface shows the number of `pending` jobs. This is the number of unscheduled jobs that the scheduler assumes need to be executed before the end of the operation. The number of `pending` jobs may differ from the number of jobs that will actually be completed: data slicing depends on the job completion speed. #### Aborted jobs { #aborted_jobs } Jobs with the `aborted` state contain information on the class of reasons that led to the interruption: * `scheduler` — the job was interrupted by the scheduler, exact reason unknown. * `failed_chunks` — the job was interrupted by the cluster node due to inability to read input data or write output data. * `resource_overdraft` — the job was aborted by the cluster node because the memory limit was exceeded. By default, the scheduler allocates to the job less memory than requested by the user, and if the allocated memory is not enough for the job (in other words, the memory on the cluster node runs out), the job is interrupted and restarted. * `other` — the job was interrupted by the cluster node due to an unclassified reason. * `preemption` — the scheduler interrupted the job to run a different job. * `user_request` — the job was interrupted by the user. * `node_offline` — the job was interrupted because the scheduler lost connection with the cluster node. * `waiting_timeout` — the job was interrupted due to being queued on the cluster node longer than permitted. * `account_limit_exceeded` — the job was interrupted after trying to to write data past the user account's limit. Such jobs are deemed `failed`, unless `suspend_operation_if_account_limit_exceeded` is enabled. * `unknown` — the job was interrupted for an unknown reason. This behavior occurs when the compute nodes and the scheduler running on the cluster have different versions and the cluster node reports to the scheduler a reason that is unknown to the latter. * reasons appended with `scheduling_` refer to cases where the scheduler has scheduled the job, but ended up canceling it before making a schedule for it to run on a cluster node. This type of job wastes the resources of the scheduler, but not of the cluster: * `scheduling_timeout` — the scheduler was unable to schedule the job in the allotted time. May occur with big operations, where the input data numbers in the petabytes, or in cases of scheduler CPU overload. * `scheduling_resource_overcommit` — after the job was scheduled, it turned out that the `resource_limits` of the operation or one of the parent pools were exceeded. * `scheduling_operation_suspended` — after the job was scheduled by the controller, it turned out that the operation was suspended. * `scheduling_job_spec_throttling` — after the job is scheduled, a specification is generated for it, which, among other things, contains meta-information on the job's input chunks. Where there are too many input chunks, a mechanism for limiting the creation of jobs in the particular operation is activated. Some of the operation's jobs may be interrupted in the process. * `scheduling_intermediate_chunk_limit_exceeded` — the job was interrupted due to the intermediate chunk limit being exceeded. The limit determines how many such chunks an operation can create with automatic merging of output chunks enabled in `economy` and `manual` modes; * `scheduling_other` — the job was interrupted by the scheduler for an unclassified reason. #### Completed jobs { #completed_jobs } Sometimes, the scheduler decides to interrupt a job and stops inputting new data to the job as per the operation's guarantees, giving some time for the job to complete. If the job completes within the allotted time, it switches to the `completed` state — not `aborted` despite having been interrupted. Such `completed` jobs have the `interrupt_reason` attribute with a value other than `none`, specifying the reason for the scheduler interrupting the job. In cases like this, the values of the job interruption reason can be: * `none` — the job completed successfully, without interruption. * `preemption` — the scheduler interrupted the job to run a different job. * `user_request` — the job was interrupted by the user. * `job_split` — the job was interrupted to be split into more jobs. * `unknown` — the job was interrupted for an unknown reason. This happens when the decision to interrupt the job was sent to the cluster node, and then the scheduler restarted. #### Standard error stream Data in an operation's job can be written to the standard error stream (stderr) to be saved and available for reading. This can also be useful for debugging purposes. To get a job's stderr, use the `get_job_stderr` command, to get a job list — `list_jobs`. For convenience, you can adjust the `list_jobs` parameters to only show the jobs where stderr was written to. The `list_jobs` and `get_job_stderr` commands are available in Python (see "Python Wrapper"). To prevent stderr operations from taking up too much space, writing is governed by the following policies: 1. No more than `max_stderr_size` bytes written to stderr are saved. Anything written beyond this limit is ignored. 2. No more than 150 jobs with such stderr log files can be saved (for schedulers). 3. No more than 20 jobs with such stderr log files can be saved (for operations). May take the value up to 150. ## Unavailable data { #chunk_strategy } ### Strategies and tactics Specification features two settings for each operation: `unavailable_chunk_strategy` and `unavailable_chunk_tactics`. Their possible values are `wait`, `fail`, and `skip`. These values determine the behavior of the scheduler when the necessary input data for the operation is not available: * `wait` — wait for the data for an indefinite period * `fail` — immediately abort the operation * `skip` — skip unavailable data and perform calculations on an incomplete set Strategy and tactics come into play at different stages of the operation: * **strategy** is used upon operation start and during initial scheduling of the work scope * **tactics** are used when handling unavailable data for an operation that is already running Theoretically, 9 combinations are possible, but not all of them are useful. The following combinations are useful: * `wait` strategy, `wait` tactic — used by default, calculations are only performed on complete data, even at the cost of waiting. * `fail` strategy, `wait` tactic — data integrity is checked at startup, and if any losses are detected you must wait for the missing data to become available. * `skip` strategy, `skip` tactic — calculations are performed on any available data without delay. * `skip` strategy, `wait` tactic — calculations are started immediately with the losses committed, but no additional losses are allowed — wait for the missing data to become available. * `skip` strategy, `fail` tactic — calculations are started immediately with the losses committed, but no additional losses are allowed — interrupt the calculations if unavailable data is detected. * `fail` strategy, `fail` tactic — calculations are only performed on complete data, any data unavailability is reported as soon as possible. The scheduler periodically queries the master for the status of an operation's input chunks. If the scheduler detects unavailable data, the respective part of the calculation stops. Jobs using this data are not started, but chunks are still requested from the master. Each `erasure` request for a chunk causes it to move up in the queue of chunks awaiting recovery ("move to front"). The chunks required by the current operations to run are recovered first. These procedures are fully automated and do not require manual set-up. ### Availability of erasure chunks {#chunk_erasure_strategy} To configure the operation behavior when erasure-coded chunks are unavailable, you can use the `chunk_availability_policy` option. This option regulates when a chunk is considered available: - `data_parts_available`: A chunk is considered available if all data parts are available. At the same time, parity parts may be lost. With this operation setup, you do not need to wait for data recovery before reading. - `repairable`: A chunk is considered available if there are enough parity parts to recover the data. At the same time, data parts may be lost. In this case, the operation waits for the data to be recovered from parity parts. - `all_parts_available`: A chunk is considered available if all of its parts (data parts and parity parts) are available. ## C++ and Python Wrapper Running operations via C++ or Python Wrapper is associated with a number of priming works in addition to the calling of the corresponding driver command. Those include: * Uploading files to the [File cache](https://ytsaurus.tech/storage/file-cache.md). **Note**: Automatic file upload to the cache always takes place outside of the user transaction, which may lead to conflicts on Cypress nodes if the file cache directory was not created in advance. * Сreating output tables or clearing them using the `erase` command. When it comes to the system, all output tables should already be there at the start of the operation. * Deleting files after the operation is completed. * Setting the `jobcount` and `memorylimit` parameters, if they are specified by the user. * Some parameters may have default values that are different from the system ones. ## Limits on the number of tables and jobs The system has restrictions on operation parameters in place that need to be taken into account. 1. There is a limit on the number of input tables for any operation. By default, this limit on "big" production clusters is 3,000. Testing clusters, as well as clusters that do not run large-scale MapReduce operations, can have lower restrictions, which can still be increased to 3,000 if necessary. 2. There is no special limit on the number of output tables, but keep in mind that each output table has memory buffer requirements. If an operation has 1,000 output tables, its jobs will have impossible memory requirements and therefore not run. In practice, the upper bound for output tables is approximately 100. If you need more output tables, split the operation into several smaller ones and stagger the processing. 3. There is a limit on the number of jobs an operation can have. For [Map](https://ytsaurus.tech/docs/en/map.md), [Merge](https://ytsaurus.tech/docs/en/merge.md), [Reduce](https://ytsaurus.tech/docs/en/reduce.md), and [RemoteCopy](https://ytsaurus.tech/docs/en/remote-copy.md) it is 100,000 jobs. In the cases of [Reduce](https://ytsaurus.tech/docs/en/reduce.md) and sorted [Merge](https://ytsaurus.tech/docs/en/merge.md), it may be impossible to generate so many jobs due to the scheduler not having enough samples. For operations that include the shuffle stage ([Sort](https://ytsaurus.tech/docs/en/sort.md) and [MapReduce](https://ytsaurus.tech/docs/en/mapreduce.md)), there is a limit of 5,000 partitions. To learn more about partitions, see the corresponding sections dedicated to specific operation types. 4. There is a limit on the product of an operation's jobs multiplied by the number of output tables. This restriction helps prevent operations from generating a large number of chunks, which can exert an excessive load on the master servers. For "big" production clusters, this limit is 2,000,000. ## Transactions in data processing { #transactions } Any operation can be viewed as a black box with a set of inputs and outputs (I/O), as well as a number of artifacts used in its work (files and porto layers). The general logic of the operation is quite simple: it takes exclusive locks on output tables within a special output transaction and snapshot locks on input tables (and artifacts) within a special input transaction. Then, if and when all calculations have been successfully performed, output transactions are committed. If the operation is interrupted or ends in an error, the output transaction is aborted. There are certain caveats: * If a table is present in both input and output tables, then only the exclusive lock in the output transaction is taken for that table, and the data is both read and written from this version of the table. * If the output table is flagged with `append`, it takes a shared lock. The previous item's logic does not work if the tables are present in both the input and output. * The operation can be run as part of a user transaction, in which case the input and output transactions are created as the user transaction's subtransactions. * Scheduler transactions are created with a timeout sufficient to survive cluster downtimes which can take hours. Then they are pinged by the scheduler. When a standard scheduler update takes place, it reuses operations' existing transactions. In the case of long shutdown drills or major updates of the master server, transactions are interrupted and operations are restarted from scratch, with locks on tables being taken anew. Operations that contain core or stderr tables can also have a debug transaction. This transaction houses the writing of core files and the stderr, with the transaction being committed even if it is interrupted or ends in an error. These transactions can be viewed via operation attributes: 1. `input_transaction_id`, `nested_input_transaction_ids` – transaction with locks on input tables and artifacts. This can also be a list of transactions if `transaction_id` on input tables is used. 2. `output_transaction_id` – transaction within which data is written to output tables. 3. `debug_transaction_id` – transaction within which core and stderr tables are written. 4. `async_scheduler_transaction_id` – transaction within which temporary data is written under an operation that always interrupts upon completion. For example, this is where intermediate data lives. 5. `output_completion_transaction_id`, `debug_completion_transaction_id` – transactions serving a technical purpose, used upon successful completion of an operation to atomically commit the operation's result. ## Using data from a remote cluster For any operation, you can specify the `cluster` attribute on one or more input tables. This will initiate data reading from the specified cluster. The cluster name should be the same as one would use for RemoteCopy operations. Files delivered to each job (`/mapper/file_paths` or `/reducer/file_paths` section) can be read only from a local cluster. So if you need to use them from a remote cluster, transfer them in advance using the RemoteCopy operation. {% note info %} Reading data from another cluster (remote read) is subject to inter-cluster network bandwidth limits. For more information, see [Inter-cluster Network Bandwidth Throttling](https://ytsaurus.tech/docs/en/cluster-throttlers.md). {% endnote %} # Map The Map operation may consist of one or more jobs. Each job receives part of the input table data that is processed by the user script. The result is written to the output table. The Map operation has two modes: **unordered** (default) and **ordered**. The ordered Map mode is enabled through the `ordered=%true` option in the operation's specification. The mode affects the guarantees that the system provides for jobs' input data. Each of the two modes delivers the basic guarantee of the Map operation: each output table is produced by merging the results of all the successfully completed jobs, with each input data row ending up at the input of precisely one successful job. - In the unordered Map mode, there are **no** guarantees beyond those described above. In particular: - The rows of one input table can end up with random jobs without any continuity (one mapper may get the first and third rows of the table, while another would get the second and fourth). - The rows of an input table can be fed to a job in any order, not necessarily based on ascending row numbers. - The rows of different input tables can be fed to the same job randomly, including in mixed fashion (first may come a table A row, then a table B row, followed again by a table A row). - How Ordered Map works: - It concatenates the input data according to how it's entered in the input_table_paths section of the input data. - The resulting aggregate input is divided into continuous segments, each of which is fed to a separate job. - If there are several input tables, some jobs **can** receive rows from several tables for input (the process has to land in between two tables as a result of the above mentioned concatenation). - Each job reads its data segment strictly in order within the concatenation described above: first in the order of the tables, and within the tables — based on ascending row numbers. Both modes have trade-offs: ordered Map mode offers more guarantees that can be used in certain cases, but is less efficient due to having more restrictions. Unordered Map mode makes active use of its ability to adaptively read input data with a high degree of parallelism from different chunks, compensating for the slowdown of individual disks with input data, so it is generally more efficient. Unordered Map mode can regroup rows in a pool of yet unprocessed input data in whichever order. For example, the input of an unsuccessful job (aborted or failed) can be regrouped in an arbitrary fashion and processed within several new jobs. Note that the `ordered=%true` option determines the way input tables are split into jobs, but **does not** result in any additional sorting of the data therein other than the initial one. The order of output data is determined both by the operation mode and by the presence of a sorted schema on the output table (or of the `sorted_by` attribute on its path in the operation's specification). Possible scenarios: - If the output table is sorted, the following takes place: - The output data of each job is checked for being sorted based on the output table schema (or the one specified for the output table path). If the job produces an unsorted row sequence, the operation ends with an error. - If all the output chunks are sorted, then at the end of the operation they aim to arrange in accordance with the sort order in the output table. If two chunks happen to overlap on bordering keys and no matter their arrangement the rows do not form a sorted sequence, the operation ends with an error. Chunks consisting of one key are ordered in an arbitrary, non-deterministic fashion. - If the output table is not sorted and the operation mode is unordered, the output chunks are ordered in an arbitrary, non-deterministic manner. - If the output table is not sorted and the operation mode is ordered, the output chunks are ordered in accordance with the global job order described above. The ordered version of the Map operation is characterized by the determinism of output table rows and their order, provided that the mappers' logic is deterministic, regardless of the number of jobs in the operation and the order of their execution. This characteristic is not there for unordered Map. General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The Map operation supports the following additional options (the default values, if set, are specified in brackets): * `mapper` — user script. * `input_table_paths` — list of input tables with full paths (cannot be empty). * `output_table_paths` — list of output tables. * `job_count`, `data_size_per_job` (256 MB) — options specifying how many jobs should be started (these are recommendations). The `job_count` option takes precedence over `data_size_per_job`. If `job_count` is less than or equal to `total_input_row_count`, the number of jobs is guaranteed to be met exactly, so long as it does not conflict with the limit on the maximum number of jobs for the operation. Specifically, if `job_count` is equal to `total_input_row_count`, then each job gets exactly one row, and if `job_count` equals 1, only one job is launched. * `ordered` — controls the way input is split into jobs. * `auto_merge` — dictionary containing the settings for automatic merge of small-sized output chunks. By default, automatic merge is disabled. ## Example specification ```yson { pool = "my_cool_pool"; job_count = 100; input_table_paths = [ "//tmp/input_table" ]; output_table_paths = [ "//tmp/output_table" ]; mapper = { command = "cat"; memory_limit = 1000000; } } ``` # Reduce The Reduce operation may consist of one or more jobs. Each job receives part of the input table data processed by the user script, and the result is written to the output table. The input data is grouped according to the set of columns indicated in the `reduce_by` option. This grouping guarantees that all data with the same value in the `reduce_by` columns is input to the same job. The guarantee can be eased using the `enable_key_guarantee` option. For the operation to start, each input table must be sorted according to a column set beginning with `reduce_by`. General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The Reduce operation supports the following parameters (default values, if set, are specified in brackets): * `reducer` — user script. * `input_table_paths` — list of input tables with full paths (cannot be empty). * `output_table_paths` — list of output tables. * `reduce_by` — set of columns used for grouping. * `sort_by` (defaults to `reduce_by`) — set of columns used for sorting primary input tables. The option enables an additional check for whether input tables are sorted and provides guarantees the rows are going to be sorted according to the column set within the user script. The `reduce_by` field sequence should be set as the prefix of the `sort_by` field sequence. * `job_count`, `data_size_per_job` (256 MB) — options specifying how many jobs should be started (these are recommendations). The `job_count` option takes precedence over `data_size_per_job`. * `pivot_keys` — enables manual data partitioning, so you can determine which jobs get certain data ranges. As its value, this option takes a sorted list of table keys (bordering ones). For example, if `pivot_keys = [["a", 3], ["c", -1], ["x", 42]]`, then four jobs (and no more) start: * for keys strictly up to `["a", 3]`. * for keys from `["a", 3]` (inclusive) to `["c", -1]` (non-inclusive). * for keys from `["c", -1]` (inclusive) to`["x", 42]` (non-inclusive). * for keys from `["x", 42]` until the end. The scheduler will not try to split such jobs into smaller ones. If some of the ranges do not contain keys, then the corresponding jobs may not launch. It is impossible to guarantee which jobs do not run, because the scheduler does not see the data in full, only samples from it. * `enable_key_guarantee` — enables/disables the guarantee that all records with one key are given as input to one job (`true` by default). [Reduce with foreign tables](https://ytsaurus.tech/docs/en/reduce.md#foreign_tables) describes a special scenario for using this option. * `auto_merge` — dictionary containing the settings for automatic merge of small-sized output chunks. By default, automatic merge is disabled. The order of inputs in the Reduce operation is associated with the following guarantees: * data from each input table is divided into continuous parts that are fed to jobs based on `reduce_by` columns. In other words, one job cannot get keys `A` and `C`, while another job gets key `B`. * within a job, data from all the input tables is merged and sorted — first according to the `sort_by` fields, and then by input table index (`table_index`). For foreign (`foreign`) tables — within one `join_by` key — sorting is done by external table index. More information about foreign tables [below](https://ytsaurus.tech/docs/en/reduce.md#foreign_tables). Input tables whose path has the `teleport=%true` attribute get processed by the Reduce operation in a special way. Chunks of such tables without key range overlaps with other input tables' chunks are not fed to the user script. Such unprocessed chunks are written to the output table whose path has the `teleport=%true` attribute. There can be no more than one such output table. You can still specify the `sorted_by` attribute for the output table to make it sorted. In this case, the user must ensure that the key range at the user script output is not wider than the key range at the operation's input. ## Application A typical use case is updating a directory or aggregates. Suppose there is a `//some_user/stat` table that stores visit statistics broken down by day. The table has two columns: `time` and `count`, with the data sorted based on the `time` column. The data in the table is stored in two chunks, the first one being dates from 2013-06-01 to 2013-08-15, and the second one — 2013-08-16 to 2013-09-16. Additionally, there is a sorted `//some_user/update` table featuring statistics updates for September. You can run the Reduce operation to update the main table as shown below. ``` yt reduce --src //some_user/stat --src //some_user/update --dst //some_user/stat ``` As a result, the first chunk of the `//some_user/stat` table goes to the output table in its original form, without processing. Another use case is filtering out rows whose keys match those of a large sorted table from a small sorted table. Here, you can add an output table with the `` attribute and delete it after execution. The output table will only contain those chunks of the large table that were not present in the small one. Filtering example: ``` yt reduce --src //fat_table --src //small/table --dst //results --dst //temp_table_to_throw_out ...; yt remove //temp_table_to_throw_out ``` ## Reduce with foreign tables { #foreign_tables } Reduce with foreign tables is a modified Reduce operation where part of the input tables are marked as foreign (`foreign`) tables and act as directories, with their data being added to the data stream from "regular" (`primary`) tables. For foreign tables, a special set of `join_by` key columns is used, which should be set as the prefix for the `reduce_by` column set. Records with the same `join_by` key, but different `reduce_by` keys may end up in different jobs — in this case, the corresponding records from foreign tables are passed in to all the jobs, and therefore processed more than once. The reverse is also possible in cases where the directory table can contain more different combinations of values than the main table. If a foreign table record matches none of the main table records, that record may not go to any job at all. {% note warning "Attention!" %} Because there are frequent reads on foreign table chunks, [erasure coding](https://en.wikipedia.org/wiki/Erasure_code) for them is strongly discouraged. Indeed, if the foreign table is small, it is best to set a higher replication factor for it. Using `erasure` for a foreign table can lead to increased read loads across cluster nodes and hurt the entire cluster's performance. {% endnote %} An important difference between Reduce with foreign tables and the regular Reduce operation is that the granularity of user script calls in the Python wrapper and C++ wrapper. Unlike the regular Reduce operation, a user script (the `IReducer::Do` method in the C++ API and the handler function in Python) is called for each `join_by` key rather than the `reduce_by` key. Specification requirements for Reduce with foreign tables: * at least one primary table must be specified (foreign attribute missing). * at least one foreign table must be specified using the `` attribute. * the `join_by` value must be specified. * `join_by` must be a prefix of or match `reduce_by`. * `join_by` cannot be used without foreign tables, and foreign tables cannot be used without `join_by`. * you cannot set the `teleport` attribute for a foreign table. * you cannot set multiple ranges for a foreign table, but the same foreign tables can be specified multiple times with different ranges. Also, Reduce operations with foreign tables have the `consider_only_primary_size` option (`%false` by default), which, if enabled, makes it so only the amount of data in `primary` tables is taken into account in job splitting. Setting the `enable_key_guarantee=%false` option in Reduce with foreign tables carries special semantics: * it disables the guarantee that all records with the same key from a primary table end up in the same job. At the same time, there still remains the guarantee that if a record from a primary table with a certain key ends up in a job, then all records with the same key from all foreign tables go to the same job. * the user must pass in the list of keys to `join_by`, specifying `reduce_by` is not allowed. We recommend using the Reduce operation with the `enable_key_guarantee=%false` option. ## Example specification ```yson { data_size_per_job = 1000000; reduce_by = ["key"]; sort_by = ["key"; "subkey" ]; input_table_paths = [ "//tmp/input_table" ]; output_table_paths = [ "//tmp/unsorted_output_table"; "//tmp/sorted_output_table" ]; reducer = { command = "python my_reducer.py" file_paths = [ "//tmp/my_reducer.py" ]; }; job_io = { table_writer = { max_row_weight = 10000000; } } } ``` # MapReduce How the MapReduce operation works: 1. On cluster nodes, the map phase starts. Each user script in the map phase (mapper) receives a separate part of the input table for input. The output data of the map phase is partitioned based on the hash value of a key. The resulting partitioned data is stored locally on the disk (replicated once), at the nodes where the map jobs were executed. 2. Next comes the reduce phase. Each user script in the reduce phase (reducer) handles one partition: as input, it receives all the records received from mappers whose hashed key has the specified value. Over the network, the reducer fetches the data corresponding to its partition from the cluster nodes where the map jobs were executed. This process is called **shuffle**, and it produces the highest load on the network. Each partition is sorted in memory by key, and if the keys match, by subkey. The sorted data then becomes input for a reduce job. The reduce job's output is written to the disk and replicated as needed — usually it is two synchronous replicas and one asynchronous, with one synchronous replica being written locally. ![](https://ytsaurus.tech/_images/mapreduce_op1.png){ .center } The MapReduce operation is similar to the [Sort](https://ytsaurus.tech/docs/en/sort.md) operation, but allows for executing a mapper user script before partitioning jobs, and a reducer user script after sorting jobs. {% note warning "Attention" %} Functionally, MapReduce is not the same as Map + Sort + Reduce. Even if the user script in the reduce phase does not change key column values, you still cannot get a sorted table as the operation's output. Therefore, using MapReduce in combination with the `sorted_by` attribute on the output table is almost always incorrect. The error will make itself known only on fairly large tables, which do not fit into a single reduce job in their entirety. {% endnote %} In the merged MapReduce operation: 1. The user may go without specifying the mapper, in which case it is assumed to be identical, and the input table data is output from the map phase without processing. In YTsaurus terms, a user-defined mapper determines which jobs will be running: partition map jobs (if a user-defined mapper is present) or partition jobs (if there isn't one). 2. If the partition is too large and cannot be stored in memory, it is impossible to sort it on a single cluster node. In these cases, the partition is divided into parts, each part is sorted separately, and the result is locally written to the disk. After that, [multiway merge](https://en.wikipedia.org/wiki/K-way_merge_algorithm) is launched, which merges the data and feeds it to a reduce job's input. In YTsaurus terms, sorting a small partition in memory is done through partition reduce jobs. If the partition is large, then partition sorting is performed with partition sort jobs, and the final merge and launch of the reducer is handled by sorted_reduce jobs. 3. Until map jobs finish, you cannot start reduce jobs. This is because the YTsaurus system must supply reduce jobs with data ordered by subkey, and that sequence cannot be generated until the data (map job outputs) is completed. A barrier of sorts therefore exists between the map and reduce phases — they must not intersect in time. If the scheduler detects a large partition that cannot be sorted within one job, it has an opportunity to save time and start processing the partitioned data before all map jobs finish. The scheduler, at its own discretion, divides the partition into several parts. The individual parts are then sorted on different cluster nodes by intermediate sort jobs, which take this data in over the network and sort it. This technique allows for a partial overlap of the phases, shortening the operation time. ## MapReduce vs Map + Sort + Reduce The merged MapReduce operation has several advantages over the [Map](https://ytsaurus.tech/docs/en/map.md) + [Sort](https://ytsaurus.tech/docs/en/sort.md) + [Reduce](https://ytsaurus.tech/docs/en/reduce.md) combination: 1. **Less I/O operations on intermediate data.** Without the merge phase, map-sort-reduce typically writes intermediate data to disk three times: after the map phase, after the partition sort phase, and after sorting. Some of these writes may have a replication factor other than 1. For a MapReduce operation, a single write to disk will suffice, and the replication factor will be 1. 2. **No unnecessary barriers (synchronization points).** With the map-sort-reduce combination, YTsaurus is forced to wait for **all** the map jobs (including the longest ones) to finish before starting partitioning. Similarly, sorting needs to be completed before the first reduce jobs can be run. As for the merged MapReduce operation, there is only one inevitable synchronization point: reduce jobs cannot be started before map jobs run their course. 3. **Better fault tolerance.** MapReduce constitutes a single operation, so the scheduler does everything it can to complete it even when some intermediate data becomes unavailable. When it comes to a chain of independent operations, the scheduler is only concerned with the individual phases. The only way to combat this is to ramp up intermediate data replication, which severely impacts speed. ![](https://ytsaurus.tech/_images/mapreduce_op2.png){ .center } ### Usage notes To merge the Map + Sort + Reduce combination into a single MapReduce operation, pass both the mapper and reducer options in the operation's specification. In some cases, users may find the merged MapReduce operation not fast enough on default settings. The reasons may be: 1. **Significant difference between the data volume at the mapper's input and output, which leads to the scheduler misjudging the number of partitions.** The scheduler estimates the partition number based on the amount of input data as it aims to ensure the partition fully fits into the sort job's memory (2 GB by default). In doing that, the scheduler assumes that the input for the mapper approximately equals its output. In practice, the mapper may be filtering input data with an output-to-input ratio of 1:1000 or less. As a result, each partition's buffer gets something like several records, which leads to a large number of random, small-portioned reads from the disk in the shuffle process at the beginning of the sort phase. Example: Let's say a partition map job is input with 2 GB of data, produces an output of 4 MB, and the `partition_count` is 1000. Then, the average pre-compression size of a block written by the partition map job will be around 2 KB. Such blocks cause a huge number of instances of random access to the disk. To combat this problem, the `map_selectivity_factor` option was added to an operation's specification, allowing users to set the approximate output-to-input ratio at the start of the operation and improve the scheduler's estimates. Alternatively, you can explicitly specify the desired number of reduce jobs using the `partition_count` option. 1. **"Heavy" mappers and a small number of parallel jobs due to memory shortage.** The scheduler allocates resources for a job based on the memory required for the user-provided code (`memory_limit`, set by the user in the operation's specification) and the memory for the system elements (JobProxy). For a map job (within the Map operation), the memory is made up of `memory_limit`, the read buffer, and the write buffer. Example: Let's say the `memory_limit` is 250 MB and the buffer sizes are 100 + 100 MB, totaling 450 MB per job. If the fair_share distribution for the operation is 0.1 and the total amount of the cluster's resources is 11 TB of memory and 7000 CPUs, then the resources at the operation's disposal are 0.1 * 11 TB = 1.1 TB, and 0.1 * 7000 = 700 CPUs. Therefore, min(1100 GB / 450 MB, 700) = min(2500, 700) = 700 jobs can run in parallel. For partition map jobs, memory is the sum of `memory_limit`, read buffer, and partition buffer. For operations with many partitions, the scheduler allocates to the partition map job a large amount of memory for the partition buffer so as to avoid small blocks. For example, given 1000 partitions and a 2 GB buffer, the worst-case block size will be 2 GB / 1000 = 2 MB (with even data distribution). Going back to our previous example, memory amount for a partition map job will be 250 MB + 100 MB + 2 GB ≈ 2400 MB, and with 1.1 TB of available memory the scheduler will be able to run only min(1100 GB / 2400 MB, 700) = min(470, 700) = 470 jobs in parallel. If the mapper is "heavy" (high CPU usage per unit of output data), then tamping down on concurrent operations will lead to a significant hike in the execution time and cancel out any possible gains from merging the map and partition phases. Since the typical size of the output from the filter mapper is very small, such a mapper should also be considered "heavy". The best solution in this situation is to use the Map + MapReduce combination (with a trivial mapper). This already results in higher intermediate data storage metrics compared to the Map + Sort + Reduce combination. ### Measurement results A good example of the improvement in performance is the classic [WordCount](https://en.wikipedia.org/wiki/Word_count) problem. It boils down to a single MapReduce operation, wherein the mapper aggregates data by key. The table shows the different times it took to solve a WordCount problem using the merged MapReduce operation, the Map + Sort + Reduce sequence, and Map + MapReduce (the last using a trivial mapper). | Operation | Time | | --------------- | ----- | | MapReduce | 03:25 | | Map + MapReduce | 05:30 | | Map + Sort + Reduce | 08:10 | These results were collected on a cluster with 200 nodes, each having 48 GB of RAM and 24 cores for jobs. The input data size was 1 TB. ## MapReduce operation options General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The MapReduce operation supports the following additional options (default values, if set, are specified in brackets): * `mapper` — user-defined script for the map phase. If not specified, input table data is output by the operation without processing. * `reducer` — user-defined script for the reduce phase. * `reduce_combiner` — description of the reduce_combiner script (more details [below](https://ytsaurus.tech/docs/en/#reduce_combiner)). * `sort_by` (defaults to `reduce_by`) — list of columns used for sorting data input at the reduce phase. The sequence of the `reduce_by` fields should be set as the prefix of the `sort_by` field sequence. * `reduce_by` — list of columns used for grouping. * `input_table_paths` — list of input tables with full paths (must not be empty). * `output_table_paths` — list of output tables. * `mapper_output_table_count` — number of tables from `output_table_paths` that will be output at the map phase. For such tables, the job's `table_index` is counted from one, and the intermediate output is a null output table. * `partition_count`, `partition_data_size` — options that specify how many partitions are to be made in the sorting process. * `map_job_count`, `data_weight_per_map_job` — options that indicate how many jobs should be run at the map phase (these serve as recommendations). * `data_weight_per_sort_job` — option to control the amount of data at the `reduce_combiner` input (more details [below](https://ytsaurus.tech/docs/en/#reduce_combiner)). * `force_reduce_combiners` (false) — forces the launch of `reduce_combiner` (more details [below](https://ytsaurus.tech/docs/en/#reduce_combiner)). * `map_selectivity_factor` (1.0) — proportion of the original amount of data that remains after the map phase (the default value is 1.0, which means the expected size of the data remains unchanged during the map phase; 2.0 means the data size doubles, and so on). * `intermediate_data_replication_factor` (1) — replication factor for intermediate data. * `intermediate_data_account` (intermediate) — account to whose quota the transaction's intermediate data goes. * `intermediate_data_medium` (default) — [type of medium](https://ytsaurus.tech/storage/media.md) to write chunks of intermediate data to. * `intermediate_compression_codec` (lz4) — codec used for compressing intermediate data. * `intermediate_data_acl` `({action=allow;subjects=[everyone];permissions=[read]})` — permissions for accessing intermediate data that are set up following the map phase. * `map_job_io, sort_job_io, reduce_job_io` — I/O settings for the respective job types; in the `reduce_job_io` option, the `table_writer` section is added for all jobs that write to output tables. * `sort_locality_timeout` (1 min) — time during which the scheduler waits for resources to free up on specific cluster nodes in order to start sorting all the parts of each partition on a node. This is necessary to ensure higher read locality in the course of the subsequent merging of sorted data. * `ordered` (false) — enables the logic that is similar to ordered map at the partition map phase (input data is divided into jobs in successive segments, with the input of each map phase job being fed rows according to the order contained in the input tables). * `pivot_keys` — list of keys to be used for data partitioning at the reduce phase. This option is exactly the same as the corresponding reduce [option](https://ytsaurus.tech/docs/en/reduce.md). * `compute_pivot_keys_from_samples` (false) — enables automatic key generation for data partitioning using samples from the table. The option can be enabled only if `mapper` is not specified. * `samples_per_partition` (1000) — number of keys for samples from the table for each partition. * `reducer/enable_input_table_index` / `reduce_combiner/enable_input_table_index` (false) — whether to send the input table index to the `reduce` / `reduce_combine` phase (the `enable_input_table_index` option is specified within the `reducer` / `reduce_combiner` section). * `mapper/output_streams` / `reduce_combiner/output_streams` — description of intermediate output streams from the `map` or `reduce_combine` phase, more details [below](https://ytsaurus.tech/docs/en/#output_streams) (the `output_streams` option is specified within the `mapper` / `reduce_combiner` section). * `disable_sorted_input_in_reducer` (false) — disables the sorting of inputs of reduce jobs. Other guarantees are maintained. In particular, all rows with the same keys from `reduce_by` columns will be sent to a single reduce job. By default (and for historical reasons), MapReduce operations support `table_index` only in a mapper. If you need to have `table_index` in a reducer, you can enable the `enable_input_table_index` option within the `reducer` (or `reduce_combiner`) section. In the future, this option may be enabled by default. To enable sorting by `table_index` in a reducer, you need to add a column with the name `$table_index` to `sort_by`. ## Working with large keys — reduce_combiner { #reduce_combiner } The reduce phase has a special data processing stage for dealing with large keys. This stage's jobs are called **reduce_combiner** jobs. They run on parts of "large" partitions and facilitate a partial reduce without waiting for all partition parts to get sorted and for reduce with the final merge to start. As input, reduce jobs running on partitions like that receive merged outputs of several `reduce_combiner` jobs. `Reduce_combiner` is triggered if the partition size exceeds `data_weight_per_sort_job`. The amount of data in `reduce_combiner` equals `data_weight_per_sort_job`. The default value for `data_weight_per_sort_job` is set in the scheduler configuration, but can be overridden via an operation's specification (in bytes). You can also force-start `reduce_combiner` by setting `force_reduce_combiners` to `true`. `Reduce_combiner` receives a sorted stream of records as input (like a regular reducer). However, these records do not necessarily form a continuous range in `sort_by` order. If two given records end up the `reduce_combiner` job, there may be records between them (in `sort_by` order) that are not in this job. There are several restrictions on the `reduce_combiner` output: * The output must be sorted. * `reduce_combiner` must not change keys — columns specified in the specification's `sort_by` field (if not specified — in the `reduce_by` field). * There must be only one output table, as in the map phase of the `MapReduce` operation. This means that any commutative and associative reducer can be used as `reduce_combiner` in its original form. Starting an operation with `reduce_combiner` may look like this: ``` yt map-reduce --reduce-combiner cat --reducer cat --reduce-by cookies --src //statbox/access-log/2013-05-15 --dst //tmp/asd --format dsv ``` ## Intermediate streams { #output_streams } In ordinary cases, intermediate data streams between `map`, `reduce_combine`, and `reduce` phases have no schema. However, a schema is required to work with composite data types and some efficient formats. To set data schemas for these intermediate streams, use the `output_streams` option. The option is specified in the `mapper` / `reduce_combiner` sections and must contain a list of dictionaries with the following keys: * `schema` — YSON description of the stream schema. If the option is specified, the user code can assume that it writes to multiple virtual tables (by the number of items in the `output_streams` list). The next phase gets combined data from all intermediate tables and can use `table_index` of input data to determine which stream a particular row came from. If both `mapper/output_streams` and `mapper_output_table_count` options are specified, then first comes intermediate data followed by output data. That is, intermediate data will have `table_index = 0 ... len(mapper.output_streams)-1` while output tables will have `table_index = len(mapper.output_streams) ... len(mapper.output_streams) + mapper_output_table_count`. Example in the specification: ```yson mapper = { output_streams = [ { "schema" = [ { name=key; type= ... }; ... ] }, ] } ``` ## Example specification Example of a MapReduce operation's specification: ```yson { partition_count = 100; reduce_by = ["key"]; sort_by = ["key"; "subkey" ]; input_table_paths = [ "//tmp/input_table" ]; output_table_paths = [ "//tmp/first_output_table"; //tmp/second_output_table" ]; mapper = { command = "python my_mapper.py"; file_paths = [ "//tmp/my_mapper.py" ]; tmpfs_path = "."; tmpfs_size = 1000000; copy_files = %true; }; reducer = { command = "python my_reducer.py"; file_paths = [ "//tmp/my_reducer.py" ]; }; } ``` # Sort The Sort operation sorts the data from the selected tables and writes it to one output table. The Reduce operation can be run on a sorted table, and when data is read from the table (in particular, with specified ranges), it appears in a sorted form. It is also possible to use ranges by value, not only by table row number. The comparison is performed first by type, and then, if the types match, in the regular fashion (numbers by value, strings — lexicographically). {% note info "Note" %} In the Sort operation, there are no guarantees when it comes to the order of rows with the same key value. {% endnote %} One of the main sorting parameters is the number of partitions (`partition_count`). This parameter is calculated in advance. Before the main jobs start, samples are read from the input table. These samples serve to calculate the key ranges for splitting input data into parts (based on `partition_count`) of approximately the same size. Next, things proceed in the following phases: 1. Running first are partition jobs, which split all their input into partitions based on `partition_count`, matching each row to a partition. 2. Second phase is sorting jobs, which sort the data from the partitions. Here, there are two options: if there is little data in the partition, it is sorted whole, and the partition processing ends there. If there is a lot of data and one job is not enough to sort it, sorting jobs are run on fixed-size parts of the partition before the transition to the third phase. 3. The third phase consists of merge jobs, which merge the sorted parts of large partitions. General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The Sort operation supports the following additional options (default values, if set, are specified in brackets): * `sort_by` — list of columns used for sorting (mandatory). * `input_table_paths` — list of input tables with full paths (cannot be empty). * `output_table_path` — full path to the output table. * `partition_count`, `partition_data_size` — options that specify how many partitions are to be made in the sorting process (these are recommendations). * `partition_job_count`, `data_size_per_partition_job` — specify how many partition jobs should be started (these are recommendations). * `intermediate_data_replication_factor` (1) — replication factor for intermediate data. * `intermediate_data_account` (intermediate) — account to whose quota the transaction's intermediate data goes. * `intermediate_compression_codec` (lz4) — codec used for compressing intermediate data. * `intermediate_data_medium` (`default`) — type of [medium](https://ytsaurus.tech/storage/media.md) storing chunks of intermediate data produced by sorting. * `partition_job_io, sort_job_io, merge_job_io` — I/O settings for the respective job types; in the `merge_job_io` option, the `table_writer` section is added for all jobs that write to output tables. * `schema_inference_mode` (auto) — schema definition mode. Possible values: auto, from_input, from_output. For more information, see the [Data schema](https://ytsaurus.tech/storage/static-schema.md#schema_inference) section. * `samples_per_partition` (1000) — number of keys for samples from the table for each partition (only available for dynamic tables). * `data_size_per_sorted_merge_job` — determines the amount of input data for merge jobs (it is a recommendation). * `sort_locality_timeout` (1 min) — time during which the scheduler waits for resources to free up on specific cluster nodes in order to start sorting all the parts of each partition on a node. This is necessary to ensure higher read locality in the course of the subsequent merging of sorted data. By default, `partition_count` and `partition_job_count` are calculated automatically based on the amount of input data so as to minimize the sorting time. ## Example specification Example of a Sort operation's specification: ```yson { data_size_per_partition_job = 1000000; sort_by = ["key"; "subkey" ]; input_table_paths = [ "//tmp/input_table1"; "//tmp/input_table2" ]; output_table_path = "//tmp/sorted_output_table"; } ``` # Merge The Merge operation merges input tables into one. The output is a table whose set of rows is equal to all the rows of the original tables merged together. The `mode` option determines how the merge is performed: * `mode = unordered` — only merge lists of input table chunks. This the default merge mode. * `mode = sorted` — merges the input tables while maintaining sorting. The columns based on which the output table is sorted are determined by the `merge_by` option. * `mode = ordered` — keeps the order of the chunks from the input table list. Meaning if `T1 = (A1, A2), T2 = (B1, B2, B3)`, then `Merge(T1, T2) = (A1, A2, B1, B2, B3)`. General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The Merge operation supports the following additional options (the default values, if set, are specified in brackets): * `mode` (unordered) — the type of Merge operation, can take the following values: `unordered`, `sorted`, `ordered`. * `input_table_paths` — list of input tables with full paths (cannot be empty). * `output_table_path` — full path to the output table. * `combine_chunks` (false) — activates the consolidation of chunks. * `force_transform` (false) — forces the read of all the input table data. * `merge_by` — list of columns used for sorted merge. * `job_count`, `data_size_per_job` (256 MB) — options specifying how many jobs should be started (these are recommendations). The `job_count` option takes precedence over `data_size_per_job`. If `job_count` is less than or equal to `total_input_row_count`, the number of jobs for the unordered/ordered merge operation is guaranteed to be met exactly, so long as it does not conflict with the limit on the maximum number of jobs for the operation. In particular, if `job_count` is equal to`total_input_row_count`, then each job gets exactly one row, and if `job_count` equals 1, only one job is launched. * `schema_inference_mode` (auto) — schema definition mode. Possible values: auto, from_input, from_output. To learn more, see [Data schema](https://ytsaurus.tech/storage/static-schema.md#schema_inference). If the `force_transform` option is `false`, the operation proceeds in such a way as to minimize the number of running jobs. The best scenario will have the merge carried out on the meta-information level. Otherwise, all chunks will be read. In addition to disk space, data also uses the chunk quota of the account where it is located. It is a good idea to run the Merge operation even on a single table to “consolidate” the existing chunks. To do this, enable `combine_chunks=true`. To learn more about account quotas, see [Quotas](https://ytsaurus.tech/storage/quotas.md). ## Example specification ```yson { pool = "my_cool_pool"; mode = "sorted"; merge_by = [ "field1"; "field2" ]; job_count = 100; input_table_paths = [ "//tmp/input_table1"; "//tmp/input_table2" ]; output_table_path = "//tmp/output_table"; combine_chunks = %true; } ``` # Erase The Erase operation deletes the specified row range from the table. If the range is not specified for the input table, all data is deleted from the table (the table itself remains). General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The Erase operation supports the following options (if set, default values are specified in brackets): * `table_path` — path to the input table. Enables specifying row ranges on the input table, such as `//path/table[#10:#100]`. You can delete only a range of rows as opposed to all the data in the table. * `combine_chunks` (false) — combine the remaining chunks in the table so that they are large enough. * `schema_inference_mode` (auto) — schema definition mode. Possible values: auto, from_input, from_output. To learn more, see [Data schema](https://ytsaurus.tech/storage/static-schema.md#schema_inference). ## Example specification ```yson { table_path = "//tmp/input_table[#10:#100]"; combine_chunks = %true; } ``` # RemoteCopy The RemoteCopy operation copies tables and files from one cluster to another. The RemoteCopy operation's jobs are run on the destination cluster and copy chunks from the source cluster ("pull"-schema). The operation copies the chunks in their original form, without uncompressing them or changing the `erasure` schema. If the input is a table and it is sorted, the output table will also be sorted. {% note info %} The data transfer rate between clusters is subject to throttling. For more information, see [Inter-cluster Network Bandwidth Throttling](https://ytsaurus.tech/docs/en/cluster-throttlers.md). {% endnote %} General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). The RemoteCopy operation supports the following additional options (default values, if set, are specified in brackets): * `cluster_name` — name of the cluster from which the data is copied. A description of the available clusters is available on the `//sys/clusters` node. * `network_name` — name of the network (YTsaurus terms) used for copying. The specified network must be configured on all nodes of the remote cluster. For example: `fastbone` or `backbone`. * `networks` — set of networks used to copy the data (in order of priority). **Important:** Do not enable this and the previous option without first consulting with the system administrator. * `input_table_paths` — list consisting of a single path to file or table to be copied. * `output_table_path` — path to the table or file where the data is copied to. If this Cypress node exists, it must have the same type as the input node. It is not possible to copy table to file and vice-versa. * `schema_inference_mode` (auto) — schema definition mode. Possible values: auto, from_input, from_output. To learn more, see [Data schema](https://ytsaurus.tech/storage/static-schema.md#schema_inference). * `cluster_connection` — configuration for connecting to the source cluster. Configured by the YTsaurus system administrator when configuring the cluster. * `copy_attributes` (false) — copy the attributes of the input table. Available only if there is one input table. Only user attributes are copied (not system attributes). * `attribute_keys` — when this option is enabled along with `copy_attributes`, only attributes from the given list are copied. ## Example specification ```yson { pool = "my_cool_pool"; network_name = "fastbone"; input_table_paths = [ "//tmp/input_table" ]; output_table_paths = "//tmp/output_table"; copy_attributes = %true; cluster_name = "my_cool_remote_cluster"; } ``` ## Dynamic tables The RemoteCopy (`remote_copy`) operation is supported only for [sorted tables](https://ytsaurus.tech/dynamic-tables/sorted-dynamic-tables). To copy an ordered table, you need to convert it to a static one. It is impossible to copy an ordered table to another cluster with preserving tablets. For dynamic tables, you need to prepare the table into which you plan to copy the data before copying. It is not possible to specify a non-existent path in `output_table_path`. The table must be created with the same schema as the input table, all user attributes and system settings from the input table must be copied and reshard by keys in advance if necessary. While working with dynamic tables, you may encounter system objects known as chunk views within the table structure. Since the remote copy operation doesn't support these objects, you must remove them before starting the operation. To do this, set the `forced_chunk_view_compaction_revision` attribute to 1 and then run `remount-table`. For more details, see [Forced compaction](https://ytsaurus.tech/_includes/user-guide/data-processing/operations/....user-guidedynamic-tablescompaction#forced_compaction). By default, the input dynamic table must be frozen (`frozen`) or unmounted (`unmounted`) — otherwise the operation will fail with an error. The `allow_unfrozen_input_tables` parameter (default `false`), specified at the root of the operation specification, removes this check and allows copying in best-effort mode. In this mode, tablet state checking is skipped, and data that has not been flushed to disk at the time of copying may not be included in the result, so copy consistency is not guaranteed. Only use this parameter when it is acceptable to obtain a potentially incomplete copy. # Vanilla Vanilla operations execute arbitrary code. They do not have input tables, but they do support output tables, tables for saving data from a standard error stream (`stderr` tables), and tables with `coredump`. ### Key points to know * Vanilla operations can be used to start a distributed process: you can describe several types of jobs and specify how many jobs of each type there should be. [The YTsaurus scheduler](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md) carries the function of keeping this distributed process running, working to have the required number of jobs of each type planned and completed, just like in any other YTsaurus operation. * A Vanilla job receives input data via the `file_paths` option in the user script specification or reads data directly over the network. {% note warning "Attention" %} If you upload data to the job directly over the network, first discuss the intended use method with a YTsaurus system administrator and with a representative of the service you plan to collect data from. This is important to avoid excessive load on the network and the service (data source). {% endnote %} {% note info "Note" %} The user is responsible for all coordination and distribution of work between the running jobs, as well as for maintaining consistency in case of job restarts. {% endnote %} * Jobs can write output data over the network (in which case network load assessment is in order) and output to tables. Each type of job is associated with its own output tables. ### Tasks and script The main part of describing a Vanilla operation is the enumeration of various **tasks**. In the YTsaurus ecosystem, a task is a group of jobs of the same type running as part of an operation. * Task description encompasses the user script description, the number of jobs to be executed within this task, and, sometimes, the description of the output tables from this task. * A task description in a Vanilla operation is very similar to a user script description, but we should draw a distinction between these concepts, because they correspond to different things in the [Scheduler](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md). * A user script description serves specifically to indicate how a user-provided code is to be run on a machine. It does not contain any information about how much data should be given to each job. **For example:** the `data_weight_per_map_job` option in a MapReduce operation is not included in the user script specification, because it is not related to executing the user script (which may be absent) at the map stage. * A task description is a specification of how the scheduler should manage jobs of this type. This concept is found only in Vanilla operations. General parameters for all operation types are described in [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). ## Operation options Vanilla operations support the following additional options: * `tasks`: A dictionary describing the operation's tasks. Each task must correspond to a key–value pair, where the key is the task's name, and the value is the task's specification. In YTsaurus, the lowercase_with_underscores task naming convention is preferred, like `partition_map`. The name of the task is displayed on the operation's page in the web interface, and it is also available as keys in the operation progress represented in yson format, so it is recommended that you not make it too long. The task specification features the same options as the user script specification, plus some additional ones: * `job_count`: The number of jobs of a given type that should run as part of the task. If the `fail_on_job_restart` option is not set in the operation's specification, the task is considered completed when exactly `job_count` of jobs of this type finish successfully (that is, with code 0 and without interruptions for any reason). * `output_table_paths`: A list of output tables (can be empty). If the same table is the output for different tasks, all its occurrences must feature the same attributes on the paths. * `gang_options`: Options for gang operations (see [Gang operations](https://ytsaurus.tech/docs/en/#gang-operations)). {% note info "Note" %} You can easily control job restarts using the `max_failed_job_count` and `fail_on_job_restart` options (common for all operations). Vanilla operations additionally support one more option: `restart_completed_jobs`. It enables restart mode for completed jobs. {% endnote %} ## Example specification ```yson { tasks = { master = { job_count = 1; command = "python run_master.py < duties.dat"; file_paths = ["//tmp/run_master.py"; "//tmp/duties_180124.dat"]; }; slave = { job_count = 100; command = "python run_slave.py"; file_paths = ["//tmp/run_slave.py"]; }; }; max_failed_job_count = 1; stderr_table_path = "//tmp/stderrs"; } ``` ## Gang operations { #gang-operations } Gang operations are a special mode of Vanilla operations that is intended for starting coordinated distributed processes. In contrast to regular Vanilla operations, where each job runs and completes independently from the YTsaurus controller agent's perspective, the controller agent ensures the restart of all jobs in a gang operation if any job in a **gang** task aborts or fails. This includes the restart of completed jobs as well (this restart type is called a gang restart). A gang task is a task whose description contains the `gang_options` option. Gang operations must contain at least one gang task. ### Operation incarnations and job ranks One of the most important concepts in the gang operation model is incarnation. * An incarnation is a string ID that uniquely defines a gang operation's lifecycle period between two consecutive start, restart, or completion events. * Jobs inherit their operation's incarnation, but their own incarnation never changes. If an operation's incarnation changes, jobs from the previous incarnation are aborted. * Each job (except for [reserved jobs](https://ytsaurus.tech/docs/en/#reserved-jobs)) also has a unique rank within a gang operation task. **For example:** a typical use case for gang operations is launching distributed ML training. In this use case, jobs interact with each other over the network. Incarnations are primarily essential for user code to prevent communication between jobs from the new incarnation and from the old one. ### Restarting a gang operation What happens when the controller agent decides to restart a gang operation: controller agent aborts all jobs and attempts to restart jobs in the new incarnation. Controller agent attempts to reuse the allocations for the new jobs due to optimization. Completed jobs are also restarted. If the controller agent manages to reuse a job allocation from the previous incarnation when the operation transitions to a new incarnation, the following guarantees apply: * `job_cookie` of jobs from the previous and new incarnations match. * If a job from the previous incarnation was assigned a `gang_rank`, the job in the new incarnation is assigned the same `gang_rank`. * If a job from the previous incarnation was assigned a `monitoring_descriptor`, the job in the new incarnation is assigned the same `monitoring_descriptor`. ### Reserved jobs { #reserved-jobs } When a gang operation is restarted, there will be at least one allocation that is not reused — the allocation of the job whose termination prompted the controller agent to restart the operation. Typically, starting jobs in reused allocations is significantly faster than scheduling a new allocation and starting a new job there. {% note tip %} To prevent idle time for jobs for which the controller agent managed to reuse an allocation, you can use the reserved jobs mechanism during the time it takes for a job to start in a newly created allocation. {% endnote %} Reserved jobs are jobs that are not part of a gang operation in the current incarnation (their termination does not restart the gang operation, nor do they have a `gang_rank`), but their allocations can be used to start jobs in the new incarnation. * Although reserved jobs are not part of a gang operation, they still run user code. To learn how user code can determine if it is running in a backup job or a gang job, see [Environment variables](https://ytsaurus.tech/docs/en/#environment-variables). * When `gang_options/size` is less than `job_count`, the controller agent starts `job_count`-`gang_options/size` reserved jobs. ### Behavior when reviving an operation When [reviving](https://ytsaurus.tech/docs/reliability.md#revival) a gang operation, the system checks that all non-reserved jobs have been successfully restored. If this is not the case, the operation will be restarted without reusing allocations. ### Special monitoring mode for gang operations Gang operations support a special mode for generating monitoring descriptors: **The `use_operation_id_based_descriptors_for_gangs_jobs` option** enables a special mode for generating descriptors for gang operations: the descriptor is generated based on `operation_id`, `gang_rank`, and task index, which guarantees uniqueness. All these descriptors use `operation_id` as a prefix. {% note tip %} This mode poses a potential risk of monitoring quota overflow, so you should use it with caution, only after consulting with the YTsaurus team. {% endnote %} ### Environment variables { #environment-variables } In gang operations, jobs have the following additional environment variables: * `YT_OPERATION_INCARNATION`: Unique operation incarnation ID. * `YT_TASK_GANG_SIZE`: The total job group size across all operation tasks. * `YT_GANG_RANK`: The current job rank on the task (from 0 to `YT_GANG_SIZE - 1`). * `YT_TASK_JOB_COUNT`: The total number of jobs in a task. ### Restrictions and special features * Gang operations are not compatible with `fail_on_job_restart`. * Gang operations are not compatible with `restart_completed_jobs = true`. * If any gang job fails or aborts, the entire group is restarted with a new incarnation. * Changing `job_count` while a gang operation is running is prohibited. * **When reviving an operation**: If not all ranked jobs can be restored, the entire group is restarted with a new incarnation. * Output tables are not supported. ### Configuring gang operations To enable gang mode, add the `gang_options` parameter to the task specification: ```yson { tasks = { worker = { job_count = 3; command = "python distributed_worker.py"; gang_options = {}; }; }; } ``` ### gang_options parameters * `size`: The job group size. By default, it equals the task's `job_count`. It cannot exceed `job_count`. Setting `size` enables the backup job mechanism. # Operation options This section describes general properties of various operations, as well as user script parameters on paths, and file attributes. ## General options for all operation types { #common_options } The parameters below can be indicated in any operation's specification root. The default values, if set, are provided in parentheses: - `pool` — name of the [compute pool](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md) where the operation runs. If this parameter is not specified, the pool name defaults to the name of the YTsaurus user who started the operation. - `weight`(1.0) — weight of the started operation. It determines how much of the pool's compute quota is allocated to the operation. For more information, see [Scheduler and pools](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md). - `pool_trees` — list of [pool trees](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md) where the operation's jobs are run. The default pool tree is configured by the cluster administrator. - **DEPRECATED** `tentative_pool_trees` — list of test pool trees that will be used for running the operation if there is no performance degradation. - **DEPRECATED** `use_default_tentative_pool_trees` — enable the use of all trees in the cluster that are `tentative` relative to the default pool tree. - **DEPRECATED** `tentative_tree_eligibility` — configuration for running the operation's jobs in `tentative` trees: - `sample_job_count` (10) — number of jobs to be run as samples. - `max_tentative_job_duration_ratio` (10) — maximum allowed latency of job execution. - `min_job_duration` (30) — job duration threshold in `tentative` trees. If the average job duration in the `tentative` tree exceeds this threshold, new jobs of the operation are not run in that `tentative` tree. - `ignore_missing_pool_trees` (false) — allows ignoring `tentative` trees that are missing on the cluster. - `resource_limits` — fair-share settings that will apply to all jobs of the operation in aggregate. For more information, see [Operation resources configuration](https://ytsaurus.tech/docs/scheduler/pool-settings.md#operations). - `time_limit` — general time limit on the operation (in milliseconds). If the operation does not complete within the specified time, it will be force-stopped by the scheduler. - `acl` — Access Control List, which sets out access permissions for the operation. The `manage` permission is associated with mutating actions on the operation and its jobs: abort, suspend, job shell, and so on. In turn, the `read` permission relates to non-mutating actions like get_job_stderr and get_job_input_context. This `acl` will be supplemented with records on the user that ran the operation and YTsaurus administrators. - `max_stderr_count` (10) — limit on the amount of stored data from the standard error stream (stderr) of jobs, by default taken from cluster settings that usually have it equal to 10. The maximum value is 150. - `max_failed_job_count` (10) — number of failed jobs past which the operation is considered failed. By default, this value is taken from the cluster settings, where it is usually set to 10. - `unavailable_chunk_strategy` and `unavailable_chunk_tactics` — determine further action in case intermediate calculation results are unavailable. For more information, see [Operation types](https://ytsaurus.tech/docs/en/overview.md#chunk_strategy). - `chunk_availability_policy` — system behavior if erasure chunks are unavailable. For more information, see [Operation types](https://ytsaurus.tech/docs/en/overview.md#chunk_erasure_strategy). - `scheduling_tag_filter` — filter for selecting cluster nodes upon job launch. The filter is a [logical expression](https://ytsaurus.tech/admin-guide/node-tags.md) that includes `&` (logical AND), `|` (logical OR), and `!` (logical NOT) operators and parentheses. The possible values of cluster node tags act as variables in the expression. A tag is a marker placed on a cluster node. Markup is done by the YTsaurus service when configuring a cluster. If the cluster node has a tag specified in the expression, the expression takes the "true" value, if not — the value is "false". If the expression as a whole returns "false", then that cluster node is not selected to run the job. - `max_data_weight_per_job` (200 GB) — maximum allowed size of input data per job (in bytes). This option sets a hard cap on the job size. If the scheduler is not able to generate a job with a smaller size, the operation ends with an error. - `data_weight_per_job` — recommended size of input data per job (in bytes). The default value depends on the operation type: 128 MB for Map and Reduce, 20 GB for Merge. This option is useless for composite operations such as MapReduce, which instead call for the following options: - `data_weight_per_map_job` - `data_weight_per_sort_job` - `data_weight_per_partition_job` - `data_weight_per_sorted_merge_job` - `data_weight_per_reduce_job` - `secure_vault` — values from this map end up in the environments of all the user jobs of a given operation and can't be viewed by unauthorized users, unlike the `environment` section in the user job specification. In particular, the entire passed-in map in the YSON format is written to the `YT_SECURE_VAULT` environment variable, and, to save time, "value" is written to the `YT_SECURE_VAULT_key` environment variable for each `key=value` pair from the map. This only happens for elementary type values, such as int64, uint64, double, boolean, or string. - `stderr_table_path` — for specifying the path to an existing table, which must be created outside of transactions. If this option is enabled, the complete data from the standard error stream (stderr) of all jobs (except for the aborted ones) is written to the specified table. That table will have the following columns: - `job_id` — job ID. - `part_index` — for large error messages, the stderr message of a single job can be split into several segments that are stored in different rows. This column contains the numbers corresponding to these segments. - `data` — data from the standard error stream. - `redirect_stdout_to_stderr` — option for redirecting a standard output stream (stdout) to a standard error stream (stderr). - `core_table_path` — path to an existing table for writing core dump. The table format is identical to that described for `stderr_table_path`. - `input_query` — for the [Map](https://ytsaurus.tech/docs/en/map.md) and [Merge](https://ytsaurus.tech/docs/en/merge.md) operations, you can pre-filter each input row using the [query language](https://ytsaurus.tech/dynamic-tables/dyn-query-language.md). Because the input table is already known, and it is assumed that each row will be individually filtered, the query syntax changes and gets simplified to `* | [AS ]... [WHERE ]`. Speeifically, you cannot use `GROUP BY` and `ORDER BY`. Queries need strictly schematized data, so input tables must have a strict schema. In the [Merge](https://ytsaurus.tech/docs/en/merge.md) operation, the schema of the final data in the query is incorporated when outputting the schema for the output table. {% note info "Note" %} If the `input_query` parameter is specified, the `enable_row_index` and `enable_range_index` attributes are excluded from the job. {% endnote %} - `omit_inaccessible_columns` (false) — skips columns that cannot be accessed. By default, operations in this scenario end with an error at startup. - `omit_inaccessible_rows` (false) — skips rows that cannot be accessed. By default, operations in this scenario end with an error at startup. - `alias` — alias of the operation. Makes the operation available in search and through the commands (e.g. `get-operation`) on the specified row. The alias must start with the `*` symbol. Only one operation with this given alias can be run simultaneously. You can run an operation with another alias or rerun the operation with the same alias after it is completed. The `get-operation` returns the recent operation. - `job_node_account` (tmp_jobs) — account under which chunks of files with the error log (stderr) and input context are stored. - `suspend_operation_if_account_limit_exceeded` (false) — suspends (pauses) the operation in case of the "Account limit exceeded" error in a job. - `enable_job_splitting` (true) — determines if the scheduler is allowed to adaptively split user jobs that take a long time. - `enable_job_interrupts` (true) — determines whether operation jobs may be gracefully interrupted. Disabling this option also disables adaptive job splitting. - `fail_on_job_restart` (false) — force-interrupts the operation if a started job terminates in any state other than success (such as abort or fail). Useful for operations that do not tolerate job restarts. - `suspend_on_job_failure` (false) — suspends (pauses) the operation if any job fails. When enabled, instead of failing the operation after reaching `max_failed_job_count`, the operation is suspended for investigation. The operation can be resumed after addressing the issue. - `title` — operation title. The passed-in text is shown in the web interface. - `started_by` — a map describing the client that started the operation. This field is not associated with a strict schema. The specified information is shown on the operation page in the web interface. - `annotations` — a map where the user can write any structured information related to the operation. The `annotations` contents are saved to the operation archive as [YSON](https://ytsaurus.tech/storage/formats.md#yson) and can then be used to look up and identify operations within the archive. {% note info "Note" %} Adding user information to the specification root is not recommended: This leads to user data appearing in Unrecognized Spec. In the future, this practice will be prohibited. {% endnote %} - `description` — a map similar to `annotations` that also appears on the operation page in the web interface. It is best to add only small amounts of human-readable information to this section to avoid overcrowding the operation page. - `use_columnar_statistics` (false) — determines whether to use columnar statistics to accurately estimate the size of jobs when starting operations on tables with column samples. This option improves automatic splitting into jobs, with columns taken from the input data forming a small fraction of the input table size (previously, column selection was ignored, and the resulting jobs were very small, which had to be rectified with the `data_weight_per_job` option). - `sampling` — configuration of large-block sampling of input data (not available for Sort, RemoteCopy, and Vanilla operations). This option is obsolete: use [read sampling](https://ytsaurus.tech/storage/io-configuration.md#sampling) instead. The `sampling` option supports only block-based sampling. It works to proportionally decrease the amount of data read from disk, which, under ideal conditions, should result in a proportional increase in operation speed. The trade-off is that the resulting process is not really sampling in its true mathematical sense, but only an approximation: the data is divided into fairly large blocks of successive rows, and then these blocks are sampled whole. The result may be adequate for applied calculations on sufficiently large tables. The parameter value is set by a map with the following keys: - `sampling_rate` (null) — share of sampled rows. - `io_block_size` (16 MB) — minimum sampling block size. We do not recommend changing the default value. - `max_speculative_job_count_per_task` (10) — limit on the number of speculative jobs (currently supported for Map, Reduce, MapReduce, Sort, and Merge operations). - `job_speculation_timeout` — optional timeout in milliseconds, after which the scheduler forcibly launches a speculative copy of the currently executing job (a parallel job on the same input data). The countdown starts from the moment the job begins and includes the preparation time (`prepare_duration`) and execution time (`exec_duration`). Not set by default. This is a guaranteed speculation trigger that is independent of the Job Splitter heuristics, which may launch a speculative job earlier — based on read/write speed statistics, preparation time, and other heuristics. The parameter can be set at two levels: in the operation `spec` (low priority) and in the `user_job_spec` of a specific user job (high priority, overrides the value from `spec`). - `try_avoid_duplicating_jobs` (false) — if possible, prevents jobs with the same input from running, unless required. In particular, this disables speculative jobs. Still, it is impossible to guarantee that there won't be any jobs run with the same input. For example, if the node hosting the first instance of a job loses connectivity with the cluster, the job continues to run on that cluster and can potentially perform observable side actions. In this case, the cluster has to restart the job with the same input on another node. You can configure [I/O parameters](https://ytsaurus.tech/storage/io-configuration.md) for each type of job in the operation. If the operation has one job type, the specification section is called `job_io`. If the operation involves jobs of different types (this is applicable to MapReduce and Sort operations), then each type has its own section. The names of these sections are listed in the settings of the corresponding operations. ## User script options { #user_script_options } The following parameters are supported for each user script (default values, if defined, are provided in parentheses): - `command` (required parameter) — string to be executed via the `bash -c` call. - `file_paths` — list of [Cypress](https://ytsaurus.tech/storage/cypress.md) paths to files or tables. These files and tables are downloaded to the cluster node where the command is executed, and a special `sandbox` environment houses symbolic links to the downloaded files. The links are read-only. The filename defaults to the basename of the Cypress path, but you can change it by specifying the `file_name` attribute for the path. If the path points to a table, the `format` attribute must be specified, which determines the [format](https://ytsaurus.tech/storage/formats.md) in which the table is saved to a file on disk. The size of table files is limited by a constant value that can be configured at the cluster level. - `docker_image` — name of the Docker image for the job's root file system. - `layer_paths` — list of paths to porto layers in Cypress. Layers are listed from top to bottom. - `cuda_toolkit_version` — [CUDA](https://ru.wikipedia.org/wiki/CUDA) version required by the job. If the version is specified, then a standard delta layer with the respective CUDA toolkit is provided to the job. - `format`, `input_format`, `output_format` (yson) — [format](https://ytsaurus.tech/storage/formats.md) in which input table data is fed to the operation. The `input_format` and `output_format` options take priority over `format`. - `environment`— a map of environment variables specified while the operation is running. This option helps avoid specifying environment variables in the `command` parameter so as not to hinder its readability. - `cpu_limit` (1) — number of compute cores that the scheduler may allocate for the job being executed. - `set_container_cpu_limit` (false) — sets the `cpu_limit` for the porto container to the requested value. By default, only the weight proportional to the requested `cpu_limit` is set. - `gpu_limit` (0) — number of GPUs to be allocated for the job. - `memory_limit` (512 MB) — limit on job memory consumption (in bytes). - `user_job_memory_digest_default_value` (0.5) — initial estimate for [memory reserve](https://ytsaurus.tech/docs/scheduler/memory-digest.md) selection. - `user_job_memory_digest_lower_bound` (0.05) — minimum threshold for the [memory reserve](https://ytsaurus.tech/docs/scheduler/memory-digest.md). - `enable_input_table_index` — whether to write the input index of a row's originating table to the input stream. By default, the attributes are only written if there is more than one input table. - `tmpfs_path` — path to the `sandbox` where tmpfs is mounted. By default, the tmpfs size matches the job's `memory_limit`, and the effective occupied space counts toward the job's memory consumption. - `tmpfs_size` — optional parameter that sets the limit of the tmpfs size for a job. This parameter only matters when `tmpfs_path` is specified. If the parameter is not specified, it defaults to `memory_limit`, and `memory_reserve_factor` is automatically set to 1. - `tmpfs_volumes` — this parameter allows specifying a list of tmpfs volumes for the job. Each list element is a map containing two fields: `size` and `path`. The paths to the requested volumes must not overlap. - `copy_files` (false) — by default, user files go to the job's `sandbox` via a [symbolic link](https://ytsaurus.tech/storage/links.md). If this option is enabled, they are copied to the `sandbox`. It can be useful in tandem with `tmpfs_path=.`. - `check_input_fully_consumed` (false) — this option allows you to run system-wide checks to make sure the job has not exited prematurely and read all the data written to its buffer. - `max_stderr_size` (5 MB) — limit on the size of stderr resulting from the job. Any data written to stderr in excess of this limit is ignored. - `custom_statistics_count_limit` (128) — maximum number of user statistics that can be written by the job (up to 1024). - `job_time_limit` — time limit (in milliseconds) on a job. Jobs exceeding this limit are deemed failed. - `disk_request` — list of disks required by the job. By default, a job is assigned a local disk on the cluster node where it was initiated. Disk space usage is not limited, but the job can be interrupted if the disk space runs out. Each disk is characterized by the following parameters: - `disk_space` — maximum total file size for the job's `sandbox` on the disk. - `inode_count` — limit on the number of [inodes](https://ru.wikipedia.org/wiki/Inode) on the disk. - `account` — account for which the requested disk space is provided. - `medium_name` (default) — name of the [medium](https://ytsaurus.tech/storage/media.md) that a disk is being requested from. - `interruption_signal` — number of the signal sent to the user process if the job is preempted. There is a timeout period between sending the signal and the job interruption. The duration of this timeout is determined by the cluster settings. During this time, the user process may attempt to finish processing the data and successfully complete the job. - `restart_exit_code` — exit code signaling the need to restart the job after it was interrupted. This option is important for vanilla jobs that represent services or handle data that is external to YTsaurus. It cannot be combined with `fail_on_job_restart` set for the same task or the whole operation. - `job_speculation_timeout` — optional timeout in milliseconds, after which the scheduler forcibly launches a speculative copy of the currently executing job (a parallel job on the same input data). The countdown starts from the moment the job begins and includes the preparation time (`prepare_duration`) and execution time (`exec_duration`). Not set by default. This is a guaranteed speculation trigger that is independent of the Job Splitter heuristics, which may launch a speculative job earlier — based on read/write speed statistics, preparation time, and other heuristics. The value set in `user_job_spec` takes higher priority than the value set in the operation `spec`. - `waiting_job_timeout` — optional timeout during which the job can remain queued for launch on the node. Not set by default. The allowed value range is from 10 seconds to 10 minutes. - `enable_rpc_proxy_in_job_proxy` (false) — if this option is specified, an RPC Proxy service will be started for each job within the Job Proxy. This provides a convenient way to communicate with the cluster from a job without the risk of overloading the cluster-wide RPC Proxies; the RPC Proxy is accessed via a local socket, the path to which is defined in the `YT_JOB_PROXY_SOCKET_PATH` environment variable. {% note info "Note" %} The options listed above characterize the script rather than the operation as a whole. They should be located not in the specification root, but in branches like `mapper` or `reducer`. The user script is a separate entity because the [MapReduce](https://ytsaurus.tech/docs/en/mapreduce.md) operation involves more than one such script (mapper, reducer, reduce_combiner). {% endnote %} ### Memory reserve { #memory_reserve_factor } By default, the operation controller looks at the jobs statistics and determines a memory reserve for newly started jobs based on that. The mechanism is transparent to users and does not impact the operation's success. However, it may result in aborted jobs due to `resource_overdraft`. For a detailed description of this mechanism, see [Selecting memory for jobs](https://ytsaurus.tech/docs/scheduler/memory-digest). {% note warning "Attention" %} When tmpfs is enabled (specifying `tmpfs_path` is equivalent to enabling tmpfs) without an explicit limit on its size (the `tmpfs_size` parameter), the memory reserve is automatically set to 1. This is because the Linux OS kernel does not allow overcommitting for the tmpfs size, and the total size exceeding the physical capabilities can therefore lead to the cluster node freezing. {% endnote %} ### Multiple input table reads { #many_input_tables } Data from input tables written according to the specified input format is sent to the user script, standard input (`stdin`). The user script has output table write descriptors open. The descriptor numbers `1, 4, 7, ..` are consistent with the order in which the output tables are passed to the operation. ### Disk requests for jobs { #disk_request } If only `disk_space` is specified in the `disk_request` specification, the default disks for jobs are used. In this case, no book-keeping is done on used space — free disk space is assumed to be abundant, no problems are expected. Only the data that the user stores on the cluster counts toward that user's disk quota (account), while the disk space used for the job does not. The scheduler ensures that no job exceeds its limits and that the sum of the limits of all jobs on the cluster node does not exceed the available disk space on the cluster node. In addition, specifying `disk_space` means that YTsaurus starts limiting the space used by the job, but also guarantees it. If you want to use other disks for your operations' jobs, such as an SSD in place of the default HDD, specify the desired `medium` and your `account` for quoting the requested space. ## Path attributes { #path_attributes } The following attributes are supported for operations' output tables (default values, if set, are specified in parentheses): - `sorted_by` — set of columns by which to sort the table. - `append` (false) — determines if data is appended to a given output table. If `append` is set to `false`, the data in the table is overwritten. - `row_count_limit` — finishes the operation ahead of time as soon as the output table reaches at least `row_count_limit` rows. Can only be set for one table. - `create` (false) — if the table at the specified path does not exist, the scheduler will create it automatically with default settings (it will execute the `create` command with the parameters `{ "path" = "//path/from/spec" ; "type" = "table" }`) The following attributes are supported on operations' input tables: - Standard [modifiers for selecting rows and columns](https://ytsaurus.tech/storage/ypath.md#rich_ypath_suffix). - `transaction_id` — specifies the transaction within which to access the input table. - `timestamp` — allows using the state of a dynamic table [at a specific point in time](https://ytsaurus.tech/dynamic-tables/mapreduce.md#dyntable_mr_timestamp). - `rename_columns` — enables renaming certain columns before table processing starts as part of the operation. Only columns within the schema can be renamed. Post-rename, all restrictions on the table schema should be met, and there should be no column name overlaps within chunks when it comes to the non-strict schema. When the `sorted_by` column set is specified for the operation's output table, or when the output table schema has key columns, the following occurs: 1. When generating chunks for the table, the operation's jobs check if records are sorted by the specified set of columns. 2. The scheduler validates that the key ranges of the chunks generated for this output table do not overlap and proceeds to form a sorted table from this set of chunks. The operation will end with an error if the validation fails at any of these steps. ### Files The following attributes are supported on file paths: - `file_name` — name of the file in the job's `sandbox`. - `executable` — makes the file executable. - `format` — format in which the file from the table should be generated if the file constitutes the path to the table. - `bypass_artifact_cache` — a flag indicating that the file should be downloaded to the job's `sandbox`, bypassing the cache. This attribute can be useful if the files are already on the SSD or in memory, and you want to deliver them to the job without extra delays. Good for cases where the operation involves a small number of jobs, rendering caching ineffective. # Job collectives Job collectives is a mechanism that enables you to process one data batch with multiple jobs. This is useful for computations that involve multiple processes running together — think distributed GPU computations or parallel data processing by multiple jobs (when you need more than one). To learn how to set up job collectives in an operation, see [Configuration](https://ytsaurus.tech/docs/en/#configuration). ## Basic concept { #concept } **Operations with job collectives function similarly to regular operations, but with one key difference: each data batch is processed not by a single job, but by a collective of multiple jobs.** This means that: - Job slicing works exactly the same way as in regular operations (meaning you can configure it in the spec the same way as with regular operations). - In terms of the operation's data stream, a collective functions as a single job, receiving one input data batch and generating one output data batch. {% note info "Note" %} Job collectives are not the same as [gang operations](https://ytsaurus.tech/docs/en/vanilla.md#gang-operations). A gang operation restarts **all** its jobs when any non-backup job is restarted, whereas job collectives restart jobs only in a **single** collective (the one processing a given data batch). {% endnote %} ## Use cases { #use-cases } ### Distributed GPU computations A typical use case is data processing using multiple GPUs. The master job reads the data and distributes it among slave jobs, each of which performs computations on its own GPU. ### Parallel processing with aggregation Slave jobs perform parallel computations, while the master job aggregates their results and writes them to the output table. ## Gang operations vs. operations with job collectives { #when-to-use } [Gang operations](https://ytsaurus.tech/docs/en/vanilla.md#gang-operations) are a type of Vanilla operations, so when you need to run an operation of a different type, operations with job collectives are the only option. For scenarios requiring a Vanilla operation, you use either gang or Vanilla operations with job collectives. The difference will lie in some details: - Gang operations provide more guarantees (which, in particular, make restarting a collective more optimal) (see [Restarting a gang operation](https://ytsaurus.tech/docs/en/vanilla.md#gang-operation-restart)). - In contrast to gang operations, operations with job collectives do not support backup jobs (see [Backup jobs](https://ytsaurus.tech/docs/en/vanilla.md#reserved-jobs)). - In contrast to gang operations, operations with job collectives can have output tables (see [Limitations and considerations](https://ytsaurus.tech/docs/en/vanilla.md#gang-operation-limitations)). ## Collective structure { #structure } A collective comprises multiple jobs working together to process a single data batch. Each job in a collective is assigned a **rank** (an integer from 0 to `size - 1` where `size` is the collective size). * **Master job** (rank 0): Reads input data and writes results to output tables. From the system's point of view, the master job is what represents the collective as it interacts with the data stream. * **Slave jobs** (ranks 1, 2, ...): Auxiliary jobs that do not have access to the operation’s input/output data streams. They perform computations and exchange data with the master job over the network. ## Collective lifecycle { #lifecycle } 1. When the controller agent initiates the processing of a data batch, it creates a collective of multiple jobs instead of just one job. 2. The master job receives input data via `stdin` (for Map/Reduce/MapReduce operations) exactly the same way as a regular job. 3. All jobs in a collective can interact with each other over the network using `YT_COLLECTIVE_ID` for identification. 4. Only the master job can write output data. 5. A collective is considered completed when the master job has completed. This means that if slave jobs finished unsuccessfully after the master job has completed (according to the controller agent), the data is still considered processed. ## Failure handling { #failure-handling } If any job in a collective finishes unsuccessfully (fails or aborts), and the master job has not completed yet, the following happens: * All other jobs in the collective are aborted. * The collective is restarted entirely, just like a regular job would. * Other collectives in that operation continue running independently. ## Supported operations { #supported-operations } Job collectives are supported in all operations with user code: | Operation | Where to specify `collective_options` | |----------|-----------------------------------| | [Map](https://ytsaurus.tech/docs/en/map.md) | In the `mapper` specification | | [Reduce](https://ytsaurus.tech/docs/en/reduce.md) | In the `reducer` specification | | [MapReduce](https://ytsaurus.tech/docs/en/mapreduce.md) | In the `mapper`, `reducer`, or `reduce_combiner` specification | | [Vanilla](https://ytsaurus.tech/docs/en/vanilla.md) | In the task specification | ## Environment variables { #environment-variables } The following environment variables are present in the job collectives: | Variable | Description | |------------|----------| | `YT_COLLECTIVE_MEMBER_RANK` | Job's rank in the collective (0 for the master job, 1, 2, ... for slave jobs) | | `YT_COLLECTIVE_ID` | The collective's unique ID (currently matches the `job_id` of the master job, but this property is not guaranteed and may change in the future) | Jobs use these variables to: - Determine their role in the collective. - Find each other for interaction over the network. - Coordinate their processes. ## Configuration { #configuration } To integrate job collectives, add the `collective_options` parameter to the job specification: ```yson { collective_options = { size = 2; # Number of jobs in the collective }; } ``` ### Collective_options parameters | Parameter | Type | Description | |----------|-----|----------| | `size` | int | Number of jobs in the collective. Must be more than 1. | ## Specification examples { #examples } ### Vanilla operation with job collectives In this example, a Vanilla operation runs with a collective of two jobs. The master job (rank 0) writes the result to the output table, while the slave job (rank 1) does nothing. ```yson { tasks = { worker = { job_count = 1; output_table_paths = ["//tmp/output"]; format = "yson"; command = "if [ \"$YT_COLLECTIVE_MEMBER_RANK\" == 0 ]; then echo '{result=42}'; fi"; collective_options = { size = 2; }; close_stdout_if_unused = %true; }; }; } ``` ### Map operation with job collectives ```yson { mapper = { command = "if [ \"$YT_COLLECTIVE_MEMBER_RANK\" == 0 ]; then cat; fi"; collective_options = { size = 2; }; close_stdout_if_unused = %true; }; } ``` ### MapReduce operation with job collectives In this MapReduce operation, job collectives are used at all of its stages: mapper, reduce_combiner, and reducer. ```yson { mapper = { command = "if [ \"$YT_COLLECTIVE_MEMBER_RANK\" == 0 ]; then cat; fi"; collective_options = { size = 2; }; close_stdout_if_unused = %true; }; reduce_combiner = { command = "if [ \"$YT_COLLECTIVE_MEMBER_RANK\" == 0 ]; then cat; fi"; collective_options = { size = 2; }; close_stdout_if_unused = %true; }; reducer = { command = "if [ \"$YT_COLLECTIVE_MEMBER_RANK\" == 0 ]; then cat; fi"; collective_options = { size = 2; }; close_stdout_if_unused = %true; }; force_reduce_combiners = %true; } ``` ## Limitations { #limitations } * **Incompatibility with gang_options**: You cannot use job collectives together with [gang operations](https://ytsaurus.tech/docs/en/vanilla.md#gang-operations). If you attempt to specify both parameters, the operation will fail with an error. * **Data output from the master job only**: Only the master job (rank 0) can write data to output tables. {% note tip %} Use the `close_stdout_if_unused = %true` option in the job specification to explicitly close `stdout` for slave jobs and avoid accidental writes. {% endnote %} * **Job interruption**: You can interrupt only the master job. Attempting to interrupt a slave job results in its abortion followed by a restart of the collective. ## See also { #see-also } * [Gang operations](https://ytsaurus.tech/docs/en/vanilla.md#gang-operations) * [Map operation](https://ytsaurus.tech/docs/en/map.md) * [Reduce operation](https://ytsaurus.tech/docs/en/reduce.md) * [MapReduce operation](https://ytsaurus.tech/docs/en/mapreduce.md) * [Vanilla operation](https://ytsaurus.tech/docs/en/vanilla.md) # Chunk auto-merge at operation output This section describes the process of merging chunks automatically at the operation output. We'll discuss the scope of this feature, its settings, and examples. ## Overview { #overview } One of the most popular data processing scenarios is filtering of a large table consisting of multiple chunks that uses the [Map](https://ytsaurus.tech/docs/en/map.md) operation without special flags (Unordered Map) to produce a small result. Because of the underlying mechanism of such operations, filtering of a table of `n` chunks can at worst create `n` resulting chunks in the output, regardless of how small each of these chunks is. Such a behavior might devour the chunk quota on the user account. For more information on quoting, see [Quotas](https://ytsaurus.tech/storage/quotas.md). You can merge the resulting chunks using the [Merge](https://ytsaurus.tech/docs/en/merge.md) operation, but this method is not convenient because it requires an additional operation. Still, however, in this case, you need to create a safety stock of quota for `n` chunks to allow for bursts: this might be as high as hundreds of thousands of chunks for large tables (and the chunk quota is quite an expensive resource). That's why YTsaurus provides automated chunk merging at operation output. This merging is proactive in the sense that it merges output chunks before they have used up your quota. Automatic merging has the following constraints: * It's only available for Unordered [Map](https://ytsaurus.tech/docs/en/map.md) and [Sorted Reduce](https://ytsaurus.tech/docs/en/reduce.md) operations. * It works only with tables that accept **unsorted data**. For the operations with sorted output, the situation is complicated by the fact that you cannot merge output chunks in an arbitrary order. * Unavailable if `row_count_limit` is set. To learn more about this parameter, see [Operation options](https://ytsaurus.tech/docs/en/operations-options.md). If the operation goes beyond the constraints, the user has to run the [Merge](https://ytsaurus.tech/docs/en/merge.md) operation manually. ## Automated merging modes { #mode } Automated merging is set up using the `auto_merge` section that needs to be added to the operation's specification. The section is a dict where the main parameter is the `mode` key that can take upon the `disabled`, `relaxed`, `economy`, `manual` values that set different behaviors in the context of automated merging. ### Relaxed { #relaxed } The mode that's not attempting to save the quota in real time, but tries to get a table with reasonably sized chunks at the output. Actually, this method relieves the use of the need to run an Unordered Merge on the output table. However, it does not guarantee that the operation will run properly under a quota shortage. ### Economy { #economy } The mode that optimizes the maximum quota usage by the operation. This mode is suitable for the operations that often hit the quota limit. Under ideal circumstances, the chunk quota usage in the `economy` mode can be evaluated as `4 * sqrt(n)`, where `n` is the number of chunks that you would have had in the output table without automated merging. Such a result is not guaranteed, it's only a best-effort behavior that requires, for example, that the quantities of chunks output by jobs are burst-free (one job wouldn't generate a thousand chunks at once). In this case, to filter a table of 100,000 chunks, you can safely do with a quota of a couple of thousand chunks. ### Manual { #manual } The mode in which you can manually set up the automated merge behavior using the following parameters in the `auto_merge` section: * `max_intermediate_chunk_count`: The scheduler will try to maintain the specified constraint on the intermediate chunk quota. However, you could never maintain this limit exactly because of the internal mechanisms of chunk generation. * `chunk_count_per_merge_job`: Defines the maximum number of chunks that the scheduler will try to merge per job (without exceeding the limit of `desired_chunk_size` in `job_io` for Merge jobs; the default value is 2 GiB). * `chunk_size_threshold`: The chunk size in bytes starting from which a chunk is considered large enough to skip merging and go to the output table as is. That is, only the chunks smaller than `chunk_size_threshold` (128 MiB by default) will be merged. ### Disabled { #disabled } The mode with no automated merging. This mode is used by default. {% note info "Note" %} The `economy` mode (as well as the `manual`mode at certain parameters), imposes quite a strong constraint on the operation parallelism, which might slow the operation down. The reason for such behavior is that you cannot run 10,000 jobs in parallel, keeping to the constraint of 1000 intermediate chunks because such number of jobs can generate 10,000 chunks at the same time. On the other hand, the `relaxed``` mode doesn't have this issue because it doesn't try to meet the chunk quota constraints in real time. {% endnote %} Auto-merging is an additional step in the operation that consumes time. That's why automated merging is disabled by default. If the growth in the operation runtime due to automated merging isn't critical for you, use the `relaxed` mode. It's assumed that this mode suits the demands of most users. ## Sample specifications { #examples } Sample specification with the `relaxed` mode: ```yson { input_table_paths = [ "//tmp/input_table" ]; output_table_paths = [ "//tmp/output_table" ]; mapper = { command = "cat"; }; auto_merge = { mode = "relaxed" } } ``` Sample specification with the `manual` mode: ```yson { input_table_paths = [ "//tmp/input_table" ]; output_table_paths = [ "//tmp/output_table" ]; mapper = { command = "cat"; }; auto_merge = { mode = "manual"; max_intermediate_chunk_count = 1000; chunk_count_per_merge_job = 10; chunk_size_threshold = 100000000; } } ``` # Chunk auto-merge on the master server side For static tables, YTsaurus supports automatic chunk merge on the master server side. Unlike the [Merge](https://ytsaurus.tech/docs/en/merge.md) operation, this method doesn't use the computing resources of the pool. To enable automatic chunk merge, set the `@chunk_merger_mode` table attribute to one of the values below: - **shallow**: Merge chunks at the metadata level, without recompression. This approach works only if the chunks are "similar" enough (have the same sort order, schema, compression codec, etc.). The resulting chunk may also have too many [blocks](https://ytsaurus.tech/storage/chunks.md#chunk-size) at a certain point. - **deep**: Merge chunks with full recompression (by reading all input chunks and writing to a new one). This mode is much more expensive than the previous one, but it allows you to merge chunks with different characteristics if they end up in the same table. You can also use this mode to remove small blocks or chunks in older formats. - **auto** (recommended): Merge chunks in shallow mode with rollback to deep mode in case of failure. - **none**: Disables chunks merge. We recommend using `auto` by default (other modes are allowed as well, but you should switch to them only if you have a good idea of why you need to do that): ```bash $ yt set //home/dir_with_tables/table_i_want_to_merge/@chunk_merger_mode auto ``` If you need to merge all tables in a particular subtree, you can set the attribute for the entire directory: ```bash $ yt set //home/dir_with_tables/@chunk_merger_mode auto ``` If you need to disable chunk merge for a particular subtree, you can set `@chunk_merger_mode` to `none`. ```bash $ yt set //home/dir_with_tables/@chunk_merger_mode auto $ yt set //home/dir_with_tables/subdir/@chunk_merger_mode none ``` {% note info "Note" %} This mechanism works only for static tables. Dynamic tables have their own chunk merge mechanisms. Setting the `chunk_merger_mode` attribute for them won't do anything. {% endnote %} Please note that `chunk_merger_mode` is an [inheritable attribute](https://ytsaurus.tech/storage/chunks.md#common). Once it is set, all new tables in the directory will inherit its value, but nothing will change for the old ones, meaning you should go over all the tables when enabling this attribute. Chunks are merged when the attribute is set and every time a write is made to the table (on additional writes, the system will attempt to remerge only the end of the table for optimization purposes). ## Merge attributes on the account Every account contains attributes for controling the chunk merge mechanism: - `allow_using_chunk_merger`: Whether it is allowed to merge chunks for tables of this account. - `chunk_merger_node_traversal_concurrency`: The number of tables for which merge jobs can be scheduled concurrently. - `merge_job_rate_limit`: The number of jobs that can be scheduled per second. The default settings allow automatic chunk merge for every account. ## About troubleshooting Tables have several system attributes for finding out the current chunk merge status. These attributes aren't displayed in the web interface but can be queried via the CLI. - `@chunk_merger_status`: The table status in the merge pipeline, can take the `not_in_merge_pipeline`, `awaiting_merge`, and `in_merge_pipeline` values. It may take a significant amount of time for the merger to halt all activities on the table after the chunk merge trigger activates. If you've set `chunk_merger_mode` for the table, but the chunks in it are still small, look at `chunk_merger_status` and wait for it to change to `not_in_merge_pipeline`. - `@chunk_merger_traversal_info/chunk_count`: The number of chunks already processed by the merger and which won't be merged any further. You can use this to understand which chunks won't be merged. Suppose a merge was performed on a table and left it with 1000 chunks, and now you want to append data to the end of the table. There's no point in going over the chunks from the very beginning after each write: if they weren't merged before, this won't be needed now. However, you may need to merge a newly added chunk with some of the last chunks from the old ones. If `@chunk_merger_traversal_info/chunk_count` is, let's say, 980, then only the last 20 chunks will be considered in the future to become merged with new chunks after future writes. Automatic chunk merge doesn't acquire locks on tables, so chunk merge is unnoticed and does not interfere with other processes. This, however, has a negative side effect: such merges may occasionally fail (for example, if the table was completely overwritten with new data during the merge). The system automatically retries failed merges if possible (this may not be possible if the table was deleted or became dynamic). However, if the table is often overwritten with new data, this process can take a long time to complete. ## Metrics in Prometheus The following time series are used in Prometheus to collect chunk merge metrics: - `yt.chunk_server.chunk_merger_account_queue_size`: Account-based chunk merge queues. If the account queue is empty, make sure that the correct attribute is set for all required tables. Also make sure that the account settings allow table merges: the `allow_using_chunk_merger` attribute is set to `true` and the `chunk_merger_node_traversal_concurrency` and `merge_job_rate_limit` attributes aren't set to 0. - `yt.chunk_server.chunk_merger_chunk_replacements_succeeded`: The number of successful merges. - `yt.chunk_server.chunk_merger_chunk_replacements_failed`: The number of failed merges. Having a certain number of failed merges is normal, and you can't get rid of them completely. But if there are more failed merges than successful ones, this is something that you want to take note of. One possible reason could be that chunk merge is enabled for tables that are constantly overwritten from scratch with new data or for tables with a short lifetime. In this case, it's best to disable automatic merge for such tables. # Using YTsaurus system properly and efficiently This section is a collection of recommendations on using the YTsaurus for massively parallel data processing scenarios. ## Chunks too small {% note info "Note" %} The small chunk scenario is relevant to static tables. Dynamic table use cases imply fine-grained reads to reduce query response times; therefore, small chunks are acceptable for dynamic tables. {% endnote %} A [chunk](https://ytsaurus.tech/storage/chunks.md) under 100 MB may be considered small. To prevent there being too many chunks in the system, you should look to have an average chunk size of at least 512 MB. Small chunks complicate cluster operation for several reasons: * Small chunks subject master servers to an additional load. The more chunks, the more master server memory is required to store chunk metainformation and the slower their performance: they are slower to write snapshots and longer to recover from them in the event of problems. * A large number of small chunks results in slower data reads. For instance, if 100 MB are scattered throughout a million chunks, one record each, a million disk `seeks` are required to read all the data which is very slow even if they are done in parallel. Operation execution or fine-grained data reads may produce tables with many small chunks. To resolve this problem, you need to increase chunk size for such tables. You can increase chunk size by calling the command below: ```bash yt merge --src //your/table --dst //your/table --spec '{combine_chunks=true;mode=}' ``` You need to set the `` attribute to `sorted` for a [sorted table](https://ytsaurus.tech/storage/static-tables.md#sorted_tables) and to `ordered` for an [unsorted](https://ytsaurus.tech/storage/static-tables.md#unsorted_tables) one. For more information on table types, please see [Static tables](https://ytsaurus.tech/storage/static-tables.md). In both the cases, merge will maintain data ordering. But for `sorted`, it will also maintain the table sort from the system standpoint: the table will remain `sorted` and keep all the relevant attributes. If you are using a [python library](https://ytsaurus.tech/api/python/userdoc.md), you can specify the `auto_merge_output={action=merge}` configuration option which will make the library automatically aggregate the resulting tables if their chunks are too small. ## MapReduce vs Map+Sort+Reduce For more information on MapReduce internals and on why it is normally faster than the Map+Sort+Reduce sequence, please see [MapReduce](https://ytsaurus.tech/docs/en/mapreduce.md). Below is a description of circumstances when using the combined MapReduce operation is **not recommended**. ### Heavy mapper with strong filter As a theoretical reference, you can use the fact that a heavy mapper spends over 100 ms of CPU to process a single row. Normally, the number of input rows of a strongly filtering mapper is 5 or more times greater than the number of its output rows or bytes. This requires there to be as many jobs as possible in the map phase for each to run more quickly. A MapReduce has a constraint on the number of map jobs because if there are too many, the sort phase will become costly to perform following a large number of small random disk reads. The correct solution in this case is to start a Map first specifying as many jobs as possible. Thereafter, apply a combined MapReduce with an empty mapper to the resulting data. ### Frequent application of MapReduce to same key fields If you need to process your data through a Reduce more than once, it will, most likely, be more efficient to sort the data first followed by conventional Reduce operations. Logs are a common example of such data. This arrangement will only work well in two situations: 1. The data does not change. 2. The data is appended. A table is time-sorted, for instance. An additional amount of data comes in with all the key (time) values greater than all the key (time) values in the table. Then, a sort can be applied to this batch of data followed by a Reduce with `teleport=%true`. For more information on the option, please see the [Reduce](https://ytsaurus.tech/docs/en/reduce.md#foreign_tables) section. ### Heavy reducer Like with a heavy mapper, you need to start as many reduce jobs as possible. But the operation has a limit on the number of partitions; therefore, on the number of reduce jobs. The right thing to do in this case is to sort the table and then run Reduce with as many jobs as possible. ## Large number of records with identical keys during reduce phase To solve this problem, YTsaurus has [Reduce combiners](https://ytsaurus.tech/docs/en/mapreduce#reduce_combiner.md) that help process large keys in several jobs in the reduce phase. ### Map combiners The idea behind a map combiner is to aggregate data during the map phase. The [WordCount task](http://wiki.apache.org/hadoop/WordCount) is a classic example of using a map combiner. The map phase of this task requires inserting `(word, count)` rather than `(word, 1)` pairs, where `count` is the number of occurrences of a specific word in this job. Therefore, it is useful to aggregate data both in the reduce phase as well as directly in the map phase. The YTsaurus system does not have dedicated map combiner support. Large keys are frequently caused by a failure to aggregate in the map phase. Thus, if aggregation is possible, we recommend aggregating. ## Numerous output tables in operation A RAM buffer is reserved for each operation output table. When operation launches, all the output table buffer sizes are added together and added to the amount of memory indicated by a user in the operation spec. Technically, output table buffer size is implicitly counted as part of the `JobProxy` process memory and depends on cluster settings as well as the value of an output table's `erasure_codec` attribute. For more information on `JobProxy`, see the Jobs todo section. If there are many output tables, the total amount of memory may turn out be fairly large; so, the scheduler will not be able quickly to find a cluster node with the right amount of free memory or may not find one at all. This will cause the operation to be aborted, and the `No online node can satisfy the resource demand` error to be displayed for the user. The error message will indicate the amount of memory requested. {% note info "Note" %} We recommend specifying no more than several dozen output tables for an operation. {% endnote %} # Inter-cluster network bandwidth throttling Some YTsaurus operations read data from other clusters. For example, the [RemoteCopy](https://ytsaurus.tech/docs/en/remote-copy.md) operation copies data from one cluster to another. MapReduce operations can also read input tables from other clusters by using the [cluster](https://ytsaurus.tech/storage/ypath.md#rich_ypath_prefix) attribute in YPath. Such operations are referred to as operations with **remote read**. To control load on network channels between clusters, YTsaurus provides an inter-cluster network bandwidth throttling mechanism based on **distributed throttlers** (cluster throttlers). For details about the mechanism and its configuration, see [Inter-cluster Network Bandwidth Throttling](https://ytsaurus.tech/admin-guide/cluster-throttlers.md) in the administrator guide. ## What is throttled { #what-is-throttled } The bandwidth between YTsaurus clusters is throttled. Throttling applies to: - All [RemoteCopy](https://ytsaurus.tech/docs/en/remote-copy.md) operations. - MapReduce operations that read input tables from remote clusters. ## Impact on user operations { #user-impact } Inter-cluster bandwidth limits can slow down operations that read data from other clusters. Throttling happens at two levels: 1. **At the job level** — a job cannot read data faster than its allocated limit. 2. **At the controller agent level** — if inter-cluster bandwidth is unavailable, new operation jobs are not started until bandwidth becomes available. ### How to tell if an operation is hitting the network limit { #network-limit-alert } If an operation cannot schedule jobs because inter-cluster bandwidth is unavailable, the operation is assigned the alert **Unavailable network bandwidth to clusters**. You can view the alert: * **In the web interface** on the operation page: ![Unavailable network bandwidth to clusters](https://jing.yandex-team.ru/files/yuryalekseev/unavailable_network3-1.png) * **Using the CLI** with the `get-operation` command: ```bash yt get-operation --attribute alerts ``` Example output when an operation is hitting the network limit: ```json { "alerts": [ { "type": "unavailable_network_bandwidth_to_clusters", "message": "Not enough network bandwidth to clusters" } ] } ``` ### How to check inter-cluster channel utilization { #channel-utilization } To view utilization of the network channel from the `remote` cluster to the `local` cluster, run the following command on the `local` cluster: ``` yt --proxy local list \ "//sys/discovery_servers//orchid/discovery_server/remote_cluster_throttlers_group/@members" \ --attribute local_throttlers --format json | jq '[.[] | ."$attributes"."local_throttlers"."bandwidth_remote"."rate"] | add' ``` ### Impact on scheduling { #scheduling-impact } Inter-cluster bandwidth is not a scheduler resource in the fair-share sense. However, throttling still affects job scheduling: if inter-cluster bandwidth is unavailable, the operation controller signals to the scheduler that it does not need resources, so new jobs are not started until bandwidth becomes available. Jobs that are already running continue to execute with the inter-cluster bandwidth limit applied. ### Recommendations { #recommendations } If you need to read a large volume of data from another cluster, on the order of tens of terabytes or more, limit the number of jobs that can run concurrently for the operation. This helps avoid excessive load on the inter-cluster network channel. #### Choosing a `user_slots` value { #user-slots-value } Assuming a read speed of about 50 MB/s per job, a good starting point is `user_slots=300-500`. This yields a transfer rate of about 15-25 GB/s, which is reasonable for most scenarios. The specific value depends on: - Available inter-cluster bandwidth - Volume of data to be transferred - Required operation completion time - Load on the network channel from other operations #### Ways to limit the number of jobs { #limit-jobs } You can limit the number of jobs in two ways: * **Set a `user_slots` limit on the operation** via the operation specification. The `user_slots` parameter limits the number of jobs per cluster node, and each job always consumes exactly one user slot. For example, you can specify `resource_limits={user_slots=200}` in the specification. In this case, no more than 200 jobs will run simultaneously for the operation in each tree. For more details on configuring operation resources, see [Configuring operation compute resources](https://ytsaurus.tech/docs/scheduler/pool-settings.md#operations). * **Use a pool with a `user_slots` limit**. You can set a resource limit for a pool by using the `resource_limits` attribute, which includes `user_slots`. For example, you can set a limit for a pool with the command `yt set //sys/pools/...//@resource_limits '{user_slots=2000}'`. For more details on pool configuration, see [Pool settings](https://ytsaurus.tech/docs/scheduler/pool-settings.md). ## Configuration { #configuration } Inter-cluster bandwidth throttling is configured by the cluster administrator. For details, see [Inter-cluster Network Bandwidth Throttling](https://ytsaurus.tech/admin-guide/cluster-throttlers.md) in the administrator guide. # Root file system images A root file system image contains a stand-alone OS distribution together with essential apps and libraries for running user commands in an isolated container. Depending on the cluster configuration (`exec_node/slot_manager/job_environment/type` in the exec node configuration file), the execution environment can support different image formats and sources: - **simple**: Doesn't support isolation. - **cri**: Docker image. - **porto**: Porto layers, Docker image from the Internal Docker Registry. ## Docker images { #docker_images } ### Terminology - **Docker layer**: a fragment of data from a root file system image. - **Docker image**: an image of a root file system consisting of metadata and an ordered set of layers. - **Docker registry**: a server for storing and distributing Docker image data and metadata. - **Internal Docker Registry**: a Docker registry that stores images directly in [Cypress](https://ytsaurus.tech/storage/cypress.md). Docker images are referenced by name in the following format: `[REGISTRY/]IMAGE[:TAG|@DIGEST]`, where `REGISTRY` represents the `FQDN[:PORT]` of the Docker registry server. Registry `FQDN` must has at least one "." or `PORT`. By default, `TAG` is set to **latest**. [More](https://docs.docker.com/engine/reference/commandline/pull/) Docker images without a specified `REGISTRY` are assumed to be from the **Internal Docker Registry**, where `IMAGE` corresponds to the `//IMAGE` path pointing to the directory with the image metadata in Cypress. The Internal Docker Registry can be accessed via the standard Docker registry protocol for uploading images using Docker or Docker-compatible tools. It doesn't support referring to Docker images with a `DIGEST`. To use docker images from [Docker Hub](https://hub.docker.com), provide their full name in the following format: `docker.io/[library/]IMAGE[:TAG|@DIGEST]`. To run jobs using Docker images from an external Docker registry, the cluster's exec nodes require network access to said registry. The Internal Docker Registry is accessed from within the cluster. When running a job, each exec node downloads the image to its local cache. Because of this, large clusters can generate a significant load on the Docker registry. ### Authorization { #docker_auth } To specify the secret to access a private Docker registry, use the `docker_auth` key in the `secure_vault` option: `secure_vault = { docker_auth = { username = "..."; password = "..."; auth = "..."; }; }`. ## Porto layers { #porto_layers } This section contains information about Porto layers. ### Terminology - **Porto layer**: a fragment of a root file system image for [Porto](https://github.com/ten-nancy/porto). - **RootFS image**: a root file system image. - **Basic layer** **(basic image)**: an autonomous layer that contains the core components of the environment (/bin/bash, libc6, etc.). By selecting a basic layer, you're confirming the Ubuntu version (Precise, Trusty, Xenial, Bionic, etc.) for running your code. - **Delta layer**: a layer that supplements the basic layer and usually contains additional libraries or binary artifacts. # Using file system images to run operations in YTsaurus { #job_rootfs } A MapReduce operation must be run with the file system image specified if its execution requires libraries or resources that aren't available in YTsaurus clusters and if there is no way to prepare a binary file with this operation. To specify a Docker image, use the `docker_image = "[REGISTRY/]IMAGE:TAG"` parameter in the operation specifications. To use Porto layers, go to the mapper or reducer section in the operation specifications and specify the following: `layer_paths = ["//path/to/upper_layer"; ... ; "//path/to/lower_layer"]`. Layers are listed from top to bottom. # Job file environment: layers and files This section describes how to work with the job file environment in YTsaurus. You’ll learn what layers and individual files are, how to specify them in the operation specification, how to prepare and upload them to Cypress, and how to speed up layer reads and diagnose issues. ## What is the file environment { #file-environment } A job’s file environment is a set of directories and files where the user process runs. The environment is represented as a root file system: a tree of directories and files, similar to any Unix system. You need dependencies to run code: interpreters, libraries, binary files, configs, and data. If your code accesses a dependency—a directory or file that isn’t in the environment—an error occurs. That’s why you build the environment for a specific task. The file environment consists of two parts: - Root file system—built from layers listed in the [layer_paths](https://ytsaurus.tech/docs/en/layer-paths.md#job_rootfs) parameter. It’s suitable for a persistent environment that you carry over from operation to operation. - Individual files—artifacts in the `/slot/sandbox` subtree, listed in the [file_paths](https://ytsaurus.tech/docs/operations/operations-options.md#user_script_options) parameter. They’re suitable for one‑time transfers of specific files to a job, such as binaries or configs. The execution environment builds the file environment—the software environment where the job runs. The execution environment provides access to the file system and runs your code in it. For details on the available environments and their differences, see the [Execution environments](https://ytsaurus.tech/docs/en/#environments) section. ## Layers { #layers } ### What is a layer { #what-is-layer } A layer is a file that contains a subset of the root file system. The subset is one or more directory subtrees with their nested structure preserved. Physically, a layer is stored as a single file that you upload to YTsaurus. The simplest example of a layer is an archive. You prepare the environment on your machine in advance, pack it into an archive, and upload it to YTsaurus. During operation execution, the system expands the archive and forms the job’s file environment from it. ### How the environment is built from layers { #rootfs } You can build the root file system from a single layer or from multiple layers. Layers from `layer_paths` are merged into a single root file system using [overlayfs](https://wiki.archlinux.org/title/Overlay_filesystem)—a Linux kernel mechanism that combines several file systems into one. The root file system is built on the exec node during job preparation, before your code starts. Containerization—Porto or CRI—makes the assembled file system the root `/` of the container and runs your code in it. Without containerization, layers aren’t applied as a root file system. #### How a layer fits into the tree { #rootfs-tree } Each layer reproduces part of the tree, from the top level down. The top level of a layer is the folders and files that sit directly in it, not nested in other folders. After assembly, they’re placed directly in the root `/`. Everything nested inside them ends up at lower levels—inside the corresponding root folders. For example, if the archive contains the `usr`, `bin`, and `home` folders at the top level, they become `/usr`, `/bin`, and `/home` after expansion. Their nested contents become `/usr/lib`, `/bin/bash`, and so on. A single layer can contain multiple subtrees at once. For example, one archive can include both `/usr` and `/var`. #### Base and delta layers { #base-delta } Most often, you build the environment using a “base layer + delta” scheme. It separates the unchanging foundation from your own files: - Base layer—a layer with the main part of the environment: system utilities and libraries, for example `/bin/bash` and `libc6`. It’s usually a Linux distribution image, such as Ubuntu, with packages and libraries installed. - Delta layer—an addition to the base layer with your own libraries or binary artifacts. A common configuration is one base layer and one or more delta layers: you use a ready‑made image as the base and put your own files in the delta. ### Layer formats { #layer-formats } YTsaurus supports two layer formats: tar archive and SquashFS. For SquashFS, network access via [NBD](https://ytsaurus.tech/admin-guide/nbd.md#how-it-works) is also supported. The table below shows how the formats differ and when to use each: #| ||**Format**|**What it is**|**When to use**|| ||Tar archive|A classic archive with a subset of the file system|When performance isn’t critical and you want to build a layer as simply as possible|| ||SquashFS|A file system image mounted without unpacking|When performance matters: it speeds up job preparation and reduces disk load|| ||SquashFS over NBD|Reading a SquashFS image over the network, without downloading it to the exec node|When you need the fastest job start and don’t want to copy large images in full|| |# #### Tar archive { #tar-layer } A tar archive is a layer in the form of a `.tar`, `.tar.gz`, `.tar.xz`, or `.tar.zstd` archive with a subset of the file system. The archive is convenient for several reasons: - You can easily build it with the standard `tar` utility. - You can easily inspect its contents: download the archive and unpack it. - It works in any container environment. During job preparation, the archive is downloaded to the exec node and unpacked to disk. Unpacking loads the CPU and Disk I/O, and the unpacked layer takes up disk space. #### SquashFS { #squashfs-layer } SquashFS is a file system image that’s mounted on the exec node without unpacking. The mount point is then used as a layer in overlayfs. Mounting is much faster than unpacking an archive. Compared to a tar archive, a SquashFS layer offers these benefits: - It speeds up job preparation—you don’t need to unpack the image. - It reduces disk load—a valuable resource on clusters. - It saves disk space—the image isn’t stored in an unpacked form. #### SquashFS over NBD { #nbd-layer } NBD (Network Block Device) is a way to use a SquashFS image over the network. Image data is read from data nodes as needed, without pre‑downloading it to the exec node. A local SquashFS layer is first downloaded in full to the exec node. NBD reads only the blocks that the job accesses from the data nodes. This way, NBD avoids using the resources required by a local layer: - CPU and Disk I/O to write the image to disk. - Disk space to store the image. - Network I/O to transfer image parts that the job doesn’t read. Already‑read blocks are cached at multiple levels: in the kernel page cache, in the chunk reader caches, and in the data node caches. Subsequent accesses to the same data are served from the cache without new network reads. This can make NBD layers significantly faster than regular layers. NBD works on top of a SquashFS image. This format compresses data and is read‑only, which matches the purpose of a layer: a layer doesn’t change while the job runs. {% note warning %} NBD works only in the Porto environment. {% endnote %} ## Execution environments { #environments } The set of available layer formats depends on the execution environment configured on the cluster in the `exec_node/slot_manager/job_environment/type` parameter: #| ||**Environment**|**Description**|**Supported layer formats**|| ||`simple`|No containerization|—|| ||`cri`|Docker containers|[Docker images](https://ytsaurus.tech/docs/en/layer-paths.md#docker_images)|| ||`porto`|Porto environment|Tar archives, SquashFS, SquashFS over NBD, Docker images|| |# {% note info %} SquashFS and NBD aren’t supported in the `simple` and `cri` environments. {% endnote %} ## Individual files { #files } ### What is an individual file { #what-is-file } An individual file is a Cypress artifact delivered directly to the job, without building a layer. Files are passed via the `file_paths` parameter and land in the `/slot/sandbox` subtree of the root file system. In effect, files supplement the root file system’s file environment. Individual files are suitable for transferring configs, binaries, and models to a job. You place the file yourself and know which path to use to access it from your code. ### Placing files in /slot/sandbox { #sandbox } Each file from `file_paths` lands in the `/slot/sandbox` subtree of the root file system. By default, the file is available to the job at `/slot/sandbox/`, where the file name matches the original name from Cypress. To place a file elsewhere in the root file system—for example, in `/bin` or `/usr/lib`—you’ll need to add it via a layer. For more details, see the [Files vs. layers](https://ytsaurus.tech/docs/en/#files-vs-layers) section. ### Files vs. layers { #files-vs-layers } Files and layers solve different tasks: - Files via `file_paths` are suitable for transferring non‑system configs, models, and binaries. You can’t use files to transfer, for example, shared C/C++ libraries or system configs: files land in the `/slot/sandbox` subtree and can’t be placed anywhere else in the root file system. - Layers via `layer_paths` are needed to build a system environment. This includes everything required to run your program: system libraries, the dynamic linker, system configs, and applications. A layer can contain files in any directory of the root file system—`/usr`, `/bin`, `/lib`, and others. If you need to place a file outside `/slot/sandbox`—for example, a library in `/usr/lib`—build a delta layer and specify it in `layer_paths`. A library often can’t reside in an arbitrary location: the system requires it to be at a specific path, or it won’t work. ## How to set the job’s file environment in the specification { #set-environment } You set the job’s file environment in the operation specification using two parameters: `layer_paths` for root filesystem layers, and `file_paths` for individual files. ### The layer_paths parameter { #layer-paths } You pass layers to the operation via the `layer_paths` parameter — a list of paths to layers in Cypress. Here’s a minimal example with a single-layer environment: ```python import yt.wrapper as yt spec = { "mapper": { "layer_paths": [ "//path/to/my_layer", ] } } yt.run_map(mapper, source_table, destination_table, spec=spec) ``` #### Layer order { #layer-order } List layers in `layer_paths` from top to bottom: delta layers first, the base layer last. Layer order matters only when layers overlap in data — that is, they contain files or folders at the same path. When they overlap, the top layer takes priority: the root filesystem uses the file from the top layer. That’s why files from the delta override those from the base layer, even if a file with the same path exists in both. If there’s no overlap, the order doesn’t matter. For example, when one layer contains `/usr` and another contains `/var`. ```python "layer_paths": [ "//path/to/delta_layer", # Top layer, delta "//path/to/base_layer", # Bottom layer, base ] ``` If a file exists at the same path in both layers, the system uses the file from `delta_layer`. It takes not only the file’s contents from the top layer but also its metadata: access rights, owner and group, and timestamps. #### Layer attributes { #layer-attributes } The layer format and access method are defined by the file attributes in Cypress: | Attribute | Values | Description | | --- | --- | --- | | `filesystem` | `archive`, `squashfs` | Layer format: `archive` is a tar archive, `squashfs` is a SquashFS filesystem image | | `access_method` | `local`, `nbd` | Access method for the image | For a SquashFS layer with NBD access, you can specify `access_method` directly in the layer path: ```python spec = { "mapper": { "layer_paths": [ "//path/to/my_layer.squashfs", # SquashFS via NBD "//path/to/porto_layer", # Base tar layer ] } } ``` The `access_method` attribute in the specification is available starting from version 25.1. It’s handy when you need to use one layer with different access methods or when you don’t have permission to change the file’s attributes. #### Default layer { #default-layer } If you don’t specify `layer_paths`, the system uses the default image configured on the cluster. Typically, this is a minimal base Ubuntu image. ### The file_paths parameter { #file-paths } You pass files to the operation via the `file_paths` parameter — a list of paths to files in Cypress with attributes. Each file goes into the `/slot/sandbox` subtree. #### File name conflicts { #file-name-conflicts } If two files in `file_paths` have the same name and you don’t set the `file_name` attribute, the last file overwrites the previous one. Set a unique `file_name` for each file. #### File attributes { #file-attributes } You can set attributes for each file in `file_paths`: | Attribute | Type | Description | | --- | --- | --- | | `file_name` | string | The file name in the `/slot/sandbox` subtree. By default, it’s the original file name from Cypress. Supports nested paths, for example `nv_tmpfs/sys/static/bundle.tar.gz` | | `executable` | bool | Set the executable flag. Default is `false` | | `bypass_artifact_cache` | bool | Don’t cache the file in the exec node’s artifact cache. Default is `false`. This can be useful, for example, when you need to copy the file directly to `tmpfs` | | `copy_file` | bool | Copy the file instead of creating a hard link. Default is `false` | Two attributes are key. The `$value` attribute specifies the path in Cypress where the file comes from. The `file_name` attribute specifies the path in the job’s file environment where to place the file. Here’s an example specification with file attributes: ```python spec = { "file_paths": [ { "$value": "//path/to/nirvana-bundle.tar.gz", "$attributes": { "file_name": "nv_tmpfs/sys/static/nirvana-bundle.tar.gz", "executable": False, "bypass_artifact_cache": True, } }, { "$value": "//path/to/job_launcher_native", "$attributes": { "file_name": "nv_tmpfs/sys/static/job_launcher_native", "executable": True, "bypass_artifact_cache": True, } }, ], "mapper": { "layer_paths": [ "//path/to/porto_layer", ] } } ``` In this example, `nirvana-bundle.tar.gz` is available at `/slot/sandbox/nv_tmpfs/sys/static/nirvana-bundle.tar.gz`. The `job_launcher_native` file is available at `/slot/sandbox/nv_tmpfs/sys/static/job_launcher_native` with execute permission. #### Default file_paths { #file-paths-default } If you don’t specify `file_paths`, the `/slot/sandbox` subtree remains without user files. ### Environment preparation order { #preparation-order } The job environment is built in two steps: 1. **Prepare the root filesystem.** The system downloads layers from `layer_paths`, mounts them using overlayfs, and builds the root filesystem. 2. **Deliver files.** The system downloads files from `file_paths` and places them in the `/slot/sandbox` subtree of the root filesystem. Your custom code runs only after both steps complete. ### Specification examples { #spec-examples } You can combine layers and files in one specification. For example, use a base tar layer in `layer_paths` and additional artifacts in `file_paths`: ```python spec = { "file_paths": [ { "$value": "//path/to/executor-config.yaml", "$attributes": { "file_name": "executor-config.yaml", "bypass_artifact_cache": True, } }, { "$value": "//path/to/my_binary", "$attributes": { "file_name": "my_binary", "executable": True, } }, ], "mapper": { "layer_paths": [ "//path/to/porto_layer", ] } } ``` The files are available at `/slot/sandbox/executor-config.yaml` and `/slot/sandbox/my_binary`. ## Prepare and upload layers and files to Cypress { #prepare-upload } ### Prepare layers { #prepare-layers } #### Create a tar archive { #create-tar } You build the archive, for example, by using the `tar` command: ```bash tar czf delta_layer.tar.gz configs data bins ``` #### Create a SquashFS image { #create-squashfs } There is no separate “NBD image”: the same SquashFS image is used for NBD access. The layer format and access method are determined not by the file extension, but by the attributes when you upload to Cypress. For more details, see the section [Upload layers to Cypress](https://ytsaurus.tech/docs/en/#upload-layers). Build the image from a folder by using `mksquashfs`: ```bash sudo apt install squashfs-tools mkdir ~/mnt # Populate ~/mnt with the required content mksquashfs ~/mnt /tmp/my_layer.squashfs ``` If you already have a tar layer, you can convert it to SquashFS by using `tar2sqfs`: ```bash sudo apt install squashfs-tools-ng tar2sqfs ~/dict.squashfs < ~/dict.tar.gz ``` When you build the image, you can set the SquashFS block size — a key parameter for performance when accessing via NBD. For more details, see the section [Optimize NBD layers](https://ytsaurus.tech/docs/en/#nbd-optimization). {% note warning %} When you convert tar layers to SquashFS, make sure there are no errors transferring the extended attributes `trusted.overlay.*`. Such errors can cause overlayfs to work incorrectly. {% endnote %} ### Upload layers to Cypress { #upload-layers } For both tar layers and SquashFS images, you can set the storage medium `ssd_blobs` and the number of replicas — this speeds up layer downloading to exec nodes. For more details, see the section [How to speed up layer reads](https://ytsaurus.tech/docs/en/#performance). #### Upload a tar layer { #upload-tar } ```bash yt write-file //path/to/layer.tar.gz < ~/layer.tar.gz ``` If the filesystem format is not specified, the layer is treated as a tar archive. #### Upload a SquashFS image { #upload-squashfs } When you upload a SquashFS image, specify the filesystem format with the `@filesystem` attribute: ```bash yt write-file //path/to/layer.squashfs < ~/layer.squashfs yt set //path/to/layer.squashfs/@filesystem squashfs yt set //path/to/layer.squashfs/@access_method local ``` {% note info %} If the file has the `.squashfs` extension, the `@filesystem` attribute is determined automatically. {% endnote %} To use a SquashFS image over the network, set the `@access_method` attribute to `nbd`: ```bash yt write-file //home/user/layers/base.squashfs < ~/base.squashfs yt set //home/user/layers/base.squashfs/@filesystem squashfs yt set //home/user/layers/base.squashfs/@access_method nbd ``` After that, you can use the layer in operations: ```python map_spec = {"mapper": {"layer_paths": ["//home/user/layers/base.squashfs"]}} yt.wrapper.run_map( Mapper(), source_table=args.input_table, destination_table=args.output_table, spec=map_spec, ) ``` Access methods: - `access_method=local` — the image is downloaded to the exec node and mounted locally. - `access_method=nbd` — the image is read over the network via NBD. At any given time, the image is used in only one mode: `local` or `nbd`. ### Prepare files { #prepare-files } Before you upload files to Cypress, prepare them on your local machine: - Collect the required files: scripts, binaries, configs, and models. - Check file integrity: compare hashes or checksums if you downloaded the files from external sources. - Check access rights: files that you plan to run must be executable. - Organize files into a logical directory structure, for example `/scripts`, `/configs`, `/models`. ### Upload files to Cypress { #upload-files } Files for `file_paths` are uploaded to Cypress in the same way as layers. You can also configure storage parameters. #### Choose a medium { #file-medium } You can increase the file download speed by using the appropriate medium and setting the `primary_medium` attribute. Store frequently used files on SSD, and rarely used files on HDD: ```bash yt create --type file \ --attributes '{primary_medium=ssd_blobs;account=sys;}' \ --path //path/to/my_file yt write-file //path/to/my_file < ~/my_file ``` For an existing file: ```bash yt set //path/to/my_file/@primary_medium ssd_blobs yt set //path/to/my_file/@account sys ``` #### Number of replicas { #file-replication } You can increase the file download speed by using the number of replicas and setting the `replication_factor` attribute. The default is 3, and the maximum is 20: ```bash yt create --type file \ --attributes '{replication_factor=10;}' \ --path //path/to/my_file yt write-file //path/to/my_file < ~/my_file ``` For an existing file: ```bash yt set //path/to/my_file/@replication_factor 10 ``` #### Naming and organization recommendations { #file-naming } To simplify file management in Cypress: - Use folders based on purpose or version: `//home//layers/`, `//home//configs/`. - Give files descriptive names so you can understand their content without opening them. - When you update a file, create a new version in a separate folder instead of overwriting the existing one — this way, you can roll back if an error occurs. #### Check availability { #file-check } After uploading, check that the file is available: ```bash yt list //path/to/ yt get //path/to/my_file/@size ``` ## Practical scenarios { #scenarios } ### Base layer and files via file_paths { #scenario-base-files } Use this scenario when you need to add a few artifacts — configs, binaries, or models — to the standard environment without creating a new layer. ```python spec = { "file_paths": [ { "$value": "//path/to/config.yaml", "$attributes": { "file_name": "config.yaml", } }, { "$value": "//path/to/my_binary", "$attributes": { "file_name": "my_binary", "executable": True, } }, ], "mapper": { "layer_paths": [ "//path/to/porto_layer", ] } } ``` The files will be available at the paths `/slot/sandbox/config.yaml` and `/slot/sandbox/my_binary`. ### Delta layer instead of multiple file_paths { #scenario-delta } Use this scenario, for example, when you need to bring complex dependencies along with the files: `pip`, `apt`, `conda`, and shared libraries. Build a delta layer from your files: ```bash mkdir -p ~/delta/usr/lib ~/delta/usr/bin cp ~/my_library.so ~/delta/usr/lib/ cp ~/my_binary ~/delta/usr/bin/ mksquashfs ~/delta /tmp/delta_layer.squashfs yt write-file //path/to/delta_layer.squashfs < /tmp/delta_layer.squashfs yt set //path/to/delta_layer.squashfs/@filesystem squashfs yt set //path/to/delta_layer.squashfs/@access_method nbd ``` Use the delta layer in the operation together with the base layer: ```python spec = { "mapper": { "layer_paths": [ "//path/to/delta_layer.squashfs", # Delta layer "//path/to/porto_layer", # Base layer ] } } ``` Files in the delta layer override files in the base layer when the paths match. ## Optimization and diagnostics { #optimization-diagnostics } ### Job environment preparation stages { #preparation-stages } Preparing a job’s environment involves several stages. Each stage takes time and consumes resources. You can view preparation metrics in the YTsaurus web interface: - On the operation page — go to the **Jobs** tab → check the **Statistics** column. - On the job page — open the **Job statistics** section → look at the **Prepare** stage. Key job environment preparation stages: #| ||**Stage**|**What happens**|**How to optimize**|| ||Downloading artifacts|Downloading layers and files from Cypress to the exec node|[Store on SSD](https://ytsaurus.tech/docs/en/#ssd-storage), [increase the number of replicas](https://ytsaurus.tech/docs/en/#replication-factor)|| ||Preparing artifacts|Preparing downloaded artifacts: unpacking tar, checking integrity|Switch from tar to [SquashFS](https://ytsaurus.tech/docs/en/#squashfs-layer)|| ||Preparing root volume|Mounting layers and building the root filesystem via overlayfs|Switch to [SquashFS over NBD](https://ytsaurus.tech/docs/en/#nbd-layer)|| ||Preparing tmpfs volumes|Preparing tmpfs volumes for the job|Depends on the tmpfs size; configured at the cluster level|| |# ### Stage analysis { #bottleneck-analysis } To find the bottleneck, compare the time spent on each stage and analyze outliers: - Open the operation page and go to the **Jobs** tab. - Find jobs with the longest preparation time — sort by the **Prepare time** column. - Open the job and check the stage breakdown in the **Job statistics** section. - Compare the stage times: the stage with the longest time is the bottleneck. - Apply the optimization methods from the table above to the identified stage. ### Methods to optimize job environment preparation stages { #stage-optimization } If the **Preparing root volume** stage takes the most time, switching from tar to SquashFS over NBD gives the biggest performance gain. If it’s **Downloading artifacts**, increase the number of replicas and verify that the layer is stored on SSD. If it’s **Preparing artifacts**, switch from tar to SquashFS to avoid unpacking. ### Troubleshooting { #troubleshooting } #### RootVolumePreparationFailed { #root-volume-error } This error occurs when mounting a layer fails during root filesystem preparation. Possible causes: - The layer image is corrupted. - The filesystem format is incorrect — `@filesystem`. - The NBD server is unavailable or not configured on the cluster. NBD‑specific causes are listed in the [Common NBD errors](https://ytsaurus.tech/docs/en/#nbd-common-errors) section. #### NbdError { #nbd-error } This error occurs when reading from an NBD device fails during job execution, for example, due to a lost connection with the data node. The job is automatically aborted and restarted. If the errors keep happening, the operation ends with an error. #### NBD volume creation error { #nbd-volume-error } This error occurs when creating an NBD volume on the exec node fails. Possible causes: - The NBD server isn’t running or isn’t reachable on the exec node. - The limit for NBD devices on the exec node is exceeded. - There aren’t enough resources to create the volume. Contact your cluster administrator for diagnostics. #### File permission error { #file-permission-error } If a file ends up in `/slot/sandbox` but your user code can’t run it, check the `executable` attribute. By default, it’s set to `false`. Set `executable: true` for executable files. #### File name conflict in /slot/sandbox { #file-name-conflict-error } If two files in `file_paths` have the same name and the `file_name` attribute isn’t set, the last file overwrites the previous one. Set a unique `file_name` for each file. #### Disk space shortage { #disk-space-error } Unpacked tar layers take up space on the exec node disk. Switch to SquashFS: the image is mounted without unpacking and isn’t stored in an unpacked form. #### Artifact download timeout { #download-timeout } If the **Downloading artifacts** stage takes too long, increase `replication_factor` and verify that the layer is stored on SSD. More replicas let you read data in parallel from different data nodes. #### Quota and resource issues { #quota-error } If the job ends with a resource shortage error — CPU, memory, or disk — check the limits in the operation specification. Increase `cpu_limit`, `memory_limit`, or `disk_request` if needed. Also, check that unpacked tar layers aren’t taking up too much space: switch to SquashFS so you don’t store the image in an unpacked form. ## How to speed up layer reads { #performance } ### Store layers on SSD { #ssd-storage } Reading from SSD is much faster than from HDD. This is especially important for NBD: data is read interactively from data nodes as the job accesses it, and HDD drives are too slow for this. ```bash # When creating a file yt create --type file \ --attributes '{primary_medium=ssd_blobs;account=sys;}' \ --path //path/to/layer.squashfs yt write-file //path/to/layer.squashfs < ~/layer.squashfs # For an existing file yt set //path/to/layer.squashfs/@primary_medium ssd_blobs yt set //path/to/layer.squashfs/@account sys ``` The migration to SSD happens in the background and may take time. ### Increase the number of replicas { #replication-factor } For NBD, three replicas can become a bottleneck when many jobs use the same layer at the same time. More replicas let you read data in parallel from different data nodes. You pay for more replicas with extra storage space. ```bash # When creating a file yt create --type file \ --attributes '{replication_factor=20;}' \ --path //path/to/layer.squashfs yt write-file //path/to/layer.squashfs < ~/layer.squashfs # For an existing file yt set //path/to/layer.squashfs/@replication_factor 20 ``` By default, `replication_factor=3`; the maximum is 20. ### Use the chunk cache { #chunk-cache } For tar and local SquashFS layers with `access_method=local`, the exec node caches downloaded layers in the chunk cache. When you rerun jobs with the same layers, they’re taken from the cache without downloading from data nodes. Two factors affect cache efficiency: - Standard base layers are highly likely to be already cached on exec nodes. - Changing the layer file invalidates the cache — the cached copy stops being used. ## NBD layers { #nbd } ### Optimizing NBD layers { #nbd-optimization } #### SquashFS block size { #squashfs-block-size } The SquashFS filesystem works with blocks. By default, the block size is 128 KB. The larger the block, the more data the kernel reads at once and the fewer NBD requests are sent to data nodes. Set the block size when building the image: ```bash mksquashfs ~/mnt /tmp/my_layer.squashfs -b 1M ``` For example, for self‑driving systems, switching from a 128 KB block to a 1 MB block sped up `md5sum` calculation for a large binary from 250–350 seconds to 40 seconds. {% note info %} A block that’s too large increases the read volume when accessing small files: you have to read “almost empty” blocks. Also, in the current Linux implementation of SquashFS, increasing the block size raises RAM consumption when mounting the image. The optimal size depends on the file access pattern in the layer. {% endnote %} #### Chunk block size { #chunk-block-size } Don’t confuse the SquashFS block size with the chunk block size. A file in Cypress consists of chunks, and a chunk is read from data nodes in blocks. By default, the chunk block size is 16 MB. The kernel reads data from NBD devices in small chunks, usually no more than 8 KB. So, with NBD access, a smaller chunk block size — `512K`, `1M`, or `2M` — reduces unnecessary reads. Set the block size when uploading the file: ```bash yt write-file \ --file-writer '{block_size=2097152;}' \ //path/to/layer.squashfs < ~/layer.squashfs ``` For example, for tasklets, reducing the chunk block size from 16 MB to 2 MB noticeably lowered the read volume. {% note warning %} `block_size` is set once when writing the file. To change it, rewrite the file. {% endnote %} ### Monitoring and diagnosing NBD { #nbd-monitoring } #### Solomon sensors { #sensors } The following NBD metrics are available on exec nodes. Server metrics: #| ||**Metric**|**Description**|| ||`nbd/server/count`|Current number of NBD servers|| ||`nbd/server/created`|Number of NBD servers created|| |# Device metrics. The `file_path` tag is the path to the layer file: #| ||**Metric**|**Description**|| ||`nbd/device/count`|Current number of NBD devices|| ||`nbd/device/created` / `nbd/device/removed`|Devices created and removed|| ||`nbd/device/read_count`|Number of read requests|| ||`nbd/device/read_bytes`|Bytes read|| ||`nbd/device/read_time`|Read time, histogram|| ||`nbd/device/read_block_bytes_from_cache`|Bytes read from the block cache|| ||`nbd/device/read_block_bytes_from_disk`|Bytes read from the data node disk|| |# Volume metrics. The tag is `type=nbd` or `type=squashfs`: #| ||**Metric**|**Description**|| ||`volumes/count`|Current number of volumes|| ||`volumes/created`|Volumes created|| ||`volumes/create_errors`|Volume creation errors|| |# Volume cache metrics: #| ||**Metric**|**Description**|| ||`exec_node/ronbd_volume_cache/missed_count`|Misses in the RO NBD volume cache|| ||`exec_node/ronbd_volume_cache/hit_count`|Hits in the cache. Tag `hit_type=sync\|async`|| ||`exec_node/squashfs_volume_cache/missed_count`|Misses in the SquashFS volume cache|| ||`exec_node/squashfs_volume_cache/hit_count`|Hits in the SquashFS volume cache|| |# ### Common NBD errors { #nbd-common-errors } Below are errors specific to NBD. General job environment preparation errors `RootVolumePreparationFailed` and `NbdError` are described in the [Troubleshooting](https://ytsaurus.tech/docs/en/#troubleshooting) section. #### NBD server is not present { #nbd-server-missing } This error occurs when you try to use NBD on a cluster where it isn’t enabled, or in an environment other than the porto environment. Contact your cluster administrator to enable NBD. #### Incorrect layer attributes { #nbd-wrong-attributes } To access a layer via NBD, it must have the `@filesystem=squashfs` and `@access_method=nbd` attributes set. If the filesystem format is specified incorrectly, mounting the layer will fail with the `RootVolumePreparationFailed` error. # Switching between tables Operations can have several input and output tables. This section describes how to switch reading between them. You can get data about which table an entry came from, and select which table to send it to, in the operation's [Map](https://ytsaurus.tech/docs/en/map.md) and [Reduce](https://ytsaurus.tech/docs/en/reduce.md) user code. This is done using **[descriptors](https://ytsaurus.tech/docs/en/#descriptors)** or **[table switches](https://ytsaurus.tech/docs/en/#switches)**. {% note info "Note" %} A user has several ways to specify which table a string is to be sent to: you can indicate the table's descriptor, or use a switch. There is no technical restriction on using both of these mechanisms simultaneously, but doing so in practice is **not recommended**. {% endnote %} {% note warning "Attention!" %} When no mapper is specified for a [MapReduce](https://ytsaurus.tech/docs/en/mapreduce.md) type operation, there will be no data about which input tables entries came from at the reducer input. {% endnote %} ## Descriptors { #descriptors } There are **descriptor numbering rules** that enable a user code (job) to write string X in table 0, and string Y in table 1. When there's one output table, the job writes all entries and descriptor 1 (in stdout). When there are multiple output tables, the numbering rules are used. The formula 3k+1, where k is the index of the output table, begins with 0. - Table 0 - descriptor 1. - Table 1 - descriptor 4. - Table 2 - descriptor 7. - ... ## Table switches { #switches } The correspondence between descriptor numbers and output tables can be changed while working with a job, by using **table switches**. The schema of configuring switches for input tables can be read in the [Input/output settings](https://ytsaurus.tech/storage/io-configuration.md). The switches form depends on the format of output data. ### YSON { #yson } In [YSON](https://ytsaurus.tech/storage/yson.md), in the entry stream, you might encounter an [entity](https://ytsaurus.tech/storage/yson.md#entity) with attributes for both input and output tables. These are control inputs that are built into the datastream. For example: `table_index=N`, where N is an integer. This command "switches" the stream in such a way that subsequent entries are connected to another table with index N. This is true for both input and output tables. Suppose our operation has 2 output tables at the output. By default, to send data to a table with index 0 we must enter it in descriptor 1, and to be sent to table 1 it must be entered in descriptor 4. Suppose the job has entered the following sequence of entries in descriptor 4: ```bash {a=1}; #; {b=2}; ``` In that case, the entry `a=1` goes into table 1, and entry `b=2` goes into table 0. Both descriptors will then write to table 0. {% note warning "Attention!" %} The order of strings when writing to one table through two descriptors is not established. {% endnote %} Switches work independently for each output descriptor. Suppose descriptor N corresponds to table X. When you write 10 lines without switches to descriptor N, they will go into table X in the exact order in which they were written in the descriptor. There will be no other strings between them. Suppose descriptors N and M correspond to table X. When you write 5 strings in descriptor N and 5 strings in descriptor M, all strings will go into table X, but their order will be unknown. Even if the user code writes only to descriptor N first, and only then to descriptor M, it cannot be guaranteed that the strings from N will appear in table X before the strings from M, or that they will not be shuffled. ### JSON { #json } Works the same as [YSON](https://ytsaurus.tech/docs/en/#yson). Table switch: ```json '{"$value": null, "$attributes": {"table_index": 1}}\n' ``` ### DSV { #dsv } Strictly speaking, the format and its derivatives do not support switches as a separate control input in the datastream. Each data entry must contain information about the table index. {% note info "Note" %} [DSV](https://ytsaurus.tech/storage/formats.md#dsv) does not support switching of input tables. {% endnote %} When the `enable_table_index` format option is enabled each line of an input table will be supplemented by a command window containing the table index. By default, the command field is called `@table_index`. The name of the field can be changed via the `table_index_column` format option. All lines from the `//path/to/table` table, which was numbered N in the list of the operation's input tables, will have the same value in their `@table_index` field, equal to N. ### SCHEMAFUL_DSV { #schemaful_dsv } When installing the `enable_table_index=true` option for tables in [SCHEMAFUL_DSV](https://ytsaurus.tech/storage/formats.md#schemaful_dsv) format, the table index will be recorded in all lines of the table with the first field, before the schema columns. ## Examples ```python # -*- coding: utf-8 -*- import yt.wrapper as yt from datetime import datetime def parse_time(time_str): "2012-10-19T11:22:58.190448Z" return datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ") def get_duration(rowA, rowB): if rowA["event_type"] == "job_completed": rowA, rowB = rowB, rowA delta = parse_time(rowB["timestamp"]) - parse_time(rowA["timestamp"]) return delta.total_seconds() + (delta.microseconds / 1000000.0) # By default the map-function accepts one entry for input. # Any function run as an operation must be a generator. def filter_event_type(row): if row.get("event_type") in ["job_started", "job_completed"]: yield row # In the event of a reduce-function, a key is sent to the input (this is a map from the key columns to their value), as well as a list of entries. # The key columns of each entry are equal to that which lies in the key def calculate_duration(key, rows): rows = list(rows) if len(rows) == 2: yield {"id": key["id"], "duration": get_duration(*rows)} else: assert len(rows) == 1 row = rows[0] # You can indicate the output table number via the special field row["@table_index"] = 1 yield row if __name__ == "__main__": yt.config["tabular_data_format"] = yt.JsonFormat(control_attributes_mode="row_fields") yt.run_map_reduce(filter_event_type, calculate_duration, "//tmp/forbeginners/event_log", ["//tmp/forbeginners/durations", "//tmp/forbeginners/filtered"], reduce_by="id") ``` An example of switching between output tables with a `table_index` with `control_attributes_mode="iterator"`: ```python import yt.wrapper as yt import random @yt.aggregator @yt.with_context def mapper(rows, context): for row in rows: input_table_index = context.table_index sum = 0 if input_table_index == 0: sum += int(row["value"]) else: sum -= int(row["value"]) output_table_index = random.randint(0, 1) # The function enables you to create an entry that is the switch to the table with the specified index. yield yt.create_table_switch(output_table_index) yield {"sum": sum} if __name__ == "__main__": yt.run_map(mapper, ["//tmp/input1", "//tmp/input2", "//tmp/input3"], ["//tmp/output1", "//tmp/output2"], format=yt.YsonFormat(control_attributes_mode="iterator")) ``` # CHYT **CHYT powered by ClickHouse** is a technology that enables you to create a cluster of ClickHouse servers directly on YTsaurus compute nodes. [ClickHouse](https://clickhouse.com/) is created within a Vanilla operation and works with the data in YTsaurus. This operation is called a *clique* (for more information, see [Concepts](https://ytsaurus.tech/docs/en/general.md#what-is)). Cliques can be public (available to all users) and private (available to a user or a team). The public clique `ch_public` is a basic public clique running on each YTsaurus cluster. ## Main advantages { #advantages } The vast majority of built-in ClickHouse features are available in CHYT. To learn more about various features of ClickHouse, see the [official documentation](https://clickhouse.com/docs/en/). Besides that, there are the following advantages: - You don't need to copy data from YTsaurus to ClickHouse. - You can use the computing quota in YTsaurus for fast computations. - You can quickly perform computations on YTsaurus data of small and medium size (up to 1 TB) up to 100 times faster than running MapReduce operations. - Working with [static](https://ytsaurus.tech/storage/static-tables.md) and [dynamic](https://ytsaurus.tech/dynamic-tables/overview.md) tables is supported. ## Restrictions { #disadvantages } Processed tables must be [schematized](https://ytsaurus.tech/storage/static-schema.md). # How to try There are several ways to try CHYT. The simplest way is to run a query via the Query Tracker web interface. You can use a public clique with the `ch_public` alias. This is the main public clique found on every cluster of CHYT's presence. {% note info "Note" %} In YTsaurus 24.1 and lower, a clique alias is specified with an asterisk at the beginning. For example, `*ch_public`. In the current version, `*ch_public` and `ch_public` are the same alias. {% endnote %} {% note warning "Attention" %} All YTsaurus users have access to the public clique, so there is a risk that it may not be available due to queries from other users or be occupied by other people's computations. In this regard, important processes and dashboards must not be based on a public clique. A public clique is essentially a demo version of CHYT. We highly recommend creating a separate clique for your team. {% endnote %} ## HTTP interface { #http } A clique is accessed via heavy YTsaurus HTTP proxies. The most convenient way to access a clique is to send a query to the name of the corresponding cluster and process the redirect. When using the cURL utility, this means that you must specify the `--location-trusted` flag. Below is an example of how to query a clique using the cURL command line utility, knowing the cluster name (`YT_PROXY`), the YTsaurus access token (`YT_TOKEN`), and the clique alias (`CHYT_ALIAS`). ```bash $ curl --location-trusted -H "Authorization: OAuth $YT_TOKEN" "$YT_PROXY/chyt?chyt.clique_alias=$CHYT_ALIAS" -d 'SELECT Avg(a) FROM "//sys/clickhouse/sample_table"' 224.30769230769232 ``` ### How to specify a clique alias { #clique_alias_specification } You can specify a clique alias in the special `chyt.clique_alias` URL parameter (see the example above) or in the `user` field using any of the authorization methods (information about the actual user is contained in the token). For example, `http://cluster.domain.com/chyt?user=my-clique&password=my-token`. {% note warning "Attention" %} Authentication (clique alias + token) via different combinations of parameters isn't allowed. For example, the following query contains an error: `-H 'X-ClickHouse-User: my-clique' 'http://cluster.domain.com/chyt?password=my-token'` (the alias is specified in the `X-ClickHouse-User/Key` headers, and the token is specified in the `user/password` URL parameters) {% endnote %} {% note info "Note" %} In the old version, queries use the `http://$YT_PROXY/query` path. When CHYT is accessed using the deprecated `/query` path, the alias is passed via the `database` url parameter. When CHYT is accessed using the new `/chyt` path, the alias is taken from `user` or the `chyt.clique_alias` URL parameter. {% endnote %} ### Exclusive connection port { #chyt_port } Not all clients support custom paths. Special ports can be configured on the HTTP Proxy to access CHYT. These ports don't require custom paths to be specified. To open standard ports 8123 and 8443, edit the very top of `cluster_v1_local.yaml`: ```yaml apiVersion: cluster.ytsaurus.tech/v1 kind: Ytsaurus metadata: name: minisaurus spec: clusterFeatures: httpProxyHaveChytAddress: true coreImage: ... ``` ## ODBC interface { #odbc } You can use CHYT from Tableau via the [Clickhouse ODBC Driver](https://github.com/ClickHouse/clickhouse-odbc). To do this, you just need to make the ODBC configuration represented below. The `YT_TOKEN` variable denotes the YTsaurus access token (`AQAD-qJ…`), `CHYT_ALIAS` is the alias of the clique (`ch_public`), and `YT_PROXY` is the cluster name. ```bash [ClickHouse] Driver = # For example /usr/local/opt/clickhouse-odbc/lib/libclickhouseodbc.dylib username = $CHYT_ALIAS password = $YT_TOKEN database = YT url = http://$YT_PROXY/chyt port = 80 # trace = 1 # TraceFile=/tmp/odbc.log ``` Below is an example of how to access CHYT via the ODBC interface from the command line using the [isql](https://en.wikipedia.org/wiki/UnixODBC) utility. To do this, write the `~/.odbc.ini` configuration file, install the Clickhouse ODBC driver from the official repository, and use the isql utility. ```bash isql -v clickhouse +---------------------------------------+ | Connected! | | | | sql-statement | | help [tablename] | | quit | | | +---------------------------------------+ SQL> SELECT * FROM "//tmp/sample_table" +--+ | a| +--+ | 2| | 9| | 6| | 4| | 8| | 3| | 1| | 5| | 7| | 0| +--+ SQLRowCount returns 10 10 rows fetched ``` ### Connecting external databases over ODBC { #external-odbc } CHYT can also act as an ODBC client and query external databases through the ClickHouse ODBC bridge. For configuration details and limitations, see [Connecting external databases over ODBC](https://ytsaurus.tech/docs/en/reference/odbc.md). ## JDBC interface { #jdbc } The JDBC driver is configured in a similar way to the ODBC driver, see the example below. The `YT_TOKEN` variable denotes the YTsaurus access token (`AQAD-qJ…`), `CHYT_ALIAS` is the ID/alias of the clique (`ch_public`), and `YT_PROXY` is the cluster name. ```bash JDBC URL = jdbc:clickhouse://{host}:{port}/chyt?chyt.clique_alias={clique_alias} host = $YT_PROXY user = $CHYT_ALIAS password = $YT_TOKEN port = 80 ``` Besides that, you need to change the driver properties: - `check_for_redirects=true`; - `use_path_as_db=false`. The JDBC driver can be used as a basis for using [DBeaver](https://dbeaver.io), an open-source database management program. DBeaver 6.1.5 was used for testing. If the ClickHouse-JDBC driver is not installed automatically, [download](https://github.com/yandex/clickhouse-jdbc) it manually. You must use a driver version higher than 0.1.55. ## CLI and Python API { #cli-and-api } CHYT has the official Python API and the official command line utility. For more information, see [CLI and Python API](https://ytsaurus.tech/docs/en/cli-and-api.md). # Introduction This article provides a rationale for the selected ClickHouse model within the YTsaurus ecosystem. In particular, why CHYT has *cliques*, while regular ClickHouse has nothing analogous. ## What is a clique? { #what-is } A *clique* is a full-fledged ClickHouse cluster that can read data from tables located in YTsaurus via the built-in YTsaurus internal protocol. From the point of view of the [YTsaurus scheduler](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md), a clique is a Vanilla operation containing in a typical case up to dozens of jobs, each of which consumes up to 16 CPU cores by default. The name *clique* was chosen as a play on words in consonance with the name ClickHouse, but the correct spelling in English is *Clique*. ## Administering a clique { #administration } Cliques are administered by *users*, not YTsaurus administrators. Specifically, any YTsaurus user can start their own clique (or even several cliques) at any time. For more information, see [Create, start, and stop a clique](https://ytsaurus.tech/docs/en/how-to-guides/create-start.md). ## Reliable isolation { #isolation } This representation solves the problem of *isolating* the load from various users. OLAP databases like ClickHouse can have a fundamentally diverse load. We can mention production processes like ETL which create a constant background of load from under robotic users. Besides that, there are one-time ad-hoc queries from human users through tools like YQL, as well as one-time or regular uploads of datasets to external BI systems, such as [DataLens](https://cloud.yandex.ru/docs/datalens/) or [Tableau](https://www.tableau.com/), for further visualization. If the database faces many "heavy" queries made at the same time, this can overload the cluster in many different ways: - The compute nodes may run out of CPU resources, causing all queries (including fast ones under normal circumstances) to get less CPU time and randomly degrade. - As a consequence, the IO subsystem or the network stack (for example, a TCP or DNS subsystem) which also need CPU time to function may degrade. - Compute nodes can bump into the network bandwidth and become [IO-bound](https://en.wikipedia.org/wiki/I/O_bound). - Finally, processing substantial amounts of data can consume a lot of RAM, both directly for computations that require maintaining large intermediate states (such as heavy `GROUP BY`, sorting, or `COUNT DISTINCT`) and elementarily for reading data for buffers over the network. Despite the fact that ClickHouse has built-in individual protection mechanisms to solve the problems described above, they do not always help in the realities of YTsaurus. A key property of YTsaurus as a data source for ClickHouse is that any even somewhat large table is "very heavily distributed". Roughly speaking, you can assume that every next GB of data in the table will live on a new host. In addition, [static YTsaurus tables](https://ytsaurus.tech/storage/static-tables.md), and especially those storing analytical data, can often be compressed with a heavy compression codec (see [Compression](https://ytsaurus.tech/storage/compression.md)). This leads to the fact that processing even a "relatively light" table of a dozen GB with only a few narrow columns with a total weight of hundreds of MB is heavily dependent on the network stack and the data decompression subsystem and is quite demanding on RAM for buffers to read from each machine over the network. Not wanting to solve the difficult task of managing CPU, RAM, network and disk IO bandwidth resources within ClickHouse, the decision was made to separate users physically between different processes, which is just achieved by having the required number of cliques used by small groups of users and independent of each other. Cliques are expected to be small-granular enough not to serve different types of load or a large number of users at the same time. The only exception to this rule are [public cliques](https://ytsaurus.tech/docs/en/try-chyt.md#public) which are intentionally open to all, but are provided on the best effort principle: they may be unavailable or overloaded at any time, although their performance is monitored. There are restrictions on the complexity of executed queries, sometimes quite severe. Consider public cliques to be a demo version of CHYT. ## CHYT as a service: reuse of resources in YTsaurus and exploitation by users { #as-a-service } This system of distributing resources between different cliques within one large YTsaurus cluster resembles ClickHouse "cloud" hosting similar to [Managed ClickHouse](https://cloud.yandex.ru/services/managed-clickhouse) in a cloud provider. The only difference is that users do not pay with rubles in the external cloud, but with a computing quota in YTsaurus. This enables many analytical and development teams to flexibly manage existing computing resources in YTsaurus. Some of them can be used in "classic" computational pipelines for minutes/hours/days with Map-Reduce, for example, via YQL over YTsaurus. And the other small part can be used to create a clique of a suitable size for fast ad-hoc computations with delays of seconds. Besides that, as is always the case with cloud technologies, providing something on an "as a service" model enables you to clearly specify the boundaries of responsibility for the operation of certain technical solutions. The greatest success in the operation of any database is achieved when two conditions are met simultaneously: - The user has a good understanding of the database structure and is able to evaluate the bottlenecks of the future computational pipeline using it. - The user is well aware of the specifics of the stored data, as well as the necessary computations on them, also knows where the data arises from in the database, and is responsible for its specific representation and form of storage. Users are expected to use the documentation to solve the majority of operational problems, including diagnosing slow queries and intelligently evaluating the need for resources. # Query anatomy before and after the ClickHouse engine This article describes the CHYT query architecture and where you can get the query execution information. ## Query IDs: `trace id`, `query id`, and `datalens request id` { #id } There are several IDs associated with any query which allow you to restore all the necessary query execution information post factum. - `query id` is the ID assigned to each query in ClickHouse. Unlike original ClickHouse, this ID in CHYT is not controlled by the user and always has the *YTsaurus GUID* form — four hexadecimal uint32 separated by hyphens. - `trace id` is the ID that allows you to chain calls in distributed systems, creating a query execution "trace". This is a part of the [opentracing](https://opentracing.io/) protocol, specifically its implementation called [Jaeger Tracing](https://www.jaegertracing.io/) that is used in YTsaurus. `trace id` is also a YTsaurus GUID that in some cases coincides with `query id` and in other cases does not as will be detailed below. ## Query execution journey from the client to heavy proxies { #way-to-proxy } To better understand the logic by which these IDs exist and are assigned, figure out through which components and in which order the query passes to CHYT. Currently, the only public API to access CHYT is the HTTP API, so the following description applies specifically to the HTTP protocol. The first important point in the query execution journey is the so-called _heavy YTsaurus proxies_, which all YTsaurus users have network access to. Heavy proxies find out where instances of the clique with a given alias currently reside, then send a query to a randomly selected instance. The three most popular ways to access YTsaurus are shown below. ![](https://ytsaurus.tech/_images/chyt_before_clique.png){ .center } When you access CHYT directly from a script or from the command line using the cURL utility, an SLB balancer (for example, `http://$YT_PROXY`) acts as the endpoint. Behind it lies a complex construction of balancers that directs the query to so-called *control proxies*, which respond with an HTTP redirect to the heavy proxies servicing all the heavy load in YTsaurus. With this access interface, the `query id` matches the `trace id`: you can see them in the `X-Yt-Trace-Id` and `X-ClickHouse-Query-Id` headers. Below is an example of interaction with CHYT using the cURL utility, with the most notable response headers indicated. {% note info "Note" %} In YTsaurus 24.1 and lower, a clique alias is specified with an asterisk at the beginning. For example, `*ch_public`. In the current version, `*ch_public` and `ch_public` are the same alias. When CHYT is accessed using the deprecated `/query` path, the alias is passed via the `database` url parameter. When CHYT is accessed using the new `/chyt` path, the alias is taken from `user` or the `chyt.clique_alias` URL parameter. {% endnote %} ```bash $ curl -v --location-trusted 'http://$YT_PROXY/chyt?chyt.clique_alias=ch_public' -d 'select max(a) from "//sys/clickhouse/sample_table"' -H "Authorization: OAuth `cat ~/.yt/token`" * Trying ip_address:80... * Connected to $YT_PROXY (ip_address) port 80 (#0) > POST /chyt?chyt.clique_alias=ch_public HTTP/1.1 > Host: $YT_PROXY > User-Agent: curl/7.69.1-DEV > Accept: */* > Authorization: OAuth ...... > Content-Length: 50 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 50 out of 50 bytes * Mark bundle as not supporting multiuse // Getting a redirect to a heavy proxy. < HTTP/1.1 307 Temporary Redirect < Content-Length: 0 < Location: http://sas4-9923-proxy-$YT_PROXY/chyt?chyt.clique_alias=ch_public < X-Yt-Trace-Id: 8e9bcc43-5c2be9b4-56f18c4e-117ea314 < * Connection #0 to host $YT_PROXY left intact * Issue another request to this URL: 'http://sas4-9923-$YT_PROXY/chyt?chyt.clique_alias=ch_public' * Trying ip_address:80... * Connected to sas4-9923-$YT_PROXY (ip_address) port 80 (#1) > POST /chyt?chyt.clique_alias=ch_public HTTP/1.1 > Host: sas4-9923-$YT_PROXY > User-Agent: curl/7.69.1-DEV > Accept: */* > Authorization: OAuth ...... > Content-Length: 50 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 50 out of 50 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Transfer-Encoding: chunked // Note: query id = trace id. < X-ClickHouse-Query-Id: 3fa9405e-15b29877-524e3e67-2be50e94 < X-Yt-Trace-Id: 3fa9405e-15b29877-524e3e67-2be50e94 // For technical reasons, X-Yt-Trace-Id will appear twice. < X-Yt-Trace-Id: 3fa9405e-15b29877-524e3e67-2be50e94 < Keep-Alive: timeout=10 // Address of the query coordinator instance. < X-ClickHouse-Server-Display-Name: sas2-1374-node-$YT_PROXY < X-Yt-Request-Id: 3fa9405d-26285349-db14531a-2a12b9f9 < Date: Sun, 05 Apr 2020 18:49:57 GMT < Content-Type: text/tab-separated-values; charset=UTF-8 < X-ClickHouse-Summary: {"read_rows":"0","read_bytes":"0","written_rows":"0","written_bytes":"0","total_rows_to_read":"0"} // Address of the heavy proxy that served the query. < X-Yt-Proxy: sas4-9923-$YT_PROXY < 1100 * Connection #1 to host sas4-9923-$YT_PROXY left intact ``` ## Executing a query within a clique { #query_execution } A heavy proxy [selects](https://ytsaurus.tech/docs/en/instance-pick.md) one instance from the clique and delegates query execution to it. This instance is called the query "coordinator" (or "initiator"). If the query doesn't use YTsaurus tables (for example, when querying system tables starting with the `system.` prefix, or when it's a primitive query like `SELECT 1`), then it's executed on the coordinator normally, just like it would be done in ClickHouse. However, if the query affects YTsaurus tables , then it's always executed in a distributed manner. Let's examine the execution of a simple scan query that supports filtering, projection, and aggregation: `SELECT someColumns, someFunctions(someColumns) FROM "//path/to/table" WHERE somePredicate(someColumns) [GROUP BY someColumns]`. This query splits `//path/to/table` into roughly equal sections, with each section being processed independently on one of the clique nodes. Essentially, each instance receives a modified query, where `//path/to/table` in the FROM clause is replaced by an expression in the form `ytSubquery(...)`, with the description of the next table section encoded in the argument. ![](https://ytsaurus.tech/_images/chyt_inside_clique.png) Thus, query execution boils down to executing a number of remote subqueries on the clique's instances. The image above illustrates this mechanism. It's worth noting that there are two kinds of queries: - `QueryKind: InitialQuery`: Initial queries received from clients. - `QueryKind: SecondaryQuery`: Modified remote subqueries that reference specific YTsaurus table sections in the FROM clause. Each secondary query inherits the `trace id` of its parent. And even though each secondary query has its own `query id`, they still remember their initial query. In system logs, the parent `query id` of the initial query is indicated as `InitialQueryId` for secondary queries. ## I/O and compression in CHYT Below is some useful information and recommendations on reading data from YTsaurus. Take these into account when using CHYT. ### Network Instances of any clique are started on random machines of the cluster, so data reading is likely to be done over the network. You can expect that roughly each successive gigabyte of data that needs to be read will reside on a different machine in the cluster. Note that even though the YTsaurus cluster network rarely becomes a bottleneck, this can happen in rare cases. ### Compression In YTsaurus, data is typically stored in **compressed** form. Compression is handled by the `/@compression_codec` table attribute, which is also found in the table view web interface. This is further described in a dedicated documentation [article](https://ytsaurus.tech/storage/compression.md). Instances read data from compressed tables in compressed form. This data is then decompressed just before being processed by the ClickHouse engine. When choosing a compression codec for your data, take into account that the stronger the codec, the more costly it will be to decompress the data. ### Uncompressed block cache CHYT has an **uncompressed block cache**, which is allocated 20 GiB by default. The cache size can be controlled with the `--uncompressed-block-cache-size` parameter in the clique start string. This cache stores uncompressed blocks of recently read tables, which significantly speeds up query execution on data recently read by another query, because some of this data may already be ready and available in instance memory. To ensure that this cache is used effectively, make sure that the following conditions are met: - The cache works without issues as long as the number of instances in the clique remains the same. If an instance disappears (for example, due to preemption, a YTsaurus node failure, or a CHYT crash), future subqueries are arbitrarily redistributed across instances, and most of the data blocks cached in instance memory may become useless, increasing the block cache miss value. However, in this case, the very next query will restore the necessary cache data. - If table data changes, the table sections are arbitrarily redistributed across subqueries, which similarly leads to block cache misses. - Finally, if the amount of processed data per instance is significantly larger than the block cache size (in uncompressed data size terms), then, quite obviously, the cache won't be of much use for any subsequent queries on the same data, because most of it will have to be re-read over the network anyway. ### HDD vs SSD Even though SSDs offer faster read speeds than HDDs, there is no need to rush to migrate your data to an SSD the moment your queries begin to slow down. It's possible that the bottleneck is actually the CPU, not I/O: for example, the data may already be in the block cache, while you still need to process a few dozens of gigabytes of decompressed data to respond to the query. In this situation, moving your tables from HDD to SSD won't help. If you believe that migrating your data to an SSD will speed up query execution, follow the [action plan](https://ytsaurus.tech/docs/faq-chyt.md#how-to-set-ssd) provided in the FAQ. ### About columnar storage In most cases when working with ClickHouse, you want to use the columnar [format](https://ytsaurus.tech/storage/chunks.md#optimize_for) for table storage, which you can implement by setting the `/@optimize_for` attribute to `scan`. Columnar storage lets you efficiently read individual table columns without having to load the entire table into memory and helps conserve disk space, as YTsaurus will apply different codecs for columnar storage, which will optimize column representation. However, there may be situations requiring the row-oriented format (`/@optimize_for = lookup`). The main disadvantage of the columnar format is high memory consumption, especially for tables with a large number of columns, because each column requires its own buffer. In addition, it's worth noting that reading lookup tables is slightly less CPU-intensive. # JOIN in CHYT This article presents a high-level description of the JOIN structure in CHYT. ## Sorting vs sharding { #sort-vs-shard } The table storage schema in YTsaurus is fundamentally different from that in ClickHouse. In YTsaurus, the basic storage primitive is a [static table](https://ytsaurus.tech/storage/static-tables.md) with rows in [chunks](https://ytsaurus.tech/storage/chunks.md) randomly scattered across the cluster. Static tables are extremely inefficient for tasks that require point data reads (since these point reads usually result in Random IO on the hard disk). For quick point reads, YTsaurus has a more efficient and complex primitive called [dynamic tables](https://ytsaurus.tech/dynamic-tables/overview.md). However, many batch applications that process a large amount of data in a flow require one way or another working in a model when some of the columns are *key*. YTsaurus supports the concept of a sorted table — a table schema can show that table rows are sorted by a column prefix. Such columns are called key columns. Such metainformation enables you to efficiently implement, for example, the Sorted Reduce operation which is missing in the original Map-Reduce paradigm. In YTsaurus, the Reduce range is very wide: in particular, operations like Reduce support foreign tables with `LEFT INNER JOIN` semantics. In ClickHouse, the basis for distributed data storage is not [sorting](https://ytsaurus.tech/dynamic-tables/sorted-dynamic-tables.md) as in YTsaurus, but *sharding*. By default, the data is distributed across shards according to a shard key based on the remainder of the sharding expression divided by the total weight of all shards (for more information, see the [ClickHouse documentation](https://clickhouse.yandex/docs/en/operations/table_engines/distributed/)). Besides that, users often control the sharding logic themselves by inserting data into specific hosts. Both schemas achieve the same goal in different ways: - In YTsaurus, sorting ensures the locality of rows with one key value in a single chunk (or in a set of consecutive chunks). - In ClickHouse, sharding ensures the locality of rows with one sharding expression value on a single machine. ## How does a query work in CHYT? { #query } The CHYT structure is described in detail In the [Query anatomy](https://ytsaurus.tech/docs/en/anatomy.md) section. Any SELECT query is split by the coordinator into portions on the main table and then each portion is processed independently on its own instance. This execution schema is very similar to that of the Distributed ClickHouse engine, except that in ClickHouse, data is read literally "from underneath" the instance, while in YTsaurus, chunks are scattered randomly across the cluster machines, which is compensated by a very broad network. This method is well-suited for single-source threaded queries that do not contain JOIN. However, as soon as the need for joins arises, you have to figure out how to combine related rows from different sources without having to perform random reads. ## JOIN types in ClickHouse { #types } How can coordination (i.e. load distribution between instances) of JOIN work in a distributed environment? In ClickHouse, the following execution strategies of the `lhs JOIN rhs USING/ON ...` construction are possible: 1. Distributed local JOIN: if the tables are sharded *in the same way*, you can execute JOIN independently on each instance, since it is true that a pair of keys joined by JOIN cannot end up on different machines. Thus, `lhs` and `rhs` on each instance are interpreted as their respective local tables. 2. GLOBAL JOIN: if you use the keyword GLOBAL next to JOIN, you can force the system to do the following. The right-hand side `rhs` argument is executed and serialized on the query coordinator and its serialized representation is sent out along with the query to the instances. The instances are invited to use this representation to retrieve the right-hand side part in their memory. This method is good when `rhs` is relatively small and there are relatively few instances: it is easy to see that if one of these conditions is not met, you can bump into distribution of the table from the coordinator by subqueries over the network (or over the memory on the coordinator). This method *does not require* any additional conditions for the consistency of the storage/sharding schema on the tables. 3. JOIN via subqueries. ClickHouse enables you to surround `lhs` and/or `rhs` with brackets and this significantly affects the execution plan: — If you surround `lhs` with brackets, ClickHouse loses any information about the lhs structure, in particular, knowledge about the distributed nature of the left-hand side part is lost. In this situation, the left-hand side part is executed independently on the coordinator, the right-hand side part is executed independently and goes up to the RAM to the hash table, and then JOIN is completely executed only on the coordinator. — If you surround `rhs` with brackets, ClickHouse will make a distributed query as if the query just looks like `SELECT lhs`. Then it will send its queries to the instances, leaving `JOIN (rhs)` as it is. Next, each instance will execute `rhs` independently, which can lead to a much heavier load, since each instance will materialize the right-hand side part independently. The latter problem is solved by the protective mechanism that prohibits such behavior by default and results in the `Double-distributed IN/JOIN subqueries is denied` error. For more information, see the ClickHouse documentation. Relevant links: - [SELECT Query](https://clickhouse.com/docs/en/sql-reference/statements/select/); - [distributed_product_mode](https://clickhouse.com/docs/en/operations/settings/settings/#distributed-product-mode). What does a similar classification look like in CHYT? Points 2 and 3 work exactly the same way: strategy 2 is good if the right-hand side of the table is small. The first option of strategy 3 is suitable if the right-hand table is very large, but you can wait for a long time. Note that CHYT is designed for fast analytical queries and if something requires to join large heterogeneous tables, better use YQL and Map-Reduce. Therefore, we do not recommend this method. There is a significant difference in strategy 1 in CHYT. You can read about it below. ## Sorted JOIN { #sorted } Instead of the *Distributed JOIN* logic that uses the identity of the argument *sharding* schema, CHYT naturally produces the *Sorted JOIN* logic that uses the identity of the argument *sorting* schema. For those who understand how the Sorted Reduce operation works, Sorted JOIN works exactly the same way. To use the Sorted JOIN strategy, use the ordinary `lhs JOIN rhs USING/ON ...` construction, but `lhs` and `rhs` are subject to the following additional restrictions: - `lhs` and `rhs` must be sorted tables. Let `lhs` be sorted by columns `l1, l2, ..., ln` and `rhs` be sorted by columns `r1, r2, ..., rm`. - The JOIN condition should look like a set of equalities `l1 = r1`, ..., `lk = rk` for a `k` (the equalities themselves can go in any order). This can be expressed as a set of equalities in the ON clause or as a set of common key columns in the USING clause, but not as a condition in the WHERE clause. If these conditions are met, you can reuse the coordination logic from the Sorted reduce operation by forming pairs of appropriate ranges from `lhs` and `rhs` and distributing them to instances in subqueries. If this condition isn't met, an error will occur and you'll have to use either strategy 2 (using GLOBAL JOIN) or the second variant of strategy 3 (placing the right-hand part in a subquery). # Working with YTsaurus tables The main task solved by CHYT is processing tables that are already in YTsaurus without having to copy them to any third-party storages. Thus, the main difference of CHYT from ClickHouse is that it does not have the variety of engines from regular ClickHouse. Tables are stored according to the logic of [table storage in YTsaurus](https://ytsaurus.tech/storage/static-tables.md). Creating tables using standard ClickHouse engines is prohibited. Instead, you can read any of the existing tables in the corresponding YTsaurus cluster. You can also save results as YTsaurus tables. "Under the hood", working with YTsaurus tables is arranged similarly to the [Distributed](https://clickhouse.com/docs/en/engines/table-engines/special/distributed) engine from ClickHouse. Exception: you do not have to think about manual or automatic distribution of data into shards, because as a storage YTsaurus solves this task in a transparent, seamless, and reliable way, distributing data to nodes. CHYT works only with [schematized tables](https://ytsaurus.tech/storage/static-schema.md). The table must have the filled in `/@schema` attribute, or the **Schema** tab in the YTsaurus web interface must have a schema (i.e. at least one column is mentioned), which is the same thing. YTsaurus tables are denoted by their full paths in Cypress wrapped in backticks or in double quotes: `` `//path/to/table` `` or `"//path/to/table"`: ```sql SELECT * FROM `//tmp/sample_table` ``` CHYT can read and write static and sorted dynamic tables, but note that due to the data storage peculiarities, reading from [dynamic tables](https://ytsaurus.tech/dynamic-tables/overview.md) may take by several times more time than reading from static tables. ## Reading multiple tables { #many } - Concatenation of several YTsaurus tables: `concatYtTables(table1, [table2, [...]])`. Example: ```sql SELECT * FROM concatYtTables("//tmp/sample_table", "//tmp/sample_table2") ``` - Concatenation of all tables along `cypressPath` (without a slash at the end): `concatYtTablesRange(cypressPath, [from, [to]])`. Example: ```sql SELECT * FROM concatYtTablesRange("//tmp/sample_tables") SELECT * FROM concatYtTablesRange('//tmp/sample_tables','2019-01-01') SELECT * FROM concatYtTablesRange('//tmp/sample_tables', '2019-08-13T11:00:00') ``` For more information about these functions, see [Reading a set of tables](https://ytsaurus.tech/docs/en/reference/functions.md#multiple_tables_read). ## Saving results { #save } CHYT can create static tables, insert data into them, and save query results in YTsaurus. Below is a list of supported queries: * `INSERT INTO … VALUES`; * `INSERT INTO … SELECT`; * `CREATE TABLE`; * `CREATE TABLE AS SELECT`. Examples: ```sql -- Inserting data into a table INSERT INTO `//tmp/sample_table`(a) VALUES (10), (11); ``` ```sql -- Inserting data from another table INSERT INTO `//tmp/sample_table` SELECT a+100 FROM `//tmp/sample_table` ORDER BY a; ``` ```sql -- Creating a table CREATE TABLE IF NOT EXISTS `//tmp/sample_table` ( a String, b Int32 ) ENGINE = YtTable(); ``` ```sql -- Creating a table with sorting by key b CREATE TABLE IF NOT EXISTS `//tmp/sample_table_with_pk` ( a String, b Int32 ) ENGINE = YtTable() order by b; ``` ```sql -- Creating a table based on a select query CREATE TABLE `//tmp/sample_table` ENGINE = YtTable() AS SELECT a FROM `//tmp/sample_table` ORDER BY a; ``` ```sql -- Creating a table based on a select query with sorting by column b CREATE TABLE `//tmp/sample_table_with_pk` ENGINE = YtTable() order by b AS SELECT b,a FROM `//tmp/sample_table` order by b; ``` To overwrite the existing data, just add the `` option before the path: Example: ```sql INSERT INTO `//tmp/sample_table` SELECT a+100 FROM `//tmp/sample_table` ORDER BY a. ``` When creating a table, you can specify additional attributes. To do this, just pass them to Engine in the form of [YSON](https://ytsaurus.tech/storage/data-types.md#yson). Example: ```sql CREATE TABLE `//tmp/sample_table`(i Int64) engine = YtTable('{compression_codec=snappy}'); CREATE TABLE `//tmp/sample_table`(i Int64) engine = YtTable('{optimize_for=lookup}'); CREATE TABLE `//tmp/sample_table`(i Int64) engine = YtTable('{primary_medium=ssd_blobs}'). ``` There are restrictions when saving results: default values, expressions for TTL, and compression formats for columns are ignored. ## Table manipulation queries { #ddl } CHYT supports the following operations on tables: * `TRUNCATE TABLE [IF EXISTS]`. * `RENAME TABLE … TO …`; * `EXCHANGE TABLES … AND …`; * `DROP TABLE [IF EXISTS]`. Examples: ```sql -- Deleting all data from a table TRUNCATE TABLE `//tmp/sample_table`; TRUNCATE TABLE IF EXISTS `//tmp/sample_table`; -- Renaming a table RENAME TABLE `//tmp/sample_table` TO `//tmp/sample_table_renamed`; -- Exchanging the names of two tables EXCHANGE TABLES `//tmp/sample_table` AND `//tmp/other_sample_table`; -- Deleting a table DROP TABLE `//tmp/sample_table`; DROP TABLE IF EXISTS `//tmp/sample_table`; ``` ## Virtual columns { #virtual_columns } Each YTsaurus table has virtual columns. These columns are not visible in the `DESCRIBE TABLE` and `SELECT * FROM` queries and you can only get them by explicitly accessing them. 3 virtual columns are currently available for each table: - `$table_index Int64`: The table number in the order of enumeration in `concatYtTables` (or in the sort order in `concatYtTablesRange`). When reading a single table, the index is always zero. - `$table_path String`: The full path to the table in YTsaurus. - `$table_name String`: The table name, i.e. the last literal of the table path. Input tables can be filtered quite effectively by described virtual columns, since their values are known before reading any data from the tables. This property can be used to read almost any subset of tables from the directory. However, take into account that although the tables themselves will not be read, the *table metainformation* (for example, the schema) will still be loaded. Besides that, even if the table does not fall under the condition and will not be read, the user's access permissions to all tables specified in `concatYtTables/concatYtTablesRange` will still be checked. Therefore, we recommend specifying restrictions using the arguments of the `concatYtTablesRange` function for directories with a large number of tables. Query examples: ```sql SELECT $table_name FROM "//home/dev/username/t1"; -- Result -- # $table_name -- 1 "t1" ``` ```sql SELECT * FROM "//home/dev/username/t1"; --Result --# a b c --1 0 1 1 ``` ```sql SELECT *, $table_name, $table_path, $table_index FROM concatYtTables("//home/dev/username/t0", "//home/dev/username/t1"); -- Result --# a b c $table_name $table_path $table_index --1 0 0 0 "t0" "//home/dev/username/t0" 0 --2 0 1 1 "t0" "//home/dev/username/t0" 0 --3 0 1 1 "t1" "//home/dev/username/t1" 1 ``` ## Working with dynamic tables { #dynamic } You can read [dynamic](https://ytsaurus.tech/dynamic-tables/overview.md) tables, including dynamic stores (fresh data in memory), from CHYT. {% note warning "Attention" %} To make data in dynamic stores visible when reading from CHYT and Map-Reduce, the table must be remounted and the `enable_dynamic_store_read = %true` attribute must be set. {% endnote %} By default, if you try to read a dynamic table without the `enable_dynamic_store_read = %true` attribute, the query will end with an error. If you really need data only from chunk stores (with a delay of tens of minutes), then you can set `chyt.dynamic_table.enable_dynamic_store_read` to `0`, after which CHYT will forcibly start reading only chunk stores (see the [query settings](https://ytsaurus.tech/docs/en/reference/settings.md) page). # Overview Before you run SQL queries through CHYT, you need to prepare a [clique](https://ytsaurus.tech/docs/general.md#what-is): create it, allocate a compute pool, and start it. This section contains reference information for working with cliques: a description of the web interface, clique settings, and access permissions. If you need step‑by‑step instructions for a specific task, see the [clique management scenarios](https://ytsaurus.tech/docs/how-to-guides/overview.md) section. ## Clique lifecycle { #lifecycle } Before using the interface and settings, it’s useful to understand which stage of the lifecycle the clique is currently at. This will help you figure out available actions for now. The lifecycle is divided into two phases. In the preparation phase, you need to create, configure, and start the clique. After that, active use phase begins: you run queries and manage the clique as needed. The diagram below shows the stages of the clique lifecycle and transitions between them: ![life cycle](https://ytsaurus.tech/_images/schemeEN.png){ .center } **Preparation for usage** 1. You create a clique: you assign it a unique name — `alias` — and basic access permissions that determine who can run queries and who can manage the configuration. For more details, see: [Creating, starting, and stopping a clique](https://ytsaurus.tech/docs/how-to-guides/create-start.md) and [Access permissions](https://ytsaurus.tech/docs/how-to-guides/acl.md). 1. You configure the clique: you assign a compute pool, the number of instances and resources to it. The _pool_ is a required parameter — the controller won’t start the clique without it. You can leave the other options at their default values. For more details, see: [clique settings](https://ytsaurus.tech/docs/en/configs.md) and [Adding compute resources](https://ytsaurus.tech/docs/how-to-guides/manage-resources.md). 1. You start the clique — the [Strawberry Controller](https://ytsaurus.tech/docs/controller.md) reads the configuration and starts a [Vanilla operation](https://ytsaurus.tech/operations/vanilla.md). The clique enters the active state and, after some time, is ready to accept queries. For more details, see the [Creating, starting, and stopping a clique](https://ytsaurus.tech/docs/how-to-guides/create-start.md) section. **Usage and maintenance** 1. You run queries, scale resources if necessary, update settings and access permissions. The controller monitors the state and applies configuration changes automatically. For more details, see [How to try CHYT](https://ytsaurus.tech/docs/try-chyt.md). 1. When you temporarily don't need the clique, you stop it: ongoing queries complete, and pool resources are released. For more details, see [Creating, starting, and stopping a clique](https://ytsaurus.tech/docs/how-to-guides/create-start.md). 1. If you no longer need the clique, you delete it. The system will delete the clique along with its metadata. ## Clique settings { #options } The clique configuration is stored in a [speclet](https://ytsaurus.tech/docs/en/configs.md#speclet) — a YSON document in Cypress. By editing the speclet, you manage compute resources, query behaviour, and other parameters. For the full list of options, see the [clique settings](https://ytsaurus.tech/docs/en/configs.md#options) section. ## Management tools { #tools } To prepare and manage a clique, you can use two tools: - [Web interface](https://ytsaurus.tech/docs/en/ui.md) — convenient for one‑off tasks and visual monitoring of the clique’s state; - [CLI and Python API](https://ytsaurus.tech/docs/cli-and-api.md) — suitable for automation, bulk actions on cliques, and integration with external applications. ## Useful links [Concepts](https://ytsaurus.tech/docs/general.md) [CLI or Python API](https://ytsaurus.tech/docs/cli-and-api.md) [How to try CHYT](https://ytsaurus.tech/docs/try-chyt.md) # The clique web interface in YTsaurus The web interface is a convenient way to manage [cliques](https://ytsaurus.tech/docs/general.md). It is suitable for: - Quick one‑off tasks. - Visual monitoring of the clique’s status. - Configuration without writing scripts. - Quick parameter review enabled by a clear visual structure. ## How to open the clique interface { #where } 1. In the main menu of YTsaurus, on the left, select **Cliques**. 1. In the **CHYT cliques** section, select the required clique from the list or [create a new one](https://ytsaurus.tech/docs/how-to-guides/create-start.md#create). {% note info %} To get familiar with the web interface, use the public clique `ch_public`. This is a publicly accessible clique that runs on every YTsaurus cluster. {% endnote %} ## Main sections of the web interface { #ui } ![UI](https://ytsaurus.tech/_images/clique-ui-os.png){ .center } _1. [Header](https://ytsaurus.tech/docs/en/#header) — displays the clique name._ _2. [Action buttons](https://ytsaurus.tech/docs/en/#action-menu) — use them to manage cliques._ _3. [Clique characteristics block](https://ytsaurus.tech/docs/en/#params) — here you can view the clique’s status._ _4. [Tabs panel](https://ytsaurus.tech/docs/en/#tabs) — links to the speclet (configuration), ACL, and logs tabs._ ### Header {#header} The header (1) displays basic information: - The name of the cluster where the clique is located, and a button to change it. - The name of section — **CHYT cliques**. - Buttons: - ![add to favourites](https://ytsaurus.tech/_images/add-to-favourites-btn.png){width=24 height=24} _Add to favourites_ — add the clique to your favourites. - ![view favourites](https://ytsaurus.tech/_images/view-favourites-btn.png){width=24 height=24} _View favourites_ — view your favourite cliques. - The clique name. - The **Create clique** button for [creating a new clique](https://ytsaurus.tech/docs/how-to-guides/create-start.md#create). ### Action buttons {#action-menu} On the right, there is an action buttons block (2): - ![sql](https://ytsaurus.tech/_images/sql-btn.png){width=24 height=24} _SQL_ — go to the web interface for running queries — [Query Tracker](https://ytsaurus.tech/query-tracker/about.md). - ![start](https://ytsaurus.tech/_images/start-btn.png){width=24 height=24} _Start_ — start the clique. - ![stop](https://ytsaurus.tech/_images/stop-btn.png){width=24 height=24} _Stop_ — stop the clique. - ![remove](https://ytsaurus.tech/_images/remove-btn.png){width=24 height=24} _Remove_ — delete the clique. - ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png){width=24 height=24} _Edit speclet_ — edit the [speclet](https://ytsaurus.tech/docs/en/configs.md#speclet) — the clique’s configuration file. ### Clique characteristics {#params} Below the header is the clique characteristics block (3). Key parameters: - `Health` — shows the operational status of the clique. The parameter can have the following values: - `Good` — the clique is healthy and ready to accept queries. - `Pending` — this is the state before `Good`, indicating that the clique is waiting to start. - `Failed` — the clique is unavailable due to a failure. {% note info %} When the `Health` parameter is set to `Failed`, the [Strawberry Controller](https://ytsaurus.tech/docs/controller.md) restarts the Vanilla operation. If the issue is resolved, `Health` will change to `Pending` and then to `Good`, or back to `Failed`, in which case the controller will try to restart the Vanilla operation again. {% endnote %} - `State` — the clique state: `Active` / `Inactive`. Shows whether the clique is running or stopped. - `Pool` — the name of the compute [pool](https://ytsaurus.tech/scheduler/scheduler-and-pools.md#scheduler), which is a link to pool's web interface. - `Instances`, `Cores`, `Memory` — the number of instances and computing resources (CPU cores and RAM) allocated to the clique. - `YT operation Id` — a link to the [YT operation](https://ytsaurus.tech/docs/en/yt-operation-ui.md) interface that corresponds to the clique. Other characteristics are for reference only and help to clarify the main metrics and identify the causes of failures. ### Tabs panel {#tabs} Key information about the clique is displayed in block (4) on the tabs: - **Speclet** — shows the contents of the YSON document with the clique’s settings ([speclet](https://ytsaurus.tech/docs/en/configs.md#speclet)). - **ACL** — shows which access permissions to the clique have been granted and for which user groups. For more details, see the [Access permissions](https://ytsaurus.tech/docs/en/access.md) section. - **Query Logs** — opens the query logs table. For more details, see [Getting Query Logs](https://ytsaurus.tech/docs/how-to-guides/query-logs.md). ## Useful links [Clique settings](https://ytsaurus.tech/docs/en/configs.md) # Clique as a YT operation In the YTsaurus system, a CHYT clique is not a separate entity but a standard distributed [Vanilla operation](https://ytsaurus.tech/operations/vanilla.md) that consists of a set of tasks — jobs. The YT operation interface helps you track the click’s status, analyze jobs, and troubleshoot issues if something goes wrong. ## How to open the YT operation web interface { #get-to-section } You can open the YT operation interface in three ways: - Using the link in the `YT Operation Id` parameter from the [clique interface](https://ytsaurus.tech/docs/en/ui.md#ui); - Using the link from the *Current operation id* field in the Strawberry operation description, which is stored in Cypress at the path `//sys/strawberry/chyt/`; - Via the CLI, using the link from the output of the `yt clickhouse ctl status` command. ## Key interface blocks { #ui-main-sections } ![UI](https://ytsaurus.tech/_images/chyt-yt-operation.png){ .center } _1. Basic information about the YT operation._ _2. Job summary._ _3. Tabs panel._ _4. **Description** section._ _5. **Tasks** section._ Blocks that are useful for practical tasks: 1. **Description** section (4). The section is located on the **Details** tab in the Tabs panel (3). It contains system information about the clique and a number of useful parameters — for example, CHYT and ClickHouse versions. To learn how to view component versions, see the [Get CHYT and ClickHouse versions](https://ytsaurus.tech/docs/how-to-guides/versions.md) section. 1. Job summary section (2). It is located to the right of the basic operation information (1). It displays information about job statuses: *Running*, *Completed*, *Failed*. The statuses are links. They lead to the **Jobs** tab in the tabs panel (3), where you can view data for each job and filter the job list by various parameters. For more information on job diagnostics, see the [Check failed jobs](https://ytsaurus.tech/docs/how-to-guides/failed-jobs.md) section. Keep in mind that one job corresponds to one instance in the clique — that is, to one ClickHouse server. 1. The **Tasks** section (5) displays job statistics. In the context of CHYT instances, the following terms are used: #| || **Status** | **Meaning** | **What to look for** || || `total` | Total number of instances | — || || `running` | The instance is up and running | If `running` = `total`, everything is fine || || `pending` | The instance has not been launched yet | A non‑zero value means the clique lacks resources to launch all instances || || `completed` | The instance was shut down normally — it completed all the queries | — || || `failed` | The instance crashed | Common causes: out‑of‑memory (OOM) or an error in the CHYT / ClickHouse code || || `aborted` | The instance was aborted | If the reason is `preemption`, it indicates a lack of resources || || `lost` | — | A failure outside CHYT, in other components || |# # Clique settings This article describes clique [options](https://ytsaurus.tech/docs/en/#options) and how to manage them: how to view and change the configuration via Cypress, CLI, or web interface. The article will be useful for anyone who wants to configure a clique or change its behavior. ## Speclet { #speclet } A clique’s configuration is described by a single [YSON document](https://ytsaurus.tech/storage/yson-docs.md), called a *speclet*. The speclet is stored in Cypress at the path `//sys/strawberry/chyt//speclet`. It is a set of [options](https://ytsaurus.tech/docs/en/#options) that are interpreted by the [controller](https://ytsaurus.tech/docs/controller.md) when the clique starts. Based on these options, the controller generates the final configuration for CHYT instances. Example of a speclet for the `ch_public` clique on the `ytdemo` cluster: ```yson { "active": true, "enable_geodata": false, "family": "chyt", "instance_count": 1, "instance_cpu": 1, "instance_memory": { "chunk_meta_cache": 100000000, "clickhouse": 100000000, "clickhouse_watermark": 10, "compressed_cache": 100000000, "footprint": 500000000, "log_tailer": 100000000, "reader": 100000000, "watchdog_oom_watermark": 0, "watchdog_oom_window_watermark": 0 }, "pool": "chyt", "stage": "production" } ``` ### View clique options { #where } You can view clique options via: - Viewing the speclet in Cypress at the path `//sys/strawberry/chyt//speclet`; - Running the following CLI commands: `yt clickhouse ctl get-speclet` and `yt clickhouse ctl status`; - Viewing the **Speclet** tab in the [web interface](https://ytsaurus.tech/docs/en/ui.md#ui) on the [Tabs panel](https://ytsaurus.tech/docs/en/ui.md#tabs). ### Change clique options { #how-to-change } To change clique options, use: - Editing the file in Cypress: open the `speclet` file at the path `//sys/strawberry/chyt//speclet` and click **Edit**; - The CLI utility: run the `yt clickhouse ctl set-option` command with the needed option from the list of [available options](https://ytsaurus.tech/docs/en/#options); - The web interface: click ![](https://ytsaurus.tech/_images/edit-btn.png){width=24 height=24} **Edit speclet** in the upper‑right corner, in the [Action buttons](https://ytsaurus.tech/docs/en/ui.md#ui) block, or the button **Edit Speclet** tab on the [Tabs panel](https://ytsaurus.tech/docs/en/ui.md#ui). For step‑by‑step instructions and practical examples of setting options, see the [Adding compute resources](https://ytsaurus.tech/docs/how-to-guides/manage-resources.md) and [Configuring advanced settings](https://ytsaurus.tech/docs/how-to-guides/manage-advanced-settings.md) sections. ### Available options { #options } Below are the clique options available for configuration via the [web interface](https://ytsaurus.tech/docs/en/ui.md) or via the CLI with `set-option` command (default values are shown in square brackets). Options are roughly grouped into three sections corresponding to the tabs in the speclet settings dialog. - **Basic options** — *General* tab #| || **Option** | **Description**|| || `active` [`false`] | If enabled, the controller will try to run the Vanilla operation assigned to the clique. If set to `false`, the clique will be inactive — it won’t process requests or consume compute pool resources. This option can be useful for temporarily disabling a clique while preserving its configuration. In the web interface, use the [Action buttons](https://ytsaurus.tech/docs/en/ui.md#ui) to start and stop the clique || || `pool` | The name of the compute pool where the clique operation should be run. To set this option, you must have the `Use` permission for the specified compute pool. Setting this option is required to run a clique operation under the controller || || `preemption_mode` [`normal`] | The preemption mode in which the YTsaurus clique operation will be started. This setting is intended for advanced users only. If you’re unsure whether you need it, we recommend keeping the default value. For more information about preemption, see the [Resources and preemption](https://ytsaurus.tech/docs/resources.md) section || || `restart_on_controller_change` [`true`] | If enabled, the clique will automatically restart whenever the controller changes. Otherwise, you’ll need to manually restart the clique to apply its settings || || `restart_on_speclet_change` [`true`] | If enabled, the clique will automatically restart whenever the speclet changes. Otherwise, you’ll need to manually restart the clique to apply its settings || |# - **Compute resources** — *Resources* tab #| || **Option** | **Description**|| || `instance_count` [1] | The number of clique instances || || `instance_cpu` | The amount of CPU allocated to each clique instance. This setting is intended for advanced users only. If you’re unsure whether you need it, we recommend keeping the default value || || `instance_total_memory` | The amount of Random Access Memory (RAM, in bytes) allocated to each clique instance. This setting is intended for advanced users only. If you’re unsure whether you need it, we recommend keeping the default value || |# - **Advanced options** — *Advanced* tab {% note warning %} These settings are intended for advanced users only. If you’re unsure whether you need them, we recommend keeping the default values. For more information, see the [Configuring advanced settings](https://ytsaurus.tech/docs/how-to-guides/manage-advanced-settings.md) section. {% endnote %} #| || **Option** | **Description**|| || `enable_geodata` [`true`] | Automatic configuration of system dictionaries required for [certain ClickHouse Geo functions](https://clickhouse.com/docs/en/sql-reference/functions/ym-dict-functions/) || || `query_settings` | A dictionary with default settings applied for all queries executed within the clique || || `clickhouse_config` | A dictionary with the ClickHouse-part configuration of CHYT instances. These settings affect the logic of the ClickHouse compute engine and are identical to the original ClickHouse engine settings || || `yt_config` | A dictionary with the YTsaurus-part configuration of CHYT instances || || `enable_sticky_query_distribution` [`false`] | Distribute queries across instances based on the hashes of those queries. This increases the efficiency of cache use || || `query_sticky_group_size` [2] | The size of the group of instances selected deterministically based on the query hash. A coordinator for executing the query will be selected uniformly from among these instances. This is only relevant when `enable_sticky_query_distribution=true` || || `odbc_config` | A dictionary with ODBC settings: drivers, data sources, and extra files. Enables queries to external databases. For more information, see [Connecting external databases over ODBC](https://ytsaurus.tech/docs/reference/odbc.md) || |# ## Useful links [Adding compute resources](https://ytsaurus.tech/docs/how-to-guides/manage-resources.md) [Configuring advanced settings](https://ytsaurus.tech/docs/how-to-guides/manage-advanced-settings.md) # Access permissions Access permissions for a clique are stored in a dedicated system node — **Access Control Object** (ACO). For more information about this object, see the [section](https://ytsaurus.tech/user-guide/query-tracker/about.md#access-control). ## Authentication { #authentication } Users are authenticated in CHYT in the same way as in YTsaurus — using a token from YTsaurus (for more information, see [Authentication](https://ytsaurus.tech/user-guide/storage/auth.md)). ## Types of permissions { #types } When working with cliques, four types of permissions are used: - `use` — allows you to run queries against the clique; - `read` — grants access to read the clique configuration; - `manage` — allows you to modify the clique configuration; - `remove` — grants the permission to delete the clique. ## How permission checks work { #how-it-works } When working with cliques, CHYT checks two types of permissions: 1. Data access permissions. When you access any data in YTsaurus, the system checks read and write permissions for all tables mentioned in the query. This happens via the standard ACL mechanism built into YTsaurus. This ensures secure data access. 2. Permissions to access clique resources. Before running a query, the clique checks whether the user has the required permissions: - SQL queries require the `use` permission; - modifying, viewing, or deleting the configuration requires the `manage`, `read`, and `remove` permissions. Clique permissions only control access to the computational resources of the cliques. ## Access permission settings interface { #ui } Access settings are located on the **ACL** (Access Control List) tab in the [interface](https://ytsaurus.tech/user-guide/data-processing/chyt/cliques/ui.md) of the clique. There, you’ll find: - a list of people responsible for the clique (**Responsibles**); - a list of users with different access levels (**Object permissions**); - a list of available permissions: `use`, `manage`, `read`, `remove`; - buttons for assigning permissions (**Request Permissions**) and managing responsibles (**Manage responsibles**). For information on how to manage access permissions, see the [section](https://ytsaurus.tech/user-guide/data-processing/chyt/how-to-guides/acl.md). # Scenarios overview This section contains typical scenarios and step‑by‑step instructions for the main clique management tasks. If you’re just getting started, we recommend beginning with the [Clique lifecycle](https://ytsaurus.tech/docs/cliques/overview.md#lifecycle) section. This will help you understand the steps required to prepare a clique for running SQL queries and what happens at each stage. Select a task and go to the corresponding instruction: #| || **Practical task** | **Article** || || Starting a Clique for the first time | [Creating, starting, and stopping a clique](https://ytsaurus.tech/docs/en/create-start.md) || || Adding compute resources to a Clique | [Adding compute resources](https://ytsaurus.tech/docs/en/manage-resources.md) || || Granting access permissions for a Clique | [Access permissions managing](https://ytsaurus.tech/docs/en/acl.md) || || Getting query logs | [Getting query logs](https://ytsaurus.tech/docs/en/query-logs.md) || || Checking the CHYT or ClickHouse version | [Getting CHYT and ClickHouse versions](https://ytsaurus.tech/docs/en/versions.md) || || Learning about versioning specifics and installing a custom CHYT version | [Installing a custom CHYT version](https://ytsaurus.tech/docs/en/custom-chyt-version.md) || || Changing query settings, ClickHouse config, YT config. Configuring memory allocation and query caching | [Advanced settings configuration](https://ytsaurus.tech/docs/en/manage-advanced-settings.md) || || Jobs are failing, looking for the cause | [Checking failed jobs](https://ytsaurus.tech/docs/en/failed-jobs.md) || |# # Creating, Starting, and Stopping a Clique {% note info "Note" %} The operations and actions described below do not start computations — they only prepare the clique for use. {% endnote %} ## How to create a new clique { #create } You can create a new clique in two ways: {% list tabs %} - Via the web interface 1. In the main menu of YTsaurus, go to the **Cliques** section. 1. In the opened **CHYT cliques** section, click **Create clique** in the upper‑right corner. 1. Fill in the form fields: - _Alias name_ — the clique name; - _Instance count_ — the number of clique instances (from 1 to 100); - _Pool tree_ — pool tree, select a value from the list or keep the default value; - _Pool_ — the name of the compute [pool](https://ytsaurus.tech/scheduler/scheduler-and-pools.md#scheduler) where you want to run the clique operation. {% note info %} This option is optional at the clique creation stage. You must [set it](https://ytsaurus.tech/docs/en/#set-up-options) before starting the clique. {% endnote %} 1. To complete the creation, click **Confirm**. After confirmation, you’ll be forward to the created clique’s interface, and it will become available in the clique list in the **CHYT cliques** section. - Using the CLI 1. Install [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) as part of the `ytsaurus-client` package, if you haven’t done so yet. 1. Save the proxy address to an environment variable. This allows you to avoid specifying the YTsaurus cluster using the `--proxy` argument in every subsequent command. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address for the same reason: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the controller address. For example, for the demo cluster, the address looks like this: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can get the controller address from the `controller` field in the output of the command: ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Also save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Create a clique using a command from [CHYT Controller CLI](https://ytsaurus.tech/docs/controller.md): ```bash yt clickhouse ctl create chyt_example_clique ``` {% endlist %} ## Setting up options for starting { #set-up-options } After creating a clique, you need to configure it — set the required [options](https://ytsaurus.tech/docs/cliques/configs.md#options). The main option for starting a clique is `pool`; you need to pass the name of the compute pool where the clique instances will run. Before setting the `pool` option, make sure you have the `Use` permission for the specified pool. Check your permissions on the **ACL** tab in the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#ui) in the **Object permissions** section. If you’re unsure which pool to use, contact the YTsaurus cluster administrator. You can set the `pool` option via: {% list tabs %} - Web interface 1. Open the clique interface as described in the [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where) section. 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png){width=24 height=24} in the upper‑right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#ui) block, or the button **Edit speclet** on the **Speclet** tab in the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#ui). 1. In the opened settings window, enter the compute pool name in the _Pool_ field. 1. To finish, click **Confirm**. {% note info %} Since clique operations are run under the system robot `robot-strawberry-controller`, grant this robot the `Use` permission for the specified pool in advance. {% endnote %} - CLI 1. Install [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) as part of the `ytsaurus-client` package, if you haven’t done so yet. 1. Save the proxy address to an environment variable. This allows you to avoid specifying the YTsaurus cluster using the `--proxy` argument in every subsequent command. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address for the same reason: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the controller address. For example, for the demo cluster, the address looks like this: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can get the controller address from the `controller` field in the output of the command: ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Also save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Set the option using the command: ```bash yt clickhouse ctl set-option pool chyt_example_pool --alias chyt_example_clique ``` Now the clique is configured and all settings are saved in the [speclet](https://ytsaurus.tech/docs/cliques/configs.md#speclet) in Cypress. {% endlist %} ## How to start a clique { #start } You can start a configured clique via: {% list tabs %} - Web interface 1. Open the clique interface as described in the [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where) section. 1. Click ![start](https://ytsaurus.tech/_images/start-btn.png){width=24 height=24} in the upper‑right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) block. 1. Make sure that: - the `Health` parameter changed to the `Pending` state and then, after some time, to `Good`; - the `State` parameter has the `Active` value. You can view the statuses of these parameters in the clique interface — in the block with [characteristics](https://ytsaurus.tech/docs/cliques/ui.md#ui). 1. Check the clique’s functionality. To do this, make a test query in the **Query Tracker** interface: - click ![sql](https://ytsaurus.tech/_images/sql-btn.png){width=24 height=24} in the upper‑right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) block; - in the opened window, enter and run an SQL query. - CLI 1. Install [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) as part of the `ytsaurus-client` package, if you haven’t done so yet. 1. Save the proxy address to an environment variable. This allows you to avoid specifying the YTsaurus cluster using the `--proxy` argument in every subsequent command. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address for the same reason: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the controller address. For example, for the demo cluster, the address looks like this: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can get the controller address from the `controller` field in the output of the command: ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Also save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Start the clique using the command: ```bash yt clickhouse ctl start chyt_example_clique ``` 1. Check the clique status using the `status` command. When the clique operation is started, the `status` field value should become `Ok`, and `operation_state` should change to `running`: ```bash $ yt clickhouse ctl status chyt_example_clique { "status" = "Waiting for restart: oplet does not have running yt operation"; } # a few moments later $ yt clickhouse ctl status chyt_example_clique { "status" = "Ok"; "operation_state" = "running"; "operation_url" = "https://domain.com//operations/"; } ``` 1. Make sure the clique is working. To do this, run a test query, for example: ```bash yt clickhouse execute --alias chyt_example_clique 'select 42' ``` {% endlist %} {% note warning %} If the сlique has not started within 10 minutes, check the YT operation status: - in the web interface — via the link from the `YT operation id` parameter; - in the CLI — via the `operation_url` link from the output of the `status` command. If you can’t resolve the issue yourself, ask for help in the [YTsaurus chat](https://t.me/ytsaurus_ru) or in your personal support channel. {% endnote %} ## How to stop a clique { #stop } You can stop a clique via: {% list tabs %} - Web interface 1. Open the clique interface as described in the [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where) section. 1. Click ![stop](https://ytsaurus.tech/_images/stop-btn.png){width=24 height=24} in the upper‑right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) block. 1. Make sure that: - the `Health` parameter changed to the `Pending` state and then, after some time, to `Failed`; - the `State` parameter has the `Inactive` value. You can view the statuses of these parameters in the clique interface — in the block with [characteristics](https://ytsaurus.tech/docs/cliques/ui.md#ui). - CLI 1. Install [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) as part of the `ytsaurus-client` package, if you haven’t done so yet. 1. Save the proxy address to an environment variable. This allows you to avoid specifying the YTsaurus cluster using the `--proxy` argument in every subsequent command. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address for the same reason: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the controller address. For example, for the demo cluster, the address looks like this: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can get the controller address from the `controller` field in the output of the command: ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Also save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Stop the clique using the command: ```bash yt clickhouse ctl stop chyt_example_clique ``` {% endlist %} # Adding compute resources Sometimes, when working with cliques, you may experience performance issues due to full utilization of available resources and, as a result, query degradation. In such cases, you need to increase the amount of compute resources. Compute resources are ClickHouse instances: servers with dedicated CPUs and RAM. Most of the time, users only need to adjust the number of instances. {% note warning "Important" %} To configure a clique, you must have the `manage` permission for that clique. Check whether you have this permission type on the **ACL** tab in the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs) under **Object permissions**. {% endnote %} You can add compute resources to a clique in two ways: {% list tabs %} - Through the web interface 1. Open the clique interface as described in the [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where) section. 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png){width=24 height=24} in the upper-right corner of the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section or the button **Edit speclet** on the **Speclet** tab in the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. Select the **Resources** tab on the left. 1. In the _Instances_ field, enter the number of instances for the clique (from 1 to 100). {% note info %} The default field value is `1`. There is no universal recommendation for choosing the number of instances: the parameter depends on your tasks and is determined experimentally. We recommend starting with the default value. {% endnote %} 1. To apply the changes, click **Confirm**. - Using the CLI 1. Install the [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) as part of the `ytsaurus-client` package, if you haven’t done so already. 1. Save the proxy address to an environment variable. This allows you to avoid specifying the YTsaurus cluster using the `--proxy` argument in every subsequent command. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address: ```bash export CHYT_CTL_ADDRESS=
``` where `
` is the controller address. For example, for a demo cluster, the address looks like this: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can get the controller address from the `controller` field in the output of the following command: ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Set the required number of instances using the `instance_count` option: ```bash yt clickhouse ctl set-option instance_count COUNT --alias chyt_example_clique ``` where `COUNT` is the number of instances. {% endlist %} ## Advanced compute resource settings { #advanced } {% note warning "Important" %} These options are intended for advanced users. If you’re unsure whether you need them, keep the default values. {% endnote %} Advanced settings include CPU and RAM parameters for each instance. When choosing CPU and RAM values, consider the physical resources of your servers and the requirements of your clique workloads. Recommended values: - 1–100 CPU cores per instance; - 20–300 GB of RAM per instance. {% note info %} Choose option values so that they don’t exceed the physical resources of the servers. For example, on a server with 10 CPU cores and 40 GB of memory, you can’t allocate an instance with 16 cores and 65 GB. {% endnote %} You can set the `Instance CPU` and `Instance Total Memory` options via: {% list tabs %} - Web interface 1. Open the clique interface as described in the [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where) section. 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png){width=24 height=24} in the upper-right corner of the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section or the button **Edit speclet** on the **Speclet** tab in the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. Select the **Resources** tab on the left. 1. In the _Instance CPU_ field, specify the number of CPU cores per instance. 1. In the _Instance Total Memory_ field, specify the amount of RAM per instance. 1. To save the settings, click **Confirm**. - CLI 1. Install the [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) as part of the `ytsaurus-client` package, if you haven’t done so already. 1. Save the proxy address to an environment variable. This allows you to avoid specifying the YTsaurus cluster using the `--proxy` argument in every subsequent command. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address: ```bash export CHYT_CTL_ADDRESS=
``` where `
` is the controller address. For example, for a demo cluster, the address looks like this: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can get the controller address from the `controller` field in the output of the following command: ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Set the amount of CPU to be allocated for each clique instance using the `instance_cpu` option: ```bash yt clickhouse ctl set-option instance_cpu CPU_AMOUNT --alias chyt_example_clique ``` where `CPU_AMOUNT` is the number of CPU cores. 1. Set the amount of RAM to be allocated for each clique instance using the `instance_total_memory` option: ```bash yt clickhouse ctl set-option instance_total_memory RAM_AMOUNT --alias chyt_example_clique ``` where `RAM_AMOUNT` is the amount of RAM in GB. {% endlist %} {% note alert "Important" %} Do not set the following options simultaneously: - RAM size on the **Resources** tab in the _Instance Total Memory_ field. - Memory allocation configuration on the **Advanced** tab in the _Instance memory_ field. If you set both options at once, the controller cannot determine which one to apply. The clique will not start, and the system will output an error: ``` Failed to start chyt: instance_memory and instance_total_memory cannot be specified simultaneously. ``` {% endnote %} Note that with the **Restart on speclet change** option enabled, any setting changes will trigger an automatic clique restart, and the restart will take some time. You can enable the **Restart on speclet change** option on the **General** tab in the speclet interface dialog box. # Access permissions management Basic information about access permissions and their types is described in the [Access permissions](https://ytsaurus.tech/docs/cliques/access.md) section. ## How to grant permissions to clique resources { #instruction } 1. Open the clique interface as described in the [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where) section. 1. On the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs), select the **ACL** tab. 1. Click **Request Permissions** (available to the clique owner or administrator). 1. In the form that opens, in the *Permissions* section, select the required options: `use`, `manage`, `read` and `remove`. 1. In the *Subjects* section, specify the users or group to whom you want to grant the permissions. # Getting Query Logs To analyze query execution, identify performance issues, or track failed jobs, you can review the clique's query logs. {% note warning %} Query Logs functionality is available in CHYT starting from version `2.16`. {% endnote %} Logs of all queries are stored in tables. They are located in [Cypress](https://ytsaurus.tech/overview/about.md#cypress) at the following path: `//sys/strawberry/chyt//artifacts/system_log_tables/query_log`. The `query_log` folder contains: - one or more tables named from `0` to `n`; - a link to the latest table `latest` — for example, `//sys/strawberry/chyt//artifacts/system_log_tables/query_log/n`. New tables in `query_log` appear when the schema changes — for example, when new fields are added or data types are modified. This can happen when you upgrade or downgrade the CHYT version. For example, if the CHYT version in the clique is upgraded from 2.16 to 2.17: - a new table is automatically created with the next sequential number (if there was table `0`, table `1` will appear); - the `latest` link is automatically switched to the latest table version (in this case, to table `1`). If you then downgrade the CHYT version back to 2.16, the system will add a new table numbered `2` and start writing logs to it. ## How to view query logs { #instruction } 1. Open the [web interface](https://ytsaurus.tech/docs/cliques/ui.md#ui) of the clique. 1. In the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs), select the **Query Logs** tab. 1. Switch to the tab that opens in your browser. It will display the **Strawberry operation** interface with the current log table version. 1. Analyze queries using standard SQL queries against this table. ## Analyzing the log table { #analysis } To properly search for and analyze information in the log table, it’s important to understand that queries in CHYT are processed in a distributed manner: they are split into phases (primary and secondary queries). Query logs tables contain information about all queries — both primary and secondary — so during analysis you need to be able to tell them apart. A **primary** user query arrives at the clique instance, which is called the **coordinator**. The coordinator performs the initial query preparation, after which subqueries — **secondary** queries — are generated and sent from the coordinator to the other instances. ### How to find the required queries in Query logs { #filter-queries } To find information about primary queries only, use the following filter: ```sql WHERE query_id = -- where is the ID of the query you’re interested in ``` Secondary queries have their own unique `query_id` values, so they won’t be included in the result set. Also, in a primary query, the flag column `is_initial_query` will be equal to 1. To find secondary queries associated with a primary one, use: ```sql WHERE initial_query_id = -- filters both primary and secondary queries AND NOT is_initial_query -- excludes primary queries, leaving only secondary ones ``` ### Query parameters in the Query logs table { #params } The columns in the log table partially match the columns in ClickHouse log tables and include additional CHYT query parameters. {% note info %} To decode ClickHouse log names, refer to the [ClickHouse documentation](https://clickhouse.com/docs/ru/operations/system-tables/query_log). {% endnote %} CHYT parameters are marked in the Query logs table with the `chyt_` prefix. #| || **Option** | **Data type** | **Description** || || `chyt_instance_cookie` | `Optional[Int64]` | The instance ID that the log row belongs to. Matches the `job_cookie` of the job within which the instance is running. Contains a value in the range `[0, n]`, where `n` is the clique size || || `chyt_instance_id` | `Optional[String]` | Instance ID; contains the `job_id` of the job, which is unique within an operation. For example, after restarting any job, the new one may have the same cookie but a new `id` || || `chyt_instance_fqdn` | `Optional[String]` | Contains the fully qualified domain name (FQDN) of the job/instance || || `chyt_version` | `Optional[String]` | The CHYT version used to record this log line || || `chyt_secondary_query_ids` | `Optional[List[String]]` | Relevant only for `type = 'QueryFinish'` and `is_initial_query`, i.e., for the final log line of a primary query. Contains a list of all `id`s of secondary queries launched within the primary query || || `chyt_query_runtime_variables` | `Optional[Yson]` | Contains YSON with `key‑value` pairs for simple query execution analysis. For example, it’s used to determine whether optimization X occurred. Relevant only for `type = 'QueryFinish'`. {% cut "YSON example" %} ```yson { "try_optimize_distinct_read" = %false; "use_input_specs_pulling" = %false; "use_min_max_optimization" = %false; "use_read_range_inferring" = %false; } ``` {% endcut %} || || `chyt_query_statistics` | `Optional[Yson]` | Contains YSON with quantitative measurements of various query metrics. Relevant only for `type = 'QueryFinish'` || |# #### The `chyt_query_statistics` parameter This parameter contains a large YSON and has a different structure for primary and secondary queries within a single CHYT query. The most important options are described below. {% cut "Main options for primary queries" %} - `input_fetcher` — contains quantitative information about the data that the coordinator received from the master and will distribute to the other clique instances for further execution. These are not the data themselves, but **descriptors** that help understand where and how much data needs to be read. This key may contain two additional ones: - `filtered_data_slices` — contains information about how much data the coordinator filtered out, i.e. removed from the selection. This happens according to the query filter; - `data_slices` — the opposite value, i.e. how much data the coordinator kept for reading. ```yson { "data_slices" = { "data_weight" = { "count" = 1; "last" = 936245229; "max" = 936245229; "min" = 936245229; "sum" = 936245229; }; "row_count" = { "count" = 1; "last" = 9694599; "max" = 9694599; "min" = 9694599; "sum" = 9694599; }; }; "filtered_data_slices" = { "data_weight" = { "count" = 2; "last" = 243363; "max" = 20320269; "min" = 243363; "sum" = 20563632; }; "row_count" = { "count" = 2; "last" = 2563; "max" = 88233; "min" = 2563; "sum" = 90796; }; }; } ``` Each key contains a `row_count` counter that shows how many measurements were taken — `count` — and their aggregates. - `phase_duration_us` — information about the execution duration of various query phases: - `Start` — the period from when the instance receives the query over the network until it actually starts executing; - `Preparation` — the phase when the coordinator collects metadata about the query and composes secondary queries; - `Execution` — the phase that starts when the coordinator sends secondary queries to the instances and ends when the coordinator sends the entire result to the user. It includes the time the coordinator spends receiving responses from secondary queries and processing them locally as needed. ```yson { "Execution" = { "count" = 1; "last" = 13367; "max" = 13367; "min" = 13367; "sum" = 13367; }; "Preparation" = { "count" = 1; "last" = 112808; "max" = 112808; "min" = 112808; "sum" = 112808; }; "Start" = { "count" = 1; "last" = 318883; "max" = 318883; "min" = 318883; "sum" = 318883; }; } ``` {% endcut %} {% cut "Main options for secondary queries" %} - `phase_duration_us` — contains a breakdown by phases. It differs from `phase_duration_us` in the primary query only in the semantics of the preparation phase: during this phase, the secondary instance only parses the input from the coordinator and prepares for execution; - `granule_min_max_filter` — when reading data in secondary queries, there is a step for skipping chunks using `min`/`max` statistics. This option contains information about how many chunks were reviewed and how many were skipped; ```yson { "can_skip" = { "count" = 92; "last" = 1; "max" = 1; "min" = 0; "sum" = 42; }; } ``` - `secondary_query_source` — an option with information about reading data in secondary queries. It contains the following child options: - `block_bytes`, `block_rows`, `block_columns` — aggregates for the read data that eventually go to the ClickHouse engine, in different metrics; - `steps` — CHYT can read and convert different columns in stages. This logic is similar to the `PREWHERE` predicate in ClickHouse, where filtering columns are read first, and then, depending on their content, the rest. In this section, you can see how many steps were executed and how much data passed through them; - `columnar_conversion_cpu_time_us`, `columnar_conversion_wall_time_us` — CHYT has to convert the read data from the YTsaurus format to the ClickHouse format. Conversion sometimes takes a significant amount of time, and these options show exactly how much; - `chunk_reader` — the main section with information about reading data from YTsaurus. The main sub‑options are `data_bytes_read_from_cache` and `data_bytes_read_from_disk`, which show how much data was taken from the cache and how much was read from disks; - `wait_ready_event_time_us` — this option is semantically related to `chunk_reader` and shows how long it took to wait for the next data portion to start converting it. ```yson { "block_bytes" = { "count" = 3661; "max" = 4080068; "min" = 0; "sum" = 7888434964; }; "block_columns" = { "count" = 3661; "max" = 8; "min" = 0; "sum" = 29272; }; "block_rows" = { "count" = 3661; "max" = 10000; "min" = 0; "sum" = 31148625; }; "chunk_reader" = { "data_bytes_read_from_cache" = { "count" = 2; "max" = 297136447; "min" = 149780474; "sum" = 446916921; }; "data_bytes_read_from_disk" = { "count" = 2; "max" = 0; "min" = 0; "sum" = 0; }; "data_bytes_transmitted" = { "count" = 2; "max" = 0; "min" = 0; "sum" = 0; }; "data_io_requests" = { "count" = 2; "max" = 0; "min" = 0; "sum" = 0; }; "meta_bytes_read_from_disk" = { "count" = 2; "max" = 0; "min" = 0; "sum" = 0; }; }; "columnar_conversion_cpu_time_us" = { "count" = 3659; "max" = 14601; "min" = 36; "sum" = 4846058; }; "conversion_sync_wait_time_us" = { "count" = 3659; "max" = 19267; "min" = 106; "sum" = 6489348; }; "idle_time_us" = { "count" = 3663; "max" = 4150; "min" = 8; "sum" = 145923; }; "processed_reader_count" = { "count" = 2; "max" = 2; "min" = 1; "sum" = 3; }; "read_count" = { "count" = 2; "max" = 2441; "min" = 1220; "sum" = 3661; }; "read_impl_us" = { "count" = 3661; "max" = 34904; "min" = 122; "sum" = 6904768; }; "step_count" = { "count" = 2; "max" = 1; "min" = 1; "sum" = 2; }; "steps" = { "0" = { "block_bytes" = { "count" = 3659; "max" = 4080068; "min" = 6268; "sum" = 7888434964; }; "block_columns" = { "count" = 3659; "max" = 8; "min" = 8; "sum" = 29272; }; "block_rows" = { "count" = 3659; "max" = 10000; "min" = 30; "sum" = 31148625; }; }; }; "total_rows" = { "count" = 3661; "max" = 10000; "min" = 0; "sum" = 31148625; }; "wait_ready_event_time_us" = { "count" = 36; "max" = 32988; "min" = 2; "sum" = 144348; }; } ``` {% endcut %} # Getting CHYT and ClickHouse versions Component versions in YTsaurus are important parameters for stable and consistent operation of the entire system. Version incompatibilities can cause errors during operations. Therefore, identifying the current component versions helps you quickly find the source of the problem. You can get the versions via: {% list tabs %} - Web interface 1. Go to the [web interface](https://ytsaurus.tech/docs/cliques/ui.md#where) of the clique. 1. In the clique characteristics block, find the **YT operation Id** parameter and click the link provided in it. 1. In the opened Vanilla operation interface, on the left, in the *Tabs panel*, select the **Details** tab. 1. In the **Description** section, find the **Artifacts** block. 1. To see the full list of parameters, click the **Show more**. 1. In the list of parameters, find the `ytserver-clickhouse` field. It contains: - `ch_version` — ClickHouse version; - `version` — CHYT clique version. - Query to the clique 1. Go to the Query Tracker web interface: click ![sql](https://ytsaurus.tech/_images/sql-btn.png){width=24 height=24} in the upper-right corner of the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) block. 1. Make sure that the CHYT engine is selected in the panel at the top. 1. In the opened window, enter and run the SQL query: ```sql SELECT chytVersion(), version(); ``` {% endlist %} {% note tip "Tip" %} If you contact technical support about issues with the clique, check the component versions in advance and provide them to the support specialist. This will speed up diagnostics and help find a solution faster. {% endnote %} # Installing a custom CHYT version By default, the latest stable CHYT version is automatically installed when you start a clique. However, you can manually pin a different required version — this lets you control updates and avoid compatibility and stability issues. In this section, you’ll find information on when it’s important to specify a version manually, where to find the list of available versions, and how to specify a custom version. ## When to specify a version manually { #manual } Sometimes, automatic installation of the latest stable version isn’t suitable, so you need to select a CHYT version yourself. Below are possible reasons for such situations: 1. Compatibility. Some integrations or services work only with a specific CHYT version. If you don’t pin the version, they might not work correctly after an update (for example, if the JSON processing format changes). 1. Update policies. In organizations, software is often updated only after review (audit) and during pre‑agreed periods (outside peak loads). 1. Testing. You can create a separate clique with a new CHYT version to test functionality without risking production workflows, or compare results with the old version without affecting service operations. ## CHYT version format { #schema } A CHYT version has the following format: `ytserver-clickhouse-2.17.16028654~22a615876d+sb`, where: - `2.17` — the base version; - `16028654~22a615876d+sb` — the update identifier. ## Where to find available CHYT versions { #versions } All available CHYT versions are stored in binary files in [Cypress](https://ytsaurus.tech/overview/about.md#cypress) at the path `//sys/bin/ytserver-clickhouse`. In addition to binary files with CHYT builds, the directory contains symlinks (files that store references to binary files) for the *latest stable* and *latest base* versions. ## Ways to specify CHYT versions { #types } The update behaviour depends on the option you choose. There are three approaches: 1. Strict version specification — for example, `ytserver-clickhouse-2.17.16028654~22a615876d+sb` — completely disables updates. This is useful when you need absolute stability and predictable system behaviour. 1. Loose version specification — for example, just the base part `ytserver-clickhouse-2.17` — means the system will update the version within the `2.17` range. This is suitable if minor fixes are acceptable but major changes should be avoided. 1. If no version is specified, the system installs the latest stable CHYT version when you start the clique. This approach is convenient for test environments or systems where stability isn’t critical. {% note warning %} Updating to a higher stable version only happens when you restart the clique. This applies to both cliques without a specified version and those with a base version specified. Example: A new clique is started without specifying an exact version. The system automatically installs the latest stable CHYT version `2.17.some_id`. The clique runs continuously: when a new update `2.17.some_new_id` or a new base CHYT version `2.18.yet_another_id` is released, no automatic update occurs. Only after restarting the clique will the version automatically update to the new stable version `2.17.some_new_id` or `2.18.yet_another_id`. {% endnote %} ## How to install a custom version { #instruction } 1. Go to Cypress at the path `//sys/bin/ytserver-clickhouse`. 1. In the list of available versions, select the required CHYT version or symlink. 1. Copy the version name (for example, `ytserver-clickhouse-2.17`). 1. Open the clique interface as described in the section [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where). 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png){width=24 height=24} **Edit speclet** in the upper-right corner of the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) block, or the button **Edit speclet** on the **Speclet** tab in the [Tabs panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. On the left, select the **Advanced** section. 1. In the *Chyt version* field, paste the copied version name (for example, `ytserver-clickhouse-2.17`). Useful links: [Clique web interface in YTsaurus](https://ytsaurus.tech/docs/cliques/ui.md) [Getting CHYT and ClickHouse versions](https://ytsaurus.tech/docs/en/versions.md) # Advanced settings configuration This section describes how to work with advanced clique settings. In the web interface, they are located under *Advanced settings*. The full list of advanced settings is provided in [Advanced settings](https://ytsaurus.tech/docs/cliques/configs.md#options). {% note warning %} The settings described in this section are intended for advanced users only. If you are unsure whether you need them, we recommend keeping the default values. {% endnote %} Advanced settings include: - configuring queries within the clique — the [Query settings](https://ytsaurus.tech/docs/en/#query-settings) section; - configuring server settings — the [ClickHouse config](https://ytsaurus.tech/docs/en/#ch-config) section; - configuring the YT components of instances — the [YT config](https://ytsaurus.tech/docs/en/#yt-config) section; - allocating memory — the [Instance memory](https://ytsaurus.tech/docs/en/#memory) section; - caching queries — the [Query cache](https://ytsaurus.tech/docs/en/#query-cache) section. ## Query settings { #query-settings } The parameters in the *Query settings* section are a subset of the [ClickHouse session settings](https://clickhouse.com/docs/ru/operations/settings/settings) that will be applied to all queries within the clique. To change query behavior, override the default parameter values using: {% list tabs %} - Web interface 1. In the [ClickHouse documentation](https://clickhouse.com/docs/ru/operations/settings/settings), find the required settings and copy their names. 1. Open the clique interface as described in [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where). 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png%20=24x24) in the upper-right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section, or click **Edit speclet** on the **Speclet** tab of the [Tab panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. Select **Advanced** on the left. 1. Find the **Query settings** section. 1. In the *Use JSON syntax* field, enter the parameters and their values using JSON syntax as `key: value` pairs enclosed in curly braces `{ }`. For example: ```json { "max_execution_time": 200000, "max_insert_threads": 32, "max_threads": 32, "parallel_distributed_insert_select": 2 } ``` 1. To apply the changes, click **Confirm**. - CLI 1. Install the [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) included in the `ytsaurus-client` package if you have not already done so. 1. Save the proxy address to an environment variable. This avoids having to specify the YTsaurus cluster in every command using the `--proxy` argument. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the Controller address. For example, the address for a demo cluster has the following format: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can obtain the Controller address from the `controller` field in the command output ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Find the required settings in the [ClickHouse documentation](https://clickhouse.com/docs/ru/operations/settings/settings) and copy their names. 1. Set the required parameters using the `query_settings` option. For example: ```bash yt clickhouse ctl set-option query_settings "{\"max_execution_time\": 200000,\"max_insert_threads\": 32,\"max_threads\": 32,\"parallel_distributed_insert_select\": 2}" ``` {% endlist %} ## ClickHouse config { #ch-config } The *Clickhouse config* setting is used to manage the configuration of the ClickHouse component. Set it to match the standard ClickHouse XML configuration. The rules for converting an ClickHouse XML configuration to a CHYT YSON configuration can be described as follows: - Any configuration node that is not semantically repeatable in the ClickHouse configuration is a map (`map`). - A repeatable node is represented as a list (`list`). {% note info "Example of converting a sample XML configuration to a YSON configuration" %} #| || **XML** | **YSON** || || ```xml 42 qwe 3.14 hi! ``` | ```yson { foo = 42; bar = "qwe"; baz = [ {quux = 3.14}; {}; "hi!"; ]; } ``` || |# {% endnote %} ### ClickHouse configuration options that may be useful in CHYT { #ch-options } The main option is `dictionaries`, which configures external dictionaries. Its value must be a list of dictionary configurations. Each dictionary is configured using a map with the following fields, which retain the semantics of the original ClickHouse configuration: - `name` — external dictionary name; - `source` — [data source](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-sources/) for the external dictionary; - `layout` — [layout](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-layout/) of the external dictionary in the instance memory; - `structure` — [schema of the data](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-structure/) stored in the dictionary; - `lifetime` — dictionary [lifetime](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-lifetime/). Other options are available in the ClickHouse configuration in the [repository](https://github.com/ytsaurus/ytsaurus/blob/main/yt/chyt/server/clickhouse_config.h). {% note info %} The ClickHouse configuration also has a `settings` option containing the settings described in the [**Query settings**](https://ytsaurus.tech/docs/en/#query-settings) section. Thus, query settings can also be defined within the ClickHouse configuration. {% endnote %} ### How to modify the ClickHouse configuration { #ch-instruction } {% list tabs %} - Web interface 1. Open the clique UI as described in [How to access the clique UI](https://ytsaurus.tech/docs/cliques/ui.md#where). 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png%20=24x24) in the upper-right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section, or click **Edit speclet** on the **Speclet** tab in the [Tab panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. Select **Advanced** on the left. 1. Find the **Clickhouse config** section. 1. In the *Use JSON syntax* field, enter the parameters and their values as a JSON configuration. Example configuration of a simple dictionary and [**Query settings**](https://ytsaurus.tech/docs/en/#query-settings) in the `settings` section: ```json { "settings": { "max_execution_time": 30 }, "dictionaries": [ { "name": "dict", "layout": {"flat": {}}, "structure": { "id": {"name": "key"}, "attribute": [ {"name": "value_str", "type": "String", "null_value": "n/a"}, {"name": "value_i64", "type": "Int64", "null_value": 42} ] }, "lifetime": 0, "source": {"yt": {"path": "//home/user/table"}} } ] } ``` 1. To apply the changes, click **Confirm**. - CLI 1. Install the [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) included in the `ytsaurus-client` package if you have not already done so. 1. Save the proxy address to an environment variable. This prevents you from having to specify the YTsaurus cluster in every command using the `--proxy` argument. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the controller address. For example, the demo cluster address has the following format: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can obtain the controller address from the `controller` field in the command output ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Find the required parameters in the list of [ClickHouse settings](https://clickhouse.com/docs/ru/operations/settings/settings) and copy their names. 1. Set the required parameters using the `clickhouse_config` option. For example: ```bash yt clickhouse ctl set-option clickhouse_config "{ ... some JSON params}" ``` {% endlist %} ## YT config { #yt-config } The YT part of the instance configuration is specified using the `yt_config` option. This option lets you specify advanced settings that are not available as separate speclet options. For a list of available YT configuration parameters, see [Instance configuration](https://ytsaurus.tech/docs/reference/configuration.md#yt_config). ### How to modify the YT configuration { #yt-instruction } {% list tabs %} - Web interface 1. Open the clique interface as described in [How to access the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where). 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png%20=24x24) in the upper-right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section, or click **Edit speclet** on the **Speclet** tab in the [Tab panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. Select **Advanced** on the left. 1. Find the **YT config** section. 1. In the *Use JSON syntax* field, enter the parameters and their values as a JSON configuration, for example: ```json { "subquery": { "max_data_weight_per_subquery": 12942417591810 } } ``` 1. To apply the changes, click **Confirm**. - CLI 1. Install the [CHYT CLI](https://ytsaurus.tech/docs/cli-and-api.md) included in the `ytsaurus-client` package if you have not already done so. 1. Save the proxy address to an environment variable. This eliminates the need to specify the YTsaurus cluster in every command using the `--proxy` argument. ```bash export YT_PROXY= ``` 1. Set an environment variable with the controller address: ```bash export CHYT_CTL_ADDRESS=
``` , where `
` is the controller address. For example, the demo cluster address has the following format: `https://strawberry-XXXXXXXX.demo.ytsaurus.tech`. You can obtain the controller address from the `controller` field in the command output ```bash yt get //sys/strawberry/chyt//@strawberry_info_state ``` 1. Save the cluster name to an environment variable: ```bash export CLUSTER_NAME= ``` , where `` is the cluster name. For example, the demo cluster name is `ytdemo`. 1. Set the required parameters using the `yt_config` option. For example: ```bash yt clickhouse ctl set-option yt_config "{\"subquery\":{\"max_data_weight_per_subquery\": 12942417591810}}" ``` {% endlist %} ## Instance memory { #memory } To fine-tune memory allocation, it is helpful to understand the memory allocation model in a CHYT instance, which parameters control each memory allocation, and how to change them. {% note info %} In the context of memory management: - A watermark is an amount of memory measured downward from the upper bound of the allocated memory limit. When memory usage enters this range, the CHYT instance initiates termination according to one of the predefined scenarios. - A window is a time interval (15 minutes by default) during which the *average* amount by which memory usage exceeds the lower watermark boundary is calculated. This helps exclude random memory usage spikes. - RSS (Resident Set Size) is the actual amount of RAM used by a process. - OOM (out of memory) means memory exhaustion. {% endnote %} ### Memory allocation in a CHYT instance { #memory-allocation } The diagram shows how memory is allocated within a CHYT instance: which memory allocations make up the overall limit and which thresholds determine system behavior when memory is low. ![memory](https://ytsaurus.tech/_images/chyt_memory.png) Each parameter in the diagram is described below, including the memory allocations that make up the overall instance limit and the thresholds: - `MaxServerMemoryUsage` is the overall memory usage limit for a CHYT instance. It includes: - `Reader` is the amount of memory allocated to reader caches for prefetching and faster data reads. - `Caches` is the designation in the diagram for the amount of memory allocated to caches, including: - `CompressedBlockCache` is the cache for *compressed* data blocks. It can hold large amounts of data because the data is stored in compressed form, which is the default behavior, but decompression takes additional time. It is used when rereading *large* data blocks. - `UncompressedBlockCache` is the cache for *uncompressed* data blocks. It is useful when many sequential queries access the same data because it saves resources on both reading and decompressing the data. - `ChunkMetaCache` is the chunk metadata cache. Chunk metadata is read before any read operation. This cache is useful when rereading data because it avoids repeatedly retrieving the same metadata. - `CH Memory` is a dedicated amount of memory reserved for the internal needs of ClickHouse; - `Footprint` is a dedicated memory reserve that allows the process to exceed the thresholds without causing the total memory usage to exceed critical values; - `MemoryLimit` is a conditional hard limit on the RSS (memory usage) of a CHYT instance, used for the `WatchdogOomWatermark` and `WatchdogOomWindowWatermark` categories; - `WatchdogOomWatermark` is an additional amount of memory used to check whether an OOM condition has occurred: the sum of the current RSS value and `WatchdogOomWatermark` is compared against the specified `MemoryLimit`; - `WatchdogOomWindowWatermark` is an additional amount of memory used to check whether an OOM condition has occurred. The check is performed using the following algorithm: - The average RSS value is calculated over the `Window` time window (15 minutes by default); - `WatchdogOomWindowWatermark` is added to the calculated value; - The sum is compared against the specified `MemoryLimit`. - `ClickHouseWatermark` is an additional amount of memory used to separate `MaxServerMemoryUsage` from the overall `MemoryLimit`. ### Critical RSS (memory usage) ranges { #critical-limits } On the RSS scale in the memory allocation diagram, the critical value ranges at which the system terminates the process are numbered. ![memory](https://ytsaurus.tech/_images/chyt_memory_rss.png) If the process's physical RAM usage reaches the following range: - `1` (the average RSS value over more than 15 minutes exceeds the lower bound of the `WatchdogOomWindowWatermark` range) — the instance performs a graceful shutdown: it stops accepting new queries, waits for active queries to complete, and shuts down; - `2` (the average RSS value exceeds the lower bound of the `ClickHouseWatermark` threshold) — ClickHouse stops allocating memory for any operations, and the system reports insufficient memory; - `3` (RSS exceeds the lower bound of the `WatchdogOomWatermark` range) — the instance terminates immediately, and active queries fail; - `4` (RSS exceeds `MemoryLimit`) — YTsaurus deletes the instance. ### How to configure memory thresholds { #memory-instruction } We recommend using the web interface: 1. Open the clique interface as described in [How to access the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where). 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png%20=24x24) in the upper-right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section, or click **Edit speclet** on the **Speclet** tab of the [Tab bar](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. Select **Advanced** on the left. 1. Find the **Instance memory** section. 1. In the *Use JSON syntax* field, enter the parameters and their values as a JSON configuration. You can use the following configuration example as a template: ```json { "clickhouse": 10500000000, "chunk_meta_cache": 100000000, "compressed_cache": 4000000000, "uncompressed_cache": null, "reader": 500000000, "clickhouse_watermark": 10, "watchdog_oom_watermark": 0, "watchdog_oom_window_watermark": 0, "footprint": 2000000000 } ``` 1. To apply the changes, click **Confirm**. {% note alert "Important" %} Do not set the following options simultaneously: - RAM size on the **Resources** tab in the _Instance Total Memory_ field. - Memory allocation configuration on the **Advanced** tab in the _Instance memory_ field. If you set both options at once, the controller cannot determine which one to apply. The clique will not start, and the system will output an error: ``` Failed to start chyt: instance_memory and instance_total_memory cannot be specified simultaneously. ``` {% endnote %} ## Query cache { #query-cache } Use the following settings to cache queries: - **Enable sticky query distribution** — enables query distribution across instances based on query hashes. - **Query sticky group size** — specifies the size of the group of instances selected deterministically based on the query hash. A coordinator will be selected from these instances to execute the query. This setting works only when **Enable sticky query distribution** is enabled. To enable caching, use the web interface: 1. Open the clique interface as described in [How to open the clique interface](https://ytsaurus.tech/docs/cliques/ui.md#where). 1. Click ![edit speclet](https://ytsaurus.tech/_images/edit-btn.png%20=24x24) in the upper-right corner, in the [Action buttons](https://ytsaurus.tech/docs/cliques/ui.md#action-menu) section, or click **Edit speclet** on the **Speclet** tab in the [Tab panel](https://ytsaurus.tech/docs/cliques/ui.md#tabs). 1. On the left side of the dialog, select **Advanced**. 1. Enable **Enable sticky query distribution**. 1. Set **Query sticky group size** to `1`. 1. Add the following parameters under **Query settings**: ```json { "query_cache_ttl": 1800, "use_query_cache": true } ``` 1. Under **Clickhouse config**, add the `query_cache` field. Set the parameter values according to the clique's requirements: ```json { "query_cache": { "max_entries": 20000, "max_entry_size_in_bytes": 524288, "max_entry_size_in_rows": 100000, "max_size_in_bytes": 104585760000, "min_query_duration": 500 } } ``` These parameters correspond to the [query cache settings in ClickHouse](https://clickhouse.com/docs/operations/query-cache). 1. To apply the changes, click **Confirm**. # Checking failed jobs When working with cliques in YTsaurus, failures sometimes occur — for example, queries complete with errors or are interrupted. These failures cause jobs to fail within a YT operation. To quickly identify and resolve the issue, you need to be able to analyze the logs of failed jobs. {% note warning %} If the number of failed jobs in an operation exceeds 100, logging will stop — the logs won’t be saved. Therefore, it’s important to respond to failures promptly to avoid losing information. {% endnote %} ## How to view and analyze logs { #instruction } ![Failed Jobs](https://ytsaurus.tech/_images/failed-jobs.png){ .center } 1. Open the YT operation web interface (1), as described in the section [How to open the YT operation web interface](https://ytsaurus.tech/docs/cliques/yt-operation-ui.md#get-to-section). 1. On the right, in the **Failed** block (2), check the number of failed jobs. 1. Go to the **Jobs** tab (3) in the tab bar. 1. In the job list that opens, use the `State`: `Failed` filter (4) to find the failed jobs. 1. In the row with the failed job, find the **Error / Debug** column. 1. To download the job log, click ![download](https://ytsaurus.tech/_images/download-log.png){width=24 height=24} in the **Stderr** field (5) . 1. Review the log and pay attention to the following: - error messages (usually contain the keywords *Error*, *Exception*, *Failed*); - timestamps (to understand exactly when the failure occurred); - mentions of resources (for example, tables or files) that the job was working with. {% note info %} Save the logs of failed jobs — they will be useful for: - monitoring system stability; - analyzing recurring errors; - providing data to support teams. {% endnote %} # CLI and Python API You can use the CHYT CLI and the CHYT Python API to run your clique or send a query to CHYT from a program or the command line. They can be obtained as part of the `ytsaurus-client`. The command line utility accepts two environment variables: `YT_PROXY` and `CHYT_ALIAS`. You can use the former to specify the YTsaurus cluster, and the latter to specify the used clique. For example, you can use the following command for Linux and macOS to set environment variables and no longer pass the `--proxy ` and `--alias ch_public` parameters to all subsequent calls: ```bash export YT_PROXY= CHYT_ALIAS=ch_public ``` {% note info "Note" %} To ensure compatibility with older versions, a clique alias can be specified with an asterisk at the beginning. In this case, it is ignored. {% endnote %} The process that launches the clique can sometimes be informally called a *launcher*. ## ytsaurus-client { #ytsaurus-client } The main way to get started with YTsaurus is to install the `ytsaurus-client` package. For more information, see [Python Wrapper](https://ytsaurus.tech/api/python/start.md). The package includes `yt`. In order to use the CHYT features, you can run any command as `yt clickhouse [command]`. For example, you can make a test query to the public clique `ch_public` as follows: ```bash yt clickhouse execute "select 'Hello world'" --alias ch_public ``` The package additionally contains the CHYT Python API available in the `yt.clickhouse` module: ```bash python3 Python 3.7.3 (default, Oct 7 2019, 12:56:13) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import yt.clickhouse as chyt >>> import yt.wrapper as yt >>> client = yt.YtClient("") >>> list(chyt.execute("select * from `//home/user/sample_table`", alias="ch_public", client=client)) [{'a': 100}, {'a': 101}, {'a': 102}, {'a': 205}, {'a': 206}, {'a': 1100}] ``` The library is available for Python 3. For more detailed specification of the Python API and CLI features, see [Executing queries](https://ytsaurus.tech/docs/en/reference/execute.md). # CHYT reference This section contains CHYT query execution parameters and configuration reference. - [Executing queries](https://ytsaurus.tech/docs/en/execute.md) - [Query settings](https://ytsaurus.tech/docs/en/settings.md) - [Instance configuration](https://ytsaurus.tech/docs/en/configuration.md) - [Data types](https://ytsaurus.tech/docs/en/data-types.md) - [Functions](https://ytsaurus.tech/docs/en/functions.md) # Executing queries You can use this command to execute a random query in the clique. Also see the [How to try](https://ytsaurus.tech/docs/try-chyt.md) article which lists alternative ways to query a CHYT clique. ## CLI { #cli } The `yt clickhouse execute` command has the following parameters (default values are specified in parentheses): - `query`: The query. - `--proxy`: The cluster on which the clique will be started, for example. This option can also be specified via the `YT_PROXY` environment variable (for example, `export YT_PROXY=`). - `--alias`: The alias under which the clique will be available. This option can also be set via the `CHYT_ALIAS` environment variable (`export CHYT_ALIAS=example`). - `--format` [TabSeparated]: The [ClickHouse format](https://clickhouse.tech/docs/en/interfaces/formats/) in which to output the result. - `--setting`: Enables you to pass a random setting to the query in `=` format, can be specified several times. ## Python API { #python } {% note warning "Attention" %} To use this function , be **sure to** configure logging at the **Debug** level. It is impossible to answer any question about query execution (including "why does it run so long", "why did it fail", "why did it output such responses") without debug logs. {% endnote %} The following method (some of the parameters are intentionally not mentioned because they are not intended to be used in a usual situation) is available in the `yt.clickhouse` module: ```python def execute( query, alias=None, format=None, settings=None, raw=None, client=None): pass ``` Some of the parameters overlap with the command line options. The same is true for them as in the previous section. The function returns a string generator from the result and the strings can be python objects (lists, dictionaries, numbers, and strings) and strings in ordered format. Let's describe the individual parameters and the differences from starting via the CLI: - `format` (None) and `raw` (False): These options control the type of returned values. If `raw = False`, the function will turn values into python objects similarly to how the YTsaurus Python API read_table method does it. If `raw = True`, you have to additionally specify the ClickHouse format. Then the returned value will be a string iterator (str) in ordered format. - `settings` ({}): A dict with additional ClickHouse [settings](https://clickhouse.tech/docs/ru/operations/settings/settings/) for the query. - `client`: A client from the YTsaurus cluster in which the query to the clique should be run. The global client is taken by default. You can, for example, pass `yt.wrapper.YtClient("")` as the client and thus specify the desired cluster. Learn more in the relevant section of the Python API documentation. # Selecting a coordinator A heavy proxy receives a user query and delegates it to one of the instances, which becomes the "coordinator" instance. The user can influence this choice: for example, for more efficient cache use. Below are different strategies for picking the coordinator, listed in descending order of priority (if you attempt to include more than one strategy, the one with the higher priority will be selected): 1) **Selecting a specific instance.** You need to add `job-cookie` of the instance to the clique alias using the `@` character. For an instance with `job-cookie=3`, for example, the query will look like this: `http://cluster.domain.com/chyt?chyt.clique_alias=my-clique@3`. 2) **Selecting an instance within a session.** Within a single ClickHouse session, the proxy will* select the same instance. {% note info "Note" %} *If no instances have been restarted in the last few minutes. {% endnote %} A session is defined by the `session_id` URL parameter (just like in the original ClickHouse). This results in the query `http://cluster.domain.com/chyt?chyt.clique_alias=my-clique&session_id=abracadabra`. 3) **StickyGroup strategy.** You can either specify the `QueryStickyGroupSize` parameter in the clique configuration or pass it along with the query in the `chyt.query_sticky_group_size` URL parameter. In this case, the proxy will delegate identical queries to a group of instances of the `QueryStickyGroupSize` size, which means that the same query will get into a random instance from that group. The choice of the `QueryStickyGroupSize` value can be based on the following reasoning: if the value is small, the cache in the selected group warms up quickly; if the value is large, the group is almost unaffected if one instance goes down, since most instances will preserve their cache. 4) **Selecting a random instance.** If there are no other strategies, the coordinator is selected equally likely among all available instances. # Query settings This section contains information about CHYT-specific settings. ClickHouse has a large number of different settings which can be specified in different ways: via the GET parameter of a query, directly in the query via the SETTINGS clause, when using YQL via pragma, or in other ways. For a list of the original ClickHouse settings, see the [documentation](https://clickhouse.com/docs/en/operations/settings/settings/). The settings related to CHYT start with the `chyt.` prefix. Some settings are placed into separate subsections: in this case, the name of the setting will consist of several tokens separated by dots, for example, `chyt.composite.enable_conversion`. As usual in ClickHouse, the `1` and `0` values are used as `True` and `False` for logical settings. As usual, many of the settings include certain optimizations that are enabled by default and are listed only to emphasize the possibility of disabling them in case of incorrect optimization. ## Available settings - `enable_columnar_read` [`%true`]: Enables the use of the fast columnar reading interface for tables in scan format. - `enable_computed_column_deduction` [`%true`]: Enables the output of values for the computed key columns using the predicate in WHERE. For example, if the `key_hash` key column is specified in the schema as a result of the `farm_hash(key)` expression, then if the query condition contains the `key = 'xyz'` expression, the `key_hash = 16518849956333482075` consequence will be automatically added to the predicate. Only conditions of the `column = constant expression`, `column tuple = tuple of constant expressions`, `column IN tuple of constant expressions`, and `column tuple IN tuple of constant expression tuples` type are supported. - `infer_dynamic_table_ranges_from_pivot_keys` [`%true`]: Enables the additional step of range output when querying via a dynamic table using [tablet pivot keys](https://ytsaurus.tech/dynamic-tables/overview.md). - `composite`: A section with settings related to composite `type_v3` and `YSON any/composite` types: - `default_yson_format` [`binary`]: The default format for representing YSON strings. Possible values are `binary, text, and pretty`. Note that options other than `binary` are less efficient, because they require an explicit conversion from binary format. [Functions for working with YSON](https://ytsaurus.tech/docs/en/functions.md) work with any of the possible YSON formats. - `dynamic_table`: A section with settings related to working with dynamic tables. - `enable_dynamic_store_read` [`%true`]: Work with dynamic tables by reading data from dynamic stores. If you enable this option, you cannot read a table that was not mounted with the `enable_dynamic_store_read = %true` attribute. Attempting to read will cause an error. If this option is disabled, any query will forcibly read only data from chunk stores, i.e. not including the most recent data from memory store (regardless of whether the table is mounted with `enable_dynamic_store_read = %true` or not). - `execution`: A section with settings that affect the process of scheduling and executing a query: - `query_depth_limit` [0]: The limit on the maximum depth of a distributed query. When reaching the specified depth, the query ends with an error. The 0 value means there is no limit. - `join_policy` [`distribute_initial`]: JOIN query mode on top of a YTsaurus table. Possible options: - `local`: JOIN is executed locally on the query coordinator. The left- and right-hand side tables (or subquery) are read according to `select_policy` mode. - `distribute_initial` (default): `JOIN` in the initial query is executed in a distributed manner, while JOIN in the secondary queries are executed locally. - `distribute`: `JOIN` in all the queries (initial and secondary) is executed in a distributed manner. We do not recommend using this mode, because it can lead to an exponential number of secondary queries in case of multiple `JOIN`. - `select_policy` [`distribute_initial`] - `local`: The `SELECT` query is read and processed completely only on one instance (coordinator). - `distribute_initial` (default): `SELECT` is executed in a distributed manner on clique instances in the initial query. In secondary queries, the execution is local. - `distribute`: `SELECT` in all the queries (initial and secondary) is executed in a distributed manner. We do not recommend using this mode, because it can lead to an exponential number of secondary queries. - `join_node_limit` [0]: The maximum number of clique nodes on which the distributed `JOIN` query is allowed. The 0 value means there is no limit (default). - `select_node_limit` [0]: The maximum number of clique nodes on which the distributed `SELECT` query is allowed. The 0 value means there is no limit (default). - `distribution_seed` [42]: The seed used for deterministic distribution of the query to the clique nodes. - `input_streams_per_secondary_query` [0]: The limit on the number of concurrent table read threads on each clique node during query execution. In case of the 0 value, the value of the `max_threads` clickhouse setting (default) is used as a limit. - `filter_joined_subquery_by_sort_key` [`%true`]: Enables optimization that pre-filters the right-hand side JOIN table (or subquery) by the sort key of the left-hand side table. - `keep_nulls_in_right_or_full_join` [`%true`]: Adjusts the behavior of `RIGHT` and `FULL JOIN` when the right-hand side table (subquery) has the `Null` values in the key columns. If `%true` is set, the query is executed as usual. The query result will have all rows with the `Null` value in the key columns (default). If `%false` is set, all or some rows from the right-hand side table with the `Null` value in the key columns may be missing and the query can be executed more efficiently. If there are no `Null` values in the key columns of the right-hand side table (subquery), the query execution result does not depend on the values of this option. - `distributed_insert_stage` [`with_mergeable_state` (may be changed in the future)] is the minimum query execution stage after which distributed writing to the table is allowed when the `parallel_distributed_insert_select` ClickHouse setting is enabled. Possible values: - `none`: Never use distributed writing. - `with_mergeable_state` (default, may be changed in the future): Use distributed writing if the query can be executed in a distributed manner at least to the `with_mergeable_state` stage. Data aggregation (`GROUP BY`, `DISTINCT`, `LIMIT BY`) and `ORDER BY` and `LIMIT` clauses can be executed independently on each clique node, so in case of distributed writing the query execution result may contain several rows with the same aggregation key, rows may not be sorted in the ORDER BY order, and the number of rows may exceed a specified LIMIT (but the total number of rows will not exceed the `number of instances in the clique` * `specified LIMIT`). - `after_aggregation`: Use distributed writing if the query can be executed in a distributed manner at least to the `after_aggregation` stage. Data aggregation (`GROUP BY`, `DISTINCT`, `LIMIT BY`) is guranteed to be executed to the end and the `ORDER BY` and `LIMIT` clauses can be executed independently on each clique node (which can cause sorting to fail and exceeding the specified row limit, as described above). If the query cannot be executed in a distributed manner to the `after_aggregation` stage, data will be written locally from the query coordinator. - `complete`: Use distributed writing only if the query can be executed in a completely distributed manner. The result of execution of the query with distributed writing in this case is indistinguishable from an ordinary query, all the clauses (aggregation, `ORDER BY`, and `LIMIT`) will be executed to the end. If the query cannot be executed in a distributed manner to the `complete` stage, data will be written locally from the query coordinator. - `caching`: A section with settings related to the different caches used in CHYT: - `table_attributes_invalidate_mode` [`sync`]: The data invalidation mechanism mode in the table attribute cache when the table is changed (the `CREATE TABLE/INSERT INTO/DROP TABLE` queries). Possible values: - `none`: Data invalidation in the cache does not occur. Reading tables immediately after a change may return the old data or cause an error. - `local`: Data invalidation occurs only in the local instance cache, without any rpc queries. Reading tables on other instances immediately after a change may return the old data or cause an error. - `async`: Data invalidation occurs synchronously in the local cache and asynchronously on all clique instances. The query completes without waiting for data invalidation on the other clique instances. Errors during data invalidation in the cache do not cause a query to fail. Reading tables on other instances immediately after a change may still return the old data or cause an error, but this time interval after a data change is much shorter than in the `none` and `local` cases. We recommend using an invalidation mode not lower than this one. - `sync` (default): Data invalidation occurs synchronously on all clique instances. The query execution time may be slightly longer, because it is necessary to wait for confirmation of cache invalidation on all instances before completing. Errors during cache invalidation will cause a query to fail. After a successful modifying query, you can immediately read the modified tables. - `invalidate_request_timeout` [5000]: The timeout in milliseconds (ms) for rpc queries to clique instances at the cache invalidation stage. If this timeout is exceeded, an error will be shown. - `concat_tables`: A section with settings related to the functions for merging tables of the `concatYtTables*` and `ytTables` range: - `missing_column_mode` [`read_as_null`]: The output mode of the general column schema if it is missing in one of the tables. Possible values: - `throw`: The query will end with an error. - `drop`: The column will be missing in the general schema. You cannot read such a column. - `read_as_null` (default): The column type will be set to `Optional(T)`. In the rows from tables where this column is missing, the value in the column will be `Null`. - `type_mismatch_mode` [`throw`]: The general column schema output mode if column types in different tables are different and cannot be reduced to a general form. Possible values: - `throw` (default): The query will end with an error. - `drop`: The column will be missing in the general schema. You cannot read such a column. - `read_as_any`: The column type will be set to `Any`. All values will be represented as YSON strings which can be handled using the functions of the `YSONExtract*` and `YPath*` range. - `allow_empty_schema_intersection` [`%false`]: Allows combining multiple tables if they have no common columns. - `max_tables` [250]: The maximum number of tables that can be combined for reading. If this limit is exceeded, the query will end with an error. - `omit_inaccessible_rows` [`%false`] — Allows to read tables with [row-level ACLs](https://ytsaurus.tech/storage/row-level-security.md). # Clique configuration The entire clique configuration is described by a single [YSON document](https://ytsaurus.tech/storage/yson-docs.md) called a *speclet*. The clique speclet is stored in Cypress at `//sys/strawberry/chyt//speclet`. You can also view its contents in the [web interface](https://ytsaurus.tech/docs/cliques/ui.md) on the **Speclet** tab or retrieve it by running the CLI command `yt clickhouse ctl get-speclet`. A speclet is a set of [options](https://ytsaurus.tech/docs/cliques/configs.md#options) that are interpreted by the controller at clique startup. In particular, the controller generates the final configuration of CHYT instances based on these options. An important difference between CHYT and original ClickHouse is that the settings are specified not in XML format, but in YSON format that is native for YTsaurus. The principle by which the settings are interpreted in this format is described [below](https://ytsaurus.tech/docs/en/#clickhouse_config). ## How the configuration is formed { #how-form } The following information is used to form a configuration: 1. The basic config from Cypress: `//sys/clickhouse/config`. 2. [Options](https://ytsaurus.tech/docs/cliques/configs.md#options) described in the speclet. 3. The native cluster connection configuration taken from the contents of `//sys/@cluster_connection`. {% note warning "Attention" %} All clique instances always work with the same configuration and it is impossible to change the configuration without restarting the clique. {% endnote %} ## Advanced configuration of the instance's YTsaurus part { #yt_config } The YTsaurus part of the instance configuration resides in the `yt_config` option. With this option, you can configure advanced settings that don't exist as individual speclet options. The following sub-options are supported: - `settings`: CHYT‑specific default query [settings](https://ytsaurus.tech/docs/en/settings.md). Changing the setting in this section changes the default value for this setting for all queries in the clique. - `table_writer`: [Table Writer configuration](https://ytsaurus.tech/storage/io-configuration.md#table_writer). - `table_attribute_cache`: Table attribute cache configuration. This cache significantly improves the responsiveness of CHYT, though at the moment it may lead to non‑consistent reads (when data has already appeared in the table, but CHYT does not see it yet). To disable this cache, use the following configuration: ```yson {read_from=follower;expire_after_successful_update_time=0;expire_after_failed_update_time=0;refresh_time=0;expire_after_access_time=0} ``` - `create_table_default_attributes` [`{optimize_for = scan}`]: The default attributes with which tables will be created during `CREATE` queries in CHYT. - `health_checker`: The Health Checker configuration. Consists of 3 fields: — `queries` [``["select * from `//sys/clickhouse/sample_table`"]``]: A list of test queries whose performance will be checked regularly. {% note warning "Attention" %} Queries from Health Checker are currently executed on behalf of user `yt-clickhouse` who must have the `read` permissions to access the table. {% endnote %} — `period` [60000]: The test triggering period in milliseconds. — `timeout` [`0.9 * period / len(queries)`]: The triggering timeout for each of the configured queries. If the query does not fit into the timeout, it is considered `failed`. - `subquery`: The configuration of the main system part which coordinates the execution of ClickHouse queries on top of YTsaurus tables. Before configuring this part, read the article about [query execution within a clique](https://ytsaurus.tech/docs/queries/anatomy.md#query-execution). — `min_data_weight_per_thread` [64 MiB]: (in bytes) When splitting the input into subqueries, the coordinator will try to give no less than the given amount to each core of each instance. — `max_data_weight_per_subquery` [50 GiB]: (in bytes) The maximum allowable amount of data to be processed on one core of one instance. This restriction is protective and protects clique users from accidentally running a huge query that processes petabytes. The constant of 50 GiB is selected, because such amount is processed on a single core in about dozens of minutes. - `show_tables`: The `SHOW TABLES` query behavior configuration. Enables you to configure the list of directories in which `SHOW TABLES` will show a list of tables. — `roots`: The list of YPath paths of directories in Cypress from which the tables will be collected for `SHOW TABLES`. {% note warning "Attention" %} The `max_data_weight_per_subquery` limit uses columnar statistics to account for column sampling from processed tables. Columnar statistics may be missing for old tables created before the statistics became available. For such tables, column orientation is not taken account in the limit. This means that when processing a very narrow slice of columns which forms 1% of the total table volume and is 1 GiB per core, CHYT will calculate that 100 GiB per core are processed. CHYT will not run such a query with the default settings. If this scenario is required or you need to process large amounts of data, configure this setting to a random large value. {% endnote %} ## Advanced configuration of the instance's ClickHouse part { #clickhouse_config } The `clickhouse_config` option is used to set up the configuration of the ClickHouse part. This option should be set so that it matches the standard ClickHouse XML configuration. The rules for converting a ClickHouse XML configuration into a CHYT YSON configuration can be described as follows: - Any non‑multiple configuration node in the ClickHouse configuration is a dict (`map`). - A multiple node is represented by a list (`list`). Below is an example of converting an artificial XML configuration into a YSON configuration: ```xml 42 qwe 3.14 hi! ``` ```yson { foo = 42; bar = "qwe"; baz = [ {quux = 3.14}; {}; "hi!"; ]; } ``` Let's examine the `clickhouse_config` sub‑options that may be of use in CHYT: - `dictionaries` (`[]`): The configuration of external dicts. The value must be a list of dict configurations. Each dict is configured by the map with the following fields that retain the meaning of the original ClickHouse configuration: - `name`: The name of the external dict. - `source`: The [data source](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-sources/) for the external dict. - `layout`: [Representation](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-layout/) of the external dict in the instance memory. - `structure`: The [schema of data](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-structure/) stored in the dict. - `lifetime`: The dict [lifetime](https://clickhouse.com/docs/en/sql-reference/dictionaries/external-dictionaries/external-dicts-dict-lifetime/). ## External dicts { #external-dict } CHYT supports all `layout`, `structure`, and `lifetime` settings of regular ClickHouse. {% note info "Note" %} You can specify sources from original ClickHouse as the `source`, but they are not guaranteed to work. In particular, sources that require networking should technically work, but may face a lack of network access in practice. {% endnote %} There is also an additional data source type in which you can use static tables in YTsaurus. This type is called `yt` and has one parameter: - `path`: The path to the static table on the cluster which will serve as a data source for the dict. As an example of connecting a YTsaurus table as an external ClickHouse dict, you can look at: - The CHYT [configuration example](https://ytsaurus.tech/docs/en/configuration.md#configuration_example) that connects this table as an external ClickHouse dict. - Example of using an external dict in queries: ```sql select dictGet('OS', 'OS', toUInt64(38)) as os_name, dictGetHierarchy('OS', toUInt64(38)) as hierarchy, dictGetChildren('OS', toUInt64(101)) as children ``` ## Configuration example { #configuration_example } Below is an example of a complete configuration that can be set by the `set-speclet` command. ```yson { instance_count = 1; query_settings = { extremes = 1; }; yt_config = { subquery = { max_data_weight_per_subquery = 1000000000000; }; }; clickhouse_config = { dictionaries = [ { name = OS; layout = {flat = {}}; structure = { id = {name = Id}; attribute = [ { name = "OS"; type = "Nullable(String)"; null_value = "NULL"; }; { name = "ParentId"; type = "Nullable(UInt64)"; null_value = "NULL"; hierarchical = %true; }; { name = "RootId"; type = "Nullable(UInt64)"; null_value = "NULL"; }; ]; }; lifetime = 0; source = {yt = {path = "//sys/clickhouse/dictionaries/OS"}}; }; ]; }; } ``` # Connecting external databases over ODBC CHYT can act as an ODBC client and query an external database from SQL through the [ClickHouse ODBC bridge](https://clickhouse.com/docs/concepts/features/interfaces/odbc). This works with MySQL, PostgreSQL, MariaDB, and other databases that have a Linux ODBC driver. ## How it works { #how-it-works } When a clique starts, the controller combines the drivers from `drivers_dir` with `odbc_config` and places the bridge binary, driver libraries, and generated unixODBC configuration in every instance's job sandbox. Each instance runs its own local bridge; before startup, the trampoline expands environment variables and configures the ODBC and library paths. Drivers and data sources are fixed at clique startup. Configuration changes take effect after the clique restarts. ## Configuring a clique { #speclet } ODBC is configured through `odbc_config` in the clique [speclet](https://ytsaurus.tech/docs/cliques/configs.md#speclet). Changing it requires the `manage` permission for the clique. #| || **Option** | **Description** || || `enable` [`false`] | Enables ODBC support || || `drivers_dir` [`//sys/bin/odbc-drivers`] | Cypress directory containing pre-deployed drivers and `config.yson` with their descriptions || || `drivers` | Additional drivers. Each entry has `name`, `description`, `path` to the driver `.so` file, and optional `options` for its `odbcinst.ini` section || || `sources` | Data sources used to generate `odbc.ini`. Each entry has `name`, `description`, `driver`, and driver-specific connection `options` || || `extra_files` | Additional driver files, such as shared libraries or certificates, copied from Cypress to the job sandbox || || `bridge_path` [`//sys/bin/odbc-drivers/clickhouse-odbc-bridge`] | Cypress path to the ODBC bridge binary || |# Most cliques only need `enable` and `sources`. The `drivers_dir`, `drivers`, `extra_files`, and `bridge_path` options are intended for administrators and custom driver setups; keep their defaults unless you need to override the deployed driver set. The first letter of every key in `options` is capitalized, so `password` and `Password` are equivalent. The following example uses a pre-deployed MySQL driver: ```yson { odbc_config = { enable = %true; sources = [ { name = "my_mysql"; description = "My MySQL database"; driver = "MySQL"; options = { Server = "mysql.example.com"; Port = "3306"; Database = "mydb"; User = "reader"; Password = "${YT_SECURE_VAULT_mysql_password}"; }; }; ]; }; } ``` All files from `drivers` and `extra_files` must have unique base names because they share one job sandbox. ## Passwords and other secrets { #secrets } Do not put passwords directly in the speclet. Store them as clique secrets and reference the corresponding `YT_SECURE_VAULT_*` environment variables from `options`: ```bash yt clickhouse ctl set-secrets --alias my_clique --secrets '{mysql_password="password"}' ``` The speclet example above contains only the `${YT_SECURE_VAULT_mysql_password}` reference. The trampoline substitutes its value inside the clique job before starting ClickHouse. ## Queries { #queries } Use the [odbc](https://clickhouse.com/docs/sql-reference/table-functions/odbc) table function to query a configured data source: ```sql SELECT * FROM odbc('DSN=my_mysql', 'my_table') ``` To specify the external database explicitly: ```sql SELECT * FROM odbc('DSN=my_mysql', 'mydb', 'my_table') ``` The query coordinator reads data from the external database; the read is not distributed across clique instances. ODBC is therefore intended for dictionaries and small tables rather than large scans. ## Limitations { #limitations } - Every user with the `use` permission for the clique can query every data source configured in it. - Access to external data is controlled by the account configured in the data source, not by the identity of the user running the CHYT query. # Data types ClickHouse has a wide variety of [data types](https://clickhouse.com/docs/en/sql-reference/data-types) available. YTsaurus uses its own data type system. Although the ClickHouse data type system largely overlaps that of YTsaurus, some data types from YTsaurus may not exist in ClickHouse, and vice versa. For a complete list of the types supported in the YTsaurus system, see [YTsaurus data types](https://ytsaurus.tech/storage/data-types.md). ## Primitive data types The table below shows the correspondence between the __primitive__ (not composite) ClickHouse and YTsaurus types. | YTsaurus type | ClickHouse type | Comment | | -------------- | --------------- | ------------------------------------------------------------ | | `intN, uintN` | `intN, uintN` | Integer numeric columns from the ClickHouse point look like (U)Int of the appropriate digit capacity. | | `boolean` | `YtBoolean` | There are no Booleans in regular ClickHouse, they are represented as single-byte unsigned numbers (`UInt8`) with values 0 and 1. CHYT supports the additional `YtBoolean` type that is similar to the `UInt8` type in ClickHouse computations, but is interpreted as a `boolean` when written to a YTsaurus table. | | `string, utf8` | `String` | Strings are available as string | | `double` | `Float64` | | | `date` | `Date` | | `datetime` | `DateTime` | | `any` | `String` | Any columns are read by the ClickHouse engine as string columns containing data in YSON. See also: [`chyt.composite.default_yson_format`](https://ytsaurus.tech/docs/en/settings.md). | | `T` with `required = %false` | `Nullable(T)` | If a column of a `T` type is not `required`, it is seen as a Nullable column. | ## Composite data types Useful links: - [YTsaurus type system](https://ytsaurus.tech/storage/objects.md). In YTsaurus, the following composite data types are available: lists, dicts, optional types, structures, and tuples. By default, composite values are seen by the CH engine in the same way as the `Any` type, i.e. as a YSON string. If you configure the `chyt.composite.enable_conversion` setting to 1, CHYT starts converting composite types (specified in the schema, that is, having the `type_v3` type) into the corresponding ClickHouse composite types. Query example: ```bash # A query with no composite type conversion enabled. Composite types # are seen as strings in binary YSON. yt --proxy clickhouse execute 'select * from `//home/user/sample_table_composite`' --format Vertical Row 1: ────── list: [foo;bar;] dict: [(binary unreadable characters)] struct: [(binary unreadable characters)] Row 2: ────── list: [] dict: [] struct: [(binary unreadable characters)] # A query with enabled conversion. yt --proxy clickhouse execute 'select * from `//home/user/sample_table_composite`' --format Vertical --setting chyt.composite.enable_conversion=1 --format Vertical Row 1: ────── list: ['foo','bar'] dict: [('key1',42),('key2',-17)] struct: (3.14,'pi') Row 2: ────── list: [] dict: [] struct: (0,'') ``` Unfortunately, the ClickHouse and YTsaurus type systems are quite different as far as composite data types are concerned. Below are the main differences. ### Optional types { #optional } In the YTsaurus type system, any type `T` can be made optional by surrounding it with `optional`, for example, `optional>`, `optional>`, and `optional>` data types are possible. In ClickHouse, placing values into `Nullable` is very limited. In particular, you cannot place into `Nullable`: - The type that is already `Nullable`. - `Array`. - `Tuple`. Thus, none of the three examples of types above have their own representation in the ClickHouse type system. To process such types in CHYT, the following conversion is used. If type `T` corresponds to type `T'` of the ClickHouse type system and type `Nullable(T')` does not exist in CH, `optional` is mapped to `T'` replacing null with the default value for `T'`. Examples: - `optional>` is mapped to `Nullable(Int64)`, with two types of different initial `null` merged into one. - `optional>` is mapped to `Array(String)`, with initial `null` turning into empty array `[]`. - `optional>` is mapped to `Tuple(Int64, String)`, with initial `null` turning into a tuple of two default values for Int64 and String, i.e. tuple `(0, '')`. ### Dicts { #dict } In the Yandex type system, for any two types `K` and `V` you can make type `dict`. ClickHouse has nothing like dicts, so `dict` is mapped to `Array(Tuple(K', V'))`. See a query example above: the first value in the `dict` column contains the `{'key1' -> 42, 'key2' -> -17}` dict. ### Tuples { #tuple } Yandex type system tuples are mapped to ClickHouse tuples in a trivial way. ### Structures { #structures } Structures from the Yandex type system could be mapped to named CH tuples (`Named Tuple`) CH. Unfortunately, ClickHouse has problems with supporting named tuples. They are now supported only on the external level of the complex type description (see the relevant [issue](https://github.com/ClickHouse/ClickHouse/issues/15587)). As a result, you cannot place `Named Tuple` inside, for example, a list. To simplify the current implementation, the decision was made to map the structures to regular tuples. Thus, you can work with structures as with tuples, i.e. you can get elements from them by index through `.1`, `.2`, and so on or using the `tupleElement` function: ```bash yt --proxy clickhouse execute 'select toTypeName(struct), struct.1, struct.2 from `//home/user/sample_table_composite`' --format Vertical --setting chyt.composite.enable_conversion=1 Row 1: ────── toTypeName(struct): Tuple(Float64, String) tupleElement(struct, 1): 3.14 tupleElement(struct, 2): pi Row 2: ────── toTypeName(struct): Tuple(Float64, String) tupleElement(struct, 1): 0 tupleElement(struct, 2): ``` ### Tagged types { #tagged } `Tagged` types from the general Yandex type system are mapped to the corresponding types without a tag. The tag information is lost. ### Variants, Named variants, Any { #var-any } Variants and named variants are not supported. Their display logic is the same as if there is a value of the `Any` type somewhere inside a composite type. Any value of the `Variant`, `Named Variant`, or `Any` type simply turns into a YSON string with a value representation. ## Tips for working with insufficiently schematized data { #schema } Below are a few methods to get around some data type problems. Many tables contain dates and times in text or numeric representation (for example, in ISO or Unix ts format). To convert them to a **time** data type, you can use the standard [ClickHouse functions for working with date and time](https://clickhouse.com/docs/en/sql-reference/functions/date-time-functions), [reinterpret](https://clickhouse.com/docs/en/sql-reference/functions/type-conversion-functions/#reinterpretasdate), and [CAST](https://clickhouse.com/docs/en/sql-reference/functions/type-conversion-functions/#type_conversion_function-cast). YTsaurus has historically used the `Any` data type for working with composite data (arrays, dicts, structures). Despite the appearance of composite type support in the YTsaurus type system, a lot of old data still remains represented in `Any`: it will be available as a binary YSON to the ClickHouse engine. However, `Any` representation as a binary YSON means almost nothing for the ClickHouse engine, since YSON support is not built into ClickHouse. ClickHouse interprets such data simply as a binary string with a custom format. Therefore, the ClickHouse and YTsaurus compatibility layer provides a number of auxiliary functions that enable you to address over a YSON structure using the [YPath language](https://ytsaurus.tech/storage/yson-docs.md). And also interpret its nodes as primitive YTsaurus types (`Int64`, `UInt64`, `Double`, or `Boolean`), strings (`String`), or lists of primitive YTsaurus types (`Array(Int64)`, `Array(UInt64)`, `Array(Double)`, or `Array(Boolean)`) with the conversion into the corresponding built-in ClickHouse type. For more information about these functions, see [Functions for working with YSON](https://ytsaurus.tech/docs/en/functions.md#yson_functions). {% note warning "Attention" %} Conversion from YTsaurus representation to ClickHouse representation will inevitably result in longer query execution time. {% endnote %} # Functions This section describes CHYT-specific functions. ## Functions for working with YSON { #yson_functions } ### YPathType (simple types and arrays) { #ypath_simple_types } `YPath[Strict](https://ytsaurus.tech/docs/en/yson,%20ypath)`: Extracts a `Type` type value at `ypath` from the string `yson`. `Type` can be one of the following: `Int64`, `UInt64`, `Boolean`, `Double`, `String`, `ArrayInt64`, `ArrayUInt64`, `ArrayBoolean`, or `ArrayDouble`. The optional `Strict` suffix indicates whether the function is strict. Examples of possible function names: `YPathInt64`, `YPathStringStrict`, `YPathArrayDouble`. These functions work as follows: 1. The passed `yson` string is interpreted as a YSON document. 2. The document is addressed at `ypath` relative to the document root. 3. The contents of the document are then interpreted at `ypath`, which points to the `Type` type. Errors may occur at each step of this function operation: 1. The string may not be a YSON document (this is possible when a string constant is specified as a function argument, but not when reading actual Any columns from YTsaurus). 2. The path may be missing from the document. 3. The path may correspond to a data type other than `Type` (for example, a string literal instead of a numeric value). The difference between the strict (version with the `Strict` suffix) and non-strict function versions lies in how they handle errors: the strict version crashes the query, while the non-strict version returns the default value for the corresponding column type (`Null`, `0`, empty string, or empty list) and continues operation. Usage examples (the first argument in each of these examples is a string literal containing YSON, but it can be replaced by any Any column of the table): ```sql SELECT YPathInt64('{key1=42; key2=57; key3=29}', '/key2'); 57 SELECT YPathInt64('[3;4;5]', '/1'); 4 SELECT YPathString('["aa";"bb";"cc"]', '/1'); "bb" SELECT YPathString('{a=[{c=xyz}; {c=qwe}]}', '/a/1/c'); "qwe" SELECT YPathInt64('{key=xyz}', '/key'); 0 SELECT YPathInt64Strict('{key=xyz}', '/key'); std::exception. Code: 1001, type: NYT::TErrorException, e.what() = Node /key2 has invalid type: expected one of {"int64", "uint64"}, actual "string" code 500 origin ... SELECT YPathString('{key=3u}', '/nonexistentkey'); "" SELECT YPathArrayInt64('[1;2;3]', ''); [1, 2, 3] SELECT YPathUInt64('42u', ''); 42 ``` Suppose that a table has two `Any` columns, `lhs` and `rhs`, both containing lists of numbers of equal length. Table 1: Example of a table with `Any` columns | lhs | rhs | | --------- | --------- | | [1, 2, 3] | [5, 6, 7] | | [-1, 1] | [1, 3] | | [] | [] | Then we can calculate the scalar product of `lhs` and `rhs` vectors as follows: ```sql SELECT arraySum((x, y) -> x*y, YPathArrayInt64(lhs, ''), YPathArrayInt64(rhs, '')) FROM "//path/to/table" 38 2 0 ``` ### YPathExtract (composite data type) { #ypath_complex_types } `YPathExtract[Strict](https://ytsaurus.tech/docs/en/yson,%20ypath,%20type)`: Extracts a value of an arbitrary `type` type value at `ypath` from the string `yson`. This function is similar to the `YPath` functions but allows specifying an arbitrary return type using the additional `type` argument. It also supports composite, or compound, data types, for which there is no corresponding `YPath` version. {% note warning "Attention" %} With the `YPathExtract` function, you can also extract simple type values. For example, `YPathExtract(yson, ypath, 'Int64')` returns the same result as `YPathInt64(yson, ypath)`. However, the `YPath` functions are more specialized and better optimized, so we recommend using the `YPathExtract` function only when there is no specialized function for the type you want to extract. {% endnote %} Example: ```sql SELECT YPathExtract('[[1;2];[2;3]]', '', 'Array(Array(Int64))'); [[1, 2], [2, 3]] SELECT YPathExtract('{a=1;b="abc"}', '', 'Tuple(a Int64, b String)'); ("a": 1, "b": "abc") SELECT YPathExtract('{x=["abc";"bbc"]}', '/x', 'Array(String)') ["abc", "bbc"] ``` ### YPathRaw (raw YSON) { #ypath_raw } `YPathRaw[Strict](https://ytsaurus.tech/docs/en/yson,%20ypath,%20[format])`: Extracts a raw YSON value at `ypath` from the string `yson`. You can use this function when the type of the result is unknown or varies between different rows in the same column. With the optional `format` parameter, you can specify the representation format of the returned YSON value. Supported formats are the following: `binary` (default), `text`, `pretty`, `unescaped_text`, or `unescaped_pretty`. Examples: ```sql SELECT YPathRaw('{x={key=[1]}}', '/x', 'text'); '{"key"=[1;];}' SELECT YPathRaw('{a=1}', '/b', 'text'); NULL ``` ### YSON family functions* { #yson_extract } ClickHouse supports `JSON` family functions* for working with JSON format. To ensure a workflow that is more similar to standard ClickHouse, CHYT offers complete equivalents of these functions that are compatible with YSON format. The function arguments and their returned values are described in the [ClickHouse documentation](https://clickhouse.com/docs/en/sql-reference/functions/json-functions). The currently supported YSON function equivalents are as follows: `YSONHas`, `YSONLength`, `YSONKey`, `YSONType`, `YSONExtractInt`, `YSONExtractUInt`, `YSONExtractFloat`, `YSONExtractBool`, `YSONExtractString`, `YSONExtract`, `YSONExtractKeysAndValues`, `YSONExtractRaw`, `YSONExtractArrayRaw`, `YSONExtractKeysAndValuesRaw`. {% note info "Note" %} Unlike the `YPath` functions, in which array elements are indexed from 0, indexing in `YSON` functions* is one-based. {% endnote %} {% note warning "Attention" %} These functions are implemented using shared ClickHouse code and don't have optimal performance. To speed up computations on large amounts of data, we recommend using the more optimal `YPath` functions. {% endnote %} Example: ```sql -- Equivalent to YPathString('["aa";"bb";"cc"]', '/0') -- Note: indexing starts from 1 SELECT YSONExtractString('["aa";"bb";"cc"]', 1); "aa" -- Equivalent to YPathString('{a=[{c=xyz}; {c=qwe}]}', '/a/1/c') SELECT YSONExtractString('{a=[{c=xyz}; {c=qwe}]}', 'a', 2, 'c'); "qwe" ``` ### ConvertYsonToJson If the supported YSON functionality is insufficient, CHYT provides the capability to convert YSON to JSON. Example: ```sql SELECT ConvertYsonToJson('{key1=42; key2=57; key3=29}') '{ "key3":29, "key1":42, "key2":57 }' ``` ### YSON representation formats Data from `Any` type columns can be stored in binary `YSON` representation. Since it's not always convenient to read such data, CHYT has the `ConvertYSON` function that converts different `YSON` representations into each other. `ConvertYson(yson, format)`: Converts a string with a YSON document to the specified format. In total, there are five possible representation formats: - `binary` - `text` - `pretty` - `unescaped_text`* - `unescaped_pretty`* {% note info "* Note" %} YTsaurus offers three representation formats: `binary`, `text`, and `pretty`. In `text` and `pretty` representation formats, string values escape all non-ASCII characters, including Cyrillic letters and various UTF-8–encoded characters. Two additional formats, `unescaped_text` and `unescaped_pretty`, were added to CHYT to make the resulting YSON containing strings with Cyrillic characters more readable. These formats only differ from `text` and `pretty` in that they only escape special YSON characters in string values. All other string characters are stored without changes. {% endnote %} Examples: ```sql SELECT ConvertYson('{x=1}', 'text'); '{"x"=1;}' SELECT ConvertYson('{x=1}', 'pretty'); '{ "x" = 1; }' SELECT ConvertYson('{x=1}', 'binary'); '{\u0001\u0002x=\u0002\u0002;}' SELECT ConvertYson('{x="This example shows escaping the © character"}', 'text'); '{"x"="This example shows escaping the \xC2\xA9 character";}' SELECT ConvertYson('{x="Here, the © character is not escaped"}', 'unescaped_text'); '{"x"="Here, the © character is not escaped";}' ``` ### Example of working with YSON ```sql SELECT operation_id, task_id, YPathInt64(task_spec, '/gpu_limit') as gpu_limit, YPathInt64(task_spec, '/job_count') as job_count FROM ( SELECT tupleElement(tasks, 1) as task_id, tupleElement(tasks, 2) as task_spec, operation_id FROM ( SELECT operation_id, tasks FROM ( Select YSONExtractKeysAndValuesRaw(COALESCE(tasks, '')) as tasks, operation_id FROM ( SELECT YSONExtractRaw(spec, 'tasks') as tasks, operation_id FROM `//home/dev/chyt_examples/completed_ops` WHERE YSONHas(spec, 'tasks') ) ) ARRAY JOIN tasks ) ) ORDER BY gpu_limit DESC; ``` ## Getting a clique version { #version_functions } #### Getting a CHYT version { #chyt_version } `chytVersion()`: Gets a string representation of the CHYT server version. Example: ```sql SELECT chytVersion() ``` #### Getting a YTsaurus version { #yt_version } `ytVersion()`: Gets a string representation of the YTsaurus code version used in the current CHYT version. Example: ```sql SELECT ytVersion() ``` ## Working with Cypress { #cypress } {% note info "Note" %} All functions used to work with Cypress are table functions in ClickHouse terms. This means that their returned value can be used in place of a table: for example, in the `FROM` section of a `SELECT` query. {% endnote %} ### ytListNodes { #list_nodes } `ytListNodes[L](https://ytsaurus.tech/docs/en/dir_path,%20[from,%20[to]])`: Gets a list of all nodes and their attributes in the directory at `dir_path`. The result contains one row for each node in the directory at `dir_path`. Each row contains: - Two predefined columns, `$key` and `$path`, which hold the node's name and full path (always starts with `dir_path`). The values in these columns may differ from the `key` and `path` attributes if the node is a link (`link` type) to another node; - Three columns corresponding to the `resource_usage` attribute fields: `disk_space`, `tablet_count`, and `master_memory`; - A large number of columns with names matching the corresponding node attributes: `key`, `path`, `account`, `owner`, `erasure_codec`, `id`, `acl`, and other. These columns include almost every system attribute. {% note info "Note" %} Since users don't normally need all the node attributes, most of the columns are virtual in ClickHouse terms. This means that by default they are not returned for `SELECT * FROM ytListNodes(...)` and `DESCRIBE ytListNodes(...)` expressions but can be requested explicitly: ```sql SELECT id, key_columns, * FROM ytListNodes(...) ``` {% endnote %} By default, the `ytListNodes` function doesn't expose links (nodes of `link` type). This means that for these nodes, the function returns the attributes of the link node itself, not the node it refers to. To change this behavior and expose all links, use the `L` function modifier (`ytListNodesL`). Keep in mind that links leading to a non-existent node remain unexposed, so the result may still contain `link` type strings that correspond to unexposed links. {% note warning "Attention" %} Exposing each link requires an additional query to the master server, so executing the `ytListNodeL` function may take longer and be more resource-intensive than `ytListNode`. We strongly discourage using `ytListNodeL` for directories with a large number of links. {% endnote %} With the two optional string arguments `from` and `to`, you can filter the returned nodes by name (`$key` column). Keys are compared lexicographically. Example: ```sql SELECT * FROM ytListNodes('//some/dir/path'); SELECT sum(disk_space), sum(chunk_count) FROM ytListNodes('//some/dir/path'); SELECT * FROM ytListNodesL('//some/dir/path'); ``` ### ytListTables { #list_tables } `ytListTables[L](https://ytsaurus.tech/docs/en/dir_path,%20[from,%20[to]])`: Gets a list of all tables and their attributes in the directory. The function arguments and the structure of the returned value are similar to the `ytListNodes[L]` function. The function result is equivalent to the output of `ytListNodes[L]` with `WHERE type = 'table'` filtering applied. The `L` suffix has a similar meaning and exposes all links before filtering. Thus, the result of a `ytListTables` call only contains the tables located in the specified directory, whereas the result of `ytListTablesL` also contains the tables linked in the specified directory. ```sql SELECT path, chunk_count FROM ytListTables('//dome/dir/path'); ``` ### ytListLogTables { #list_log_tables } `ytListLogTables(project_name_or_path, [from_datetime, [to_datetime]])`: Gets a non-overlapping list of log tables and their attributes from the specified project. `project_name_or_path` can hold either the name of the project with the logs (if the project is located in the `//logs` directory) or the full path to the directory with the logs. The function reads the log tables in four subdirectories (`1h`, `30m`, `1d`, and `stream/5m`), then merges the sets with these tables, avoiding any overlaps in the time intervals of the tables. With the two optional arguments `from_datetime` and `to_datetime`, you can specify the log time interval that you need. Log tables that don't have any overlaps with the specified interval are filtered out. {% note info "Note" %} Filtering by time interval is only performed at the table level. Log tables returned in the function result may cover a longer interval than that specified in the arguments. {% endnote %} Example: ```sql SELECT * FROM ytListLogTables('chyt-structured-http-proxy-log', today() - 1); SELECT * FROM ytListLogTables('//logs/chyt-structured-http-proxy-log', today() - 5, today() - 4); ``` ### ytNodeAttributes { #node_attributes } `ytNodeAttributes(path1, [path2, [...]])`: Gets the attributes of all the nodes specified in the arguments. The returned value contains a single string with attributes for each path specified in the function arguments. If the specified path is invalid or doesn't exist, the function generates an error. The structure of the returned value is similar to the `ytListNodes` function. Example: ```sql SELECT * FROM ytNodeAttributes('//sys', '//sys/clickhouse') ``` ## Reading multiple tables { #multiple_tables_read } {% note info "Note" %} All functions used to read sets of tables are table functions in ClickHouse terms. This means that their returned value can be used in place of a table: for example, in the `FROM` section of a `SELECT` query. {% endnote %} ### ytTables { #yt_tables } `ytTables(arg1, [arg2, ...])`: Reads the union of a set of tables specified in the arguments. The function can accept the following arguments: - Strings containing paths to specific tables. - Functions `ytListNodes[L]`, `ytListTables[L]`, and `ytListLogTables`. - The `view` function that accepts a *subquery* returning a set of paths to tables as an argument. {% note info %} Before CHYT 2.17, you could pass a subquery to `ytTables` without the `view` function. Starting with CHYT 2.17, attempting to do so will result in the following error: `Scalar subquery returned more than one row`. {% endnote %} The `ytTables` function returns the union of the set of all tables specified in the arguments. It works similarly to the ClickHouse's `merge` function (see the [documentation](https://clickhouse.com/docs/en/sql-reference/table-functions/merge)) but offers more flexible methods for specifying tables and is optimized for YTsaurus. In addition to the main table columns, the query output always contains the virtual columns `$table_index`, `$table_name`, and `$table_path`. Use them to determine which table each row is taken from. To learn more about virtual columns, see [the corresponding section](https://ytsaurus.tech/docs/yt-tables.md#virtual_columns). If tables in the specified set have different schemas, the function will infer a common schema suitable for all tables. {% cut "Algorithm for inferring a common schema" %} - **Compatible types**: For each column, the algorithm selects the "most generic type" that all types in the column can be cast to. {% note info "Example" %} For `optional` and `int64`, the "most generic type" is `optional`. {% endnote %} {% note warning "Please note" %} Due to the way data is stored in YTsaurus, unsigned numeric types are incompatible with signed ones (that is, `int32` and `uint32` are incompatible). {% endnote %} - **Incompatible types**: If the column types in different tables are incompatible, CHYT proceeds based on the set `chyt.concat_tables.type_mismatch_mode` option value. {% note info "Example" %} `string` and `int64` types are incompatible. {% endnote %} Possible option values are `drop`, `throw`, and `read_as_any`: - By default, the function throws a query execution error (`throw` value). - You can read columns of different types as YSON strings by setting the `read_as_any` option value. This may be inefficient, so we recommend that you structure your tables properly. - If these columns are not needed for the query's execution, you can discard them by setting the `drop` option value. - **Missing column**: If a column is missing in one or multiple input tables, CHYT proceeds based on the set `chyt.concat_tables.missing_column_mode` option value. Possible values are `drop`, `throw`, and `read_as_null`: - By default, the column becomes optional, and values for the tables that don't have this column will be read as `NULL` (`read_as_null` value). - If you do not expect missing columns in any of the tables, you can set the `throw` option value to generate a query execution error if that happens. - As with incompatible types, you can discard these columns by setting the `drop` option value. {% note warning "Please note" %} The `missing_column_mode` option only affects the function behavior if a table that is missing a column has a strict schema. If a column is missing in a non-strict schema, it may still exist in the actual data with any type. In this case, the algorithm makes a pessimistic assumption that the column is present with an incompatible type, and its subsequent behavior is determined by the previously described option `type_mismatch_mode`. {% endnote %} {% endcut %} Example: ```sql -- Read the union of 2 tables: SELECT * FROM ytTables('//tmp/t1', '//tmp/t2') -- Read the union of all tables from the directory '//tmp/dir': SELECT * FROM ytTables(ytListTables('//tmp/dir')) -- Read the union of all tables from the directory and their names: SELECT *, $table_name FROM ytTables(ytListTables('//tmp/dir')) -- Read the last (lexicographically) table: SELECT * FROM ytTables(view(SELECT max(path) FROM ytListTables('/tmp/dir'))) -- Read tables with a specific suffix: SELECT * FROM ytTables(view(SELECT concat(path, '/suffix') FROM ytListNodes('//tmp/dir'))) )) ``` ### concatYtTables { #concat_yt_tables } `concatYtTables(table1, [table2, [...]])`: Reads the union of multiple YTsaurus tables. This is a deprecated variant of the `ytTables` function. Only table paths can be used as arguments. Example: ```sql SELECT * FROM concatYtTables('//tmp/sample_table', '//tmp/sample_table2'); ``` ### concatYtTablesRange { #concat_yt_tables_range } `concatYtTablesRange(cypress_path, [from, [to]])`: Reads the union of all tables in the directory at `cypress_path`. This is a deprecated function equivalent to `ytTables(ytListTables(cypress_path, from, to))`. Example: ```sql SELECT * FROM concatYtTablesRange("//tmp/sample_tables"); SELECT * FROM concatYtTablesRange('//tmp/sample_tables', '2019-01-01'); SELECT * FROM concatYtTablesRange('//tmp/sample_tables', '2019-08-13T11:00:00'); ``` # User-defined SQL functions ## Creating and deleting SQL UDFs { #create_delete_udf } [CREATE FUNCTION](https://clickhouse.com/docs/en/sql-reference/statements/create/function): Creates a function from a lambda expression. Functions created this way are available to all users within the used clique, but not outside it. ```sql CREATE FUNCTION linearEquation AS (x, k, b) -> k*x + b; SELECT number, linearEquation(number, 2, 1) FROM numbers(3); ``` [DROP FUNCTION](https://clickhouse.com/docs/en/sql-reference/statements/drop#drop-function): Deletes a user-defined SQL function. ```sql DROP FUNCTION linearEquation; ``` Both commands require the `Manage` permission for the clique. ## Using functions in queries { #udf_usage } UDFs are stored in the clique state, so you don't have to recreate them in every query. You can call a UDF in the same way as any built-in ClickHouse function. If someone recreates a UDF currently in use while a query is being executed, the calls to this function may use different implementations. There is no guaranteed way to know which one will be selected on each clique instance. ## Listing all SQL UDFs in a clique { #get_all_udfs } ```sql SELECT name, create_query FROM system.functions WHERE origin = 'SQLUserDefined' ``` Result: |name |create_query | | -------------- | ----------- | | linearEquation | CREATE FUNCTION linearEquation AS (x, k, b) -> ((k * x) + b) | # Controller Controller is a service responsible for administering and maintaining the operability of private cliques. ## Tasks solved { #why } - Store the clique configuration persistently. - Generate the appropriate [instance configurations](https://ytsaurus.tech/docs/en/reference/configuration.md) based on the clique configuration and YTsaurus cluster configuration. - Start and restart Vanilla operations on the clique, if they have been terminated because of failures or maintenance works on the cluster. ## Controller CLI { #cli } To access the controller, you can use the Command Line Interface (CLI) provided with the `ytsaurus-client` package. {% note info %} We're continuously adding new [commands](https://ytsaurus.tech/docs/en/#commands) for the controller, so some of them might not be in the documentation yet. To view all the currently available commands, use `yt clickhouse ctl --help`. {% endnote %} All the commands have the optional `--proxy` parameter for setting the YTsaurus cluster to access the cliques from: ```bash yt --proxy clickhouse ctl list ``` If you omit this argument, the default value from the `YT_PROXY` environment variable will be used. ## Clique permissions { #access } Clique permissions are stored at `//sys/access_control_object_namespaces/chyt//@principal_acl`. Replace the `@principal_acl` attribute with the necessary permissions: - `use`: Use the clique (run queries). - `read`: Read the clique's configuration. - `manage`: Edit the clique's configuration. - `remove`: Remove the clique. ## Commands { #commands } ### Create { #create } `yt clickhouse ctl create `: Create a clique with the specified alias (name). The clique's alias must meet the ID requirements, that is, include only Latin characters, digits, and underscores (`_`) and never begin with a digit. You can also use `*` at the beginning (however, it will be ignored) and the `-` character. Once the command is successfully executed, the `//sys/strawberry/chyt//speclet` node is created in Cypress to store the clique's configuration. In the web interface, the `//sys/strawberry/chyt/` node will now additionally show the current state of the clique, including possible errors preventing the Vanilla operation from running on the clique's instances. Cliques are created in an inactive state in which they cannot accept or process queries. To activate your clique, first set it up using appropriate commands. ### Remove { #remove } `yt clickhouse ctl remove `: Fully delete the clique and its persistent state. Once the operation is completed, you can't recover the clique anymore. To execute this operation, you need the **Manage** role for this clique. ### Exists { #exists } `yt clickhouse ctl exists `: Check whether the clique exists on the cluster. The result of the command is a `bool` value. Result example: ```bash $ yt clickhouse ctl exists test_clique_name %true ``` ### Status { #status } `yt clickhouse ctl status `: Return the current clique status. Result example: ```bash $ yt clickhouse ctl status test_clique_name { "status" = "Waiting for restart: speclet changed"; "operation_state" = "running"; "operation_url" = "https://ytsaurus.ru//operations/de569537-f8cded7d-3fe03e8-b5278252"; "speclet_diff" = { "enable_geodata" = { "new_value" = %true; "old_value" = %false; }; }; } ``` ### List { #list } `yt clickhouse ctl list`: Look up all aliases of the cliques running in the cluster. Result example: ```bash $ yt clickhouse ctl list [ ""; ""; ""; ] ``` ### Set-option { #set_option } `yt clickhouse ctl set-option [--alias ]`: Set the speclet's `key` option to `value`. To execute this command, you need the **manage** role for this clique. An arbitrary YSON value is used for `value`. For example, the value can be: - A number: ```bash yt clickhouse ctl set-option instance_count 1 ``` - A boolean value. Such values begin with `%` in YSON syntax: ```bash yt clickhouse ctl set-option enable_geodata %false ``` - An ID string: ```bash yt clickhouse ctl set-option query_settings/chyt.execution.join_policy local ``` - An arbitrary string. If the string includes characters other than alphanumeric ones, underscores (`_`), and dashes (`-`), to ensure it's parsed correctly as a string, enclose it in double quotes (`"like this"`). Most popular command‑line implementations, such as bash and zsh, parse `"` as a control character and might deliver values without the enclosing double quotes to the CLI. To overcome this issue, enclose the argument additionally in single quotes: ```bash yt clickhouse ctl set-option chyt_version '"stable-2.08"' ``` The optional `--alias ` argument sets the alias for the clique to be updated. You can omit this argument; in this case, its value will be taken from `CHYT_ALIAS`. If it is empty, an error will be returned. To view all the set options, run the [get-speclet](https://ytsaurus.tech/docs/en/#get-speclet) command. For a description of all available options, see [Configuration](https://ytsaurus.tech/docs/en/reference/configuration.md#options). ### Remove-option { #remove_option } `yt clickhouse ctl remove-option [--alias ]`: Remove a previously assigned option value from the clique configuration. The options omitted in the clique configuration will be taken from their defaults when the clique's operation is started. ### Get-speclet { #get-speclet } `yt clickhouse ctl get-speclet [--alias ]`: Get all the options set for the clique. Result example: ```bash $ yt clickhouse ctl get-speclet --alias test_clique_name { "family" = "chyt"; "stage" = "production"; "instance_count" = 8; "active" = %true; "pool" = "chyt"; ... } ``` ### Set-speclet { #set-speclet } `yt clickhouse ctl set-speclet [--alias ]`: Set the clique's speclet in YSON string format. To execute this command, you need the **manage** role for this clique. Example: ```bash yt clickhouse ctl set-speclet '{instance_count=2;pool="chyt";}' --alias test_clique_name ``` ### Start { #start } `yt clickhouse ctl start [--untracked]`: Start a previously created clique. With the `untracked` option, you can start the clique without linking it to the controller and without any guarantees. This option is intended for starting test cliques without a compute pool. We strongly advise against running production cliques without specifying a compute pool, as this may make the clique unstable. The clique will start under the user's name. The controller doesn't monitor the state of such cliques or restart them if there are any errors. If the clique's operation ends with an error, you'll need to restart the clique manually by running the command again. To execute this command, you need the **manage** role for this clique. Result example: ```bash $ yt clickhouse ctl start test_clique_name { "status" = "Ok"; "operation_state" = "initializing"; "operation_url" = ...; } ``` ### Stop { #stop } `yt clickhouse ctl stop `: Stop the clique. The clique operation will be stopped, and all pool resources allocated to the clique will be released. The clique configuration won't be deleted; the user can restart the clique later using the `start` command. To execute this command, you need the **manage** role for this clique. ## Available options { #options } For a list of the clique options that can be set by the `set-option` command, see [Configuration](https://ytsaurus.tech/docs/en/reference/configuration.md#options). # Resources and preemption A CHYT clique is a Vanilla YTsaurus operation and instances are run in the jobs of this operation and consume resources. Before continuing with this article, consider reading about the [Scheduler and pools](https://ytsaurus.tech/docs/scheduler/scheduler-and-pools.md) first. ## Preemption { #preemption } The main mechanism for ensuring fair resource allocation in the YTsaurus scheduler is preemption. This means that if at some point an operation starts consuming more resources than it's entitled to, any of its jobs will be "preempted", i.e. forcibly stopped, and another operation that is actually entitled to the resources will be started in its place. This job is called an *aborted job* and the reason for `abort` is `preemption`. This approach works well for jobs of regular YTsaurus operations, since a typical job in YTsaurus has a duration of a few minutes, and losing a job progress is not a problem — the scheduler will restart the job later when there are available resources for it. Map-Reduce has delays of about several minutes, so this situation is absolutely tolerable. The situation with CHYT is a bit more complicated. The built-in ClickHouse protocol does not provide a mechanism to recover queries that failed due to a component failure at runtime. This means that if queries are now running on the instance and `abort` is sent to the job where the instance is running, then all running queries will fail with network layer errors (`NetException`, `Attempt to read after eof`, `Connection refused`, and others). By then, the client could have returned a random set of strings from the response to the query, which only complicates the task of restarting the query. ## Interruption { #interruption } MR jobs have long used a mechanism called *Interruption*: if YTsaurus realizes that it is about to interrupt a job, it simply stops feeding data to the job's input. If the job manages to complete within a certain `interruption_timeout`, it is considered `completed`, and the data it has not processed will be fed to a new job. If the job does not manage to complete within an `interruption_timeout`, it is simply aborted and its input is fully processed by another job. ClickHouse instances do not process any input in the usual way for YTsaurus, independently executing queries coming from the outer world, so information about the upcoming interruption comes to such jobs in a different way. For CHYT, the `interruption_signal` option is supported in the operation specification. This is a signal that will be sent to the job to notify it of an imminent completion. When receiving the signal, the instance goes into completion mode, sends this information to the others using gossip, and no longer accepts any new queries, but continues executing those that are already in progress. After all current queries are executed, the instance will end with the `completed` status. ## Regular preemption and graceful preemption { #preemption-dif } The YTsaurus scheduler preempts jobs based on the following logic: if a node has jobs whose operations have a lower `fair_share` than their `usage_share`, this job is considered `preemptible`. If the scheduler has a job of the starving operation that can be run on a node if preemptible jobs are preempted, then it does it — it preempts the required number of preemptible jobs and plans new ones in their place. In this model, it is unacceptable to wait very long to preempt jobs, so the `interruption_timeout` is only 15 seconds. A typical usage scenario for CHYT is small and medium queries that run from single-digit seconds to several minutes; `interruption_timeout = 15 sec` is often less than the standard query time, which means that many queries that were running on a given instance will end with an error. To combat instance preemption, you can start a clique in the pool with guarantees, but not all users have guarantees. Besides that, some users don't want the clique to interfere with other operations in the pool: they need it to scale down when there is a demand for pool resources and expand when there are extra resources. To solve this problem, a new operation scheduling mode — *Graceful Preemption* — was added. To avoid preemption problems in this mode, preemption is performed in advance: if there are `preemptible` jobs, a completion signal is sent to them, regardless of whether there is a candidate for these resources. Thus, the operation independently seeks that `usage_share = fair_share`, catching up with the fair amount of resources it's due at the moment. Due to this, you can increase the preemption timeout to 10 minutes, because no one is idle, waiting for the release of resources. Below is an example of how `usage_share` (the orange graph) and `fair_share` (the green graph) change after a heavyweight operation is started in the same pool as the clique. ![](https://ytsaurus.tech/_images/chyt_graceful_preemption.png){ .center } To use *Graceful Preemption*, set it with the `--spec {preemption_mode = graceful}` parameter. # Performance and recommendations When choosing between CH and CHYT, the question arises — which is faster and by how much? We cannot give a general answer to this question, since the query execution speed depends on many factors, including the query itself, the number of resources, and the data storage schema. The data in YTsaurus is not originally designed to execute OLAP queries. To get normal speed, you need to prepare them. You are unlikely to achieve the speed of CH, but you can get closer, choosing the proper schema for storing data and queries to it. But if you store data improperly, you can easily achieve query speeds hundreds of times lower than in CH. ## Where the time goes { #where_my_time } Below are the query execution stages that can be a bottleneck. 1. Reading table and chunk metainformation, distributing a query to instances. 2. Reading data from the disk. 3. Transmitting data over the network. 4. Data uncompressing and decoding. 5. Version merging (for [dynamic](https://ytsaurus.tech/dynamic-tables/overview.md) tables only). 6. Converting data from YTsaurus format into CH format. 7. Data processing by the ClickHouse engine. 8. Converting data from CH format into YTsaurus format (for write operations only). 9. Writing data in YTsaurus (for write operations only). Depending on how the data is stored and what queries are processed, different stages can be a bottleneck. ## Reading data { #read } Below are the factors that affect the read query speed. ### Processed data size { #data_weight } CHYT is primarily designed for fast (a few seconds) processing of small (about 1 GB) and medium (about tens of GB) data. You cannot use CHYT effectively to process TB of data. You can reduce the amount of processed data with a right data storage [schema](https://ytsaurus.tech/storage/static-schema.md) (using column orientation and sorting). To build dashboards on top of a large amount of data, we recommend making separate tables with the necessary data and pre-aggregated values. For example, if the data is specified in milliseconds and you need to build analytics by days, you need to pre-aggregate the data to a day. How to reduce the amount of computations on the fly: * For example, you can compute the date by the `datetime` field, but filtering by this field will be slower. * Avoiding unreasonably heavy queries. For example, sorting 100 GB, `SELECT DISTINCT` or `GROUP BY` by the column that has millions of different values, or using the `JOIN` operator for large tables. Such queries will always be processed slowly. Reading large tables can bump into any of the query execution points from the list above. ### Erasure coding { #erasure } Erasure coding is intended for "cold" tables. In erasure coding, data takes up less disk space, but this has a negative impact on the read speed of such data: 1. Each chunk is divided into 16 (or 9 depending on the coding algorithm) parts, which increases the amount of metainformation (stage 1). 2. Data is stored as a single copy, so parallel reading isn't possible (stage 2). 3. If the disk on the node is loaded, you cannot switch to read data from another node. And since the chunk is divided into 16 parts, the probability that reading at least one of them will freeze greatly increases. 4. If data is unavailable due to a cluster node failure, it must be restored. This is a rather long and resource-consuming process. (points 2, 4?). As a result, it takes much longer to read erasure-coded data and reading latency becomes more likely and less predictable. **We strongly do not recommend** building dashboards or making other regular queries on top of erasure-coded data. ### Data storage format { #optimize_for } Data in YTsaurus tables can be stored both in row-based (`optimize_for=lookup`) and columnar (`optimize_for=scan`) formats. {% note warning "Attention" %} ClickHouse is a column-oriented DBMS, so we highly recommend using columnar storage. {% endnote %} Columnar data storage has the following advantages: - Converting data into CH format from the YTsaurus columnar format is many times (if not dozens of times) more efficient than from row-based format (stage 6). - When columnar storage, only the requested columns will be read during the query. (stages 2, 3, and 4). - Columnar stored data is better compressed, so it takes up less space (stages 2, 3). {% note info %} The ordinary change of the `optimize_for/erasure_codec` attributes does not convert old data into the new format. To change the format, run the `merge` operation with the `force_transform=%true` option. {% endnote %} ### Key columns { #key_columns } ClickHouse has indexes and sharding keys for efficient reading. In YTsaurus, the data storage model is a bit different, there are no usual indexes, but there are sorted tables. Queries on top of sorted tables use the sort key as a primary index (primary key) and the sharding key. This enables you to: - Effectively filter and not read unnecessary data from the disk (stage 2). - Make a query, in particular `Sorted Join`, more effectively. - Better compress sorted tables so that they take up less space (stage 2). Select the sort key based on your queries: - For example, if the queries look like ``` ... where date between '2021-01-01' and '2021-02-01'``` A good key will be `date`. - For queries of type ``` ... where user_id = 1234``` A good key will be `user_id`. In some cases, if you sort the table, there will be no effective filtering. The column is string and the `DateTime` <-> `String` transformation is not monotonic and unambiguous. The `Int` <-> `DateTime` transformation is monotonic, but such optimization cannot be generally applied with string representation. For example, `2020-01-01 00:00:00` and `2020-01-01T00:00:00` are the correct representation of the same time moment in ClickHouse, but when sorting with string representation, the `2020-01-01 00:00:01` value may appear between them, so the `String` -> `DateTime` transformation is not monotonic and you cannot use this optimization. ### Number of chunks { #chunk_count } The number of chunks affects several query execution stages. The more chunks a table has, the more metainformation must be read at step 1. If a table has thousands of chunks, it may take several seconds to read metainformation. If there are many chunks in a very small table, reading each chunk becomes a random read and can be slow (step 2). On the other hand, if there are too few chunks (for example, 1 chunk per 10 GB), all data is stored on the same disks, which will limit the concurrency of reads at step 2. Use the `Merge` operation to enlarge the chunks. ### Number of input tables { #table_count } Similar to the number of chunks, but metainformation for a table is much heavier than metainformation for a chunk. Besides that, access permissions must be checked for each table separately. We recommend making no more than hundreds of tables. You better combine many tables into a single large one. ### Number of columns { #column_count } The number of columns in the table also affects the speed of data even if it is stored in the columnar format. The block size is selected so that the reader can read a block for each column and output the result. If you want the data to fit into memory in this case, the block size must decrease as the number of columns increases. When blocks are very small, reading becomes a random read (step 2). We recommend having dozens or a maximum of 100 columns. Thousands of columns are guaranteed to perform poorly. Besides that, the large number of columns increases the amount of metainformation at step 1. ### Dynamic vs static tables { #dyn-vs-stat } Data in dynamic tables is stored in a different way and is not intended for full-scan reading. Additional expenses are required to read them: 1. First, you have to read its own version for each value. 2. Second, you must read all the key columns for each row even if they are not used in the query. 3. Next, you need to merge all the rows with the same key, leaving only 1 most recent version. It's a very hard process with a lot of comparisons. In addition, the process is purely row-by-row, so even if the data is stored in the columnar format, it will have to be converted into the row-based format. 4. After that, you need to convert the data back into the columnar format. `Merge` can add x10 to the data read time. We strongly do not recommend building dashboards or making other regular queries on top of large dynamic tables due to the inefficiency of reading. ### SSD vs HDD { #ssd_vs_hdd } SSDs have the obvious advantage of higher read speed and lower latency at step 2. ### Optional columns { #optional_columns } Most columns in YTsaurus are optional (`required: false`), `Null` can be stored instead of a value. In CH, this is not the case, most columns are not `Nullable`. Using optional speakers slows down almost all stages, from reading and converting data to processing in CH. We recommend applying a more rigid schema and getting rid of optional columns when they are not required. ### Data caches { #data_caches } CHYT has a large number of caches to speed up some query execution stages. The main caches are `compressed_block_cache` and `uncompressed_block_cache`. Using the second one eliminates stages 2 and 3. Using the first one eliminates stages 2, 3, and 4 (uncompressing). Small tables will probably be always read from the cache. # FAQ #### **Q: Why does CHYT have cliques, while regular ClickHouse has nothing analogous? What is a clique?** {#chyt-cliques-explanation} **A:** There is a dedicated [article](https://ytsaurus.tech/docs/en/general.md) about this. ------ #### **Q: I am getting the "DB::NetException: Connection refused" or the "DB::Exception: Attempt to read after eof: while receiving packet" error. What does it mean?** {#connection-refused-error} **A:** This normally means that the CHYT process inside the Vanilla transaction crashed. You can view the aborted/failed job [counters](https://ytsaurus.tech/docs/en/cliques/ui.md) in the operation UI. If there are recent jobs aborted due to preemption, it means that the clique is short on resources. If there are recent failed jobs, please contact your system administrator. ------ #### **Q: I am getting the "Subquery exceeds data weight limit: XXX > YYY" error. What does it mean?** {#subquery-data-weight-limit} ------ #### **Q: How do I save to a table?** {#save-to-table} **A:** There are **INSERT INTO** and **CREATE TABLE** functions. Learn more in the section on [Differences from ClickHouse.](https://ytsaurus.tech/docs/en/yt-tables.md#save) ------ #### **Q: How do I load geo-dicts in my own clique?** {#load-geodictionaries-chyt} **A:** When starting any clique, you can specify the `--cypress-geodata-path` option that enables you to specify the path to geo-dicts in [Cypress](https://ytsaurus.tech/storage/cypress.md). ------ #### **Q: Can CHYT handle dates in TzDatetime format?** {#chyt-tzdatetime-handling} **A:** CHYT can handle dates in TzDatetime format just as well as conventional ClickHouse. You will have to store data as strings or numbers and convert them for reading and writing. Example date extraction by **@gri201**: ```sql toDate(reinterpretAsInt64(reverse(unhex(substring(hex(payment_dt), 1, 8))))) ``` ------ #### **Q: How do I move a table to an SSD?** {#how-to-set-ssd} **A:** First, make sure that your YTsaurus account has a quota for the **ssd_blobs** medium. To do this, go to the account page, switch your medium type to **ssd_blobs**, and enter your account name. If you have no quota for the **ssd_blobs** medium, you can request it via a special form. After obtaining the quota for the **ssd_blobs** medium, you will need to change the value of the `primary_medium` attribute, and the data will be moved to the corresponding medium in the background. Learn more in the section on [storage](https://ytsaurus.tech/faq/index.md). For static tables, you can force a move using the [Merge](https://ytsaurus.tech/docs/operations/merge.md) operation: ```bash yt set //home/dev/test_table/@primary_medium ssd_blobs yt merge --mode auto --spec '{"force_transform"=true;}' --src //home/dev/test_table --dst //home/dev/test_table ``` If the table is dynamic, to change the medium, you must first unmount the table, set the attribute, and then re-mount it: ```bash yt unmount-table //home/dev/test_table --sync yt set //home/dev/test_table/@primary_medium ssd_blobs yt mount-table //home/dev/test_table --sync ``` You can speed up the move further with [forced_compaction](https://ytsaurus.tech/dynamic-tables/overview.md#attributes) but using this method creates a heavy load in the cluster and is strongly discouraged. To verify that the table has in fact changed its medium, use the command below: ```bash $ yt get //home/dev/test_table/@chunk_media_statistics { "ssd_blobs" = { "chunk_count" = 2126; "uncompressed_data_size" = 9667220402266; "compressed_data_size" = 4954465956017; "data_weight" = 10764306825793; "max_block_size" = 6584787; }; } ``` ------ #### **Q: Is the SAMPLE construct of the ClickHouse language supported?** {#chyt-sample-support} **A:** CHYT supports the Sample construction. The difference is that CHYT ignores the `OFFSET ...` command, so you cannot get a sample from another part of the selected data. Example: ```SQL SELECT count(*) FROM "//tmp/sample_table" SAMPLE 0.05; SELECT count(*) FROM "//tmp/sample_table" SAMPLE 1/20; SELECT count(*) FROM "//tmp/sample_table" SAMPLE 100500; ``` ------ #### **Q: How do I get the table name in a query?** {#get-table-name-query} **A:** You can use the `$table_name` and `$table_path` virtual columns. For more information about the virtual columns, see [Working with YTsaurus tables](https://ytsaurus.tech/docs/en/yt-tables.md##virtual_columns). # Overview ## What is Spark? { #what-is-spark } [Apache Spark](https://spark.apache.org/) is an engine for computations using big data (joins, groups, filters, ets.). Spark uses RAM to process its data. The key difference between computing in memory and the "conventional" 2005 MapReduce model is that the data has minimum disk impact, which minimizes I/O costs as the slowest part of computing. For a single Map transaction, the effect from using Spark will be negligible. However, even a single Map and Reduce sequence saves on writing intermediate results out to disk provided there is enough memory. For each subsequent MapReduce sequence the efficiencies mount, and you can cache the output. For large and complex analytics pipelines, efficiency will increase many fold. Spark is also equipped by a full-scale [Catalyst](https://github.com/tupol/spark-catalyst-study/blob/master/docs/catalyst-description.md) query optimizer that plans execution and takes into consideration: - Input data location and size. - Predicate pushdown to the file system. - Expediency and step sequence in query execution. - Collection of final table attributes. - Use of local data for processing. - Potential for computation pipelining. ## What is SPYT? { #what-is-spyt } SPYT powered by Apache Spark enables a Spark cluster to be started with YTsaurus computational capacity. The cluster is started in a [YTsaurus Vanilla operation](https://ytsaurus.tech/user-guide/data-processing/operations/vanilla.md), then takes a certain amount of resources from the quota and occupies them constantly. Spark can read [static](https://ytsaurus.tech/user-guide/storage/static-tables.md), as well as [dynamic YTsaurus tables](https://ytsaurus.tech/dynamic-tables/overview.md), perform computations on them, and record the result in the static table. ## Compatibility of SPYT versions with Apache Spark{ #spyt-compatibility }, Java, Scala, and Python #| || **SPYT version** | **Spark version** | **Java** | **Scala** | **Python** || || 1.x.x, 2.0.x | 3.2.2 | 11 | 2.12 | 3.8, 3.9, 3.11, 3.12 || || 2.1.x, 2.2.x | 3.2.2 - 3.2.4 | 11 | 2.12 | 3.8, 3.9, 3.11, 3.12 || || 2.3.x, 2.4.x | 3.2.2 - 3.3.4 | 11 | 2.12 | 3.8, 3.9, 3.11, 3.12 || || 2.5.0 | 3.2.2 - 3.5.3 | 11 | 2.12 | 3.8, 3.9, 3.11, 3.12 || || 2.6.x, 2.7.x, 2.8.x | 3.2.2 - 3.5.7 | 11, 17 | 2.12 | 3.8, 3.9, 3.11, 3.12 || || 2.9.x | 3.2.2 - 3.5.8 | 11, 17 | 2.12 | 3.8, 3.9, 3.11, 3.12, 3.13 || || 2.10.x | 3.3.0 - 4.1.x | 17 | 2.12, 2.13 | 3.11, 3.12, 3.13 || || 2.11.x | 3.3.0 - 4.2.x | 17, 21, 25 | 2.12, 2.13 | 3.11, 3.12, 3.13 || |# {% note info %} Java 21 is supported starting from Spark 4.0.x, Java 25 — starting from Spark 4.2.x. Use Java 17 with Spark 3.x. {% endnote %} ## When to use SPYT { #what-to-do } SPYT is optimal in the following cases: - Developing in Java and using MapReduce in YTsaurus. - Optimizing pipeline performance on YTsaurus with two or more joins or groupings. - Writing integrational ETL pipelines from other storage systems. - Ad-hoc analytics in interactive mode using `Jupyter`, `pyspark`, `spark-shell`, or the [Query Tracker](https://ytsaurus.tech/query-tracker/about.md) component built into the UI. Do not use SPYT if: - You need to process over 10 TB of data in a single transaction. - Your processing boils down to individual Map or MapReduce operations. ## Ways to run Spark calculations in YTsaurus { #submit } - Submitting directly to YTsaurus using the `spark-submit` command. [Learn more](https://ytsaurus.tech/docs/en/launch.md#submit). - Creating a Standalone Spark cluster as a persistent resource within YTsaurus using a Vanilla operation. [Learn more](https://ytsaurus.tech/docs/en/launch.md#standalone). ## Supported programming languages { #lang } Spark supports the following languages and development environments: * [Jupyter](https://ytsaurus.tech/docs/en/API/spyt-jupyter.md). * [Python](https://ytsaurus.tech/docs/en/API/spyt-python.md). * [Java](https://ytsaurus.tech/docs/en/API/spyt-java.md). * [Scala](https://ytsaurus.tech/docs/en/API/spyt-scala.md). # Launching Spark tasks in YTsaurus ## Client install { #install } Install the `ytsaurus-spyt` package: ```bash $ pip install ytsaurus-spyt ``` ## Submitting Spark applications directly to YTsaurus (available from SPYT version 1.76.0) { #submit } This method is applicable when there is no need for continuous cluster operation. It allows to use cluster resources only on demand. This method works a little bit longer comparing to inner Spark standalone cluster due to the need to launch a separate operation for every application, but it allows to release cluster resources immediately after application completes. Direct submitting to YTsaurus is recommended in the following cases: - One-time calculations. - Low periodic tasks (less than once per hour). - Ad-hoc analytinc using `spark-shell` or `pyspark` console utilities (For analytics using `Jupyter` it is still needed to use inner standalone cluster). To use this method follow these steps: 1. Activate SPYT configuration with `source spyt-env` command. 2. Upload an application executable file and it's dependencies to Cypress. 3. Submit the application with the following command: ```bash $ spark-submit --master ytsaurus:// --deploy-mode cluster --num-executors 5 --queue research yt:/ ``` Options: - `--master` — cluster proxy address; - `--queue` — the name of the scheduler pool where the task should be launched The usage of other options corresponds to their description in `spark-submit` documentation (the full list is available with `spark-submit --help` command). Almost all of the options can be used except these: - `--files, --archives` — do not work with local files, you can only use those that have been pre-loaded to Cypress. ## Launching inner Spark Standalone cluster { #standalone } This method is applicable for intensive cluster usage. In this mode YTsaurus allocates resources for inner Spark standalone cluster which is used to launch Spark applications. This mode is recommended in the following cases: - Launching high-frequent tasks (more than once per hour). The efficiency is achieved because the task startup time in standalone cluster is significantly less than YTsaurus operation startup time. - Ad-hoc analytics in Jupyter notebooks. - Ad-hoc analytics using Query Tracker and [Livy](https://ytsaurus.tech/docs/en/cluster/livy.md) in SPYT versions below 2.10.0. Starting with SPYT 2.10.0 and Query Tracker 0.4, use [SPYT Connect](https://ytsaurus.tech/docs/en/connect.md). For launching an inner standalone Spark cluster follow these steps: 1. Select an account you will use to start your cluster. You will need to upload any code that regularly runs on Spark to the YTsaurus system. The account used to start the cluster must have enough privilege to read the code. 2. Create a directory for Spark housekeeping data, such as `my_discovery_path`. The account used to start the cluster must have write privileges to the directory. Users that will run Spark jobs must have read access to the directory. 3. Start your cluster: ```bash $ spark-launch-yt \ --proxy \ --pool my_pool \ --discovery-path my_discovery_path \ --worker-cores 16 \ --worker-num 5 \ --worker-memory 64G ``` Options: - `spark-launch-yt` — Start the Vanilla YTsaurus transaction from a client host. - `--proxy` — Cluster name. - `--pool` — YTsaurus computational pool. - `--spyt-version` — Spark housekeeping data directory. - `--worker-cores` — Number of [worker](https://ytsaurus.tech/docs/en/cluster/cluster-desc.md#spark-standalone-%D0%B2-yt--spark-standalone) cores. - `--worker-num` — Number of workers. - `--worker-memory` — Amount of each worker's memory. - `--spark-cluster-version` — Cluster [version](https://ytsaurus.tech/docs/en/version.md) (optional). 4. Start a test job on your cluster: ```bash $ spark-submit-yt \ --proxy \ --discovery-path my_discovery_path \ --deploy-mode cluster \ yt:///sys/spark/examples/smoke_test.py ``` Options: - `spark-submit-yt` — [spark-submit](https://spark.apache.org/docs/latest/submitting-applications.html) wrapper that enables you to find out the Spark master address from the Vanilla transaction. The search uses `proxy`, `id`, and `discovery-path` as arguments. - `--proxy` — Cluster name. - `--discovery-path` — Spark housekeeping data directory. - `--deploy-mode` (`cluster` or `client`) — Cluster startup [mode](https://ytsaurus.tech/docs/en/cluster/cluster-desc.md#cluster-mode--cluster-mode). - `--spyt-version` — SPYT version (optional). - Address of the file with the code in YTsaurus. ## Use { #use } - **spark-launch-yt** ```bash $ spark-launch-yt \ --proxy \ --pool my_pool \ --discovery-path my_discovery_path \ --worker-cores 16 \ --worker-num 5 \ --worker-memory 64G \ --spark-cluster-version 2.11.0 ``` - **spark-discovery-yt** Retrieving links to the UI master, transaction, Spark History Server: ```bash $ spark-discovery-yt \ --proxy \ --discovery-path my_discovery_path ``` - **spark-submit-yt** ```bash $ spark-submit-yt \ --proxy \ --discovery-path my_discovery_path \ --deploy-mode cluster \ --spyt-version 2.11.0 \ yt:///sys/spark/examples/smoke_test.py ``` {% note info "Note" %} You can set environment variables to use instead of some of the command arguments, such as `YT_PROXY` instead of `--proxy`. ```bash $ export YT_PROXY= $ spark-submit-yt \ --discovery-path my_discovery_path \ --deploy-mode cluster \ yt:///sys/spark/examples/smoke_test.py ``` {% endnote %} ## Additional parameters For additional cluster startup parameters, see [Starting a Spark cluster](https://ytsaurus.tech/docs/en/cluster/cluster-start.md). # Definitions in SPYT SPYT sits at the intersection of two systems: - Apache Spark as a compute engine; - YTsaurus as a data storage system, resource manager, and process launcher. Because of this, the same terms used in documentation and discussions may refer to different entities depending on the context. For example, the terms “cluster”, “job”, “operation”, “partition”, “table”, etc. are used differently in Spark and YTsaurus. The goal of this section is to provide a unified set of definitions and help distinguish overlapping concepts in Spark, SPYT, and YTsaurus. ## SPYT overview and launch modes ### SPYT overview **SPYT** (Spark over YTsaurus) is the integration of Apache Spark with the YTsaurus infrastructure. SPYT lets you run Spark applications on data stored in YTsaurus and use YTsaurus as an environment for resource allocation, data storage, logging, and storing service information. SPYT is not a separate compute engine: Spark performs the computations, while YTsaurus provides the infrastructure layer. ### SPYT cluster { #spyt-cluster } **SPYT cluster** is a pre‑launched Spark Standalone cluster inside YTsaurus. Such a cluster consists of a Spark Master, one or more Spark Workers, and usually a Spark History Server. In YTsaurus, it is launched as one or more [Vanilla operations](https://ytsaurus.tech/docs/en/#vanilla). It’s important not to confuse an SPYT cluster with a YTsaurus cluster: - **YTsaurus cluster** is the entire compute cluster; - **SPYT cluster** is a Spark Standalone cluster deployed inside a YTsaurus cluster. ### Direct Submit { #direct-submit } **Direct Submit** is a mode for launching a Spark application without a pre‑launched SPYT cluster. In this mode, separate Spark Master and Spark Worker instances are not used. The driver is launched directly in YTsaurus, and executors are allocated on demand. From an architectural perspective, the YTsaurus scheduler largely takes on the role of resource management. Direct Submit is contrasted with the standalone cluster mode, where the application connects to an already running SPYT cluster. ## Spark components in the context of YTsaurus ### Spark Master { #spark-master } **Spark Master** is a component of a Spark Standalone cluster that manages workers and allocates resources among Spark applications. In SPYT, Spark Master exists only in standalone cluster mode. It is launched inside YTsaurus as part of an SPYT cluster and publishes information about itself in [discovery_path](https://ytsaurus.tech/docs/en/#discovery-path). There is no Spark Master in Direct Submit mode. ### Spark Worker { #spark-worker } **Spark Worker** is a component of a Spark Standalone cluster that manages the resources of a specific node and launches executors when instructed by the Spark Master. Spark Worker exists only in an SPYT cluster. Workers are absent in Direct Submit mode. It’s important not to confuse Spark Worker and Executor: - **Worker** is part of the Spark cluster that manages node resources; - **Executor** is a process of a specific Spark application that performs computations. ### Spark Driver { #spark-driver } **Spark Driver** is the coordinating process of a Spark application. The driver builds an execution plan, breaks down computations into stages and tasks, requests resources, sends tasks to executors, and collects results. In SPYT, the driver can be launched: - as a separate process inside YTsaurus; - locally on a client machine if client mode is used. It’s important not to confuse the driver and [Spark Master](https://ytsaurus.tech/docs/en/#spark-master): - **Spark Master** manages the cluster; - **Spark Driver** manages one specific Spark application. ### Executor { #executor } **Executor** is a Spark application process that directly performs tasks on data. Executors are launched upon request from the driver and operate within a single Spark application. They read data, perform computations, participate in shuffle, and can store cached data. In SPYT, executors are launched inside YTsaurus and use resources allocated by YTsaurus. It’s important not to confuse an executor with [Spark Worker](https://ytsaurus.tech/docs/en/#spark-worker): - **Worker** provides resources; - **Executor** consumes those resources to perform application tasks. ### Spark History Server { #shs } **Spark History Server** (SHS) is a service for viewing logs of completed Spark applications. It lets you analyze completed applications, their stages, tasks, resource consumption, and errors after they finish running. In SPYT, SHS is usually launched as part of an SPYT cluster. Its data source is the event logs of Spark applications. ## Data — YTsaurus tables vs Spark abstractions { #data } ### Spark Partition and YTsaurus Chunk/Partition { #partitions } In the context of SPYT, the word “partition” can refer to different entities, so it’s important to distinguish at least three concepts: **Spark Partition**, **YTsaurus Chunk**, and **YTsaurus Partition**. **Spark Partition** is a unit of parallelism in Spark. One Spark task usually processes one Spark partition. Spark Partition determines how data is distributed across tasks and define the parallelism level for reads and computations. **YTsaurus Chunk** is a unit of physical data storage in YTsaurus. A chunk is a low‑level storage and replication object. A YTsaurus table physically consists of chunks, and reading data in batch scenarios ultimately relies on them. **YTsaurus Partition** is a logical data partitioning in YTsaurus, depending on the table type and processing scenario. When reading YTsaurus tables via SPYT, these entities do not directly correspond: - **chunk** refers to the physical storage of data in YTsaurus; - **YTsaurus partition** refers to the logical or internal data partitioning in YTsaurus; - **Spark partition** refers to computation execution in Spark. ### DataFrame { #dataframe } **DataFrame** is the main structured data abstraction in Spark SQL. A DataFrame is a collection of rows with named columns and a schema. In SPYT, a DataFrame is usually created by reading YTsaurus tables. It’s important not to confuse a DataFrame and a YTsaurus table: - **YTsaurus table** is a data storage object in YTsaurus; - **DataFrame** is a data representation inside a Spark application. ## Infrastructure and integration { #infra } ### discovery_path { #discovery-path } **`discovery_path`** is a path in YTsaurus Cypress that Spark applications use to find a running SPYT cluster. In cluster mode, the Spark Master publishes service information in this path: service addresses, connection parameters, and other cluster metadata. `discovery_path` is a concept specific to SPYT. Standard Spark does not have such a mechanism. ### Vanilla operation in YTsaurus { #vanilla } **Vanilla operation in YTsaurus** is a universal type of YTsaurus operation for launching arbitrary user processes. In SPYT, Vanilla operations are used to launch Spark infrastructure components: - `Spark Master`; - `Spark Worker`; - `Spark Driver`; - `Executor`; - `Spark History Server`. ### Shuffle { #shuffle } **Shuffle** is a mechanism for redistributing data among executors in Spark. Shuffle occurs when data needs to be regrouped by key or redistributed among nodes for further computations. Typical examples include `join`, `groupBy`, `distinct`, sorts, and some window operations. Shuffle is one of the most expensive operations in Spark because it involves network communication, serialization, writing intermediate data, and additional load on memory and disk. ### YTsaurus Shuffle Service { #yt-shuffle-service } **YTsaurus Shuffle Service** is an implementation of the shuffle infrastructure for SPYT on top of YTsaurus. It is used for storing and transferring intermediate shuffle data in the integration of Spark with YTsaurus. This is an infrastructure component that helps adapt Spark’s shuffle model to the YTsaurus environment. It’s important to distinguish between: - **shuffle** is a Spark mechanism; - **YTsaurus Shuffle Service** is an infrastructure implementation for storing and transferring shuffle data in YTsaurus. ## Work units — Spark vs YTsaurus { #units } ### Spark Application { #spark-application } **Spark Application** is a unit of a user program in Spark. A Spark application consists of: - one driver; - a set of executors; - a logical computation plan and related job, stage, and task. From a practical perspective, one launch of `spark-submit`, `spark-submit-yt`, or one user Spark session corresponds to one Spark Application. It’s important not to confuse Spark Application with a YTsaurus operation, Spark Job, or YTsaurus Job: - **Spark Application** is a logical execution unit in Spark; - **YTsaurus operation** is an infrastructure unit for launching processes in YTsaurus. One Spark application in SPYT can use one or more YTsaurus operations to launch its components. Conversely, one YTsaurus operation can be used to launch several Spark applications (`standalone cluster`). ### Spark Job { #spark-job } **Spark Job** is a unit of computation inside a Spark application, typically spawned by a single action. For example, calls to `count()`, `collect()`, `show()`, or writing a result can launch separate Spark Jobs. A Spark Job is divided into: - **stage** — an execution stage; - **task** — an individual task within a stage. ### YTsaurus Job { #yt-job } **YTsaurus Job** is a process launched by the YTsaurus scheduler within a YTsaurus operation. This is an infrastructure execution unit in YTsaurus. For example, in the context of SPYT, a YTsaurus Job may correspond to a process where an executor or another Spark component runs. The difference between Spark Job and YTsaurus Job is fundamental: - **Spark Job** is a logical unit of the Spark execution plan; - **YTsaurus Job** is an infrastructure process launched by YTsaurus. # Direct SPYT Submit This section provides a high-level description of direct submit to YTsaurus and its components. The launch of Spark applications in direct submit mode is described in a separate [section](https://ytsaurus.tech/docs/launch.md#submit). ## Operating Principle {#explanation} The key idea is that Spark delegates resource management to the YTsaurus scheduler. ### Resource Allocation for the Driver {#driver-resources} - **Cluster mode**: The driver is also launched as a separate Vanilla operation within the YTsaurus cluster. In this mode, the driver's lifecycle (including logging, monitoring, and restart on failure) is fully controlled by YTsaurus. Suitable for production tasks. - **Client mode**: The driver is launched on a client host — for example, on a developer's local machine or in a Jupyter notebook. This mode is convenient for debugging and interactive work, as it provides direct access to the driver's logs and state. {% note warning "Warning" %} In client mode, network access between executors and the driver must be ensured. {% endnote %} ### Resource Allocation for Executors {#executor-resources} In this mode, the scheduler allocates resources to executors directly as part of a separate Vanilla operation on the YTsaurus cluster. A job within the operation corresponds to one Spark executor. The specifications for executors (number of CPUs, amount of memory, disk type, etc.) are described in the Task section of the operation specification. It is possible to describe different resource profiles for Spark executors for tasks with heterogeneous workload during the lifecycle. ![](https://ytsaurus.tech/_images/spyt-direct-submit-operation-concept.png){ .center } ### Configuring YTsaurus Operation {#operation-config} In addition to the standard Spark parameters for the driver and executors, you can further configure operations and tasks through the corresponding conf parameters `spark.ytsaurus.{driver|executor}.{operation|task}.parameters`. The value is specified as a YSON string with valid Vanilla operation parameters. For example, you can set the type and amount of attached disks: ```bash --conf spark.ytsaurus.executor.task.parameters='{"disk_request"={"disk_space"=1234567; "account"="disk_account_name"; "medium_name"="ssd_slots_physical";}; }' ``` The full list of parameters can be found in the section [operation options](https://ytsaurus.tech/operations/operations-options.md). ### Using Docker Images and Porto Layers {#images} When directly submitting Spark applications to YTsaurus, you can use both Docker images and Porto layers for driver and executor tasks. This allows you to prepare the execution environment with the required system libraries, Python packages, and other dependencies. You can find information on building your own Docker image in the [cluster configuration section](https://ytsaurus.tech/docs/cluster/configuration.md#build-image). Docker images or Porto layers are passed via additional parameters: ```bash OP_SPEC='{"docker_image"="[REGISTRY/]IMAGE:TAG";}' spark-submit --master "ytsaurus://:" \ --deploy-mode cluster \ --num-executors 1 \ --queue \ --conf spark.ytsaurus.executor.task.parameters="$OP_SPEC" \ --conf spark.ytsaurus.driver.task.parameters="$OP_SPEC" \ ./spark-job.py ``` For more details, see the [root filesystem images section](https://ytsaurus.tech/layers/layer-paths.md). ## Advantages of the Approach {#advantages} - **Efficient resource usage** — allocation on demand, minimal downtime. - **Simple management** — a single level of control instead of duplication (YTsaurus + Standalone). - **Flexibility** — operation without losses at low load. - **Fast task startup** — no need to pre-launch the cluster. - **Easy migration** — compatibility with other clusters (including Hadoop) due to standard SparkAPI. ## Executing Local Files {#submit-local} Starting with SPYT version 2.4.0, when launching tasks directly in YTsaurus, you can specify local files as executable modules and dependencies without pre-loading them to Cypress. In this case, local files will be pre-loaded into the file cache on Cypress and then used from there in the application. On re-launch, the cached files will be used. An example of the `spark-submit` command for this method is provided below: ```bash spark-submit --master ytsaurus:// \ --deploy-mode cluster \ --num-executors 5 \ --executor-cores 4 \ --py-files ~/path/to/my/dependencies.py \ ~/path/to/my/script.py ``` # Dynamic Resource Allocation (available since SPYT version 2.8.0) Dynamic allocation allows you to scale resources to meet the needs of a Spark application within specified limits. To use dynamic allocation, you need a connected external shuffle service that ensures data integrity when executors are removed. To enable the option, you need to set the following parameters: ``` --conf spark.dynamicAllocation.enabled=true --conf spark.ytsaurus.shuffle.enabled=true ``` In addition, you need to specify the minimum and maximum number of executors, and additional parameters if necessary, using the standard [Spark configuration](https://spark.apache.org/docs/latest/configuration.html#dynamic-allocation): ``` --conf spark.dynamicAllocation.minExecutors=1 # minimum during idle --conf spark.dynamicAllocation.maxExecutors=10 # maximum under load --conf spark.dynamicAllocation.executorIdleTimeout=120s # timeout for stopping due to inactivity --conf spark.dynamicAllocation.initialExecutors=5 # initial number of executors ``` Due to the specifics of operations in YTsaurus, the `minExecutors` parameter must not be zero. {% note info "Note" %} Currently, dynamic allocation does not support multiple executor resource profiles. {% endnote %} If everything is set up correctly and the nature of the application implies uneven use of computing resources, you will see the CPU count changing over time on the operation monitoring page. ![](https://ytsaurus.tech/_images/spyt-direct-submit-dyn-allocation.png){ .center width="40%" } # Diagnostics To diagnose the operation of direct submit, you can use the following tools: - **Job log analysis** — diagnostic information can be obtained in [YTsaurus operation logs](https://ytsaurus.tech/docs/en/desc.md#joblogs). - **SparkUI** — for monitoring during application execution. [Below](https://ytsaurus.tech/docs/en/desc.md#info) is information on how to get a link to SparkUI. - **Spark History Server** — for analyzing the Spark application event logs after it has completed. [More details](https://ytsaurus.tech/docs/en/shs.md). ### Job Log Analysis {#joblogs} To analyze job logs, you need to go to the page of the corresponding driver or executor operation, go to the Jobs tab, and select the stderr of the desired job for viewing. Please note the following: - Only logs that are written to stderr are available, so you need to make the appropriate logging settings in the running Spark application. - Currently, the driver and executors are launched in different YTsaurus operations, so logs need to be checked in both. ![](https://ytsaurus.tech/_images/spyt-direct-submit-job-logs.png){ .center } ### Reference Information {#info} After launching a Spark application, additional reference information about the Spark application becomes automatically available in the driver operation: the operation ID with executors for quick access to logs, as well as the SparkUI URL. ![](https://ytsaurus.tech/_images/spyt-direct-submit-info.png){ .center width="75%"} # Spark History Server (SHS) To use the History Server in this mode, it must be launched separately. The main parameters of the `shs-launch-yt` command are a subset of the parameters of the [spark-launch-yt](https://ytsaurus.tech/docs/cluster/cluster-start.md#spark-launch-yt-params) command that are relevant for launching the History Server. Example of launching the History Server: ```bash $ shs-launch-yt --proxy --discovery-path ``` Example: ```bash $ shs-launch-yt --proxy my.ytsaurus.cluster.net --discovery-path //home/user/spark/discovery ``` To save event logs for a task launch, the `spark-submit` command should include the following parameters: `--conf spark.eventLog.enabled=true --conf spark.eventLog.dir=ytEventLog://logs/event_log_table` In this case, the event logs for the launch will be saved in the corresponding table and will be available in the History Server, which uses this table as a log storage. {% note info %} When started, the History Server indexes all records from the event logs source, and processing each record may take 10–15 seconds. New logs become available only after complete indexing of all existing data is finished. In high-load clusters, this can lead to significant delays when starting or restarting the History Server. Therefore, for production clusters with an intense flow of tasks, it is recommended to use a new (empty) event logs table when launching a new Spark cluster. If necessary, the old table can be archived (move the table). {% endnote %} # SPYT cluster This section features a top-level description of the SPYT cluster, its component, and the Spark application framework. Cluster start is described in a separate [section](https://ytsaurus.tech/docs/en/cluster-start.md). ## Spark app { #spark-app } The Spark app consists of a *driver* and *executors*. 1. The driver performs the main code, plans SQL queries, and breaks them up into *tasks* that can be performed at the same time. 2. The driver doesn't perform tasks itself, but delegates them to executors. The driver sends the scheduler a request to create executors. 3. The scheduler identifies containers of the needed size on the cluster nodes, and sends the executors to them. 4. Executors are created in the cluster and exchange messages with the driver over the network. ![](https://ytsaurus.tech/_images/cluster_structure.png){ .center } When the app is launched, the executors' characteristics (cores and memory) must be specified, as well as the maximum number of executors. By default, executors with 4 cores and 16 GB of memory (4 GB per core) are specified in all SPYT clients. This setting should fit most tasks. {% note info "Note" %} The executor can perform several tasks simultaneously. Usually, the number of subtasks the executor can perform is equal to the number of cores it has. {% endnote %} ### Number of cores in the executor { #number-cores } Spark cannot change the characteristics of the executor on the fly, but it can work with a smaller number of executors than was initially intended. An executor is a single process, a single JVM. If the cluster has no workers with 16 free GB of memory, the executor won't be created even if a smaller amount of memory is available in the cluster. 16 GB of memory must be free all at once. A separate thread that will perform tasks is created for each core of the executor, in its JVM. This brings with it all the usual downsides of multi-thread models: the more threads, the more expensive the model is. Executors with a single core are not profitable due to the expense of communications between executors, broadcast messages, and singleton objects that executor threads can recycle among themselves. For example, to make a map-side join, a copy of a small table is attached to each executor. If you have one executor with four cores, you'll only need to store one copy of the table. If you have four executors with one core each, you'll need to store four copies of the table, and therefore more memory. A more detailed description is available in the [Spark documentation](https://spark.apache.org/docs/latest/cluster-overview.html). ## Spark Standalone in YTsaurus { #sparkl-standalone } To get executors, the driver sends a request to the resource *scheduler*. To create containers of the appropriate size, you need to know what cluster nodes exist, and how many resources they have. Usually, container creation is performed by [YARN](https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/YARN.html) or [Mesos](https://mesos.apache.org/), and YTsaurus has its own resource scheduler. There are two methods for launching Spark applications in the current implementation: [submitting directly using YTsaurus scheduler](https://ytsaurus.tech/docs/launch.md#submit) or deploying an inner Spark standalone cluster using Vanilla operation. Spark Standalone consists of a *master* and *workers*. Each worker knows how many resources can be borrowed on the cluster node where the worker is launched, and how to provide resources on request. The master coordinates the work: it receives requests from the driver and decides what workers to create executors for. ![](https://ytsaurus.tech/_images/cluster_scheme.png){ .center } To request a resource from the master, the driver must know the master host and port. When the Vanilla operation is launched, the Spark master goes to a random YTsaurus cluster node, and opens a port ranging from 27000 to 27200 there. Then, the Spark master specifies its host and port in Cypress, in a special `discovery-path` directory. There is code in the driver that talks to Cypress, finds out the host and port of the Spark master, and then talks to it. ![](https://ytsaurus.tech/_images/cluster_scheme_cyp.png){ .center } The scheduler's cluster is started with the `spark-launch-yt` command. The command argument must include: - How many resources (cores/memory) each worker will have. - Number of workers. - Which Cypress directory to use as the `discovery-path`. {% note warning "Attention!" %} The difference between an app and a cluster: - The driver and the executors comprise the Spark app. The Spark app performs the application task of the user: for example, to make a join of two tables and record the result. - The master and workers comprise the Spark cluster. The Spark cluster answers requests from apps to launch new executors. {% endnote %} ## Cluster mode { #cluster-mode } In the schemas of the previous unit, the driver was represented inside of Jupyter. This is one of the options for starting Spark - *client mode*. In this option, the driver is performed on the client host (outside of the cluster). Executors, meanwhile, are created in the cluster. There is also another start option, called *cluster mode*. In this option, the driver is launched in the cluster along with the executors. To work in cluster mode, the Spark Standalone master must be commanded to launch the driver from a pre-created and pre-assembled application code. For this, the `spark-submit-yt` utility exists. The master creates a container for the driver on one of the workers. The worker downloads the code into the container and launches the driver. After that, the driver works the same as in *client mode*. The driver requests executors from the master, performs the code, and delegates tasks. By default, workers can download driver code in HTTP, FTP, and other APIs. With SPYT, workers can upload the driver code from YTsaurus. ![](https://ytsaurus.tech/_images/cluster_mode.png){ .center } # Starting a Spark cluster This section contains expanded instructions for starting a Spark cluster. Basic start operations are described in the [Quick start](https://ytsaurus.tech/docs/launch.md#standalone) section. {% note warning "Attention!" %} A started Spark cluster statically occupies the resources allocated to it. So, it is recommended that the cluster be started in a separate computational pool with guaranteed resources. It makes sense to use one cluster for the command, and recycle the resources among several users. We recommend [launching tasks directly](https://ytsaurus.tech/docs/launch.md#submit) if you do not plan to launch Spark tasks at a high intensity (more than once per hour). {% endnote %} ## Auto-scaler { #auto-scale } To save resources of the computational pool if the load is low, a special auto-scaler mode can be turned on in Spark, which proportionally decreases the resources used. ### How it works { #how-works } The operation YTsaurus has an `update_operation_parameters` method that enables the operation parameters to be changed. The number of jobs in the operation can be changed via the `user_slots` parameter. When the parameter is changed, the scheduler stops some of the jobs, or launches new ones (within the limit specified at the start of the operation). Since the scheduler believes that all jobs in the operation are the same, this scaling method, performed in the regular mode in Spark, could lead to loss of the master or the history server, as well as of workers that Spark job drivers are being performed for. To prevent disruptions to the Spark cluster's operation, it is started not as a single YTsaurus operation, but as several. This way, one operation is allocated to the dynamically changing set of workers, and can scale within the limits configured at the start. In one or two other operations, a master and history-server (one operation), or a driver (when drivers are launched on the cluster, two operations) are performed. ### Starting a cluster with an auto-scaler { #start-auto } Additional parameters are used for the `spark-launch-yt` launch script, or similar parameters of the SPYT client library. - The `autoscaler-period ` is the frequency of auto-scaler launches, and (potentially) of changes in the operation settings. The period is programmed in ``. - `enable-multi-operation-mode` turns on Spark start mode in multiple YTsaurus operations. - `enable-dedicated-driver-operation-mode` launches workers for drivers in a separate YTsaurus operation. - `driver-num ` allocates a certain number of workers for the driver. - `autoscaler-max-free-workers` is the maximum value of free workers (all superfluous workers will be stopped). - `autoscaler-slot-increment-step` is the increment in which the number of workers is increased when the cluster is automatically expanded. Example: ```bash $ spark-launch-yt --proxy --autoscaler-period 1s --enable-multi-operation-mode --discovery-path //discovery/path ``` ## Updating a cluster { #update-cluster } To update a Spark cluster, the following actions must be performed: 1. Stop the operation with the current cluster in YTsaurus. You can find a link to the operation using `spark-discovery-yt`. You can also use the `--abort-existing` flag of the `spark-launch-yt` command. In this case, the current cluster will be stopped before the new one starts. 2. Start a cluster using `spark-launch-yt`. The desired version can be specified in the `spark-cluster-version` argument. If no version is specified, the last version will be started. ## spark-launch-yt parameters { #spark-launch-yt-params } To start an internal Spark cluster, use the `spark-launch-yt` command and pass a number of parameters to it. They are described in the table below: | **Parameter** | **Required** | **Default value** | **Description** | **Starting with version** | | ------------ | ---------------- | ------------------------- | ------------ | ------------------ | | `--discovery-path` | yes | - | Path to the directory for service data on Cypress (Discovery path). | - | | `--proxy` | no | Value from the `YT_PROXY` environment variable. | YTsaurus cluster address | - | | `--pool` | no | - | YTsaurus compute pool that will be used to start the cluster. | - | | `--operation-title` | no | `spark_` | The name of the YTsaurus operation with a Spark cluster | 2.9.0 | | `--operation-alias` | no | - | Alias for the YTsaurus operation with a Spark cluster. The operation alias must start with the `*` character | - | | `--params` | no | - | Additional YTsaurus operation parameters specified as a YSON string. Learn more on the [configuration](https://ytsaurus.tech/docs/en/configuration.md#add) page. | - | | `--spyt-version` | no | Version of the `ytsaurus-spyt` package on the client. | SPYT version that will be used to start the cluster. | - | | `--preemption-mode` | no | normal | Preemption mode used by the YTsaurus scheduler. Possible values: "normal" or "graceful". | - | | `--enable-mtn`, `--disable-mtn` | no | `--disable-mtn` | Use Multi-Tenant Network (MTN) to start the cluster. When using MTN, you must also specify the network project via the `--network-project` parameter. | - | | `--network-project` | no | - | Name of the network project where a cluster operation will run. | - | | `--prefer-ipv6`, `--prefer-ipv4` | no | `--prefer-ipv4` | The type of IP addresses used for working with the cluster. If you use IPv6 addressing when working with Spark 3.4.0 and higher, set the `SPARK_PREFER_IPV6=true` environment variable. | - | | `--enable-tmpfs`, `--disable-tmpfs` | no | `--disable-tmpfs` | Use part of the RAM allocated to the Spark worker to mount tmpfs. When using tmpfs, you must also specify the amount via the `--tmpfs-limit` parameter. | - | | `--tmpfs-limit` | no | 8G | Amount of memory used for tmpfs. | - | | `--enable-tcp-proxy`, `--disable-tcp-proxy` | no | `--disable-tcp-proxy` | Use a TCP proxy for external access to the cluster. | 1.72.0 | | `--tcp-proxy-range-start` | no | 30000 | Start of the range of ports used for the TCP proxy. | 1.72.0 | | `--tcp-proxy-range-size` | no | 100 | Size of the range of ports used for the TCP proxy. | 1.72.0 | | `--enable-rpc-job-proxy`, `--disable-rpc-job-proxy` | no | `--enable-rpc-job-proxy` | Use an RPC proxy embedded in a job proxy. If this option is disabled, a general RPC proxy will be used, which may result in cluster performance degradation. | 1.77.0 | | `--enable-ytsaurus-shuffle`, `--disable-ytsaurus-shuffle` | no | `--disable-ytsaurus-shuffle` | Use the [YTsaurus Shuffle service](https://ytsaurus.tech/docs/shuffle.md) | 2.7.2 | | `--enable-spark-shuffle`, `--disable-spark-shuffle` | no | `--enable-spark-shuffle` | Run the native external Spark shuffle service on the cluster (see [Shuffle service](https://ytsaurus.tech/docs/shuffle.md#enabling-standalone)) | 2.9.3 | | `--rpc-job-proxy-thread-pool-size` | no | 4 | Size of a thread pool for the RPC job proxy. | 1.77.0 | | `--group-id` | no | - | Discovery group ID. Used when starting a cluster with multiple operations. | - | | `--enable-squashfs`, `--disable-squashfs` | no | `--disable-squashfs` | Use pre-configured SquashFS layers in YTsaurus jobs. | 2.6.0 | | `--cluster-java-home` | no | `/opt/jdk11` | Path to JAVA HOME in YTsaurus cluster containers. | 2.6.0 | | `--master-memory-limit` | no | 4G | Amount of memory allocated to the container with the Master server. | - | | `--master-port` | no | 27001 | Port for Spark RPC calls on the Master server. | - | | `--worker-cores` | yes | - | Number of CPU cores allocated to one worker. | - | | `--worker-memory` | yes | - | Amount of memory allocated to a worker. This amount will further be distributed among Spark processes, such as the driver and executors running on that worker. | - | | `--worker-num` | yes | - | Number of workers in the cluster. | - | | `--worker-cores-overhead` | no | 0 | Extra CPU cores allocated to a worker. These cores will not be used directly by Spark applications, but are required for auxiliary processes launched along with the worker. | - | | `--worker-memory-overhead` | no | 2G | Additional memory allocated to a worker. This amount is required for additional processes launched in the worker container, such as child processes run by executors. This amount is also needed when using `pyspark`, especially when working with `Python UDFs`. In this case, the executor will start a child `Python` process that will execute the `UDF` code and require additional memory. The standard recommended value for `--worker-memory-overhead` when working with Python UDFs is 40% of the amount allocated to the worker via the `--worker-memory` parameter. | - | | `--worker-timeout` | no | 10 min | The maximum time a worker will wait to register on the Spark master. After this time, the worker process will terminate with an error. The following time units are available: s (seconds), m or min (minutes), h (hours), and d (days). If no unit is specified, defaults to seconds. | - | | `--worker-gpu-limit` | no | 0 | Number of GPUs on a worker. | - | | `--worker-disk-name` | no | default | Name of the disk requested in the operation specification. For more information, see [Disk requests for jobs](https://ytsaurus.tech/operations/operations-options.md#disk_request). | - | | `--worker-disk-limit` | no | - | Maximum disk capacity requested in the operation specification. For more information, see [Disk requests for jobs](https://ytsaurus.tech/operations/operations-options.md#disk_request). | - | | `--worker-disk-account` | no | - | [Account](https://ytsaurus.tech/storage/accounts.md) for requesting a disk in the operation specification. For more information, see [Disk requests for jobs](https://ytsaurus.tech/operations/operations-options.md#disk_request). | - | | `--worker-port` | no | 27001 | Port for Spark RPC calls on the Worker server. | - | | `--enable-history-server`, `--disable-history-server` | no | `--enable-history-server` | Flag for running the History Server as part of the cluster. | - | | `--history-server-memory-limit` | no | 4G | Amount of memory for the History Server process. | - | | `--history-server-memory-overhead` | no | 2G | Extra memory allocated for auxiliary processes in the container with the History Server. | - | | `--history-server-cpu-limit` | no | 1 | Number of CPU cores allocated to the History Server. | - | | `--shs-location` | no | `--discovery-path`/logs | Path to the directory that contains event logs and is used in the History Server. | - | | `--ssd-account` | no | - | YTsaurus [account](https://ytsaurus.tech/storage/accounts.md) used for SSD allocation. | - | | `--ssd-limit` | no | 0 | The SSD disk capacity allocated to a YTsaurus job. | - | | `--abort-existing` | no | - | Stopping a running Spark cluster on the specified `--discovery-path` before starting a new one. | - | | `--cluster-log-level` | no | INFO | Cluster logging level. | - | | `--enable-stderr-table`, `--disable-stderr-table` | no | `--disable-stderr-table` | Write YTsaurus operation logs to a table. The table is located inside `--discovery-path`, in the `logs` directory. | - | | `--enable-advanced-event-log`, `--disable-advanced-event-log` | no | --disable-advanced-event-log | Write Spark application event logs to dynamic YTsaurus tables. | - | | `--enable-worker-log-transfer`, `--disable-worker-log-transfer` | no | --disable-worker-log-transfer | Transfer Spark application logs from a worker container to a table on Cypress. | - | | `--enable-worker-log-json-mode`, `--disable-worker-log-json-mode` | no | --disable-worker-log-json-mode | Write worker logs in JSON format. | - | | `--worker-log-update-interval` | no | 10 min | Frequency of transferring Spark application logs from a worker to Cypress. | - | | `--worker-log-table-ttl` | no | 7d | Lifetime of a log table for Spark applications on Cypress. | - | | `--enable-dedicated-driver-operation-mode`, `--disable-dedicated-driver-operation-mode` | no | `--disable-dedicated-driver-operation-mode` | Run a dedicated YTsaurus operation for a driver. | - | | `--driver-cores` | no | `--worker-cores` | Number of CPU cores allocated to a worker used to run drivers. | - | | `--driver-memory` | no | `--worker-memory` | The amount of memory allocated to a worker used to run drivers. | - | | `--driver-num` | no | `--worker-num` | Number of workers running specifically for drivers. | - | | `--driver-cores-overhead` | no | `--worker-cores-overhead` | Extra cores allocated to workers for drivers (see `--worker-cores-overhead`). | - | | `--driver-timeout` | no | `--worker-timeout` | The maximum time a worker will await registration on the Spark master. After this time, the worker process will terminate with an error. | - | | `--autoscaler-period`, `--autoscaler-metrics-port`, `--autoscaler-sliding-window`, `--autoscaler-max-free-workers`, `--autoscaler-slot-increment-step` | no | - | Parameters used for working with an [auto-scaler](https://ytsaurus.tech/docs/en/cluster-start.md#auto-scale). | - | | `--enable-livy` | no | false | **Deprecated and unavailable starting with SPYT 2.10.0.** Run the Livy server as part of the cluster. You can learn more about the Livy server [here](https://ytsaurus.tech/docs/en/livy.md). | 1.74.0 | | `--livy-driver-cores` | no | 1 | **Deprecated and unavailable starting with SPYT 2.10.0.** Number of CPU cores allocated to a driver started via Livy. | 1.74.0 | | `--livy-driver-memory` | no | 1G | **Deprecated and unavailable starting with SPYT 2.10.0.** Amount of CPU memory allocated to a driver started via Livy. | 1.74.0 | | `--livy-max-sessions` | no | 3 | **Deprecated and unavailable starting with SPYT 2.10.0.** Maximum number of sessions that can run on the Livy server simultaneously. | 1.74.0 | | `--id` | no | - | Deprecated. Left for backward compatibility. | - | | `--discovery-dir` | no | - | Deprecated. Left for backward compatibility. | - | | `-h`, `--help` | no | - | Command parameter reference | - | # Versioning rules SPYT includes two components: - A cluster with Spark pre-installed. - A client where the `ytsaurus-spyt` package is installed via `pip`. SPYT is built from the following artifacts: - A `.tar` archive with patched Spark that is deployed to YTsaurus and read at cluster startup; - `ytsaurus-pyspark` pip package with patched Spark files as in the `.tar` above; - `ytsaurus-spyt` pip package which in turn depends on `ytsaurus-pyspark` package. It contains the assembly of all SPYT modules and their dependencies that are not included in Spark distributive. The package is deployed to pypi repository and client hosts. It also contains CLI utilities for launching an inner Spark standalone cluster and for submitting applications to it. When a new version of `ytsaurus-spyt` is installed `ytsaurus-pyspark` updates automatically. In most cases, a `ytsaurus-spyt` update is sufficient. However, sometimes, to make new functionality work, you will have to update your cluster. You can find out the update procedure and scope based on the specific version. ## Update procedure { #how } - Cluster and client version number consists of three parts. Update of the last (minor) part means that backward compatibility is preserved and the change affects only one component. - When a middle (major) part of the version is updated backward compatibility with clusters of previous versions may not be preserved so it is recommended to use a client with the same major version as cluster. - Cluster version can be chosen on launching via `spark-launch-yt` in `--spyt-version` parameter. When the version is not specified the last release version will be used. - Client version in `client mode` (Jupyter notebooks) is the same as `ytsaurus-spyt` package version. - Client version in `cluster mode` can be specified using `--spyt-version` parameter for `spark-submit-yt` command. When the version is not specified the last compatible with the cluster version will be used. # Configurations For a listing of all the options supported by Spark, see the [documentation](https://spark.apache.org/docs/latest/configuration.html). ## Basic options { #main } Most options are available starting with version 1.23.0 if otherwise is not specified. | **Name** | **Default value** | **Description** | | ------------------- | --------------- | ------------------------------------------------------------ | | `spark.yt.write.batchSize` | `500000` | The amount of data transferred in a single `WriteTable` transaction | | `spark.yt.write.miniBatchSize` | `1000` | The amount of data part transmitted to `WriteTable` | | `spark.yt.write.timeout` | `120 seconds` | The timeout to write a single data block | | `spark.yt.write.typeV3.enabled` (`spark.yt.write.writingTypeV3.enabled` before 1.75.2) | `true` | Write tables with schema in [type_v3](https://ytsaurus.tech/storage/data-types.md) instead of `type_v1` | | `spark.yt.read.vectorized.capacity` | `1000` | The maximum number of rows in a batch for reading via `wire` protocol | | `spark.yt.read.arrow.enabled` | `true` | Use `arrow` format for reading (if possible) | | `spark.hadoop.yt.timeout` | `300 seconds` | The timeout to read from YTsaurus | | `spark.yt.read.typeV3.enabled` (`spark.yt.read.parsingTypeV3.enabled` before 1.75.2) | `true` | Read tables with schema in [type_v3](https://ytsaurus.tech/storage/data-types.md) instead of `type_v1` | | `spark.yt.read.keyColumnsFilterPushdown.enabled` | `true` | Pushdown Spark query filters to read from YTsaurus | | `spark.yt.read.keyColumnsFilterPushdown.union.enabled` | `false` | Merge table ranges into a single continius range when filter pushdown is enabled | | `spark.yt.read.keyColumnsFilterPushdown.mergeAdjacent.enabled` | `false` | Merge ranges over consecutive integer key values into a single range | | `spark.yt.read.keyColumnsFilterPushdown.ytPathCount.limit` | `100` | The maximum number of ranges for a table generated by filters | | `spark.yt.transaction.timeout` | `5 minutes` | Write operation transaction timeout | | `spark.yt.transaction.pingInterval` | `30 seconds` | Ping interval for write operation transaction | | `spark.yt.globalTransaction.enabled` | `false` | Use of a [global transaction](https://ytsaurus.tech/docs/read-transaction.md) | | `spark.yt.globalTransaction.id` | `None` | Id of a global transaction | | `spark.yt.globalTransaction.timeout` | `5 minutes` | Global transaction timeout | | `spark.hadoop.yt.user` | - | Name of YTsaurus user | | `spark.hadoop.yt.timeout` | - | Token of YTsaurus user | | `spark.yt.read.ytPartitioning.enabled` | `true` | Use YTsaurus API for table partitioning | | `spark.yt.read.planOptimization.enabled` | `false` | Enable aggregation and join optimization rules for sorted data | | `spark.yt.read.keyPartitioningSortedTables.enabled` | `true` | Use key table ranges for reading, required for plan optimization | | `spark.yt.read.keyPartitioningSortedTables.unionLimit` | `1` | The maximum number of partitions that need to be merged when key table ranges are used | ## Aggregation and join optimizations Spark cluster ignores a sort metadata about YTsaurus tables and creates many Shuffle and Sort operations, even if it's not required. For these cases extra optimization rules were added. When a logical plan is being constructing, a rule adds sort mark to source vertexes of sorted tables. Later on generating physical plan stage the marks are transformed to nodes that will not do data processing but it will notify optimizer about sorting and partitioning. Static tables partitioning is performed by index ranges, but the option `spark.yt.read.keyPartitioningSortedTables.enabled` enables partitioning by key ranges. When index ranges are being transforming into key ranges the amount of partitions might be reduced. It can lead increased a fraction of data that one executor processes. ## Additional options for cluster configuration { #add } Additional options are passed in via `--params`: ```bash spark-launch-yt \ --proxy \ --discovery-path my_discovery_path \ --params '{"spark_conf"={"spark.yt.jarCaching"="True";};"layer_paths"=["//.../ubuntu_xenial_app_lastest.tar.gz";...;];"operation_spec"={"max_failed_job_count"=100;};}' ``` ### Spark configuration When using `spark-launch-yt` to configure a cluster, `--params '{"spark_conf"={...};}` is available as an option: ```bash spark-launch-yt \ --proxy \ --discovery-path my_discovery_path \ --params '{"spark_conf"={"spark.sql.shuffle.partitions":1,"spark.cores.max":1,"spark.executor.cores"=1};}' ``` When using `spark-submit-yt` to configure a task, `spark_conf_args` is available as an option: ```bash spark-submit-yt \ --proxy \ --discovery-path my_discovery_path \ --deploy-mode cluster \ --conf spark.sql.shuffle.partitions=1 \ --conf spark.cores.max=1 \ --conf spark.executor.cores=1 \ yt:///sys/spark/examples/grouping_example.py ``` ```bash spark-submit-yt \ --proxy \ --discovery-path my_discovery_path \ --deploy-mode cluster \ --spark_conf_args '{"spark.sql.shuffle.partitions":1,"spark.cores.max":1,"spark.executor.cores"=1}' \ yt:///sys/spark/examples/grouping_example.py ``` When launching programmatically, you can configure via `spark_session.conf.set("...", "...")`. Python example: ```python from spyt import spark_session print("Hello world") with spark_session() as spark: spark.conf.set("spark.yt.read.parsingTypeV3.enabled", "true") spark.read.yt("//sys/spark/examples/test_data").show() ``` Java example: ```java protected void doRun(String[] args, SparkSession spark, CompoundClient yt) { spark.conf.set("spark.sql.adaptive.enabled", "false"); spark.read().format("yt").load("/sys/spark/examples/test_data").show(); } ``` ### Operation options When using `spark-launch-yt` to configure a cluster, `--params '{"operation_spec"={...};}` is available as an option. [List of all operations options](https://ytsaurus.tech/operations/operations-options.md). It will be useful if you need to change the standard operation settings, for example, to increase the number of failed jobs past which the operation is considered failed. ```bash spark-launch-yt \ --proxy \ --discovery-path my_discovery_path \ --params '{"operation_spec"={"max_failed_job_count"=100;owners=[...]};}' ``` ### Updating Python version There are two ways to update the Python version: 1. Install the required Python version: 1. Install the required Python version on the exec nodes 2. Add the Python version to `//home/spark/conf/global` and the path to the new interpreter. 3. After that, `spark-submit-yt` will be able to use it. The `--python-version` parameter 2. Build your own image with the required Python version ### Installing additional packages You need to build an image with the installed packages and use it as a base image to run the task. ### Building an image with installed packages #### Building an image { #build-image } Dockerfile example for building a python3.12 image with installed packages: ```docker # Dockerfile FROM mirror.gcr.io/ubuntu:focal USER root RUN apt-get update && apt-get install -y software-properties-common RUN add-apt-repository ppa:deadsnakes/ppa RUN apt-get update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \ containerd \ curl \ less \ gdb \ lsof \ strace \ telnet \ tini \ zstd \ unzip \ dnsutils \ iputils-ping \ lsb-release \ openjdk-11-jdk \ libidn11-dev \ python3.12 \ python3-pip \ python3.12-dev \ python3.12-distutils RUN ln -s /usr/lib/jvm/java-11-openjdk-amd64 /opt/jdk11 RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 \ && update-alternatives --install /usr/bin/python python /usr/bin/python3.12 1 COPY ./requirements.txt /requirements.txt # Ensure pip is installed correctly RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ && python3.12 get-pip.py \ && python3.12 -m pip install --upgrade pip setuptools wheel \ && rm get-pip.py RUN python3.12 -m pip install -r requirements.txt ``` ```text # requirements.txt ytsaurus-client==0.13.49 ytsaurus-spyt==2.11.0 pyspark==4.2.0 ``` #### Create a cluster with a docker image ```bash spark-launch-yt \ --params '{operation_spec={tasks={history={docker_image="MY_DOCKER_IMAGE"};master={docker_image="MY_DOCKER_IMAGE"};workers={docker_image="MY_DOCKER_IMAGE"}}}}' ``` # Logs ## Event log { #event-log } *Event log* is a special log format in Spark. Logs are written by a [driver](https://ytsaurus.tech/docs/en/cluster/cluster-desc.md#spark-app) and read by a special service called *Spark History Server (SHS)*. SHS launches alongside the master and the workers when a SPYT cluster is started. SHS uses the log to draw a UI similar to Spark UI. The difference between them is that Spark UI is available on the driver only while a job is running, whereas SHS is always up and can read the logs of completed jobs. Logs are written to a [dynamic table](https://ytsaurus.tech/dynamic-tables/overview.md) in `discovery-path`. SHS can delete them [automatically](https://ytsaurus.tech/docs/en/#log-settings). ## Enabling { #log-on } Set `spark.eventLog.enabled` to `true` on starting a job: ```bash $ spark-submit-yt ... --conf spark.eventLog.enabled=true ``` ## Configuring { #log-settings } For a detailed description of SHS settings, please see the [Spark documentation](https://spark.apache.org/docs/latest/monitoring.html#spark-history-server-configuration-options). You can pass any settings from this description in to `spark-launch-yt ... --params '{spark_conf={"spark.param"=value;...}}'`. By default, the `spark.history.fs.cleaner.enabled` is set to `true`. The other parameters' default values are as per the Spark documentation. If the parameters are set to their defaults, SHS deletes all logs older than one week. You can change this age limit using the setting: `spark.history.fs.cleaner.maxAge` (`spark-launch-yt ... --params '{spark_conf={"spark.history.fs.cleaner.maxAge"="14d";...}}'`). ## Worker logs {#worker-logs} When you start a cluster, you can configure [worker](https://ytsaurus.tech/docs/en/cluster/cluster-desc.md#spark-standalone) logs to upload to YTsaurus tables. Logs will be written to `{cluster_path}/logs/worker_log` with a separate table created for each date. To enable sending of logs when starting a cluster, you must use one of these options: * `--enable-worker-log-transfer`: Enables log forwarding. * `--enable-worker-log-json-mode`: Enables json mode for the logs. Table data are displayed in a more convenient format and broken down into component parts. If the option is disabled, logs are written to a single column called `messages`. * `--worker-log-update-interval ...` : Sets the frequency of log upload; the default value is 10 minutes (10m), the minimum value is 1 minute. * `--worker-log-table-ttl ...` : Sets the table age limit; the default is 7 days (7d). You can view logs on SHS: the *executors* tab of any *application* contains `[HS]`-prefixed links for every worker. A log page referenced by these links is generated from the YTsaurus tables; therefore, it is available even if a worker no longer is. A log persists until the relevant table is deleted: ```bash spark-launch-yt ... \ --enable-worker-log-transfer \ --enable-worker-log-json-mode \ --worker-log-update-interval 30m \ --worker-log-table-ttl 30d ``` # Troubleshooting ## Cluster generates errors on startup To get information on a running transaction, use the `spark-discovery-yt` utility: ```bash spark-discovery-yt --discovery-path my_discovery_path ``` A task list for this transaction always includes a master, a worker, and the Spark History Server. If the master is non-functional, so are the other components. If everything is running properly, the worker log will include data on the accepted task and the start of task execution. If a task produces an error at startup, you need to: - Open the task log via Master Web UI. It has links to the driver and the executor logs. - Open Worker UI via Master Web UI and review the application startup sequence. - View the [Event Log](https://ytsaurus.tech/docs/en/logs.md). # SPYT Connect SPYT Connect is a remote connection mechanism for a Spark driver, built on the [Spark Connect](https://spark.apache.org/docs/latest/spark-connect-overview.html) protocol. It lets you run Spark SQL queries via [Query Tracker](https://ytsaurus.tech/query-tracker/about.md) in YTsaurus. You can also work with data directly from Python code via the Spark Connect API, without installing a JVM on the client side. {% note info %} This mechanism replaces [Livy](https://ytsaurus.tech/docs/en/cluster/livy.md) starting from SPYT 2.10.0 and Query Tracker 0.4. {% endnote %} ## When query latency may occur {#request-latency} SPYT Connect starts the Spark driver on demand. If the driver is not active at the moment, you need to start it first — this takes time. The driver may be inactive in three cases: - First access to SPYT Connect. You’re just getting started, and the driver launches from scratch. - After idle time. To avoid wasting resources, the driver automatically stops after 10 minutes of inactivity. The next query will trigger its launch again. The idle timeout is configured via the `spark.ytsaurus.connect.idle.timeout` parameter in the [SPYT configuration settings](https://ytsaurus.tech/docs/en/thesaurus/configuration.md). - After changing settings. If you change the resource configuration for a query (for example, the number of cores), the old driver stops, and a new one starts with the updated settings. When you notice initialization delays depends on how you work with SPYT Connect: - **In Query Tracker (UI or API)**, session launch and sending a query to SPYT Connect happen within a single QT query. So when you launch it in the interface or send a query via the API, it seems that the query itself takes a long time to run. All subsequent queries will run quickly. - **In Python (Spark Connect API)**, you explicitly control the launch. The delay will occur exactly when you call the `start_connect_server` function (waiting for readiness). The actual computations and DataFrame operations will start without delays. ## Launch modes {#launch-modes} SPYT Connect operates in two modes — they determine how the Spark application is launched. The mode you choose affects the configuration and code in all connection methods. #| || **Mode** | **When to use** || || [Direct submit](https://ytsaurus.tech/docs/en/direct-submit/desc.md) | No dedicated cluster; the Spark application launches on demand for each query || || [Internal cluster](https://ytsaurus.tech/docs/en/cluster/cluster-desc.md) | The cluster is already running; SPYT Connect connects to it || |# ## Connection methods {#choose} #| || **Method** | **When to use** || || [Via Query Tracker UI](https://ytsaurus.tech/docs/en/#ui) | Suitable for analysts and anyone working with data via the YTsaurus interface || || [Via Query Tracker API](https://ytsaurus.tech/docs/en/#qt-api) | For automating SQL queries from Python || || [Via Spark Connect API](https://ytsaurus.tech/docs/en/#spark-connect-api) | For those who want to use the DataFrame API or manually manage the driver lifecycle || |# ### Via Query Tracker UI {#ui} To run an SQL query using SPYT Connect: 1. Open the **Queries** tab in the YTsaurus interface. 1. Select **SPYT** from the engine list. 1. Enter an SQL query. 1. In the **Settings** field, specify the [configuration](https://ytsaurus.tech/docs/en/#config) in JSON format. 1. Click **Run** and wait for the result. ![SPYT Connect in Query Tracker](https://ytsaurus.tech/_images/spyt-connect-qt-ui.png) To work with an internal Spark cluster, add `discovery_path` — the path to the running cluster. The cluster must run on SPYT 2.9.0 or higher: ```json { "discovery_path": "//home/spark/my-cluster" } ``` ![SPYT Connect with internal cluster](https://ytsaurus.tech/_images/spyt-connect-qt-ui-cluster.png) ### Via Query Tracker API {#qt-api} The example below shows how to send an SQL query via the API and read the result: ```python from yt.wrapper import YtClient, start_query, get_query_result, read_query_result client = YtClient(proxy="", token="") settings = { "cluster": "", "spark_conf": { "spark.cores.max": "4" # Spark-native parameter: maximum number of cores for the entire application } } # For an internal cluster, add discovery_path: # settings["discovery_path"] = "//home/spark/my-cluster" query_id = start_query( "spyt", "SELECT * FROM yt.`//home/my-table`", settings=settings, client=client ) # Get metadata (for example, data schema) result_meta = get_query_result(query_id=query_id, result_index=0, client=client) # Iterate over the result result = read_query_result(query_id=query_id, result_index=0, client=client) for row in result: print(row) ``` For a description of configuration parameters, see the [Configuration parameters](https://ytsaurus.tech/docs/en/#config) section. ### Via Spark Connect API {#spark-connect-api} The main difference from classic Spark is only in how the session is created; the rest of the code remains the same. The examples below show how to create a Spark session in each launch mode: {% list tabs %} - Direct submit (without a dedicated cluster) ```python import spyt from yt.wrapper import YtClient from spyt.connect import start_connect_server, wait_for_spark_connect_endpoint from pyspark.sql import SparkSession client = YtClient(proxy="", token="") operation = start_connect_server(client) endpoint = wait_for_spark_connect_endpoint(client, operation.id) try: spark = SparkSession.builder.remote(f"sc://{endpoint}").getOrCreate() df = spark.read.format("yt").load("yt:///home/my-table") df.show() finally: if spark: spark.stop() ``` - With an internal SPYT cluster (SPYT 2.9.0+) ```python import spyt from yt.wrapper import YtClient from spyt.connect import start_connect_server_inner_cluster from pyspark.sql import SparkSession client = YtClient(proxy="", token="") endpoint = start_connect_server_inner_cluster(client, discovery_path) try: spark = SparkSession.builder.remote(f"sc://{endpoint}").getOrCreate() df = spark.read.format("yt").load("yt:///home/my-table") df.show() finally: if spark: spark.stop() ``` {% endlist %} ## QT configuration parameters {#config} Parameters apply **only when working via Query Tracker** and are passed in JSON format — in the **Settings** field (UI) or in the `settings` dictionary (API). | **Parameter** | **Description** | **Default** | |:---|:---|:---| | `driver_cores` | CPU for the driver | 1 | | `driver_memory` | Memory for the driver | 1.5G | | `num_executors` | Number of executors | 2 | | `executor_cores` | CPU per executor | 1 | | `executor_memory` | Memory per executor | 4G | | `spark_conf` | Configuration parameters for launching a Spark application. You can specify both [standard Spark parameters](https://spark.apache.org/docs/latest/configuration.html) and [SPYT parameters](https://ytsaurus.tech/docs/en/thesaurus/configuration.md) | — | ## Migrating from Livy {#migration} Starting from SPYT 2.10.0 and Query Tracker 0.4, integration via Livy is no longer supported. SPYT Connect replaces it. The main difference from Livy: in Livy, all drivers ran on the same machine as the server, which limited the number of simultaneous sessions and didn’t allow flexible resource configuration. In SPYT Connect, each user launches a Spark driver as a separate YTsaurus operation with an individual configuration. | **Feature** | **Livy** | **SPYT Connect** | |:---|:---|:---| | Simultaneous sessions | Limited: all drivers ran on one server, and users waited in line under load | Unlimited: each user gets a separate YTsaurus operation | | Session isolation | Shared driver — one user’s load could affect others | Each user works in an isolated execution environment | | Quotas | Not available | Queries run in a user pool in YTsaurus with standard quotas | | Resource configuration | Fixed, set at the server level | Flexible: CPU and memory are configured separately for the driver and each executor | | Resource pool selection | Not available | You can explicitly specify the pool where tasks will run | ## Versions and compatibility {#versions} | **Feature** | **Minimum version** | |:---|:---| | SPYT Connect with direct submit | SPYT 2.8.0 | | SPYT Connect with internal cluster | SPYT 2.9.0 | | Spark 4.x support | SPYT 2.10.0 | | Replacing Livy in Query Tracker | SPYT 2.10.0 + Query Tracker 0.4 | The Spark Connect protocol appeared in Spark 3.4, so earlier Spark versions are not supported. The current SPYT Connect works starting from Spark 3.5.x. {% note info %} To work via the Spark Connect API, you’ll need the `ytsaurus-spyt` and `pyspark-client` packages. {% endnote %} ## What’s next {#see-also} - [Direct submit](https://ytsaurus.tech/docs/en/direct-submit/desc.md) - [Internal SPYT cluster](https://ytsaurus.tech/docs/en/cluster/cluster-desc.md) - [SPYT configuration parameters](https://ytsaurus.tech/docs/en/thesaurus/configuration.md) # Livy server {% note alert %} Starting with SPYT 2.10.0 and Query Tracker 0.4, Livy integration is no longer supported. For integration with Query Tracker, use [SPYT Connect](https://ytsaurus.tech/docs/connect.md#migration). {% endnote %} SPYT versions from 1.74.0 up to, but not including, 2.10.0 come with [Livy](https://livy.apache.org/), a service that allows communication between the client and a Spark cluster over a REST interface. The [Query tracker](https://ytsaurus.tech/query-tracker/about.md) module uses this functionality to execute Spark SQL queries in YTsaurus. The Livy distribution is already included in the release image of SPYT and can be found on the YTsaurus cluster at the path `//home/spark/livy/livy.tgz`. ## Running Livy {#start} To run the Livy server, specify the `--enable-livy` option in the command for starting a SPYT cluster. The maximum number of concurrent connections to the server is controlled by the `--livy-max-sessions` option. Attempting to establish a connection after reaching the limit will result in an error. The driver for Spark jobs executed through Livy is deployed in the same container as the server. For this reason, to ensure correct calculation and allocation of resources in YTsaurus, use the `--livy-driver-cores` and `--livy-driver-memory` options at cluster startup to configure the number of cores and driver memory size. ```bash $ spark-launch-yt ... --enable-livy --livy-max-sessions 5 --livy-driver-cores 1 --livy-driver-memory 1G ``` To retrieve the address of the Livy server or other components, run the `spark-discovery-yt` command. ## Using Livy in Query Tracker {#query-tracker} You can send queries to a running SPYT cluster from Query Tracker (SPYT tab) using Spark SQL. To do this, set the `cluster` (if your YTsaurus installation doesn't have a default cluster) and `discovery_path` fields in `settings`. In addition, you can use the `spark_conf` field to pass an arbitrary Spark session configuration as a YSON map. QT version 0.0.5 added support for authenticating and reusing sessions: 1. When Query Tracker starts executing a query, it issues the user a temporary [token](https://ytsaurus.tech/storage/auth.md) with a TTL of tens of minutes. The received token is used to authenticate the user in YTsaurus when executing the query on a SPYT cluster. This ensures that the user has the necessary [permissions](https://ytsaurus.tech/storage/access-control.md#authorization) when reading or writing data. If query execution takes a long time, the temporary token's TTL will be periodically extended. 2. If `session_reuse` is set to true (default value) in `settings`, queries don't close the established cluster connection and reuse it in the future if possible. This reduces query execution time by 10–20 seconds. However, keep in mind that idle sessions also count toward the limit on the number of concurrent connections to the cluster (`livy-max-sessions`). The session is automatically terminated if no new queries are received for more than 10 minutes. ## Configuring a session when connecting to the server directly {#direct-connect} Livy server endpoints are described in the [official documentation](https://livy.apache.org/docs/latest/rest-api.html). To use YTsaurus, during the initialization of a Livy session, specify two configuration parameters — the paths to the Java (`spark.yt.jars`) and Python (`spark.yt.pyFiles`) libraries — in the `spark_conf` field: In the example below, replace all three occurrences of `` with the SPYT version installed on your cluster. Livy requires a version below 2.10.0. ```python data = {'kind': 'spark', 'conf': {'spark.yt.version': '', 'spark.yt.jars': 'yt:///home/spark/spyt/releases//spark-yt-data-source.jar', 'spark.yt.pyFiles': 'yt:///home/spark/spyt/releases//spyt.zip'}} req = requests.post(host + '/sessions', data=json.dumps(data)) resp = req.json() ``` ## Sparkmagic {#sparkmagic} You can connect to the Livy server via [Sparkmagic](https://github.com/jupyter-incubator/sparkmagic) to work with a SPYT cluster in a Jupyter notebook over a REST interface. This reduces the number of network accesses required to use interactive Python while maintaining all functionality. In addition to Python, Sparkmagic also supports Scala and SQL. ## Running a separate Livy server { #livy-launch-yt } To run the Livy server, you can use the `livy-launch-yt` command that comes with the SPYT distribution. Its basic parameters match those of the spark-launch-yt command, with a few differences given in the table: | **Parameter** | **Required** | **Default value** | **Description** | **Starting with version** | | ------------ | ---------------- | ------------------------- | ------------ | ------------------ | | `--spark-master-address` | Yes | - | Address of the Spark master that will be used to run executors. It can be `ytsaurus://` for launching tasks using the YTsaurus scheduler. | - | | `--master-group-id` | No | - | Name of the group in the YTsaurus Discovery server, used for launching via Strawberry. | - | # Structured Streaming SPYT version 1.77.0 introduces support for [streaming processes](https://spark.apache.org/docs/latest/streaming/index.html) on top of YTsaurus. ## General scheme of Spark Structured Streaming in SPYT ![](https://ytsaurus.tech/_images/spyt-streaming-general-scheme.png){ .center } ### Key concepts `Queue` — any ordered dynamic table. `Consumer` — a sorted table with a fixed schema. A consumer has a many‑to‑many relationship with queues and acts as a consumer of one or more queues. The consumer’s task is to store offsets for partitions of the queues being read. `Source` — a data source. In streaming over the Queue API, this is a queue (an ordered dynamic table). `Sink` — a data sink. For example, an output dynamic table. `Microbatch` — a batch of data processed in a single streaming iteration. `Streaming Query` — a continuously running process that processes a data stream in microbatches. It reads data from the `Source`, applies specified transformations, and writes the results to the `Sink`. Created by calling the `start()` method of the `DataStreamWriter` class. ## Checkpoint storage { #checkpoint-location } YTsaurus can serve as a reliable storage for offsets and other metadata. To do so, specify the `checkpointLocation` option with the value `yt:///...`. After that, all metadata for this task will be saved at the specified path. Listing 1 — Example of using checkpoint storage ```scala val numbers = spark .readStream .format("rate") .option("rowsPerSecond", 1) .load() .select($"timestamp", floor(rand() * 10).as("num")) val groupedNumbers = numbers .withWatermark("timestamp", "5 seconds") .groupBy(window($"timestamp", "5 seconds", "3 seconds"), $"num") .count() val job = groupedNumbers .writeStream .option("checkpointLocation", "yt:///tmp/spark-streaming/sample01/checkpoints") .trigger(ProcessingTime(1000)) .foreachBatch { (frame: DataFrame, batchNum: Long) => frame.write.mode(SaveMode.Append).yt("//tmp/spark-streaming/sample01/result") } val query = job.start() query.awaitTermination() ``` ## Streaming queues in YTsaurus { #queues } YTsaurus provides its own implementation of [queues](https://ytsaurus.tech/dynamic-tables/queues.md) based on ordered dynamic tables. {% note info %} Currently, Spark Streaming running on a YTsaurus cluster can only operate on data located in the same YTsaurus cluster. {% endnote %} Before starting a streaming task, create and configure queues according to the [documentation](https://ytsaurus.tech/dynamic-tables/queues.md#api). For reading, create queue and consumer tables and register them. Streaming results are written to a pre‑created and mounted ordered dynamic table. After processing the latest data batch, a new offset is committed, notifying the input table that it can delete obsolete rows. When using queues for reading and writing, at‑least‑once guarantees apply. Listing 2 — Example of using queues in Scala ```scala val numbers = spark .readStream .format("yt") .option("consumer_path", "//tmp/spark-streaming/sample02/consumer") .load("//tmp/spark-streaming/sample02/queue") val job = numbers .writeStream .option("checkpointLocation", "yt:///tmp/spark-streaming/sample02/checkpoints") .trigger(ProcessingTime(2000)) .format("yt") .option("path", "//tmp/spark-streaming/sample02/result") val query = job.start() query.awaitTermination() ``` Listing 3 — Example of using queues in Python ```python from pyspark.sql.functions import length from spyt import spark_session with spark_session() as spark: df = spark \ .readStream \ .format("yt") \ .option("consumer_path", "//tmp/spark-streaming/sample02/consumer") \ .load("//tmp/spark-streaming/sample02/queue") df\ .select("data") \ .withColumn('data_length', length("data")) \ .writeStream \ .format("yt") \ .option("checkpointLocation", "yt:///tmp/spark-streaming/sample02/checkpoints") \ .option("path", "//tmp/spark-streaming/sample02/result") \ .start().awaitTermination() ``` ## Microbatch processing mechanism in Streaming Query { #streaming-query-processing-mechanism } 1. **Batch initialization** The driver starts processing a new microbatch **#N**: * Retrieves the current offsets (lowerIndex) for all queue partitions. * Calculates the upperIndex for each partition using the formula: ``` upperIndex = min( lowerIndex + max_rows_per_partition, current_end_of_queue ) ``` 2. **Comparing lowerIndex and upperIndex** For each partition: * If lowerIndex < upperIndex: * Calls the [advanceConsumer](https://ytsaurus.tech/dynamic-tables/queues.md#rabota-s-konsyumerom) method to commit offsets for batch `#N‑1` in the consumer. * Generates tasks for executors. * Otherwise: skips processing (empty batch). 3. **Execution on executors** Each executor for its partition: * Reads data using the [pullConsumer](https://ytsaurus.tech/dynamic-tables/queues.md#chtenie-dannyh) method. * Applies transformations (if any). * Writes data to the output table. 4. **Creating checkpoint files on Cypress for batch #N** * A file in the `offsets` directory. * A file in the `commits` directory. 5. **New iteration** * The system moves to batch `#N+1`. * Repeats steps 1–3 with new offsets. * Only in step 2 of iteration `#N+1` is batch **#N** committed if there are available rows. ## Working with offsets { #streaming-offsets } Currently, offsets are stored in two locations: 1. In [checkpoint files](https://ytsaurus.tech/docs/en/#checkpoint-location) automatically created by Spark in a directory. 2. In the consumer table. ### Determining lowerIndex and upperIndex for each partition 1. Spark tries to find the latest checkpoint file on Cypress: * If the file exists, it retrieves an offset structure containing `lowerIndex` for each partition. * If not, it uses offsets from the consumer. 2. For each input queue partition, it retrieves the maximum $row_index — `upper_row_index`. 3. It analyzes the `max_rows_per_partition` option: * If set, it calculates `upperIndex` for each partition using the formula: ``` upperIndex = min( lowerIndex + max_rows_per_partition, upper_row_index ) ``` ### Possible offset desynchronization Due to the late call of the `commit` method implemented in Spark itself, offsets in the consumer may lag behind offsets in checkpoint files by one batch if: * Batch **#N** is fully processed, but batch `#N+1` has not yet been initialized. * Batch `#N+1` is empty and will not be processed because there are no (or no more) unread rows in the input queue. In this case, offsets in the latest checkpoint file will correspond to the `upperIndex` of batch **#N**, and the `offset` field in the consumer will correspond to `upperIndex`. ## Write guarantees { #exactly-once } SPYT Structured Streaming supports several levels of write guarantees. By default, `at‑least‑once` applies: on restarts, a microbatch may be processed again, leading to duplicates. If duplicates are not acceptable, tools are available to achieve `exactly‑once`. | **Mode** | **Guarantee** | **When to use** | |----------|---------------|-----------------| | Non‑transactional (default) | `at‑least‑once` | When duplicates are acceptable: logs, metrics, cache warming. No additional configuration required | | [Transactional mode](https://ytsaurus.tech/docs/en/exactly-once/transactional-mode.md) (SPYT 2.10+) | `exactly‑once` for any transformations | When data accuracy is critical: financial analytics, ML features, incremental data mart construction | | [Idempotent receiver](https://ytsaurus.tech/docs/en/exactly-once/idempotent-receiver.md) | `exactly‑once` for stateless 1:1 transformations | When you want to avoid the additional load on the RPC proxy from transactional mode, or when maintaining legacy code | For a comparison of modes, operation schemes, and instructions on enabling them, see the article [Exactly‑once guarantee](https://ytsaurus.tech/docs/en/exactly-once/index.md). ## Configuring the number of rows per batch { #rows-limit } By default, Spark attempts to read all available rows in a queue in one streaming microbatch. For queues with many unread rows, this will lead to an `OutOfMemoryError` on executors. There are two options for setting a row limit per microbatch: 1. The `max_rows_per_partition` option sets the maximum number of rows that can be read from a single queue partition within one batch. For example: * If a queue consists of 3 partitions and `max_rows_per_partition` is 1 000, no more than 1 000 rows per partition will be read — i.e., no more than 3 000 rows per batch. Partitions are distributed evenly among executors. If there are at least as many executors as partitions, each will process no more than one partition. * In the simplest case, when a queue consists of one partition, `max_rows_per_partition` effectively sets the row limit per batch. 2. The configuration parameter `spark.yt.write.dynBatchSize` sets the maximum number of rows that can be written to a dynamic table in one call of the `modifyRows` command. For example: * If an executor reads 1 000 rows and `spark.yt.write.dynBatchSize` is 100, 10 `ModifyRowsRequest` instances will be sequentially generated, each containing 100 rows. Listing 5 — Using the max_rows_per_partition option ```python spark = SparkSession.builder.appName('streaming example') \ .config("spark.yt.write.dynBatchSize", 100) \ .getOrCreate() df = spark \ .readStream \ .format("yt") \ .option("consumer_path", consumer_path) \ .option("max_rows_per_partition", 1000) \ .load(queue_path) query = df\ .writeStream \ .outputMode("append") \ .format("yt") \ .option("checkpointLocation", checkpoints_path) \ .option("path", result_table_path) \ .start() ``` ## Composite types { #type-v3 } To process [composite data types](https://ytsaurus.tech/storage/data-types.md) with streaming, you must enable the `parsing_type_v3` and `write_type_v3` options, just as for batch jobs. Listing 6 — Processing composite types in Structured Streaming ```python df = spark \ .readStream \ .format("yt") \ .option("consumer_path", consumer_path) \ .option("parsing_type_v3", "true") \ .load(queue_path) query = df\ .writeStream \ .outputMode("append") \ .format("yt") \ .option("write_type_v3", True) \ .option("checkpointLocation", checkpoints_path) \ .option("path", result_table_path) \ .start() ``` ## Spark Structured Streaming parameters set via Spark methods { #streaming-required-params } | Parameter | Description | Required | Default value | |---------|-------------|----------|---------------| | format | Format. Specified separately for readStream and writeStream | Yes (for Structured Streaming over dynamic tables, you must specify `yt`) | — | | load | Input queue | Yes | — | | outputMode | Write mode | No | "append" | ## Options and parameters { #options } For the complete reference of streaming options (DataFrameReader/Writer options, service columns, version compatibility matrix), see the [Streaming options](https://ytsaurus.tech/docs/thesaurus/streaming-options.md) page. For Spark session parameters (including `spark.yt.streaming.transactional` and `spark.yt.write.dynBatchSize`), see the [Configuration parameters](https://ytsaurus.tech/docs/thesaurus/configuration.md) page. ## Monitoring in Spark webUI { #monitoring } The Jobs, Stages, and SQL/DataFrames pages, as in a regular Spark application, are used to monitor jobs, execution stages, and query plans. The Environment page is used to view all Spark session configuration parameters and some metrics. On the Executors page, you can see the number of active, failed, and completed tasks and assess how optimally the executor cores are loaded. You can also view memory usage statistics. In case of a memory leak, it is sometimes useful to go to Thread Dump or Heap Histogram to find the cause. ![](https://ytsaurus.tech/_images/spyt-streaming-monitoring-executors.png){ .center } For streaming processes, the webUI displays a Structured Streaming page by default. Here you can view statistics for active and completed streams. In particular, it is useful to check what error caused a Streaming Query to fail. ![](https://ytsaurus.tech/_images/spyt-streaming-monitoring-streaming.png){ .center } By clicking Run ID, you can view more detailed statistics for a specific Streaming Query. ![](https://ytsaurus.tech/_images/spyt-streaming-monitoring-streaming-query-stats.png){ .center } ## Best practices { #best-practices } * Properly configure the `max_rows_per_partition` option and the `spark.yt.write.dynBatchSize` config to limit the number of rows processed per batch. Set them not too high, otherwise you risk an `OutOfMemoryError`. But also not too low, otherwise batches will be created too frequently, significantly increasing the load on master servers and proxy servers in YTsaurus. * Set a low value for the `spark.sql.streaming.minBatchesToRetain` config. This parameter sets the minimum number of recent batches whose metadata must be stored. These are files in the checkpoint directory on Cypress and objects in the driver’s internal structures. By default, this config’s value is 100. For streaming without stateful transformations, it is sufficient to set `--conf spark.sql.streaming.minBatchesToRetain=2`. This saves chunks on Cypress and driver memory. * When creating a Spark session, set the following configuration parameters: * `.config("spark.streaming.stopGracefullyOnShutdown", True)` — Gracefully complete processing of all created batches and clean up resources before stopping the stream. * `.config("spark.streaming.stopGracefullyOnShutdown.timeout", ...)` — Time for graceful shutdown in milliseconds. * `.config("spark.sql.adaptive.enabled", False)` — Regular adaptive execution does not work for streaming. Disable it. * `.config("spark.sql.streaming.adaptiveExecution.enabled", True)` — Enable special adaptive execution for streaming jobs. * Configure memory allocation using the `spark.memory.fraction` and `spark.memory.storageFraction` parameters, for example: * `.config("spark.memory.fraction", 0.5)` — 50 % of memory for execution, 50 % for storage. * `.config("spark.memory.storageFraction", 0.2)` — 20 % of memory allocated to cache and 80 % to data processing (suitable if there is no caching). # Exactly-once guarantee By default, streaming in SPYT provides an `at-least-once` guarantee — writing to the output table and committing the consumer offset are performed independently. Therefore, duplicate records may appear in the table if a micro‑batch is reprocessed after a failure. If duplicates are not acceptable, SPYT offers two ways to ensure `exactly‑once` delivery: #| || **Approach** | **How it works** | **When to use** || || [Transactional mode](https://ytsaurus.tech/docs/en/transactional-mode.md) (SPYT 2.10+) | `exactly-once` for any transformations, including `join` and stateful ones. Data writing and offset committing are performed atomically in a single transaction | When data accuracy is critical: financial analytics, ML features, incremental data mart construction || || [Idempotent receiver](https://ytsaurus.tech/docs/en/idempotent-receiver.md) | `exactly-once` only for stateless 1:1 transformations. Re‑writing rows using a unique composite key does not create duplicates (upsert) | When you want to avoid the additional RPC proxy load caused by the transactional mode, or when maintaining legacy code || |# {% note warning %} Both approaches guarantee `exactly‑once` only within Spark jobs in YTsaurus: the guarantee covers writing to the output table and committing the consumer offset. It does not apply to external systems — such as writing to an external database, sending messages to other queues, or calling external APIs. For such scenarios, you need additional measures at the application level. {% endnote %} If duplicate output data is acceptable, no additional configuration is required — non‑transactional streaming is enabled by default. ## Performance { #performance } Below are the throughput measurement results for the transactional mode compared to other approaches: non‑transactional streaming and the idempotent receiver. Conditions: synthetic NEXMark‑style loads, a queue of 5 million rows (16 tablets, average row size ~70 bytes), 8 executors × 2 cores, shared RPC proxy in both modes for a fair comparison. #| || **Load type** | **Mode** | **Throughput** || || Stream‑static `join` with key‑based shuffle | Non‑transactional streaming | ~22 700 rows/s || || ^ | Transactional streaming | ~26 200 rows/s (+13 %) || || Stateful aggregation in 1‑minute event windows | Non‑transactional streaming | ~13 900 rows/s || || ^ | Transactional streaming | ~14 000 rows/s (within noise) || || Comparison with idempotent receiver (passthrough, 1 million rows, 1 executor × 1 core) | Idempotent receiver (sorted table) | ~2 000 rows/s || || ^ | Transactional streaming | ~2 600 rows/s (+23 %) || |# The transactional mode performs better when there are many small writes or shuffle operations (`join`, `groupBy`, aggregations): a single transaction instead of dozens reduces the overhead for commits. For stateful operations with a small output volume, there is little to amortize, so there is practically no difference. Compared to the idempotent receiver (which requires creating a sorted table), the transactional mode is consistently faster: data is written to an ordered table without maintaining a sorted index. ## See also { #see-also } - [Transactional mode](https://ytsaurus.tech/docs/en/transactional-mode.md) — setup instructions - [Idempotent receiver](https://ytsaurus.tech/docs/en/idempotent-receiver.md) — an alternative for stateless 1:1 transformations - [Structured Streaming](https://ytsaurus.tech/docs/index.md) — overview and key use cases - [Streaming options](https://ytsaurus.tech/thesaurus/streaming-options.md) — options reference # Transactional streaming mode {% note warning %} Currently, the transactional mode is experimental. Due to architectural specifics, this mode may create increased load on shared RPC proxy pools. We recommend enabling this option only where the `exactly‑once` guarantee is truly required. {% endnote %} ## How it works { #how-it-works } In transactional mode, data writing and consumer offset committing are combined into a single tablet transaction. This eliminates duplicates when re‑executing a micro‑batch. Unlike the [idempotent receiver](https://ytsaurus.tech/docs/en/idempotent-receiver.md), this approach works with any transformations, including shuffle (`join`, aggregations). Sequence of actions when processing each micro‑batch: 1. The driver creates a tablet transaction on the shared RPC proxy. 2. The driver passes the transaction ID and proxy address to the executors. 3. Each executor joins the transaction via `attachTransaction` and writes data from its partition. 4. The driver calls `advanceConsumer` within the same transaction. 5. The driver commits the transaction. 6. The transaction atomically finalizes the result: data is written to the output table, and the offset is updated in the consumer table.
```mermaid %%{init: {'theme':'base', 'themeVariables': { 'fontFamily': 'Arial', 'primaryColor': '#fff3e0', 'primaryTextColor': '#000', 'primaryBorderColor': '#000', 'lineColor': '#000' }}}%% sequenceDiagram participant D as Driver participant E as Executors participant T as Tablet transaction
(shared RPC proxy) participant DT as Dynamic table participant CT as Consumer table D->>T: create transaction D->>E: transaction ID + proxy address E->>T: attachTransaction E->>T: write partition data D->>T: advanceConsumer rect rgb(240, 253, 244) Note over D,CT: Atomically — commit or rollback applies to everything at once D->>T: commit T->>DT: data written T->>CT: offset updated end ```
## Prerequisites { #prerequisites } Before enabling the transactional mode: 1. Check your SPYT version — the transactional mode is available starting from version 2.10. To check: Spark UI → **Environment** tab → **Spark Properties** section → `spark.yt.version`. 2. Create an output table with a sufficient number of tablets and mount it. Do this before starting the stream — the transaction writes the entire micro‑batch atomically. If there are too few tablets, the transaction will fail due to the row limit per tablet. For details, see the [Output table sharding](https://ytsaurus.tech/docs/en/#sharding) section. ## How to enable { #enable } Set two Spark session parameters: - `spark.yt.streaming.transactional = true` — enables transactional writing of micro‑batches. - `spark.ytsaurus.rpc.job.proxy.enabled = false` — disables local Job Proxies and switches the driver and executors to work via shared pools of external RPC proxies. The `spark.ytsaurus.rpc.job.proxy.enabled = false` flag is an architectural requirement. By default, the driver and each executor run their own RPC proxy. A transaction opened in the driver’s RPC proxy is not visible in the executors’ RPC proxies — therefore, all participants must work through a single shared proxy. {% note info "Transactional mode specifics" %} Due to shared pool usage, commit stability and speed depend on other cluster tenants. If the pools are overloaded with other tasks, micro‑batch write time may increase. In rare cases, `transaction expired` errors may occur. Consider this when estimating throughput and be ready to adjust the micro‑batch size in case of frequent timeouts — see the `max_rows_per_partition` parameter in [Streaming options](https://ytsaurus.tech/thesaurus/streaming-options.md). {% endnote %} Examples: {% list tabs %} - Python ```python spark = SparkSession.builder \ .config("spark.yt.streaming.transactional", "true") \ .config("spark.ytsaurus.rpc.job.proxy.enabled", "false") \ .getOrCreate() # The rest of the code is standard Spark Structured Streaming, unchanged ``` - CLI ```bash spark-submit \ --conf spark.yt.streaming.transactional=true \ --conf spark.ytsaurus.rpc.job.proxy.enabled=false \ ... ``` {% endlist %} ## How to verify { #verify } Open Spark UI → **Environment** tab → **Spark Properties** section and look for: ``` spark.yt.streaming.transactional true spark.ytsaurus.rpc.job.proxy.enabled false ``` If either parameter is missing or has a different value, the transactional mode is not enabled. ## Output table sharding { #sharding } YTsaurus has an internal [limitation](https://ytsaurus.tech/dynamic-tables/transactions.md#restrictions): within a single transaction, you cannot write more than a certain number of rows to one tablet (100 000 by default). Since the transactional mode writes the entire micro‑batch atomically in one transaction, all its rows are distributed across the output table’s tablets. If there are too few tablets and the micro‑batch is large, each tablet will receive too much data — the limit will be exceeded, and the transaction will fail with the `Transaction affects too many rows in tablet` error. To ensure transactions stay within limits, shard the output table — split it into more tablets before starting the stream. There is no exact formula for calculating the required number of tablets — determine it empirically based on the load pattern: - For non‑shuffle loads, when data is written unchanged or with simple transformations (`filter`, `select`), follow the number of tablets in the source queue. - For shuffle operations (`join`, `groupBy`, aggregations), there is no universal rule — estimate the expected micro‑batch result size and adjust as needed. For instructions on setting the number of tablets when creating a table or changing it later, see the [Sharding](https://ytsaurus.tech/dynamic-tables/resharding.md) section. ## Failure behavior { #failures } Error during batch processing or writing: : The transaction is aborted (`abort`). No data is written, and the consumer offset is not advanced. Spark automatically retries the micro‑batch — thanks to transactionality, no duplicates will be written. Driver restart after a successful commit: : The transaction has already been committed: data is written, and the consumer offset is advanced. On restart, Spark reads the current offset from YTsaurus and continues from the new position — no duplicates will occur. ## Troubleshooting { #troubleshooting } #| || **Issue** | **Description and solution** || || `Transaction affects too many rows in tablet` error | The row limit per tablet within a single transaction is exceeded. Solution: Increase the number of tablets in the output table. See [Output table sharding](https://ytsaurus.tech/docs/en/#sharding) || || `transaction expired` error | The micro‑batch exceeds the transaction timeout. Solution: Reduce `max_rows_per_partition` or optimize the transformation || || Executor cannot write data (transaction not found) | A sticky transaction is tied to a single RPC proxy; if the driver and executor go through different proxies, the executor will not find the transaction. Solution: Make sure the `spark.ytsaurus.rpc.job.proxy.enabled` flag is set to `false` || |# ## See also { #see-also } - [Exactly‑once guarantee](https://ytsaurus.tech/docs/en/index.md) — choosing an approach to write guarantees - [Idempotent receiver](https://ytsaurus.tech/docs/en/idempotent-receiver.md) — alternative for stateless 1:1 transformations - [Streaming options](https://ytsaurus.tech/thesaurus/streaming-options.md) — options reference - [Configuration parameters](https://ytsaurus.tech/thesaurus/configuration.md) — Spark session parameters # Idempotent receiver This article provides instructions for configuring an idempotent receiver. {% note info %} The idempotent receiver is slower than non‑transactional streaming and the [transactional mode](https://ytsaurus.tech/docs/en/transactional-mode.md). This is because data is written to a sorted dynamic table, which requires index updates on every write. If write speed is critical, consider the transactional mode: it’s faster and supports any transformations. {% endnote %} ## How it works { #how-it-works } The idempotent receiver achieves `exactly‑once` semantics differently from the [transactional mode](https://ytsaurus.tech/docs/en/transactional-mode.md): not through atomicity, but through idempotent writes. If the same rows are written to the table when a batch is re‑executed, no duplicates are created. The mechanism is based on unique identification of each queue row: 1. Each row in an ordered dynamic table (queue) is addressed by an immutable pair (`$tablet_index`, `$row_index`) — the tablet index and the row’s position within it. 2. SPYT passes these values to the streaming dataframe as service columns `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index`. 3. The output table is created as a sorted dynamic table with a composite primary key on these two columns. 4. Writing occurs via upsert: if a row with this composite key already exists, it is overwritten with the same data. When a batch is processed again, the result is the same as the first write.
```mermaid %%{init: {'theme':'base', 'themeVariables': { 'fontFamily': 'Arial', 'primaryColor': '#fff3e0', 'primaryTextColor': '#000', 'primaryBorderColor': '#000', 'lineColor': '#000' }}}%% sequenceDiagram participant Q as Queue participant S as SPYT participant T as Output table
(composite key: (tablet_idx, row_idx) | value: ...) rect rgb(240, 253, 244) Note over Q,T: First batch processing Q->>S: row (tablet_idx=1, row_idx=5, value=...) S->>T: upsert → key (1, 5), value written end rect rgb(255, 243, 224) Note over Q,T: Re‑processing the same batch Q->>S: row (tablet_idx=1, row_idx=5, value=...) — same row S->>T: upsert → key (1, 5) already exists, value overwritten with same data Note over T: no duplicate end ```
## When to use it { #applicability } The idempotent receiver works only with stateless 1:1 transformations — that is, those where: - Row processing does not depend on previous micro‑batches. - Each input row is transformed into exactly one output row. Below are examples of Spark operations that do and do not work with the idempotent receiver: | **Works with the idempotent receiver** | **Does not work with the idempotent receiver** | |---|---| | `select`, `filter`, `withColumn`, projections, renaming | `join`, `groupBy`, `explode`, `flatMap`, `union`[*](https://ytsaurus.tech/docs/en/*union), aggregations | With operations that produce multiple output rows (e.g., `explode` or `join` in one‑to‑many mode) or with aggregations (`groupBy`), the link between the row’s position in the queue and the output data is broken — service columns are either lost or stop uniquely identifying the result. ## How to enable it { #steps } 1. When reading the queue, set the [include_service_columns](https://ytsaurus.tech/thesaurus/streaming-options.md#service-columns) option to `true`. Then the streaming dataframe will contain the columns `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index`, which correspond to `$tablet_index` and `$row_index` of the row in the queue being read. 2. Create an output [sorted dynamic table](https://ytsaurus.tech/dynamic-tables/sorted-dynamic-tables.md) and mount it before starting streaming. - If reading from a single queue, use `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index` as key columns. Key columns must be specified in the schema and form its prefix; see the [table schema](https://ytsaurus.tech/storage/static-schema.md). You can rename these columns — then rename them in the dataframe too, as shown in the listing below. - If reading from more than one queue, add another column with a unique identifier of the source queue (e.g., its path) to the output table’s key and to the dataframe. See the [example](https://ytsaurus.tech/docs/en/#multi-queues) for how to do this. After this setup, the output table’s key is uniquely determined by the original queue row, so re‑writing the same row does not create a duplicate. ## How to verify it { #verify } To check that the setup is correct, open the output table after several micro‑batches have been processed — it should contain rows with key columns (or their renamed equivalents). If there is no data, check that the table is mounted and that the `include_service_columns` option is set to `true`. ## Example { #example } ### Single queue Listing — Using an idempotent receiver for a single queue ```python # In the example, the output table contains only key columns — this is the minimum # configuration required for idempotent writing. # In a real task, add the remaining result columns to the table schema and dataframe. import os import spyt import yt.yson as yson from pyspark.sql import SparkSession from yt.wrapper import YtClient queue_path = "//path/to/queue" consumer_path = "//path/to/consumer" result_table_path = "//path/to/result" checkpoints_path = "//path/to/checkpoints" yt = YtClient( proxy="", token=os.environ["YT_SECURE_VAULT_YT_TOKEN"], ) spark = SparkSession.builder.appName("streaming example").getOrCreate() # The table key must uniquely identify the original queue row. schema = yson.YsonList([ {"name": "tablet_idx", "type": "int64", "sort_order": "ascending"}, {"name": "row_idx", "type": "int64", "sort_order": "ascending"}, ]) schema.attributes["unique_keys"] = True # Create and mount the output dynamic table in advance. yt.create( "table", result_table_path, recursive=True, attributes={ "dynamic": True, "schema": schema, }, ) yt.mount_table(result_table_path, sync=True) # Rename service columns so they match the output table schema. df = ( spark.readStream.format("yt") .option("consumer_path", consumer_path) .option("include_service_columns", True) .load(queue_path) .withColumnRenamed("__spyt_streaming_src_tablet_index", "tablet_idx") .withColumnRenamed("__spyt_streaming_src_row_index", "row_idx") .select("tablet_idx", "row_idx") ) query = ( df.writeStream.outputMode("append") .format("yt") .option("checkpointLocation", checkpoints_path) .option("path", result_table_path) .start() ) ``` ### Multiple queues { #multi-queues } If reading from multiple queues, add another key column with the queue identifier (e.g., its path) to the output table schema and to the dataframe. There is no service column with the queue path in SPYT: only `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index` are available, and there is no equivalent of `input_file_name()`. Therefore, you must add the queue identifier manually when reading each queue: Listing — Reading from multiple queues with a source identifier ```python from pyspark.sql.functions import lit df1 = ( spark.readStream.format("yt") .option("consumer_path", consumer_path) .option("include_service_columns", True) .load(queue_path_1) .withColumn("queue_id", lit(queue_path_1)) ) df2 = ( spark.readStream.format("yt") .option("consumer_path", consumer_path) .option("include_service_columns", True) .load(queue_path_2) .withColumn("queue_id", lit(queue_path_2)) ) df = df1.union(df2) ``` In this case, the output table key must include `queue_id` together with columns derived from `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index`. ## Common configuration errors { #violations } | **Error** | **Consequence** | |---|---| | One input row produces multiple output rows | The idempotent key is no longer one‑to‑one: multiple output rows compete for the same key. As a result, some data may be overwritten without the job failing, leading to silent data loss | | The transformation loses the service columns `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index` (or their renamed equivalents) | The batch write will fail: the dataframe will lack the key columns required for the output table | | Reading from multiple queues (e.g., combined via `union`) but the queue identifier is not included in the key | Each queue numbers rows independently, so rows from different queues may get the same key `(tablet_index, row_index)`. The `upsert` operation will overwrite one row with another — data will be lost without errors or warnings | | The output table is not configured as a sorted dynamic table with the required key columns | The idempotent write mechanism will not work: re‑writing the same original row will not be reliably deduplicated, and the `exactly‑once` guarantee is not achieved | ## See also { #see-also } - [Exactly‑once guarantee](https://ytsaurus.tech/docs/en/index.md) — choosing an approach to write guarantees - [Transactional mode](https://ytsaurus.tech/docs/en/transactional-mode.md) — recommended approach for new tasks - [Streaming options](https://ytsaurus.tech/thesaurus/streaming-options.md) — options reference [*union]: Each queue numbers rows independently: queue A and queue B may have a row with the same key `(tablet_index=0, row_index=42)`, although these are different data. If you combine them directly via `union`, both rows will go to the output table with the same key — upsert will overwrite one with the other, and data will be lost without any errors or warnings. To keep keys unique, add a source identifier column to each row before combining: `.withColumn("queue_id", lit(queue_path))` — and include `queue_id` in the output table key. # Spark SQL You can work with YTsaurus tables from [Spark SQL](https://spark.apache.org/docs/latest/sql-ref-syntax.html). This SQL dialect is used for queries in [Query Tracker](https://ytsaurus.tech/query-tracker/about.md) using SPYT. When working with YTsaurus, `yt` is used as a database ID, and `ytTable:/` is used as a file system. The latter can be omitted, so the below pair of queries is equivalent: ```sql SELECT `key` FROM yt.`ytTable:///home/service/data` ``` ```sql SELECT `key` FROM yt.`//home/service/data` ``` Queries to other systems are executed in the same way as in the original Spark: ```sql SELECT * FROM json.`s3a://bucket/file.json` ``` {% note info "Note" %} Spark doesn't support running multiple sequential commands in a single query. All commands (`CREATE`, `INSERT`, and others) must be executed by separate queries. {% endnote %} ## Working with tables Creating a table: ```sql CREATE TABLE yt.`//tmp/users` ( id INT, name STRING ) USING yt ``` ```sql CREATE TABLE yt.`//tmp/users_copy` USING yt AS SELECT * FROM yt.`//tmp/users` ``` Deleting a table: ```sql DROP TABLE yt.`//tmp/users` ``` When a query is executed, table metainformation is cached into the Spark session memory to be reused for subsequent accesses. If the table has been modified by external processes after the query, the cached information becomes irrelevant, and new queries will be executed with errors. In this case, you need to clear the cache manually: ```sql CREATE TABLE yt.`//tmp/users` (id INT, name STRING) USING yt -- Any changes to the table by external processes REFRESH TABLE yt.`//tmp/users` -- Clearing cache SELECT * FROM yt.`//tmp/users` ``` ## Working with data Reading static tables: ```sql SELECT t1.value FROM yt.`//home/service/table1` t1 JOIN yt.`//home/service/table2` t2 ON t1.id == t2.id ``` To read dynamic tables, you need to specify a data slice timestamp: ```sql SELECT * FROM yt.`//home/service/dynamic_data/@latest_version` ``` ```sql SELECT * FROM yt.`//home/service/dynamic_data/@timestamp_XXX` ``` Inserting into an existing table: ```sql INSERT INTO TABLE yt.`//home/service/copy` SELECT * FROM yt.`//home/service/origin` ``` ```sql INSERT OVERWRITE TABLE yt.`//home/service/copy` VALUES (-1, "Existed data is overwritten") ``` Apache Spark doesn't support update queries. # Spark UI SPYT operations launch the Spark UI web interface. Usually, it cannot be opened directly: for example, if cluster nodes are in a closed internal network (as in Kubernetes) or job addresses change when moving to other machines. To access the interface, [Task proxy](https://ytsaurus.tech/proxy/task.md) is used. It creates a stable link that works even when jobs move between nodes, and verifies access rights upon entry. {% note info %} To work with Task proxy, the cluster administrator must first [deploy](https://ytsaurus.tech/admin-guide/install-task-proxy.md) this component. {% endnote %} ## Configuring Spark cluster launch For Spark UI to work through Task proxy, when launching a Standalone cluster, you need to enable the special configuration flag `spark.ui.reverseProxy`. To do this, add launch parameters: ```bash spark-launch-yt ... --params '{"spark_conf"={"spark.ui.reverseProxy"="true"}}' ``` If the cluster is already running without this flag, to fix the UI operation, it will need to be restarted with the required parameter. ## How to access the UI To open Spark UI, follow these steps: 1. **Find out the operation ID.** It can be found on the **Operations** page in the YTsaurus interface or in the output (stdout) of the `spark-launch-yt` / `spark-submit` console commands. 2. **Find the service address.** Task proxy publishes information about all active web services in the Cypress system table: `//sys/task_proxies/services`. Find rows in this table corresponding to your `operation_id`. Example table contents: | __domain__ | __operation_id__ (example) | __task_name__ | __service__ | __protocol__ | |------------------------------------------|-----------------------------------|---------------------|-------------|--------------| | 2ef4261c.my-cluster.ytsaurus.example.net | a6e04b98... | master | ui | http | | 51a6d485.my-cluster.ytsaurus.example.net | a6e04b98... | history | ui | http | | 37a5f11c.my-cluster.ytsaurus.example.net | 6699a5a9... | driver | ui | http | - Row 1 (`master`): UI of the master node of a [standalone](https://ytsaurus.tech/docs/en/cluster/cluster-start.md) cluster. - Row 2 (`history`): UI of the history server. - Row 3 (`driver`): UI of the driver when launched via [direct submit](https://ytsaurus.tech/docs/en/direct-submit/desc.md). 3. **Open the link.** Copy the value from the `domain` column (for example, `2ef4261c.my-cluster.ytsaurus.example.net`) and paste it into the browser address bar. # Building an offline Docker image of SPYT During a normal SPYT deployment, the Spark distribution and supporting artifacts are downloaded from the internet (Apache archive, Maven Central), and the control image is pulled from the `ghcr.io` registry. In isolated environments without internet access, this is not possible. To deploy SPYT under such conditions, an **offline image** is used — a self-contained Docker image `ghcr.io/ytsaurus/spyt:-pyspark-` that includes the required SPYT build and the Spark distribution. When deploying, this image does not need internet access: all artifacts are taken from the image itself. {% note warning %} The Spark version must be `3.4.0` or upper — the distribution embedded into the image only for such kind of versions. The offline image is not supported for lower Spark versions. {% endnote %} ## How to build the image {#build} Build executon runs from the [ytsaurus-spyt](https://github.com/ytsaurus/ytsaurus-spyt) repository at the required release tag. First, clone the repository: ```bash git clone https://github.com/ytsaurus/ytsaurus-spyt.git cd ytsaurus-spyt ``` The repository includes a helper script [`tools/release/build_offline_image.sh`](https://github.com/ytsaurus/ytsaurus-spyt/blob/main/tools/release/build_offline_image.sh) performing all the build steps. You should run it from the root of the cloned repository (or specify the path to it with the `--repo ` flag). For example, to build an image for SPYT `2.9.2` and Spark `3.5.8` run the command: ```bash tools/release/build_offline_image.sh --spyt-version 2.9.2 --spark-version 3.5.8 --checkout ``` The output will be represented as a local image `ghcr.io/ytsaurus/spyt:2.9.2-pyspark-3.5.8`. You should know that the `--checkout` flag is **optional**. If you pass it, the script will switch the repository to the release tag `spyt/` (`git fetch --tags` + `git checkout`). All the tracked files must have the committed changes only, otherwise the script stops with an error. If you do not pass the flag, the build will run from the current state of the working copy — this is convenient when you are on the required tag already or building an image from your own branch. Anyway the `--spyt-version` and `--spark-version` flags are required: they specify the SPYT artifact version, the version of the embedded Spark distribution, and the image tag. ### Build steps {#build-steps} The script performs the next steps sequentially: 1. `git checkout tags/spyt/` — switches to the release tag (with the `--checkout` flag only). 2. `./gradlew assemble -PcustomSpytVersion=` — builds the SPYT artifacts (requires JDK 17). 3. Downloads the supporting Livy distribution, if required by the Dockerfile for this version. 4. `tools/release/spyt_image/build.sh --spyt-version --spark-version ` — builds the image with the embedded Spark distribution. 5. Verifies the image built successfully. Also you can perform these steps manually without the script if needed. ## Publishing to a custom registry {#registry} The built image is only available locally. In an isolated environment, the deployment host usually does not have access to `ghcr.io` but does have access to an internal (corporate) Docker registry. Publish the offline image to such a registry to deploy SPYT from it instead of from the public `ghcr.io`. Tag the image with your registry and push it using the command: ```bash docker tag ghcr.io/ytsaurus/spyt:-pyspark- \ /ytsaurus/spyt:-pyspark- docker push /ytsaurus/spyt:-pyspark- ``` For example, for SPYT `2.9.2`, Spark `3.5.8`, and registry `registry.example.com`: ```bash docker tag ghcr.io/ytsaurus/spyt:2.9.2-pyspark-3.5.8 \ registry.example.com/ytsaurus/spyt:2.9.2-pyspark-3.5.8 docker push registry.example.com/ytsaurus/spyt:2.9.2-pyspark-3.5.8 ``` Alternatively, build the image directly for the required registry — pass its prefix into `--image-cr`, and `--push` will publish the result: ```bash tools/release/build_offline_image.sh \ --spyt-version --spark-version \ --image-cr / --push --checkout ``` For example: ```bash tools/release/build_offline_image.sh \ --spyt-version 2.9.2 --spark-version 3.5.8 \ --image-cr registry.example.com/ --push --checkout ``` Specify the resulting image `/ytsaurus/spyt:-pyspark-` during the deployment. ## How to use {#deploy} The ready image deploys SPYT and the Spark distribution to a YTsaurus cluster. Run a container from the image you built and published, specifying the proxy address and access parameters: ```bash docker run --network=host \ -e YT_PROXY= \ -e YT_USER= \ -e YT_TOKEN= \ -e SPARK_DISTRIB_OFFLINE=true \ -e SPARK_DISTRIB_VERSIONS= \ --rm /ytsaurus/spyt:-pyspark- ``` The last argument is the image to run. In an isolated environment, this is the image from your registry (see [Publishing to a custom registry](https://ytsaurus.tech/docs/en/#registry)). The `--rm` flag is optional — it removes the container after it finishes. The deployment is one-time, the container runs and exits. If you did not publish to a registry and you are building and deploying on the same machine, specify the locally built image (its default tag is `ghcr.io/ytsaurus/spyt:-pyspark-`). `docker run` will use the ready image from the local Docker cache on that machine and will not download anything. For example, for SPYT `2.9.2`, Spark `3.5.8`, and registry `registry.example.com`: ```bash docker run --network=host \ -e YT_PROXY=cluster.example.com \ -e YT_USER=spyt-deployer \ -e YT_TOKEN= \ -e SPARK_DISTRIB_OFFLINE=true \ -e SPARK_DISTRIB_VERSIONS=3.5.8 \ --rm registry.example.com/ytsaurus/spyt:2.9.2-pyspark-3.5.8 ``` Environment variables control the publication of the Spark distribution to the cluster: | Variable | Purpose | | --- | --- | | `SPARK_DISTRIB_OFFLINE=true` | Full offline mode: use only the Spark distribution embedded in the image, do not download anything. | | `SPARK_DISTRIB_VERSIONS` | Space-separated list of Spark versions to publish to the cluster. | ## How to verify that deployment was successful {#check} The container runs and exits with a zero exit code. The message `Publication finished successfully` in the container log means that the SPYT files have been published. After that, the container uploads the Spark distribution to the cluster. A non-zero exit code means that the deployment did not succeed. Additionally, verify that the artifacts appeared on the cluster (paths are shown for SPYT `2.9.2` and Spark `3.5.8`): ```bash yt list //home/spark/spyt/releases/2.9.2 # spyt-package.zip, setup-spyt-env.sh yt list //home/spark/conf/releases/2.9.2 # spark-launch-conf and sidecar configs yt list //home/spark/distrib/3/5/8 # spark-3.5.8-bin-hadoop3.tgz, spark-connect_2.12-3.5.8.jar ``` After that, you can [launch Spark applications](https://ytsaurus.tech/docs/en/launch.md) as usual. # Using the YTsaurus Shuffle service {% note info %} The service is available starting with SPYT 2.7.3. {% endnote %} The YTsaurus Shuffle service is used to store intermediate data between computation stages. In contrast to the standard Spark Shuffle service that stores intermediate data in RAM and temporary disk directories, the YTsaurus Shuffle service stores them in YTsaurus chunks. This approach improves resilience against failures in distributed Spark stages. ## Enabling { #enabling } ### Direct task launch { #enabling-direct } When [launching tasks directly in YTsaurus](https://ytsaurus.tech/docs/en/launch.md#submit), set the conf parameter `spark.ytsaurus.shuffle.enabled` to `true`. The application will use the YTsaurus Shuffle service instead of the native Spark shuffle. {% note warning "Warning" %} When dynamic allocation is enabled, the YTsaurus Shuffle service is mandatory: without `spark.ytsaurus.shuffle.enabled=true`, the launch will fail with the error `Dynamic allocation requires YTsaurus shuffle service`. {% endnote %} ### Internal standalone cluster { #enabling-standalone } When launching an [internal standalone cluster](https://ytsaurus.tech/docs/en/launch.md#standalone) using the `spark-launch-yt` command, two independent shuffle services are available, each controlled by its own option: | Option | Shuffle service | Default | | --- | --- | --- | | `--enable-spark-shuffle` / `--disable-spark-shuffle` | Native external Spark shuffle service | Enabled | | `--enable-ytsaurus-shuffle` / `--disable-ytsaurus-shuffle` | YTsaurus Shuffle service | Disabled | The `--enable-ytsaurus-shuffle` option only makes the YTsaurus Shuffle service available on workers. Which service the application uses is determined at launch time by the `spark.ytsaurus.shuffle.enabled` parameter (`true` — YTsaurus Shuffle, otherwise — native Spark shuffle). Each application must use only one of the services. {% note warning "Warning" %} If an application is launched with `spark.ytsaurus.shuffle.enabled=true` and the cluster is started without the `--enable-ytsaurus-shuffle` option, there will be no error at the submit stage. The application will fail at runtime during the first shuffle stage. {% endnote %} {% note info %} It is possible to start the cluster with both services disabled (`--disable-spark-shuffle --disable-ytsaurus-shuffle`). There will be no error, but intermediate data will be stored only on executors and will be lost if they fail. {% endnote %} ## How it works { #how } Spark writes data to the YTsaurus Shuffle service within a transaction. The life cycle of the transaction is controlled by the driver that periodically pings it. The transaction is initiated at the moment a new shuffle is registered, with each shuffle being written within a separate transaction. Upon termination of the operation (whether successful or unsuccessful) with the shuffle instance, the transaction is rolled back, leading to the deletion of all chunks written within it. In case of driver errors and crashes, the transaction is also rolled back after the timeout expires. You can set the timeout using the parameter `spark.ytsaurus.shuffle.transaction.timeout`. Data written via the Shuffle service cannot be read via the YTsaurus API. ## Recommendations for use { #recommendations } Use the YTsaurus Shuffle service in the following cases: - For complex multi-stage computations. For example, when joining multiple large tables, each exceeding 10M rows in size. If, in addition to complex computations, there are also simple ones (such as simple aggregations or joining a large table with a small one) performed on the cluster, we still recommend enabling the Shuffle service. - When [launching tasks directly in YTsaurus](https://ytsaurus.tech/docs/en/launch.md#submit). The Shuffle service writes intermediate data to YTsaurus chunks, ensuring its survival in case of Spark executor failures or preemptions. This approach prevents re‑computation of stages that have already been computed but were stored on lost executors. ## Configuration parameters { #configuration } All parameters related to configuring the YTsaurus Shuffle service are described on the [configuration parameters page](https://ytsaurus.tech/docs/en/thesaurus/configuration.md#shuffle). # SPYT API Choose the SPYT interface for the language or environment where your application runs. - [Python](https://ytsaurus.tech/docs/en/spyt-python.md) - [Jupyter](https://ytsaurus.tech/docs/en/spyt-jupyter.md) - [Scala](https://ytsaurus.tech/docs/en/spyt-scala.md) - [Java](https://ytsaurus.tech/docs/en/spyt-java.md) # SPYT in Python ## Steps to launch { #how-to } 1. Write some code. 2. Post your code and dependencies to YTsaurus. The main `.py` file and the dependencies in `.py`, `.zip`, or `.egg`. 3. Build a binary file and post it to YTsaurus (Spark 3.2.2+). 4. Run `spark-submit-yt` for submitting to inner standalone cluster or `spark-submit` for submitting directly to YTsaurus (available from version 1.76.0) ## Differences for submitting directly to YTsaurus { #submit } All of the examples below are written for using with inner Spark standalone cluster. There's some differences for creating SparkSession object when submitting directly to YTsaurus. Instead of using `with spark_session()` or `spyt.connect()` functions the object should be created explicitly according to Spark recommendations: ```python from pyspark.sql import SparkSession spark = SparkSession.builder.appName('My Application').getOrCreate() try: ... # Application code finally: spark.stop() ``` This is because the `with spark_session()` and `spyt.connect()` functions access discovery-path created in Cypress when running a standalone cluster. This method does not involve discovery-path, because the internal cluster is not created. ## Launching tasks directly in YTsaurus in cluster mode { #submit-cluster } ```python from spyt.submit import direct_submit # method returns YT operation id for driver, throw an exception if spin failed operation_id = direct_submit( yt_proxy="yt_proxy_address", # YT proxy address as string num_executors=3, # Number of Spark executors main_file="main.py", # Main .py or .jar file for Spark job deploy_mode="cluster", # 'cluster' (default) or 'client' pool=pool, # (Optional) YT pool for the job spark_base_args=spark_base_args, # (Optional) List of extra Spark submit arguments job_args=job_args, # (Optional) Arguments passed to the main application spark_conf=spark_conf, # Spark configuration dictionary timeout_sec=10 # Submit timeout in seconds (default: 30) ) # then you can monitor your operation, e.g. while True: current_state = yt_client.get_operation_state(operation_id) logging.info(f"Operation: {operation_id}, State: {current_state}") if current_state.is_finished(): break time.sleep(1) ``` ## Running tasks directly in YTsaurus in client mode { #submit-client } ```python import spyt from pyspark import SparkConf conf = SparkConf() ... with spyt.direct_spark_session("yt_proxy_address", conf) as spark: df = spark.read.yt(path) ... ``` ## Running with no dependencies { #simple } ### Main file code ```python from spyt import spark_session print("Hello world") with spark_session() as spark: spark.read.yt("path_to_file").show() ``` ### Launch ```bash spark-submit-yt \ --proxy \ --discovery-path //my_discovery_path \ --deploy-mode cluster \ YT_path_to_file ``` ## Running with dependencies { #dependencies } ### Main file code ```python from my_lib import plus5 from pyspark.sql.functions import col from spyt import spark_session print("Hello world") with spark_session() as spark: spark.read.yt("path_to_file")\ .withColumn("value", plus5(col("value")))\ .write.mode("overwrite").yt("path_to_file") ``` ### Dependency `my_lib.zip` containing `my_lib.py`: ```python from pyspark.sql.functions import udf from pyspark.sql.types import LongType plus5 = udf(lambda x: x + 5, LongType()) ``` ### Code and dependencies in YTsaurus To be posted to YTsaurus: - A file, such as `deps_example.py`. - Dependencies, such as `my_lib.zip`. ### Launch ```bash spark-submit-yt \ --proxy \ --discovery-path //my_discovery_path \ --deploy-mode cluster \ --py-files YT_path_to_lib \ YT_path_to_file ``` ## Running with configurations Spark includes many useful configurations that you can specify when running a job. Such as [spark.sql.shuffle.partitions](https://spark.apache.org/docs/latest/sql-performance-tuning.html). There is a way to control the amount of resources per job via `spark.cores.max` and `spark.executor.memory`. For more information, please see the [Spark documentation](https://spark.apache.org/docs/latest/configuration.html#application-properties). {% note warning "Attention!" %} In [Standalone](https://ytsaurus.tech/docs/cluster/cluster-desc.md#spark-standalone) mode, the `num-executors` and the `spark.executor.instances` settings are non-functional while the number of executors depends on the `spark.cores.max` parameter. {% endnote %} ### Main file code ```python from pyspark_yt import spark_session from pyspark.sql.functions import col from spyt import spark_session print("Hello world") with spark_session() as spark: spark.read.yt("path_to_file")\ .withColumn("id_mod_10", col("id") % 10)\ .groupBy("id_mod_10")\ .count()\ .write.mode("overwrite").yt("path_to_file") ``` ### Code in YTsaurus You need to post this file to YTsaurus. ### Launch ```bash spark-submit-yt \ --proxy \ --id test \ --discovery-dir //my_discovery_path \ --deploy-mode cluster \ --conf spark.sql.shuffle.partitions=1 \ --conf spark.cores.max=1 \ --conf spark.executor.cores=1 \ YT_path_to_file ``` ## Other examples You can find additional examples in [SPYT in Jupyter](https://ytsaurus.tech/docs/en/spyt-jupyter.md). In regular jobs, you can create the `spark` object the same way as in Jupyter via a call to `connect`or, alternatively, via `with spark_session` as in the example. The difference between the options is minimal. Jupyter allows you to pass resource settings, while for regular jobs, you would normally do this at launch to avoid reposting code if, for instance, the amount of data had increased. Example job start and output verification: ```python import spyt import time from spyt.submit import java_gateway, SparkSubmissionClient, SubmissionStatus user = "user_name" token = spyt.utils.default_token() yt_proxy = "cluster_name" discovery_path = "//my_discovery_path" spyt_version = "1.4.1" with java_gateway() as gateway: client = SparkSubmissionClient(gateway, yt_proxy, discovery_path, spyt_version, user, token) launcher = ( client .new_launcher() .set_app_resource("yt:///sys/spark/examples/smoke_test.py") .set_conf("spark.pyspark.python", "/opt/python3.7/bin/python3.7") ) submission_id = client.submit(launcher) status = client.get_status(submission_id) while not SubmissionStatus.is_final(status): status = client.get_status(submission_id) time.sleep(10) SubmissionStatus.is_success(status) SubmissionStatus.is_failure(status) ``` 1. The client operates via `Py4J` calling the required `RestSubmissionClient` methods. The string `with java_gateway() as gateway` will bring the `JVM` up with the right `Classpath` and exit without error. 2. You can use the `launch_gateway` and the `shutdown_gateway` methods to control the creation of a `JVM` manually. 3. `SparkSubmissionClient` is a client to submit jobs to a specific cluster. The client finds the location of the master with `discovery_path` and communicates with it. 4. The parameters of the job to be started are described in the `launcher` object. A complete listing of the object's methods is available in the code. They match the `spark-submit` parameters. 5. After launch, you can check job status using the `client.get_status` method. The example shows the timeout to get the final output. # SPYT in Jupyter ## Setup { #prepare } Before you can use Spark in Jupyter, you need to create a [cluster](https://ytsaurus.tech/docs/cluster/cluster-start.md). Currently working with Spark using Jupyter notebooks is possible only using inner standalone cluster. If there is one already, you need to find out the values of `proxy` and `discovery_path` to be able to use it. ## Configuring Jupyter { #custom } 1. Get network access from your Jupyter machine to the SPYT cluster, ports `27000-27200`. 2. Get network access from the SPYT cluster to the Jupyter machine, ports `27000-27200`. 3. Install a deb package containing java: ```bash sudo apt-get update sudo apt-get install openjdk-11-jdk ``` Note: starting with version 2.6.0, you can install openjdk-17-jdk instead of openjdk-11-jdk. 4. Install the pip package: ```bash pip install ytsaurus-spyt ``` 5. Place your YTsaurus token in `~/.yt/token`: ```bash mkdir ~/.yt cat < ~/.yt/token $YOUR_YT_TOKEN EOT ``` 6. Place a file called `~/spyt.yaml` with the Spark cluster location in your home directory: ```bash cat < ~/spyt.yaml yt_proxy: "cluster_name" discovery_path: "$YOUR_DISCOVERY_DIR" EOT ``` ## Updating the client in Jupyter { #new-client } Update `ytsaurus-spyt` in Jupyter: ```bash pip install ytsaurus-spyt ``` If the second `ytsaurus-spyt` version component is greater than that in your cluster version, new functionality may not work. Update your cluster per the instructions. # SPYT in Scala ## Requirements { #requirements } SPYT works with Java 11 and Scala 2.12. ## Dependencies { #dependencies } All the dependencies are installed in SPYT automatically; therefore the `scope` value is `Provided`. They will be used for compilation only and will not be included in the `.jar` or run. All possible values for `spytVersion` can be found [here](https://github.com/ytsaurus/ytsaurus/tags) under the spyt/* or spyt-spark/* tag. Apache Spark compatible versions are described in [this table](https://ytsaurus.tech/docs/overview.md#spyt-compatibility). ```scala val sparkVersion = "3.5.4" val spytVersion = "2.11.0" libraryDependencies ++= Seq( // Spark dependencies "org.apache.spark" %% "spark-core" % sparkVersion % Provided, "org.apache.spark" %% "spark-sql" % sparkVersion % Provided, // SPYT library "tech.ytsaurus" %% "spark-yt-data-source" % spytVersion % Provided ) ``` ## Build { #build } The example code has been built using the `sbt assembly` command and posted to YTsaurus: `//home/spark/examples/scala-examples-assembly-0.1.jar`. ## Starting jobs { #start } Before running jobs from the example, you need to launch your SPYT cluster or find out the `discovery-path` for an already running cluster. Jobs are started via the `spark-submit-yt` utility. ```bash spark-submit-yt \ --proxy ${YT_PROXY} \ --discovery-path ${SPYT_DISCOVERY_PATH} \ --deploy-mode cluster \ --class tech.ytsaurus.spyt.examples.GroupingExample \ yt:///home/spark/examples/scala-examples-assembly-0.1.jar ``` ## Differences for submitting directly to YTsaurus (from version 1.76.0) { #submit } For submitting Spark tasks [directly to YTsaurus](https://ytsaurus.tech/docs/launch.md#submit) a `SparkSession` object should be created according to Spark recommendations: ```scala import org.apache.spark.SparkConf import org.apache.spark.sql.SparkSession object MySparkApplication { def main(args: Array[String]): Unit = { val conf = new SparkConf() val spark = SparkSession.builder.config(conf).getOrCreate() try { // Application code } finally { spark.stop() } } } ``` In this case a standard `spark-submit` command should be used: ```bash $ spark-submit \ --master ytsaurus://${YT_PROXY} \ --deploy-mode cluster \ --class tech.ytsaurus.spyt.examples.GroupingExample \ yt:///home/spark/examples/scala-examples-assembly-0.1.jar ``` # SPYT in Java ## Project configuration, dependencies, building { #prepare } All the examples in this section have been collected with `maven`. - All the dependencies required to run jobs in a cluster are specified in the Vanilla operation at cluster startup. That is why the example shows all the dependencies as having been added in the `Provided` `scope`. They will be used for compilation only and will not be included in the `.jar` or run. - Since the release of Spark 3.0, the project has been using Java 11. - The code has been built using the `mvn clean compile assembly:assembly` command and posted to YTsaurus: `//sys/spark/examples/spark-over-yt-examples-jar-with-dependencies.jar`. You must always upload your code to YTsaurus as Spark does not distribute user files to [workers](https://ytsaurus.tech/docs/cluster/cluster-desc.md#cluster-mode). ## Running examples { #run-examples } Each of the examples includes a single class inherited from `SparkAppJava`.`SparkJavaApp` initializes Sparks and exits without errors. This method is suitable for working with [inner Spark standalone cluster](https://ytsaurus.tech/docs/launch.md#standalone). For launching Spark applications using[YTsaurus scheduler](https://ytsaurus.tech/docs/launch.md#submit) you should refer to standard Spark recommendations for creating `SparkSession`: ```java import org.apache.spark.SparkConf; import org.apache.spark.sql.SparkSession; class MySparkApplication { public static void main(String[] args) { SparkConf conf = new SparkConf(); SparkSession spark = SparkSession.builder.config(conf).getOrCreate(); try { // Application code } finally { spark.stop(); } } } ``` ## Reading data from YTsaurus { #read-data } ```java package tech.ytsaurus.spyt.example; import org.apache.spark.sql.SparkSession; import tech.ytsaurus.client.CompoundClient; import tech.ytsaurus.spyt.SparkAppJava; public class SmokeTest extends SparkAppJava { @Override protected void doRun(String[] args, SparkSession spark, CompoundClient yt) { spark.read().format("yt").load("//home/spark/examples/tables/test_data").show(); } public static void main(String[] args) { new SmokeTest().run(args); } } ``` 1. The job class is inherited from `SparkAppJava`. The auxiliary `run` method initializes `SparkSession` and calls the `doRun` abstract method. In `main`, we call this method from inside an instance. 2. `SparkSession` is the entry point used to communicate with the framework. The most frequently called method is `read` that reads data from YTsaurus. 3. `spark.read().format("yt").load("//home/spark/examples/tables/test_data")`: Read data in `yt` format from `//home/spark/examples/tables/test_data`. The read operation returns a `Dataset` object aliased as `DataFrame` in Scala. DataFrames are Spark's internal representations of schematized tables. You can filter and join these, as well as add new columns. By applying operations like these to a DataFrame, you create a new DataFrame for subsequent use. A DataFrame includes several partitions that you can process individually. A DataFrame read from YTsaurus has as many partitions as the YTsaurus table had [chunks](https://ytsaurus.tech/storage/chunks.md) . The example here calls the DataFrame's `show()` method. The output is the first few rows of the DataFrame (20 by default) displayed to `stdout`. In the logs, the output will look like this: ``` +-----+ |value| +-----+ | 1| | 2| | 3| | 4| | 5| | 6| | 7| | 8| | 9| | 10| +-----+ ``` Start is accomplished with the `spark-submit-yt` command. The arguments must include Spark cluster location information, the `main class`, and the `.jar` executable: ```bash spark-submit-yt \ --proxy \ --discovery-path //my/discovery/path \ --deploy-mode cluster \ --class tech.ytsaurus.spyt.example.SmokeTest \ yt:///sys/spark/examples/spark-over-yt-examples-jar-with-dependencies.jar ``` ## Using UDF { #use-udf } ```java package tech.ytsaurus.spyt.example; import org.apache.spark.sql.*; import org.apache.spark.sql.expressions.UserDefinedFunction; import org.apache.spark.sql.types.DataTypes; import tech.ytsaurus.client.CompoundClient; import tech.ytsaurus.spyt.SparkAppJava; public class UdfExample extends SparkAppJava { @Override protected void doRun(String[] args, SparkSession spark, CompoundClient yt) { Dataset df = spark.read().format("yt").load("//home/spark/examples/tables/example_1"); UserDefinedFunction splitUdf = functions.udf((String s) -> s.split("-")[1], DataTypes.StringType); df .filter(df.col("id").gt(5)) .select(splitUdf.apply(df.col("uuid")).as("value")) .write().mode(SaveMode.Overwrite).format("yt") .save("//home/spark/examples/tables/example_1_map"); } public static void main(String[] args) { new UdfExample().run(args); } } ``` 1. The job class is inherited from `SparkAppJava` as in the previous example. 2. Read a DataFrame from `//home/spark/examples/tables/example_1`. 3. You need to write a UDF to parse the `uuid` and obtain a substring. - Create a parsing lambda function: `(String s) -> s.split("-")[1]`. - Place it in an object wrapper using `functions.udf`. - Tell Spark that its output value is of type `DataTypes.StringType`. 4. Filter a DataFrame on column `id > 5`. 5. Apply the `udf` to the `uuid` column and call the new column `value`. 6. Save the output to YTsaurus. By default, Spark does not overwrite existing tables but throws an exception instead. To overwrite data, use `.mode(SaveMode.Overwrite)`. Output: `//home/spark/examples/tables/example_1_map`. Start is accomplished with the `spark-submit-yt` command: ```bash spark-submit-yt \ --proxy \ --discovery-path //my/discovery/path \ --deploy-mode cluster \ --class tech.ytsaurus.spyt.example.UdfExample \ yt:///sys/spark/examples/spark-over-yt-examples-jar-with-dependencies.jar ``` ## Aggregations { #agg } ```java package tech.ytsaurus.spyt.example; import org.apache.spark.SparkConf; import org.apache.spark.sql.*; import tech.ytsaurus.spyt.SparkAppJava; import tech.ytsaurus.client.CompoundClient; public class GroupingExample extends SparkAppJava { @Override protected void doRun(String[] args, SparkSession spark, CompoundClient yt) { Dataset df = spark.read().format("yt").load("//home/spark/examples/tables/example_1"); Dataset dictDf = spark.read().format("yt").load("//home/spark/examples/tables/example_dict"); df .join(dictDf, df.col("uuid").equalTo(dictDf.col("uuid")), "left_outer") .groupBy("count") .agg(functions.max("id").as("max_id")) .repartition(1) .write().mode(SaveMode.Overwrite).format("yt").save("//home/spark/examples/tables/example_1_agg"); } public static void main(String[] args) { new GroupingExample().run(args); } @Override protected SparkConf getSparkConf() { return super.getSparkConf().setAppName("Custom name"); } } ``` 1. The job class is inherited from `SparkAppJava` as in the previous example. 2. Read a DataFrame from `//home/spark/examples/tables/example_1`. 3. Read a DataFrame from `//home/spark/examples/tables/example_dict`. 4. Join on the `uuid` column. In this example, Spark will evaluate the size of the right DataFrame and decide that a `map-side join` is feasible. In Spark terminology, this situation is called a `broadcast`. 5. Group the output by `count` to find the greatest `id` for each `count`. For groupings, Spark re-shuffles a DataFrame to place records with identical keys in the same partition. The number of partitions following a shuffle is defined by `spark.sql.shuffle.partitions`. By default, there are `200` partitions. You can change this value when you start a job. 6. Save the output to YTsaurus. The saved table will have as many chunks as the DataFrame being written had partitions. There were many partitions (200) because of the grouping that had been made. To avoid creating many small chunks in a write, aggregate them into a single partition with `repartition(1)` before a write. Output: `//home/spark/examples/tables/example_1_agg`. Start is accomplished with the `spark-submit-yt` command. You also need to specify `spark.sql.shuffle.partitions`: ```bash spark-submit-yt \ --proxy \ --discovery-path //my/discovery/path \ --conf spark.sql.shuffle.partitions=8 \ --deploy-mode cluster \ --class tech.ytsaurus.spyt.example.GroupingExample \ yt:///sys/spark/examples/spark-over-yt-examples-jar-with-dependencies.jar ``` ## Other examples [Example](https://github.com/ytsaurus/ytsaurus/blob/main/yt/spark/spark-over-yt/java-submit-example/src/main/java/tech/ytsaurus/spyt/example/submit/SubmitExample.java) job start and output verification. {% note warning "Attention!" %} You need to add `spark-yt-submit-client_2.12` to the dependencies. The client, in turn, has `runtime` dependencies on Spark classes. You do not need to add these dependencies to the build for jobs that run in the cluster. The Spark dependency must have the `Provided` value since the Spark classes are already available in the cluster. This dependency is only used to run jobs. {% endnote %} 1. `SubmissionClient` is a client to submit jobs to a specific cluster. The client finds the location of the master with `discovery_path` and communicates with it. 2. The parameters of the job to be started are described in the `launcher` object. See the source code for a complete listing of the object's methods. The methods match the `spark-submit` parameters. 3. Once you have submitted a job to a cluster, you can check its status with the `client.getStatus` method. The example shows the timeout to get the final output but it is optional. # SPYT reference This section contains SPYT data types, configuration parameters, and read and write options. - [Data types](https://ytsaurus.tech/docs/en/data-types.md) - [Configuration parameters](https://ytsaurus.tech/docs/en/configuration.md) - [Read options](https://ytsaurus.tech/docs/en/read-options.md) - [Write options](https://ytsaurus.tech/docs/en/write-options.md) - [Streaming options](https://ytsaurus.tech/docs/en/streaming-options.md) - [GPU usage](https://ytsaurus.tech/docs/en/gpu-usage.md) # Data type mapping: SPYT, Spark, and YTsaurus This article describes the mapping between [YTsaurus data types](https://ytsaurus.tech/storage/data-types.md) and [Apache Spark data types](https://spark.apache.org/docs/latest/sql-ref-datatypes.html) for read and write. ## Features { #features } 1. **Type hints**: Some mappings can be overridden by [type hints](https://ytsaurus.tech/docs/en/read-options.md#schema_hint) in field metadata. This enables precise control of type conversion. 2. **Decimal precision**: YTsaurus limits the `precision` of the `decimal` type to 35 (not `scale`). [Learn more about Decimal](https://ytsaurus.tech/storage/data-types.md#schema_decimal) 3. **String types**: The configuration option `stringToUtf8` controls whether strings map to `string` or `utf8` in YTsaurus. Example: ```python import spyt from pyspark.sql import Row from pyspark.sql.types import StructType, StructField, StringType, BinaryType spark.createDataFrame( spark.sparkContext.parallelize([Row("string", b"binary")]), StructType([ StructField("string", StringType(), nullable=False), StructField("binary", BinaryType(), nullable=False), ]) ).write \ .option("string_to_utf8", "true") \ .yt("//path/to/table") ``` 4. **Composite types**: Complex types such as structs, arrays, and dictionaries are mapped recursively using the same rules for their element types. 5. **Float type when reading** is mapped differently depending on its position in the schema: - **Top‑level column** → `FloatType`; - **Element inside a composite type** (`Array`, `Dict`, `Struct`, `Tuple`, `Variant`) → `DoubleType`. This is due to a feature of the YSON format: YSON does not have a 4‑byte float node, so `float` inside composite types is stored as an 8‑byte double. To avoid data loss when reading, SPYT converts it to `DoubleType`. Top‑level columns are read via the wire protocol, where the `float` type is preserved as `FloatType`. 6. **Arrow is not supported**: For types marked as such, when the [`spark.yt.read.arrow.enabled`](https://ytsaurus.tech/docs/cluster/configuration.md) option is enabled (default `true`), SPYT automatically switches to the wire protocol. {% note warning %} If at least one column in the schema contains a type without Arrow support, the entire table is read via the wire protocol, including other columns. Consider this when designing the schema if read performance is important. {% endnote %} For special techniques related to data types, see [Read options](https://ytsaurus.tech/docs/en/read-options.md) and [Write options](https://ytsaurus.tech/docs/en/write-options.md). ## Type mapping when reading from YTsaurus { #read-types } | YTsaurus type | Spark type (Basic) | Spark type (Extended) | Notes | |-----------------------|--------------------|-----------------------|-------| | null | NullType | NullType | | | int64 | LongType | LongType | | | uint64 | DecimalType(20, 0) | UInt64Type | | | float | FloatType/DoubleType | FloatType/DoubleType | Top‑level: `FloatType`; inside a composite type: `DoubleType` (see Features, item 5). Arrow is not supported | | double | DoubleType | DoubleType | | | boolean | BooleanType | BooleanType | | | string | StringType | StringType | | | binary | BinaryType | BinaryType | | | any/yson | BinaryType | YsonType | `YsonType` is a custom SPYT type (see [section below](https://ytsaurus.tech/docs/en/#yson-type)) | | int8 | ByteType | ByteType | | | uint8 | ShortType | ShortType | | | int16 | ShortType | ShortType | | | uint16 | IntegerType | IntegerType | | | int32 | IntegerType | IntegerType | | | uint32 | LongType | LongType | | | utf8 | StringType | StringType | | | date | DateType | DateType | Arrow is not supported | | datetime | DatetimeType | DatetimeType | Arrow is not supported | | timestamp | TimestampType | TimestampType | Arrow is not supported | | interval | LongType | LongType | Arrow is not supported | | date32 | Date32Type | Date32Type | Custom SPYT wide‑range type (see [section below](https://ytsaurus.tech/docs/en/#wide-datetime-types)). Arrow is not supported | | datetime64 | Datetime64Type | Datetime64Type | Custom SPYT wide‑range type (see [section below](https://ytsaurus.tech/docs/en/#wide-datetime-types)). Arrow is not supported | | timestamp64 | Timestamp64Type | Timestamp64Type | Custom SPYT wide‑range type (see [section below](https://ytsaurus.tech/docs/en/#wide-datetime-types)). Arrow is not supported | | interval64 | Interval64Type | Interval64Type | Custom SPYT wide‑range type (see [section below](https://ytsaurus.tech/docs/en/#wide-datetime-types)). Arrow is not supported | | uuid | StringType | StringType | Arrow is not supported | | json | StringType | StringType | Arrow is not supported | | void | NullType | NullType | | ### Composite types { #composite-types } In the notations below, `inner`, `key`, `value` are nested element types. Each of them follows the “inner level” rules (for example, `float` → `DoubleType`). | YTsaurus type | Mapping to Spark type | |-----------------------|---------------------| | Optional(inner) | inner type with nullable = true | | Dict(key, value) | `MapType(key_type, value_type, value.nullable)`, where `key_type` and `value_type` are Spark types of the key and value, determined by inner‑level rules | | Array(inner) | `ArrayType(inner_type, inner.nullable)`, where `inner_type` is the Spark type of the element according to inner‑level rules | | Struct(fields) | StructType with corresponding field mappings | | Tuple(elements) | StructType with fields named `_1`, `_2`, etc. | | VariantOverStruct | StructType with fields prefixed with `_v` and nullable = true | | VariantOverTuple | StructType with fields named `_v_1`, `_v_2`, etc., and nullable = true | ## Type mapping when writing to YTsaurus { #write-types } | Spark type | YTsaurus type | Notes | |---------------------|----------------------|-------| | NullType | null | | | ByteType | int8 | | | ShortType | int16 (or uint8 when using schema_hint) | See [example below](https://ytsaurus.tech/docs/en/#schema-hint) | | IntegerType | int32 (or uint16 when using schema_hint) | | | LongType | int64 (or uint32 when using schema_hint) | | | StringType | string (or utf8/json/uuid when using schema_hint, or utf8 when stringToUtf8 is enabled) | | | FloatType | float | | | DoubleType | double | | | BooleanType | boolean | | | DecimalType | Decimal(precision, scale) | `precision` is limited to 35 | | ArrayType | Array(inner) | | | StructType (tuple) | Tuple(elements) | | | StructType (variant) | VariantOverStruct/VariantOverTuple | | | StructType | Struct(fields) | | | MapType | Dict(key, value) | | | BinaryType | binary | | | DateType | date | | | DatetimeType | datetime | | | TimestampType | timestamp | | | Date32Type | date32 | | | Datetime64Type | datetime64 | | | Timestamp64Type | timestamp64 | | | Interval64Type | interval64 | | | UInt64Type | uint64 | | | YsonType | any | `YsonType` is a custom SPYT type (see [section below](https://ytsaurus.tech/docs/en/#yson-type)) | ### Redefining type via schema_hint { #schema-hint } If the default mapping is not suitable, you can explicitly specify the target YTsaurus type via [schema_hint](https://ytsaurus.tech/docs/en/read-options.md#schema_hint). {% list tabs %} - Python ```python df.write.schema_hint({"my_col": "uint8"}).yt("//path/to/table") ``` - Scala ```scala df.write.schemaHint(Map("my_col" -> YtLogicalType.Uint8)).yt("//path/to/table") ``` {% endlist %} ## Custom SPYT types { #custom-types } The types listed below are not part of standard Apache Spark — they are provided by the SPYT library. ### Wide‑range temporal types { #wide-datetime-types } YTsaurus has two sets of temporal types: - **Standard** (`date`, `datetime`, `timestamp`, `interval`) — unsigned integers; range `[1970-01-01, 2105-12-31]`. - **Wide** (`date32`, `datetime64`, `timestamp64`, `interval64`) — signed integers (int32/int64); range of about 145 000 years into the past and future from the Unix epoch. Wide types were introduced after standard ones and are preferred for data outside the 1970–2105 range. In Spark, they correspond to custom SPYT types: `Date32Type`, `Datetime64Type`, `Timestamp64Type`, `Interval64Type`. Learn more about temporal types in YTsaurus in the [Temporal types](https://ytsaurus.tech/storage/data-types.md#temporal_types) section. ### YsonType { #yson-type } `YsonType` is a custom UDT (User Defined Type) storing data as YSON bytes. - When reading from YTsaurus: type `any`/`yson` → `YsonType` (extended mode) or `BinaryType` (basic mode). - When writing to YTsaurus: `YsonType` → type `any`. Learn more about working with `YsonType` in the [Python API](https://ytsaurus.tech/docs/API/spyt-python.md) and [Scala API](https://ytsaurus.tech/docs/API/spyt-scala.md) sections. # Configuration parameters for running Spark tasks This section contains a list of configuration parameters that can be passed when launching Spark tasks. This is done by specifying additional parameters via the `--conf` option of basic Spark commands, such as `spark-submit` and `spark-shell`, as well as YTsaurus wrappers for them, such as `spark-submit-yt` and `spark-shell-yt`. ## Basic options { #main } Most of the options are available starting with version 1.23.0, unless otherwise specified. | **Parameter** | **Default value** | **Description** | **Starting with version** | | ------------------- | --------------- | ---------------------------|------------------------------ | | `spark.yt.write.batchSize` | `500000` | Size of data sent in a single `WriteTable` operation. | - | | `spark.yt.write.miniBatchSize` | `1000` | Size of a data block sent in `WriteTable`. | - | | `spark.yt.write.timeout` | `120 seconds` | Write timeout limit for a single data block. | - | | `spark.yt.write.dynBatchSize` | `50000` | Maximum number of rows in a single write operation to a dynamic table. Used in [Structured Streaming](https://ytsaurus.tech/docs/structured-streaming/index.md) | 2.6.5 | | `spark.yt.write.typeV3.enabled` (`spark.yt.write.writingTypeV3.enabled` before 1.75.2) | `true` | Writing of tables with a schema in [type_v3](https://ytsaurus.tech/storage/data-types.md) format instead of `type_v1`. | 1.75.3 | | `spark.yt.read.vectorized.capacity` | `1000` | Maximum number of rows in a batch for reading via the `wire` protocol. | - | | `spark.yt.read.arrow.enabled` | `true` | Use the `arrow` format to read data (if possible). | - | | `spark.hadoop.yt.timeout` | `30 seconds` | Timeout on reads from YTsaurus. | - | | `spark.yt.read.typeV3.enabled` (`spark.yt.read.parsingTypeV3.enabled` before 1.75.2) | `true` | Reading of tables with a schema in [type_v3](https://ytsaurus.tech/storage/data-types.md) format instead of `type_v1`. | 1.75.3 | | `spark.yt.read.keyColumnsFilterPushdown.enabled` | `true` | Use Spark query filters to selectively read from YTsaurus. | - | | `spark.yt.read.keyColumnsFilterPushdown.union.enabled` | `false` | Combine all filters into a continuous range for selective reading. | - | | `spark.yt.read.keyColumnsFilterPushdown.mergeAdjacent.enabled` | `false` | Merge ranges over consecutive integer key values into a single range. | 2.12.0 | | `spark.yt.read.keyColumnsFilterPushdown.ytPathCount.limit` | `100` | Maximum number of table ranges for selective reading. | - | | `spark.yt.transaction.timeout` | `5 minutes` | Write operation transaction timeout. | - | | `spark.yt.transaction.pingInterval` | `30 seconds` | Pinging interval of a write operation transaction. | - | | `spark.yt.globalTransaction.enabled` | `false` | Use a [global transaction](https://ytsaurus.tech/docs/read-transaction.md). | - | | `spark.yt.globalTransaction.id` | `None` | Global transaction ID. | - | | `spark.yt.globalTransaction.timeout` | `5 minutes` | Global transaction timeout. | - | | `spark.yt.streaming.transactional` | `false` | Enable [streaming transactional mode](https://ytsaurus.tech/docs/structured-streaming/exactly-once/transactional-mode.md) to guarantee `exactly‑once`. Requires `spark.ytsaurus.rpc.job.proxy.enabled = false` as well. | 2.10 | | `spark.hadoop.yt.user` | - | YTsaurus user name. | - | | `spark.hadoop.yt.token` | - | YTsaurus user token. | - | | `spark.yt.read.ytPartitioning.enabled` | `true` | Use table partitioning by YTsaurus. | 1.72.0 | | `spark.yt.read.ytPartitioning.compressedSize.enabled` | `false` | Enable data partitioning when reading by the compressed size of the table. | 2.9.1 | | `spark.yt.read.planOptimization.enabled` | `false` | Optimize aggregations and joins on sorted input data. | - | | `spark.yt.read.countOptimization.enabled` | `true` | Use metadata for count() operations. | - | | `spark.yt.read.keyPartitioningSortedTables.enabled` | `true` | Use sorted table partitioning by key, required to optimize plans. | - | | `spark.yt.read.keyPartitioningSortedTables.unionLimit` | `1` | Maximum number of partition joins when switching from reading by index to reading by key. | - | | `spark.yt.read.transactional` | `true` | Use snapshot lock for reading if transaction is not specified. It is recommended to turn this option off when reading immutable data to improve reading performance. | 2.6.0 | | `spark.yt.read.listParentDirectories` | `true` | Batch read of attributes from parent directories when reading by a list of tables. Helps when reading a large number of tables from a single parent directory. | 2.7.5 | | `spark.yt.read.ytDistributedReading.enabled` | `false` | Use distributed API for reading data from YTsaurus. This method reduces the number of requests to the YTsaurus master when reading data, but is not yet compatible with the `spark.yt.read.planOptimization.enabled` option. Does not apply to ordered dynamic tables: they are read in the regular way. | 2.8.0 | | `spark.yt.write.distributed.enabled` | `false` | Use distributed API for writing data to YTsaurus. This method reduces the number of requests to the YTsaurus master when writing data, but is only applicable to working with static tables. | 2.8.0 | | `spark.yt.read.ytOmitInaccessibleRows.enabled` | `true` | Skip inaccessible rows instead of throwing an error. | 2.9.0 | | `spark.yt.read.ytOmitInaccessibleColumns.enabled` | `true` | Skip inaccessible columns instead of throwing an error. | 2.9.0 | | `spark.ytsaurus.throttling.maxConcurrency` | `100` | Maximum number of concurrent operations in YTsaurus. A value of `0` removes the limit. | 2.11.0 | | `spark.hadoop.ytsaurus.retry.requests.enabled` | `true` | Retry requests rejected by an overloaded proxy. Applies to clients with a fixed proxy address, including job proxy: clients with proxy discovery retry requests even without this option. Retries responses with the `RequestQueueSizeLimitExceeded`, `RpcRequestQueueSizeLimitExceeded` and `TooManyRequests` codes, as well as network timeouts and connection drops (the latter are retried without a pause). Only unary requests are retried: data reading and writing are stream‑based and are not retried. | 2.11.0 | | `spark.hadoop.ytsaurus.retry.maxRetries` | `3` | Number of times a request is retried after the first failed attempt. | 2.11.0 | | `spark.hadoop.ytsaurus.retry.initialBackoff` | `3 seconds` | Pause before the first retry. | 2.11.0 | | `spark.hadoop.ytsaurus.retry.maxBackoff` | `30 seconds` | Upper limit of the pause, which doubles after each retry. Applies only to the `RequestQueueSizeLimitExceeded` code; other cases are retried using `initialBackoff`. | 2.11.0 | {wide-content title="Basic options"} ## Options for launching tasks directly { #direct-submit } | **Parameter** | **Default value** | **Description** | **Starting with version** | | ------------ | ------------------------- | ------------ | ------------------ | | `spark.ytsaurus.config.global.path` | `//home/spark/conf/global` | Path to the document with a global Spark and SPYT configuration on the cluster. | 1.76.0 | | `spark.ytsaurus.config.releases.path` | `//home/spark/conf/releases` for release versions, `//home/spark/conf/pre-releases` for pre‑release versions. | Path to the SPYT release configuration. | 1.76.0 | | `spark.ytsaurus.distributives.path` | `//home/spark/distrib` | Path to the directory with Spark distributions. Within this directory, the structure looks like `a/b/c/spark-a.b.c-bin-hadoop3.tgz`. | 2.0.0 | | `spark.ytsaurus.config.launch.file` | `spark-launch-conf` | The name of the document with the release configuration located within the directory `spark.ytsaurus.config.releases.path`. | 1.76.0 | | `spark.ytsaurus.spyt.version` | Matches the SPYT version on the client. | The SPYT version to be used on the cluster when launching a Spark application. | 1.76.0 | | `spark.ytsaurus.driver.maxFailures` | 5 | Maximum allowable number of driver failures before the operation is considered failed. | 1.76.0 | | `spark.ytsaurus.executor.maxFailures` | 10 | Maximum allowable number of executor failures before the operation is considered failed. | 1.76.0 | | `spark.ytsaurus.executor.operation.shutdown.delay` | 10000 | Maximum allowable time in milliseconds to wait for executors to finish when stopping the application before aborting the operation with executors. | 1.76.0 | | `spark.ytsaurus.pool` | - | The scheduler pool where driver and executor operations should be run. | 1.78.0 | | `spark.ytsaurus.python.binary.entry.point` | - | The function used as an entry point when using compiled Python tasks. | 2.4.0 | | `spark.ytsaurus.python.executable` | - | Path to the Python interpreter used in the driver and executors. | 1.78.0 | | `spark.ytsaurus.tcp.proxy.enabled` | false | Whether a TCP proxy is used to access the operation. | 2.1.0 | | `spark.ytsaurus.tcp.proxy.range.start` | 30000 | Minimum port number for a TCP proxy. | 2.1.0 | | `spark.ytsaurus.tcp.proxy.range.size` | 1000 | Size of the range of ports that can be allocated for a TCP proxy. | 2.1.0 | | `spark.ytsaurus.cuda.version` | - | CUDA version used for Spark applications. Makes sense if the computations consume GPU. | 2.1.0 | | `spark.ytsaurus.redirect.stdout.to.stderr` | false | Redirect user script output from stdout to stderr. | 2.1.0 | | `spark.ytsaurus.remote.temp.files.directory` | `//tmp/yt_wrapper/file_storage` | Path to cache on Cypress to load local scripts. | 2.4.0 | | `spark.ytsaurus.annotations` | - | Annotations for driver and executor operations. | 2.2.0 | | `spark.ytsaurus.driver.annotations` | - | Annotations for a driver operation. | 2.2.0 | | `spark.ytsaurus.executors.annotations` | - | Annotations for an executor operation. | 2.2.0 | | `spark.ytsaurus.driver.operation.parameters` | - | Additional parameters of the driver operation with YSON format. | 2.6.0 | | `spark.ytsaurus.driver.operation.alias` | - | Alias for the driver operation. The alias must start with the `*` character. | 2.9.0 | | `spark.ytsaurus.driver.task.parameters` | - | Additional parameters of the task within the driver operation with YSON format. | 2.6.0 | | `spark.ytsaurus.executor.operation.parameters` | - | Additional parameters of the executor operation with YSON format. | 2.6.0 | | `spark.ytsaurus.executor.task.parameters` | - | Additional parameters of the YTsaurus executor task with YSON format. | 2.6.0 | | `spark.ytsaurus.driver.watch` | true | Flag for monitoring a driver operation executed in cluster mode. | 2.4.2 | | `spark.ytsaurus.network.project` | - | Name of the network project where a Spark application is launched. | 2.4.3 | | `spark.hadoop.yt.mtn.enabled` | false | Flag for enabling MTN support. | 2.4.3 | | `spark.ytsaurus.squashfs.enabled` | false | Use squashFS layers instead of porto layers in a YTsaurus job. | 2.6.0 | | `spark.ytsaurus.client.rpc.timeout` | - | Timeout used in an RPC client to start YTsaurus operations. | 2.6.0 | | `spark.ytsaurus.rpc.job.proxy.enabled` | true | Flag of using an RPC proxy embedded in a job proxy. | 2.6.0 | | `spark.ytsaurus.java.home` | `/opt/jdk[11,17]` | Path to the JDK home directory used in cluster containers. Depends on the JDK used on the client side. Allowed versions: JDK11 and JDK17. | 2.6.0 | | `spark.ytsaurus.shuffle.enabled` | false | Use the [YTsaurus Shuffle service](https://ytsaurus.tech/docs/shuffle.md). | 2.7.2 | | `spark.ytsaurus.executor.state.poll.interval` | 20s | The period for checking the state of the operation with executors. If the operation is in a final status, the driver will be stopped. | 2.8.0 | {wide-content title="Options for launching tasks directly"} ## Configuration options for the YTsaurus Shuffle service { #shuffle } | **Parameter** | **Default value** | **Description** | **Starting with version** | | ------------ | ------------------------- | ------------ | ------------------ | | `spark.ytsaurus.shuffle.transaction.timeout` | 5m | Timeout for the transaction processing shuffle chunk writes. In regular operation mode, the transaction is periodically pinged by the driver, and the timeout sets the time between the last ping and transaction rollback with chunk deletion. | 2.7.0 | | `spark.ytsaurus.shuffle.account` | intermediate | [Account](https://ytsaurus.tech/storage/accounts.md) used for writing shuffle chunks. | 2.7.0 | | `spark.ytsaurus.shuffle.medium` | - | [Medium](https://ytsaurus.tech/storage/media.md) used for writing shuffle chunks. Defaults to the system‑wide setting. | 2.7.0 | | `spark.ytsaurus.shuffle.replication.factor` | - | Shuffle chunk replication factor. Defaults to the system‑wide setting. | 2.7.0 | | `spark.ytsaurus.shuffle.partition.column` | partition | The name of the chunk column used to store the target partition index. | 2.7.0 | | `spark.ytsaurus.shuffle.write.row.size` | 8m | The maximum size of a single row in a chunk containing shuffle data. This value is not directly related to the size of shuffle data rows but serves to partition serialized shuffle data into chunk rows. Reducing this value increases the number of rows in the chunk, while raising the value may result in exceeding the maximum allowed chunk row size. | 2.7.0 | | `spark.ytsaurus.shuffle.write.buffer.size` | 10 | Shuffle data write buffer size (in rows) in YTsaurus. Set this parameter together with `spark.ytsaurus.shuffle.write.row.size` to avoid RAM overflow. | 2.7.0 | | `spark.ytsaurus.shuffle.config` | - | Shuffle configuration in [YSON](https://ytsaurus.tech/storage/yson.md) format. It is passed once when a shuffle is started and is shared by all writers and readers of that shuffle. May contain only the section of the used shuffle mode (other top-level keys fail shuffle registration): `{pull={reader=...;writer=...}}` when push-based shuffle is disabled (matches the [TableReader](https://ytsaurus.tech/storage/io-configuration.md#table_reader) and [TableWriter](https://ytsaurus.tech/storage/io-configuration.md#table_writer) configurations) or `{push={writer=...;reader=...;journal_writer=...;session_pool=...}}` when push-based shuffle is enabled | 2.12.0 | | `spark.ytsaurus.shuffle.write.config` | - | Deprecated since 2.12.0 and has no effect. Use `spark.ytsaurus.shuffle.config` with `{pull={writer=...}}` instead. | 2.7.0 | | `spark.ytsaurus.shuffle.read.config` | - | Deprecated since 2.12.0 and has no effect. Use `spark.ytsaurus.shuffle.config` with `{pull={reader=...}}` instead. | 2.7.0 | {wide-content title="Configuration options for the YTsaurus Shuffle service"} ## Options for configuring the Spark Connect server { #spyt-connect } | **Parameter** | **Default value** | **Description** | **Starting with version** | | ------------ | ------------------------- | ------------ | ------------------ | | `spark.ytsaurus.connect.idle.timeout` | 10m | Waiting time since the last request is completed till the Spark Connect server is shut down. | 2.8.0 | | `spark.ytsaurus.connect.token.refresh.period` | - | Pinging interval of the temporary token used to execute requests via Spark Connect. Used only together with Query Tracker. | 2.8.0 | {wide-content title="Options for configuring the Spark Connect server"} ## Options for running tasks in an internal cluster { #spark-submit-yt-conf } To run tasks in an internal cluster, use the `spark-submit-yt` wrapper. Its parameters match those of the `spark-submit` command from the Spark distribution, with the following exception: - Instead of `--master`, you should use the parameters `--proxy` and `--discovery-path`. They determine which YTsaurus cluster will be used to run computations and which internal Spark cluster on that YTsaurus cluster the task will be sent to, respectively. # Read options ## Multi-cluster reads { #multicluster_reading } By default, the computation process accesses data on the cluster that provides the computational resources (when running directly or when using a standalone cluster). SPYT runs its own RPC proxies to offload shared cluster proxies. Version 2.2.0 introduced the option to read data from different YTsaurus clusters. To do this, you must explicitly indicate the cluster address in the table path. ```python spark.read.yt('//home/table').show() # Table on cluster localhost:8000 spark.read.yt('//home/table').show() # Table on home cluster ``` {% note info "Note" %} Data is read from other YTsaurus clusters through shared proxies, which may put a heavy strain on them when the volume of data is high. {% endnote %} ## schema_hint { #schema_hint } Hard-coded [column type](https://ytsaurus.tech/storage/data-types.md). Useful when a column is of type `any` (a composite data type serialized as `yson`). The value will be deserialized as the specified type. Python example: ```python spark.read.schema_hint({"value": MapType(StringType(), LongType())}).yt("//sys/spark/examples/example_yson") ``` Scala example: ```scala df.write .schemaHint(Map("a" -> YtLogicalType.VariantOverTuple(Seq( (YtLogicalType.String, Metadata.empty), (YtLogicalType.Double, Metadata.empty))))) .yt(tmpPath) ``` ## transaction { #transaction } Reading from a [transaction](https://ytsaurus.tech/storage/transactions.md). For more details, see [Reading and writing within a transaction](https://ytsaurus.tech/docs/read-transaction.md). Scala example: ```scala val transaction = YtWrapper.createTransaction(None, 10 minute) df.write.transaction(transaction.getId.toString).yt(tmpPath) transaction.commit().get(10, TimeUnit.SECONDS) ``` ## Schema v3 { #type_v3 } Read tables with schema in [type_v3](https://ytsaurus.tech/storage/data-types.md) instead of type_v1. It can be enabled in [Spark configuration](https://ytsaurus.tech/docs/cluster/configuration.md) or write option. Python example: ```python spark.read.option("parsing_type_v3", "true").yt("//sys/spark/examples/example_yson") ``` ## readParallelism { #readParallelism } Setting the target number of partitions when reading a table. Determines the maximum partition size as `totalBytes / readParallelism`, thereby setting the number of Spark tasks during reading. Python example: ```python spark.read.option("readParallelism", "5").yt("//home/table") ``` ## recursiveFileLookup { #recursiveFileLookup } This option controls how the file structure is traversed when reading data. It determines whether the system should recursively descend into nested directories. The option can take the following values: - `true`: The system recursively traverses all subdirectories starting from the specified point and finds all files in the tree. - `false`: The system does not descend recursively into subdirectories and reads only the files in the specified directory. In Apache Spark, when reading data from HDFS and S3, the `recursiveFileLookup` option is `false` by default. In SPYT, the default value is set to `true`. This ensures consistency with the behavior of YQL and CHYT. If you're reading data with Hive‑compatible directory partitioning in Cypress, set the option to `false`. In this case, it's sufficient to process only the top-level files, and recursive traversal isn't required. For example, for this directory structure: ```text home ├── table │ ├── dt=2026-01-01 │ ├── dt=2026-01-02 │ ├── dt=2026-01-03 │ └── dt=2026-01-04 │ ... ``` Python code example: ```python df = spark.read.option("recursiveFileLookup", "false").yt("//home/table") df.printSchema() ... root |-- id: long (nullable = false) |-- value: long (nullable = false) ... |-- dt: string (nullable = false) ``` # Reading and writing within a transaction For more information on transactions, please see the [Transactions](https://ytsaurus.tech/dynamic-tables/transactions.md) section. By default, Spark does not create transactions for reading or take out any locks. To assure reading consistency for changing tables, you can create your own transactions and redirect them to read a Spark DataFrame. Python example: ```python with yt.Transaction() as tr: df = spark.read.option("transaction", tr.transaction_id).yt("//sys/spark/examples/example_1") df.show() ``` At the time of the call to `spark.read.yt`, a lock is taken out on the table (snapshot lock): while a plan is being compiled and until the physical read begins. In the example provided, the two calls to `show()` will read the table twice, necessarily returning the same result both times. ```python with yt.Transaction() as tr: df = spark.read.option("transaction", tr.transaction_id).yt("//sys/spark/examples/example_1") df.show() time.sleep(60) df.show() ``` {% note info "Note" %} You can enable creation of a global transaction that will open at the top of a Spark session and remain open the entire time your job is running. When using a global transaction, you have to keep in mind that the job will take out locks on all the tables it reads and maintain them until it exits. You enable a global transaction by setting configuration parameter `spark.yt.globalTransaction.enabled=true`. {% endnote %} When you need to write a DataFrame within a transaction you should use `write_transaction` option: ```python df.write.option('write_transaction', transaction_id).yt("//target/table/path") ``` # Write options ## sorted_by A sort using a column prefix: ```python df.write.sorted_by("uuid").yt("//sys/spark/examples/test_data") ``` ## unique_keys Uniqueness of a key in a table: ```python df.write.sorted_by("uuid").unique_keys.yt("//sys/spark/examples/test_data") ``` ## optimize_for A table may be stored in row (lookup) or column (scan) format. The preferred format is selected based on the task: ```python spark.write.optimize_for("scan").yt("//sys/spark/examples/test_data") spark.write.optimize_for("lookup").yt("//sys/spark/examples/test_data") ``` ## Schema v3 Write tables with schema in [type_v3](https://ytsaurus.tech/storage/data-types.md) instead of type_v1. It can be enabled via [Spark configuration](https://ytsaurus.tech/docs/cluster/configuration.md) or write option. Python example: ```python df.write.option("write_type_v3", "true") ``` ## security_tags For batch writes to static tables, SPYT automatically propagates the union of `security_tags` from the query's static input tables in YTsaurus. This applies to DataFrame and Spark SQL writes, including cached DataFrames, temporary views, joins, and aggregations. The standard SPYT Spark extensions must be enabled. In `overwrite` mode, the output receives the inferred tags. In `append` mode, these tags are added to the existing output tags. Both ordinary and distributed writes support this behavior. To override the inferred tags, pass a string containing a YSON list: ```python df.write.option("security_tags", '["sensitive";"userdata";]').yt("//tmp/output") ``` The `attr_security_tags` option is an alias. If both options are specified, `security_tags` takes precedence. An explicit empty list, `"[]"`, disables inheritance for that write. On append, an override does not remove existing output tags. Tags are retained with input metadata, including when a DataFrame is cached. SPYT cannot infer tags for data read inside a UDF, data reconstructed after `collect()`, or data whose table provenance was lost through RDD transformations. Supply tags explicitly for these cases. Automatic inheritance is not supported for dynamic tables or streaming queries. ## Dynamic tables For dynamic tables you should explicitly specify an additional option `inconsistent_dynamic_write` with `true` value so that you do agree that there is no support for transactional writes to dynamic tables. Python example: ```python df.write.option("inconsistent_dynamic_write", "true") ``` # Streaming options This section lists SPYT options for Structured Streaming, service columns, and a version compatibility matrix. Spark session parameters (including `spark.yt.streaming.transactional`) are available on the [Configuration parameters](https://ytsaurus.tech/docs/en/configuration.md) page. ## `yt` source and sink options { #options } Passed via `.option(...)` when reading or writing a streaming DataFrame. For an input queue, the `path` parameter is usually passed via `.load(...)`. | **Option** | **Description** | **Required** | **Default value** | **Since version** | | --------- | ------------ | ---------------- | ------------------------- | ------------------ | | `consumer_path` | Path to the consumer table for reading from the queue | Yes, when reading | — | 1.77.0 | | `path` | Path to the input queue when reading or to the output table when writing | Yes | — | 1.77.0 | | `include_service_columns` | Add service columns `__spyt_streaming_src_tablet_index` and `__spyt_streaming_src_row_index` to the DataFrame (correspond to `$tablet_index` and `$row_index` of a row in the original queue) | No | `false` | 2.6.0 | | `max_rows_per_partition` | Maximum number of rows read from a single queue partition within one batch | No | `∞` | 2.6.0 | | `parsing_type_v3` | Read composite types while preserving the type. If the option is not specified, `spark.yt.read.typeV3.enabled` is used | No | `spark.yt.read.typeV3.enabled` | 2.6.0 | | `write_type_v3` | Write composite types while preserving the type. If the option is not specified, `spark.yt.write.typeV3.enabled` is used | No | `spark.yt.write.typeV3.enabled` | 2.6.0 | ## Spark Structured Streaming parameters { #spark-streaming-options } `checkpointLocation` is a Spark Structured Streaming parameter, not a specific `yt` source or sink option. | **Parameter** | **Description** | **When to specify** | **Since version** | | ------------ | ------------ | ------------------- | ------------------ | | `checkpointLocation` | Path to the directory with checkpoint files for a streaming query. Spark stores the intermediate state of stateful operations here: for example, groupBy, windowing, or join. To store in YTsaurus, specify a path like `yt:///...` | Always — this is a mandatory requirement of Spark Structured Streaming | 1.77.0 | ## Service columns { #service-columns } When `include_service_columns = true`, the following columns are added to the streaming DataFrame: | **Column** | **Description** | | ----------- | ------------ | | `__spyt_streaming_src_tablet_index` | Value of `$tablet_index` for a row in the original queue | | `__spyt_streaming_src_row_index` | Value of `$row_index` for a row in the original queue | ## Compatibility matrix { #compatibility-matrix } | **Functionality** | **Minimum SPYT version** | | -------------------- | --------------------------- | | Checkpoint storage on YTsaurus | 1.77.0 | | Structured Streaming over YTsaurus Queue API | 1.77.0 | | Support for composite data types | 2.6.0 | | `max_rows_per_partition` option | 2.6.0 | | `include_service_columns` option | 2.6.0 | | `spark.yt.write.dynBatchSize` parameter | Configurable for streaming since 2.6.5 (previously hard‑coded to 50 000) | | Transactional mode (exactly‑once) | 2.10 | ## See also - [Structured Streaming](https://ytsaurus.tech/docs/structured-streaming/index.md) — overview and main scenarios - [Exactly‑once guarantee](https://ytsaurus.tech/docs/structured-streaming/exactly-once/index.md) — choosing an approach to guarantees - [Transactional mode](https://ytsaurus.tech/docs/structured-streaming/exactly-once/transactional-mode.md) — instructions for enabling exactly‑once - [Configuration parameters](https://ytsaurus.tech/docs/en/configuration.md) — Spark session parameters, including `spark.yt.streaming.transactional` and `spark.yt.write.dynBatchSize` # GPU usage In task execution, some computations can be performed on GPU devices. You can read about GPU resource scheduling in YTsaurus [here](https://ytsaurus.tech/overview#podderzhka-gpu). GPU-based task execution relies on the [RAPIDS Accelerator](https://docs.nvidia.com/spark-rapids/user-guide/23.08/getting-started/on-premise.html) plugin. It takes some of the operations for which conversions are described and transfers them to the GPU. The main parameters are described in the plugin documentation. ## Configuring the GPU for direct execution For direct execution, pools must be configured correctly. Besides redefining the pool, you'll often also need to redefine the pool tree. The driver doesn't need a GPU, meaning you can use an arbitrary pool (the `--queue` option). However, executors do need GPUs, so the pool tree and the pool with GPU resources should be specified for them separately. ```bash spark-submit \ ... \ --queue no_gpu_pool \ --conf spark.executor.resource.gpu.amount=1 \ --conf spark.task.resource.gpu.amount=0.5 \ --conf spark.rapids.memory.pinnedPool.size=2G \ --conf spark.plugins=com.nvidia.spark.SQLPlugin \ --jars yt:///home/spark/lib/rapids-4-spark_2.12-23.12.2-cuda11.jar \ --conf spark.ytsaurus.executor.operation.parameters="{pool=gpu_pool;pool_trees=[gpu_tree]}" \ yt:///home/spark/examples/smoke_test_raw.py ``` Only an integer number of GPU devices can be allocated to each executor, but tasks can share GPUs. The library must be compiled for the CUDA version supplied to the job environment. It is specified at `//home/spark/conf/global/cuda_toolkit_version`. An example task in client mode is also available in the repository: ```bash python python-examples/gpu_example/main.py gpu_pool gpu_tree ``` ## Computations on a Standalone cluster ### Starting a cluster At [cluster startup](https://ytsaurus.tech/docs/launch#standalone), each worker will be allocated an integer number of GPUs specified in the `--worker-gpu-limit` option: ``` spark-launch-yt ... --pool gpu_pool --params '{operation_spec={pool_trees=[gpu_tree]}}' --worker-gpu-limit 2 ``` All cluster components will be run in the specified pool, with GPU devices only requested for workers. ### Running tasks Sample Spark task configuration: ```bash spark-submit-yt \ ... \ --conf spark.executor.resource.gpu.amount=1 \ --conf spark.task.resource.gpu.amount=0.25 \ --conf spark.rapids.memory.pinnedPool.size=2G \ --conf spark.plugins=com.nvidia.spark.SQLPlugin \ --jars yt:///home/spark/lib/rapids-4-spark_2.12-23.12.2-cuda11.jar \ yt:///home/spark/examples/smoke_test_raw.py ``` # Best practices - Each `DROP` call adds a step to the plan, so it is better to avoid multiple column drops. A single `SELECT` with the required set of columns is more efficient; - `LIMIT` operation has its own traits that affect perfomance. It works the following way in Apache Spark: it executes a LocalLimit operation on every partition and each of its result is sent to shuffle and then a GlobalLimit operation is executed on shuffled data. In case of using `limit(x).collect()` or `take(x)` actions the `LIMIT` operation executes iteratively. It starts with only one partition and increases its number by the value of `spark.sql.limit.scaleUpFactor` parameter (which is 4 by default) with every iteration until the requested number of rows is reached. Actions like `count()` or `write.save("/output/path")` execute LocalLimit operation on every partition which causes a performance bottleneck for `LIMIT` on large datasets. There are two solutions for this issue: - for small values passed to `LIMIT` when the result of `take(x)` is fit to driver's memory one can use the following snippet: `limitedDf = spark.createDataFrame(spark.sparkContext.parallelize(df.take(10000)), df.schema)`; - as an alternative to `LIMIT` a [TABLESAMPLE](https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-sampling.html) operation can be used. Caution: `TABLESAMPLE (x ROWS)` is an equivalent to `LIMIT x` operation with LocalLimit and GlobalLimit stages in execution plan. - To read the data which uses a Hive-compatible partitioning scheme by directories in Cypress the `recursiveFileLookup` option should be explicitly set to `false`: `spark.read.option("recursiveFileLookup", "false")`. In SPYT it is set to `true` by default for unification with YQL and CHYT behaviours. # Overview FLYT is a project for integrating [Apache Flink](https://flink.apache.org/) with YTsaurus. It lets you use Flink for streaming and batch data processing, reading data from YTsaurus and writing results back in real time. ## Components {#components} - [flink-connector-ytsaurus](https://ytsaurus.tech/docs/en/flink-connector-ytsaurus.md) — Apache Flink connector for sorted dynamic tables in YTsaurus; supports writes, reading bounded streams, and Lookup operations; - [flink-yson](https://ytsaurus.tech/docs/en/flink-yson.md) — formatter for working with [YSON](https://ytsaurus.tech/storage/yson.md) in Flink jobs. ## Getting Started {#getting-started} If you are new to FLYT, start with the [Quick Start](https://ytsaurus.tech/docs/en/flink-connector-ytsaurus.md#quick-start-guide) section in the connector documentation. # Apache Flink Connector YTsaurus Apache Flink Connector YTsaurus is a connector for streaming and batch data processing on Apache Flink. It works with [sorted dynamic tables](https://ytsaurus.tech/dynamic-tables/sorted-dynamic-tables.md) in YTsaurus and supports writes, reading bounded streams, and Lookup operations. The connector source code is available on [GitHub](https://github.com/ytsaurus/ytsaurus-flyt/tree/main/flink-connector-ytsaurus). ## Features {#features} - writing to YTsaurus dynamic tables — supports streaming writes; the basic scenario is covered in [Quick Start](https://ytsaurus.tech/docs/en/#quick-start-guide); - automatic table creation — creates tables automatically before writing if they do not exist; - advanced table resharding — provides configurable resharding strategies for optimal performance. For details, see [Table Resharding](https://ytsaurus.tech/docs/en/#table-resharding); - data partitioning — supports multiple partitioning granularities: hour, day, week, month, year. For details, see [Data Partitioning](https://ytsaurus.tech/docs/en/#data-partitioning); - synchronous and asynchronous Lookup operations — supports both execution modes for Lookup operations on YTsaurus dynamic tables. For details, see [Lookup Operations](https://ytsaurus.tech/docs/en/#lookup-operations); - Lookup caching — supports `FULL` and `PARTIAL` cache strategies to optimize performance. For details, see [Lookup Operations](https://ytsaurus.tech/docs/en/#lookup-operations); - multi-cluster Lookup support — lets you run Lookup operations against multiple YTsaurus clusters depending on availability. See an example in [Examples](https://ytsaurus.tech/docs/en/#examples); - trackable fields — lets you track field values through metrics. ## Installation {#installation} {% note info %} The current version of the connector requires: - Java 11 - Apache Flink 1.20.X {% endnote %} Replace `connectorVersion` with the latest version from [Maven Central](https://central.sonatype.com/artifact/tech.ytsaurus.flyt.connectors.ytsaurus/flink-connector-ytsaurus). {% list tabs %} - Maven ```xml tech.ytsaurus.flyt.connectors.ytsaurus flink-connector-ytsaurus ${connectorVersion} all ``` - Gradle ```kotlin implementation("tech.ytsaurus.flyt.connectors.ytsaurus:flink-connector-ytsaurus:$connectorVersion:all") ``` {% endlist %} ## Quick Start {#quick-start-guide} ### 1. Install a YTsaurus cluster {#quick-start-install-ytsaurus} You can skip this step if you already have a cluster configured. For local development and testing, follow the official documentation to [install the YTsaurus cluster via Kind](https://ytsaurus.tech/overview/try-yt.md). For production deployments, follow the [YTsaurus Admin Guide](https://ytsaurus.tech/admin-guide/install-ytsaurus.md). {% note info %} `flink-connector-ytsaurus` uses the [Java YTsaurus client](https://ytsaurus.tech/api/java/examples.md). Before you start, verify which proxy address you need to specify in the `proxy` parameter for your environment. {% endnote %} ### 2. Install an Apache Flink cluster {#quick-start-install-flink} Install an Apache Flink cluster using the [official documentation](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/try-flink/local_installation/#downloading-flink). The connector requires Apache Flink version `1.20.X`. ### 3. Install Flink Connector YTsaurus in the Apache Flink cluster {#quick-start-install-connector} Build the connector from source (see [Building from Source](https://github.com/ytsaurus/ytsaurus-flyt/tree/main/flink-connector-ytsaurus#build-steps)) or download it from the [Maven repository](https://central.sonatype.com/artifact/tech.ytsaurus.flyt.connectors.ytsaurus/flink-connector-ytsaurus). After you build or download the connector, place the resulting JAR file in `${FLINK_ROOT}/lib`. ### 4. Change the Flink Web UI port {#quick-start-change-flink-port} Open the `conf/config.yaml` file and change the `rest.port` parameter from `8081` to `8083` to avoid port conflicts with YTsaurus. ### 5. Start the Apache Flink cluster with Flink SQL Client {#quick-start-start-flink} Start the Apache Flink cluster: ```bash ./bin/start-cluster.sh ``` Start Flink SQL Client: ```bash ./bin/sql-client.sh ``` ### 6. Run the demo job {#quick-start-run-demo-job} 1. Create a Datagen source: ```sql CREATE TABLE simple_datagen_source ( id BIGINT, name STRING, age INT, salary DOUBLE, is_active BOOLEAN, created_at TIMESTAMP(3) ) WITH ( 'connector' = 'datagen', 'rows-per-second' = '50', 'number-of-rows' = '1000', 'fields.id.kind' = 'sequence', 'fields.id.start' = '1', 'fields.id.end' = '100000', 'fields.name.length' = '20', 'fields.age.min' = '22', 'fields.age.max' = '65', 'fields.salary.min' = '30000.0', 'fields.salary.max' = '150000.0' ); ``` 2. Create a YTsaurus sink: ```sql CREATE TABLE ytsaurus_simple_sink ( id BIGINT, name STRING, age INT, salary DOUBLE, is_active BOOLEAN, created_at TIMESTAMP(3), updated_at TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'path' = '//tmp/flink_simple_test_table', 'credentials-source' = 'options', 'username' = 'admin', 'token' = 'password', 'schema' = '[ {"name"="id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="name";"type"="string";"required"=%false}; {"name"="age";"type"="int64";"required"=%false}; {"name"="salary";"type"="double";"required"=%false}; {"name"="is_active";"type"="boolean";"required"=%false}; {"name"="created_at";"type"="string";"required"=%false}; {"name"="updated_at";"type"="string";"required"=%false} ]' ); ``` 3. Run a job that writes the generated data to a YTsaurus dynamic table: ```sql INSERT INTO ytsaurus_simple_sink SELECT id, name, age, salary, is_active, created_at, CURRENT_TIMESTAMP AS updated_at FROM simple_datagen_source; ``` 4. Monitor job progress at [localhost:8083](http://localhost:8083). ![](https://ytsaurus.tech/_images/flyt-connector-simple-sink-job-ui.png) 5. The table `flink_simple_test_table` will be created in the `/tmp/flink_simple_test_table` directory and will contain the results of the Flink job. ![](https://ytsaurus.tech/_images/flyt-connector-simple-sink-result-ui.png) Congratulations! You've launched your first job with YTsaurus and Apache Flink. ## Configuration Options {#configuration-options} The YTsaurus connector supports a wide range of configuration options. The options below are grouped by purpose. For most write and Lookup scenarios, specify the table path by using `path`. In multi-cluster configurations, use `path-map` instead of `path`. This section describes: - [Required Options](https://ytsaurus.tech/docs/en/#required-options); - [Path Configuration](https://ytsaurus.tech/docs/en/#path-configuration); - [Authentication Options](https://ytsaurus.tech/docs/en/#auth-options); - [Table Configuration](https://ytsaurus.tech/docs/en/#table-configuration); - [Partitioning Options](https://ytsaurus.tech/docs/en/#partitioning-options); - [Resharding Options](https://ytsaurus.tech/docs/en/#resharding-options); - [Transaction and Performance Options](https://ytsaurus.tech/docs/en/#transaction-performance-options); - [Lookup Options](https://ytsaurus.tech/docs/en/#lookup-options); - [Cache Options for Lookup Operations](https://ytsaurus.tech/docs/en/#lookup-cache-options); - [Other Options](https://ytsaurus.tech/docs/en/#other-options). #### Required Options {#required-options} | Option | Type | Description| |--------|------|------------| | `proxy` | String | YTsaurus proxy address| | `schema` | String | YSON schema definition for the YTsaurus table. See [Schema Definition](https://ytsaurus.tech/docs/en/#schema-definition) | | `credentials-source` | String | Authentication method (`options`, `env`, `your-custom-provider`) | #### Path Configuration {#path-configuration} | Option | Type | Default | Description| |--------|------|---------|------------| | `path` | String | - | Path to the YTsaurus table| | `path-map` | Map | - | Mapping from cluster to table path for multi-cluster lookups | Specify one of the following options: - `path` - for a single table in a single cluster; - `path-map` - for multi-cluster Lookup scenarios. #### Authentication Options {#auth-options} | Option | Type | Default | Description | |--------|------|---------|-------------| | `username` | String | - | YTsaurus username (when using `options` credentials source) | | `token` | String | - | YTsaurus token (when using `options` credentials source) | #### Table Configuration {#table-configuration} | Option | Type | Default | Description | |--------|------|---------|-------------| | `optimize-for` | Enum | - | Table optimization mode (`LOOKUP`, `SCAN`) | | `primary-medium` | Enum | - | Primary storage medium (`DEFAULT`, `SSD_BLOBS`) | | `tablet-cell-bundle` | String | - | Tablet cell bundle name | | `enable-dynamic-store-read` | Boolean | `true` | Enable dynamic store read attribute | | `custom-attributes` | String | - | Custom table attributes in YSON format | #### Partitioning Options {#partitioning-options} | Option | Type | Default | Description | |--------|------|---------|-------------| | `partition-key` | String | - | Column name to use for partitioning | | `partition-scale` | Enum | - | Partitioning granularity (`HOUR`, `HOUR_T`, `DAY`, `WEEK`, `MONTH`, `SHORT_MONTH`, `YEAR`, `SHORT_YEAR`) | | `partition-ttl-day-cnt` | Integer | - | Number of days to keep partitions | | `partition-ttl-in-days-from-creation` | Integer | - | TTL in days from partition creation | | `min-partition-ttl` | Integer | `20` | Minimum partition TTL in days | #### Resharding Options {#resharding-options} | Option | Type | Default | Description| |--------|------|---------|------------| | `reshard.strategy` | Enum | `NONE` | Resharding strategy (`NONE`, `FIXED`, `LAST_PARTITIONS`) | | `reshard.tablet-count` | Integer | - | Number of tablets for resharding| | `reshard.uniform` | Boolean | `false` | Use uniform partitioning| | `reshard.last-partitions-count` | Integer | `7` | Number of partitions to consider in `LAST_PARTITIONS` strategy | #### Transaction and Performance Options {#transaction-performance-options} | Option | Type | Default | Description | |--------|------|---------|-------------| | `commit-transaction-period` | Duration | - | Period for committing transactions | | `transaction-timeout` | Duration | - | Transaction timeout | | `transaction-atomicity` | Enum | - | Transaction atomicity level | | `rows-in-transaction-limit` | Integer | - | Maximum rows per transaction | | `rows-in-modification-limit` | Integer | - | Maximum rows per modification | | `retry-strategy` | Enum | `EXPONENTIAL` | Retry strategy (`EXPONENTIAL`, `NO_RETRY`) | #### Lookup Options {#lookup-options} | Option | Type | Default | Description | |--------|------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `lookup.async` | Boolean | `false` | Enable asynchronous lookup | | `lookup-method` | Enum | `LOOKUP` | Lookup method (`LOOKUP`, `SELECT`). `LOOKUP` method works only with key columns, but has better performance. `SELECT` method works with any columns, but works slowly. | | `cluster-pick-strategy` | String | `FirstAvailableClusterPickStrategy` | Strategy for picking clusters in multi-cluster setup. You can choose your own implementation of the strategy. | #### Cache Options for Lookup Operations {#lookup-cache-options} | Option | Type | Default | Description | |--------|------|---------|-------------| | `lookup.cache` | Enum | `NONE` | Cache type (`NONE`, `PARTIAL`, `FULL`) | | `lookup.partial-cache.max-rows` | Long | - | Maximum rows in partial cache | | `lookup.partial-cache.expire-after-write` | Duration | - | Cache expiration after write | | `lookup.partial-cache.expire-after-access` | Duration | - | Cache expiration after access | | `lookup.partial-cache.cache-missing-key` | Boolean | - | Cache missing keys | | `lookup.full-cache.reload-strategy` | Enum | - | Full cache reload strategy (`PERIODIC`, `TIMED`) | | `lookup.full-cache.periodic-reload-interval` | Duration | - | Periodic reload interval | | `lookup.full-cache.timed-reload-iso-time` | String | - | Timed reload ISO time | #### Other Options {#other-options} | Option | Type | Default | Description | |--------|------|---------|------------------------------| | `trackable-field` | String | - | Field name to track | | `proxy-role` | String | - | Proxy role | The `trackable-field` parameter is useful when you need to observe the values of a specific field through connector metrics. ## Schema Definition {#schema-definition} YTsaurus tables require a YSON schema definition. Provide the schema as a YSON list of column definitions. Schema format: ```yson [ {"name"="column_name";"type"="data_type";"required"=%false;"sort_order"="ascending"}; {"name"="another_column";"type"="string";"required"=%false} ] ``` For more information about YTsaurus schemas, see the [official documentation](https://ytsaurus.tech/storage/static-schema.md). ## Authentication {#authentication} The connector supports multiple authentication methods: This section describes: - [Options-based Authentication](https://ytsaurus.tech/docs/en/#auth-via-options); - [Environment-based Authentication](https://ytsaurus.tech/docs/en/#auth-via-env); - [Custom Authentication](https://ytsaurus.tech/docs/en/#custom-auth). #### Options-based Authentication {#auth-via-options} Provide credentials directly in the table configuration: ```sql 'credentials-source' = 'options', 'username' = 'your-username', 'token' = 'your-token' ``` #### Environment-based Authentication {#auth-via-env} Read credentials from environment variables: ```sql 'credentials-source' = 'env' ``` Set the following environment variables: - `YT_USER` - YTsaurus username - `YT_TOKEN` - YTsaurus token #### Custom Authentication {#custom-auth} To create your own authentication method, implement the [`CredentialsProvider`](https://github.com/ytsaurus/ytsaurus-flyt/blob/main/flink-connector-ytsaurus/src/main/java/tech/ytsaurus/flyt/connectors/ytsaurus/common/credentials/CredentialsProvider.java) interface. ## Data Partitioning {#data-partitioning} The connector supports automatic data partitioning based on timestamp fields. To configure partitioning, specify the date or time column in `partition-key` and choose the required granularity in `partition-scale`. The connector automatically calculates the partition value from the selected field. For a working configuration, see [Partitioning Example](https://ytsaurus.tech/docs/en/#partitioning-example). #### Supported Partition Scales {#supported-partition-scales} - `HOUR` - Hourly partitions (format: `YYYY-MM-DD HH:00:00`) - `HOUR_T` - Hourly partitions with T separator (format: `YYYY-MM-DDTHH:00:00`) - `DAY` - Daily partitions (format: `YYYY-MM-DD`) - `WEEK` - Weekly partitions (format: `YYYY-MM-DD`, where the date is the Monday of the corresponding week) - `MONTH` - Monthly partitions (format: `YYYY-MM-01`) - `SHORT_MONTH` - Short monthly partitions (format: `YYYY-MM`) - `YEAR` - Yearly partitions (format: `YYYY-01-01`, that is, the first day of the year) - `SHORT_YEAR` - Short yearly partitions (format: `YYYY`) #### Partitioning Example {#partitioning-example} ```sql CREATE TABLE partitioned_data_source ( id BIGINT, data STRING, event_time TIMESTAMP(3) ) WITH ( 'connector' = 'datagen', 'rows-per-second' = '50', 'number-of-rows' = '1000', 'fields.id.kind' = 'sequence', 'fields.id.start' = '1', 'fields.id.end' = '100000', 'fields.data.length' = '20', 'fields.event_time.max-past' = '7d' ); ``` ```sql CREATE TABLE partitioned_table ( id BIGINT, data STRING, event_time TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'credentials-source' = 'options', 'username' = 'admin', 'token' = 'password', 'path' = '//tmp/partitioned_table', 'partition-key' = 'event_time', 'partition-scale' = 'DAY', 'partition-ttl-day-cnt' = '30', 'schema' = '[ {"name"="id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="data";"type"="string";"required"=%false}; {"name"="event_time";"type"="string";"required"=%false} ]' ); ``` ```sql INSERT INTO partitioned_table SELECT * FROM partitioned_data_source; ``` Result: ![](https://ytsaurus.tech/_images/flyt-connector-partitioned-table-result-ui.png) ## Table Resharding {#table-resharding} The connector supports automatic table resharding at table creation time. Resharding lets you set the number of tablets upfront — either as a fixed value or based on statistics from existing partitions. This helps distribute write load evenly from the start. #### Resharding Strategies {#resharding-strategies} - **`NONE`** - Disable resharding - **`FIXED`** - Resharding with a fixed number of tablets - **`LAST_PARTITIONS`** - Resharding based on the average number of tablets in the last N partitions #### Resharding Example {#resharding-example} ```sql 'reshard.strategy' = 'FIXED', 'reshard.tablet-count' = '10', 'reshard.uniform' = 'true' ``` ## Lookup Operations {#lookup-operations} The connector supports both synchronous and asynchronous Lookup operations with caching. Lookup operations enrich a stream with data from an external table by key. In SQL terms, this corresponds to the `Lookup Join` scenario. For more information about lookup joins, see [{#T}](https://ytsaurus.tech/dynamic-tables/dyn-query-language.md#lookup-joins). This section covers stream enrichment with data from YTsaurus. If you only need the basic write scenario, the steps in [Quick Start](https://ytsaurus.tech/docs/en/#quick-start-guide) are sufficient. This section describes: - [Lookup Methods](https://ytsaurus.tech/docs/en/#lookup-methods); - [Cache Types](https://ytsaurus.tech/docs/en/#lookup-cache-types); - [Lookup Example](https://ytsaurus.tech/docs/en/#lookup-example). #### Lookup Methods {#lookup-methods} - **`LOOKUP`** - standard Lookup operation - **`SELECT`** - Lookup operation based on a `SELECT` query #### Cache Types {#lookup-cache-types} - **`NONE`** - No caching - **`PARTIAL`** - Partial caching with configurable size and TTL - **`FULL`** - Full table caching with periodic or timed reload #### Lookup Example {#lookup-example} The Lookup connector requires a YSON formatter. Build or download the YSON formatter according to the [documentation](https://github.com/ytsaurus/ytsaurus-flyt/tree/main/flink-yson) and place it in `$FLINK_ROOT/lib`. Prepare data for the Lookup operation. ```sql CREATE TABLE users_datagen ( id BIGINT, name STRING, age INT, created_at TIMESTAMP(3) ) WITH ( 'connector' = 'datagen', 'rows-per-second' = '50', 'number-of-rows' = '1000', 'fields.id.kind' = 'sequence', 'fields.id.start' = '1', 'fields.id.end' = '1000' ); ``` ```sql CREATE TABLE lookup_table_sink ( id BIGINT, name STRING, age INT, created_at TIMESTAMP(3), PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'path' = '//tmp/lookup_table', 'credentials-source' = 'options', 'username' = 'admin', 'token' = 'password', 'schema' = '[ {"name"="id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="name";"type"="string";"required"=%false}; {"name"="age";"type"="int64";"required"=%false}; {"name"="created_at";"type"="string";"required"=%false} ]' ); ``` ```sql INSERT INTO lookup_table_sink SELECT id, name, age, created_at FROM users_datagen; ``` Join order data with user data. ```sql CREATE TABLE orders_datagen ( order_id BIGINT, user_id BIGINT, total_price INT, created_at TIMESTAMP(3), proc_time AS PROCTIME() ) WITH ( 'connector' = 'datagen', 'rows-per-second' = '1', 'number-of-rows' = '100', 'fields.user_id.min' = '1', 'fields.user_id.max' = '1000', 'fields.total_price.min' = '1', 'fields.total_price.max' = '10000' ); ``` ```sql CREATE TABLE lookup_table ( id BIGINT, name STRING, age INT, PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'format' = 'yson', 'proxy' = 'localhost:8081', 'credentials-source' = 'options', 'username' = 'admin', 'token' = 'password', 'path' = '//tmp/lookup_table', 'lookup.async' = 'true', 'lookup.cache' = 'PARTIAL', 'lookup.partial-cache.max-rows' = '10000', 'lookup.partial-cache.expire-after-access' = '1h', 'lookup-method' = 'LOOKUP', 'schema' = '[ {"name"="id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="name";"type"="string";"required"=%false} ]' ); ``` ```sql SELECT o.order_id, o.total_price, o.created_at, l.id as user_id, l.name, l.age FROM orders_datagen o LEFT JOIN lookup_table FOR SYSTEM_TIME AS OF o.proc_time AS l ON o.user_id = l.id; ``` Open the Apache Flink UI at [localhost:8083](http://localhost:8083). ![](https://ytsaurus.tech/_images/flyt-connector-lookup-join-job-ui.png) Flink SQL Client displays the results of the Lookup Join operation in real time. ![](https://ytsaurus.tech/_images/flyt-connector-lookup-join-flink-sql-result.png) ## Examples {#examples} This section provides: - [Basic Sink Example](https://ytsaurus.tech/docs/en/#example-basic-sink); - [Partitioned Table with Resharding](https://ytsaurus.tech/docs/en/#example-partitioned-resharding); - [Lookup Table with Full Cache](https://ytsaurus.tech/docs/en/#example-lookup-full-cache); - [Multi-cluster Configuration](https://ytsaurus.tech/docs/en/#example-multicluster-lookup). #### Basic Sink Example {#example-basic-sink} ```sql CREATE TABLE ytsaurus_sink ( user_id BIGINT, username STRING, email STRING, created_at TIMESTAMP(3), PRIMARY KEY (user_id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'path' = '//home/your-user/users_table', 'credentials-source' = 'options', 'username' = 'your-username', 'token' = 'your-token', 'schema' = '[ {"name"="user_id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="username";"type"="string";"required"=%false}; {"name"="email";"type"="string";"required"=%false}; {"name"="created_at";"type"="string";"required"=%false} ]' ); ``` #### Partitioned Table with Resharding {#example-partitioned-resharding} ```sql CREATE TABLE events_table ( event_id BIGINT, user_id BIGINT, event_type STRING, event_data STRING, event_time TIMESTAMP(3), PRIMARY KEY (event_id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'path' = '//home/your-user/events', 'credentials-source' = 'options', 'username' = 'your-username', 'token' = 'your-token', 'partition-key' = 'event_time', 'partition-scale' = 'DAY', 'partition-ttl-day-cnt' = '90', 'reshard.strategy' = 'LAST_PARTITIONS', 'reshard.tablet-count' = '20', 'reshard.last-partitions-count' = '7', 'reshard.uniform' = 'true', 'optimize-for' = 'SCAN', 'schema' = '[ {"name"="event_id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="user_id";"type"="int64";"required"=%false}; {"name"="event_type";"type"="string";"required"=%false}; {"name"="event_data";"type"="string";"required"=%false}; {"name"="event_time";"type"="string";"required"=%false} ]' ); ``` #### Lookup Table with Full Cache {#example-lookup-full-cache} ```sql CREATE TABLE user_lookup ( user_id BIGINT, username STRING, email STRING, PRIMARY KEY (user_id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'path' = '//home/your-user/users', 'credentials-source' = 'options', 'username' = 'your-username', 'token' = 'your-token', 'lookup.cache' = 'FULL', 'lookup.full-cache.reload-strategy' = 'PERIODIC', 'lookup.full-cache.periodic-reload-interval' = '1h', 'optimize-for' = 'LOOKUP', 'schema' = '[ {"name"="user_id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="username";"type"="string";"required"=%false}; {"name"="email";"type"="string";"required"=%false} ]' ); ``` #### Multi-cluster Configuration {#example-multicluster-lookup} ```sql CREATE TABLE multi_cluster_table ( id BIGINT, data STRING, PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'ytsaurus', 'proxy' = 'localhost:8081', 'path-map' = 'cluster1://tmp/table1,cluster2://tmp/table2', 'cluster-pick-strategy' = 'FirstAvailableClusterPickStrategy', 'credentials-source' = 'options', 'username' = 'your-username', 'token' = 'your-token', 'schema' = '[ {"name"="id";"type"="int64";"required"=%false;"sort_order"="ascending"}; {"name"="data";"type"="string";"required"=%false} ]' ); ``` ## What's Next {#whats-next} - [Sorted dynamic tables](https://ytsaurus.tech/dynamic-tables/sorted-dynamic-tables.md) — learn more about the tables the connector works with; - [YSON formatter for Flink](https://ytsaurus.tech/docs/en/flink-yson.md) — if you need to work with YSON directly in Flink jobs; - [YTsaurus Java client](https://ytsaurus.tech/api/java/examples.md) — the low-level API the connector is built on. # Apache Flink YSON Formatter Apache Flink YSON Formatter is a formatter that allows you to use the YSON format in streaming data processing tasks on Apache Flink. ## Installation {% note info %} The current version of the formatter requires: - Java 11 - Apache Flink 1.20.X {% endnote %} Maven ```xml tech.ytsaurus.flyt.formats.yson flink-yson ${formatterVersion} all ``` Gradle ```kotlin implementation("tech.ytsaurus.flyt.formats.yson:flink-yson:$formatterVersion:all") ``` ## Source code The source code is available on the [project page on GitHub](https://github.com/ytsaurus/ytsaurus-flyt/tree/main/flink-yson). # Introduction YQL (Yandex Query Language) is a language of universal declarative queries against data storage and processing systems as well as an infrastructure to run such queries. YQL benefits include: - A powerful graph execution engine that can build MapReduce pipelines with hundreds of nodes and adapt during computation. - Ability to build complex data processing pipelines using SQL by storing subqueries in variables as chains of dependent queries and transactions. - Predictable parallel execution of queries of any complexity. - Efficient implementation of joins, subqueries, and window functions with no restrictions on their topology or nesting. - Extensive function library. - Support for user-defined functions in [C++](https://ytsaurus.tech/docs/en/udf/cpp.md), [Python](https://ytsaurus.tech/docs/en/udf/python.md). - Automatic execution of small parts of queries on prepared compute instances, bypassing MapReduce operations to reduce latency. YQL provides a functional web interface where, among other things, you can: - Write query code. - Start and stop query execution. - View query execution results. - View query history. ## How to try To run your first YQL query: 1. Open the web interface of the YTsaurus cluster and go to the **Queries** tab — you can get there from the menu on the left. 2. Enter the query and click the start button: ```yql SELECT "Hello, World!"; ``` ![](https://ytsaurus.tech/_images/yql-how-to-try.png){ .center } ## Glossary #| || **Term** | **English translation** | **Description** || || Query | Query | Program text in YQL || || Operation | Operation | Query execution process. In YQL terminology, a query and an operation relate to each other much like a program and a process do in an operating system. || || Table | Table | Logically, a table is a list of structures (`List>`) that is a basic primitive for processing large volumes of data in YQL. List items form rows, while structure members serve as table cells. Vertically aligned cells constitute a table column. || || Expression | Expression | A computed value. Typically, it takes one or more table cells as input, with the output becoming a cell in another table. Examples: - `2 * 2;` - `String::ToUpper("hi, " \|\| name \|\| "!")` || || Statement | Statement | Query components separated by semicolons and starting with a verb. Examples: - `SELECT ...;` - `INSERT INTO ... SELECT ... ;` - `REDUCE ... USING ... ON ...;` || || Subquery | Subquery | A query component that, similarly to tables, can be used as input for statements or other subqueries. Optionally, subqueries can be [parameterized](https://ytsaurus.tech/docs/en/syntax/subquery.md) || || Named node | Named node | A mechanism for reusing expressions and subqueries within one query. [More](https://ytsaurus.tech/docs/en/syntax/expressions.md#named-nodes) Examples: - `$foo = 2 + 2;` - `$bar = (SELECT bar FROM my_table);` || || Lambda | Lambda | A parameterizable block consisting of one or more named nodes (specifically expressions, not statements) where the result of the last node becomes the result of the entire lambda call. The call is made by passing parameters in parentheses. [Learn more](https://ytsaurus.tech/docs/en/syntax/expressions.md#lambda) || || UDF (User-Defined Function) | UDF (User-Defined Function) | Functions that let you integrate business logic into a query using one of the supported popular programming languages. [C++](https://ytsaurus.tech/docs/en/udf/cpp.md) UDFs are loaded in compiled `.so` format, with a wide range of [built-in](https://ytsaurus.tech/docs/en/udf/list/pcre.md) functions available as well. [Python](https://ytsaurus.tech/docs/en/udf/python.md) UDFs can use a script from any query string, but require additional description of their signature. Since the YQL optimizer can't peek inside the Python interpreter, **we recommend using lambda functions and C++ UDFs** for better performance. || || Action | Action | A parameterizable block consisting of one or multiple statements that can then be invoked any number of times using special keywords. Unlike lambda functions, they **don't** return any result. [Learn more](https://ytsaurus.tech/docs/en/syntax/action.md) || || Library | Library | A query component stored in a separate file for reuse or convenience. [Learn more](https://ytsaurus.tech/docs/en/syntax/export_import.md) || |# # YQL data types This section contains articles on YQL data types: - [Simple/Primitive types](https://ytsaurus.tech/docs/en/primitive.md) - [Optional types](https://ytsaurus.tech/docs/en/optional.md) - [Containers](https://ytsaurus.tech/docs/en/containers.md) - [Special types](https://ytsaurus.tech/docs/en/special.md) - [Type casting](https://ytsaurus.tech/docs/en/cast.md) - [Text representation of data types](https://ytsaurus.tech/docs/en/type_string.md) # Primitive data types The terms "simple", "primitive", and "elementary" data types are used synonymously. ## Numeric types {#numeric} | Type | Description | Notes | ----- | ----- | ----- | `Bool` | Boolean value. | | `Int8` | A signed integer.
Acceptable values: from -27 to 27–1. | | `Int16` | A signed integer.
Acceptable values: from –215 to 215–1. | | `Int32` | A signed integer.
Acceptable values: from –231 to 231–1. | | `Int64` | A signed integer.
Acceptable values: from –263 to 263–1. | | `Uint8` | Unsigned integer.
Permitted values: from 0 to 28–1. | | `Uint16` | Unsigned integer.
Permitted values: from 0 to 216–1. | | `Uint32` | Unsigned integer.
Permitted values: from 0 to 232–1. | | `Uint64` | Unsigned integer.
Permitted values: from 0 to 264–1. | | `Float` | A real number with variable precision, 4 bytes in size. | | `Double` | A real number with variable precision, 8 bytes in size. | | `Decimal` | A real number with the specified precision, up to 35 decimal digits | | `DyNumber` | Binary representation of a real number with a precision of 38 characters.
Allowed values: positive numbers from 1×10-130 to 1×10126–1, negative numbers from -1×10126–1 to -1×10-130, and 0.
Compatible with the AWS DynamoDB `Number` type. Not recommended for use in apps native to YTsaurus. | ## String types {#string} | Type | Description | Notes | ----- | ----- | ----- | `String` | A string that can contain any binary data | | `Utf8` | [UTF-8](https://en.wikipedia.org/wiki/UTF-8)-coded text. | | `Json` | [JSON](https://en.wikipedia.org/wiki/JSON) represented as text | | `JsonDocument` | [JSON](https://en.wikipedia.org/wiki/JSON) in an indexed binary representation | | `Yson` | [YSON](https://ytsaurus.tech/docs/en/user-guide/storage/formats#yson) in textual or binary presentation | | `Uuid` | [UUID](https://tools.ietf.org/html/rfc4122) universal identifier | Unlike the `JSON` data type that stores the original text representation passed by the user, `JsonDocument` uses an indexed binary representation. An important difference from the point of view of semantics is that `JsonDocument` doesn't preserve formatting, the order of keys in objects, or their duplicates. Thanks to the indexed view, `JsonDocument` lets you bypass the document model using `JsonPath` without the need to parse the full content. This helps efficiently perform operations from the [JSON API](https://ytsaurus.tech/docs/builtins/json.md), reducing delays and the cost of user queries. Execution of `JsonDocument` queries can be up to several times more efficient depending on the type of load. Due to the added redundancy, `JsonDocument` is less effective in storage. The additional storage overhead depends on the specific content, but is 20-30% of the original volume on average. Saving data in `JsonDocument` format requires additional conversion from the textual representation, which makes writing it less efficient. However, for most read-intensive scenarios that involve processing data from JSON, this data type is preferred and recommended. {% note warning %} [Double](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) type is used to store (JSON Number) numerical values in `JsonDocument` and perform arithmetic operations over them in [JSON API](https://ytsaurus.tech/docs/builtins/json.md). Precision might be lost when non-standard representations of numbers are used in the source JSON document. {% endnote %} ## Date and time {#datetime} ### Basic types {#datetime-basic} The value range for all basic temporal types is from midnight1 on 01.01.1970 to midnight1 on 01.01.2106. The value range for the basic interval type is from -136 years to +136 years. | Type | Description | Notes | ----- | ----- | ----- | `Date` | Date, precision to the day | Internal representation: Unsigned 16-bit integer | | `Datetime` | Date/time, precision to the second | Internal representation: Unsigned 32-bit integer | | `Timestamp` | Date/time, precision to the microsecond | Internal representation: Unsigned 64-bit integer | | `Interval` | Time interval (signed), precision to microseconds | Value range: From -136 years to +136 years. Internal representation: Signed 64-bit integer. | | `TzDate` | Date with time zone label, precision to the day | Internal representation: Unsigned 16-bit integer and time zone label | | `TzDatetime` | Date/time with time zone label, precision to the second | Internal representation: Unsigned 32-bit integer and time zone label | | `TzTimestamp` | Date/time with time zone label, precision to the microsecond | Internal representation: Unsigned 64-bit integer and time zone label | 1 Midnight means a point in time where all _time_ components are zero. ### Extended types {#datetime-extended} The value range for all extended temporal types is from midnight1 on 01.01.144169 BC to midnight1 on 01.01.148107. The value range for the extended interval type is from -292277 years to +292277 years. | Type | Description | Notes | ----- | ----- | ----- | `Date32` | Point in time at which midnight1 occurs in UTC, precision to the day | Internal representation: Signed 32-bit integer | | `Datetime64` | Point in time in UTC, precision to the second | Internal representation: Signed 64-bit integer | | `Timestamp64` | Point in time in UTC, precision to the microsecond | Internal representation: Signed 64-bit integer | | `Interval64` | Time interval, precision to the microsecond | Internal representation: Signed 64-bit integer | | `TzDate32` | Point in time in UTC at which midnight occurs in the specified time zone | Internal representation: Signed 32-bit integer and time zone label | | `TzDateTime64` | Point in time in UTC with a time zone label, precision to the second | Internal representation: Signed 64-bit integer and time zone label | | `TzTimestamp64` | Point in time in UTC with a time zone label, precision to the microsecond | Internal representation: Signed 64-bit integer and time zone label | 1 Midnight means a point in time where all _time_ components are zero. ### Supporting types with a time zone label Time zone label for the `TzDate`, `TzDatetime`, `TzTimestamp` types is an attribute that is used: * During ([CAST](https://ytsaurus.tech/docs/syntax/expressions.md#cast), [DateTime::Parse](https://ytsaurus.tech/docs/udf/list/datetime.md#parse), [DateTime::Format](https://ytsaurus.tech/docs/udf/list/datetime.md#format)) converting into a string and from a string. * In [DateTime::Split](https://ytsaurus.tech/docs/udf/list/datetime.md#split), the time zone component appears in `Resource`. The point in time for these types is stored in UTC, and the timezone label doesn't participate in any other calculations in any way. For example: ```yql SELECT --these expressions are always true for any timezones: the timezone doesn't affect the point in time. AddTimezone(CurrentUtcDate(), "Europe/Moscow") == AddTimezone(CurrentUtcDate(), "America/New_York"), AddTimezone(CurrentUtcDatetime(), "Europe/Moscow") == AddTimezone(CurrentUtcDatetime(), "America/New_York"); ``` Keep in mind that when converting between `TzDate` and `TzDatetime`, or `TzTimestamp` the date's midnight doesn't follow the local time zone, but midnight in UTC for the date in UTC. ## Casting between data types {#cast} ### Explicit casting {#explicit-cast} Explicit casting using [CAST](https://ytsaurus.tech/docs/syntax/expressions.md#cast): #### Casting to numeric types | Type | Bool | Int8 | Int16 | Int32 | Int64 | Uint8 | Uint16 | Uint32 | Uint64 | Float | Double | Decimal | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | **Bool** | — | Yes1 | Yes1 | Yes1 | Yes1 | Yes1 | Yes1 | Yes1 | Yes1 | Yes1 | Yes1 | No | | **Int8** | Yes2 | — | Yes | Yes | Yes | Yes3 | Yes3 | Yes3 | Yes3 | Yes | Yes | Yes | | **Int16** | Yes2 | Yes4 | — | Yes | Yes | Yes3,4 | Yes3 | Yes3 | Yes3 | Yes | Yes | Yes | | **Int32** | Yes2 | Yes4 | Yes4 | — | Yes | Yes3,4 | Yes3,4 | Yes3 | Yes3 | Yes | Yes | Yes | | **Int64** | Yes2 | Yes4 | Yes4 | Yes4 | — | Yes3,4 | Yes3,4 | Yes3,4 | Yes3 | Yes | Yes | Yes | | **Uint8** | Yes2 | Yes4 | Yes | Yes | Yes | — | Yes | Yes | Yes | Yes | Yes | Yes | | **Uint16** | Yes2 | Yes4 | Yes4 | Yes | Yes | Yes4 | — | Yes | Yes | Yes | Yes | Yes | | **Uint32** | Yes2 | Yes4 | Yes4 | Yes4 | Yes | Yes4 | Yes4 | — | Yes | Yes | Yes | Yes | | **Uint64** | Yes2 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | — | Yes | Yes | Yes | | **Float** | Yes2 | Yes4 | Yes4 | Yes4 | Yes4 | Yes3,4 | Yes3,4 | Yes3,4 | Yes3,4 | — | Yes | No | | **Double** | Yes2 | Yes4 | Yes4 | Yes4 | Yes4 | Yes3,4 | Yes3,4 | Yes3,4 | Yes3,4 | Yes | — | No | | **Decimal** | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | — | | **String** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | **Utf8** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | **Json** | No | No | No | No | No | No | No | No | No | No | No | No | | **Yson** | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | Yes5 | No | | **Uuid** | No | No | No | No | No | No | No | No | No | No | No | No | | **Date** | No | Yes4 | Yes4 | Yes | Yes | Yes4 | Yes | Yes | Yes | Yes | Yes | Yes | No | | **Datetime** | No | Yes4 | Yes4 | Yes4 | Yes | Yes4 | Yes4 | Yes | Yes | Yes | Yes | No | | **Timestamp** | No | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes | Yes | Yes | No | | **Interval** | No | Yes4 | Yes4 | Yes4 | Yes | Yes3,4 | Yes3,4 | Yes3,4 | Yes3 | Yes | Yes | No | | **Date32** | No | Yes4 | Yes4 | Yes | Yes | Yes4 | Yes | Yes | Yes | Yes | Yes | No | | **Datetime64** | No | Yes4 | Yes4 | Yes4 | Yes | Yes4 | Yes4 | Yes | Yes | Yes | Yes | No | | **Timestamp64** | No | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes4 | Yes | Yes | Yes | No | | **Interval64** | No | Yes4 | Yes4 | Yes4 | Yes | Yes3,4 | Yes3,4 | Yes3,4 | Yes3 | Yes | Yes | No | 1 `True` is converted to `1` and `False` to `0`. 2 Any value other than `0` is converted to `True`, `0` is converted to `False`. 3 Possible only in the case of a non-negative value. 4 Possible only in the case of a value falling within the allowed range. 5 Using the built-in function [Yson::ConvertTo](https://ytsaurus.tech/docs/udf/list/yson.md#ysonconvertto). #### Converting to date and time data types ##### Basic types | Type | Date | Datetime | Timestamp | Interval | --- | --- | --- | --- | --- | **Bool** | No | No | No | No | | **Int8** | Yes | Yes | Yes | Yes | | **Int16** | Yes | Yes | Yes | Yes | | **Int32** | Yes | Yes | Yes | Yes | | **Int64** | Yes | Yes | Yes | Yes | | **Uint8** | Yes | Yes | Yes | Yes | | **Uint16** | Yes | Yes | Yes | Yes | | **Uint32** | Yes | Yes | Yes | Yes | | **Uint64** | Yes | Yes | Yes | Yes | | **Float** | No | No | No | No | | **Double** | No | No | No | No | | **Decimal** | No | No | No | No | | **String** | Yes | Yes | Yes | Yes | | **Utf8** | Yes | Yes | Yes | Yes | | **Json** | No | No | No | No | | **Yson** | No | No | No | No | | **Uuid** | No | No | No | No | | **Date** | — | Yes | Yes | No | | **Datetime** | Yes | — | Yes | No | | **Timestamp** | Yes | Yes | — | No | | **Interval** | No | No | No | — | | **Date32** | Yes | Yes | Yes | No | | **Datetime64** | Yes | Yes | Yes | No | | **Timestamp64** | Yes | Yes | Yes | No | | **Interval64** | No | No | No | Yes | ##### Extended types | Type | Date32 | Datetime64 | Timestamp64 | Interval64 | --- | --- | --- | --- | --- | **Bool** | No | No | No | No | | **Int8** | Yes | Yes | Yes | Yes | | **Int16** | Yes | Yes | Yes | Yes | | **Int32** | Yes | Yes | Yes | Yes | | **Int64** | Yes | Yes | Yes | Yes | | **Uint8** | Yes | Yes | Yes | Yes | | **Uint16** | Yes | Yes | Yes | Yes | | **Uint32** | Yes | Yes | Yes | Yes | | **Uint64** | Yes | Yes | Yes | Yes | | **Float** | No | No | No | No | | **Double** | No | No | No | No | | **Decimal** | No | No | No | No | | **String** | Yes | Yes | Yes | Yes | | **Utf8** | Yes | Yes | Yes | Yes | | **Json** | No | No | No | No | | **Yson** | No | No | No | No | | **Uuid** | No | No | No | No | | **Date** | Yes | Yes | Yes | No | | **Datetime** | Yes | Yes | Yes | No | | **Timestamp** | Yes | Yes | Yes | No | | **Interval** | No | No | No | Yes | | **Date32** | — | Yes | Yes | No | | **Datetime64** | Yes | — | Yes | No | | **Timestamp64** | Yes | Yes | — | No | | **Interval64** | No | No | No | — | #### Conversion to other data types | Type | String | Utf8 | Json | Yson | Uuid | --- | --- | --- | --- | --- | --- | **Bool** | Yes | No | No | No | No | | **Int8** | Yes | No | No | No | No | | **Int16** | Yes | No | No | No | No | | **Int32** | Yes | No | No | No | No | | **Int64** | Yes | No | No | No | No | | **Uint8** | Yes | No | No | No | No | | **Uint16** | Yes | No | No | No | No | | **Uint32** | Yes | No | No | No | No | | **Uint64** | Yes | No | No | No | No | | **Float** | Yes | No | No | No | No | | **Double** | Yes | No | No | No | No | | **Decimal** | Yes | No | No | No | No | | **String** | — | Yes | Yes | Yes | Yes | | **Utf8** | Yes | — | No | No | No | | **Json** | Yes | Yes | — | No | No | | **Yson** | Yes1 | No | No | No | No | | **Uuid** | Yes | Yes | No | No | — | | **Date** | Yes | Yes | No | No | No | | **Datetime** | Yes | Yes | No | No | No | | **Timestamp** | Yes | Yes | No | No | No | | **Interval** | Yes | Yes | No | No | No | | **Date32** | Yes | Yes | No | No | No | | **Datetime64** | Yes | Yes | No | No | No | | **Timestamp64** | Yes | Yes | No | No | No | | **Interval64** | Yes | Yes | No | No | No | 1 Using the built-in function [Yson::ConvertTo](https://ytsaurus.tech/docs/udf/list/yson.md#ysonconvertto). #### Examples ```yql SELECT CAST("12345" AS Double), -- 12345.0 CAST(1.2345 AS Uint8), -- 1 CAST(12345 AS String), -- "12345" CAST("1.2345" AS Decimal(5, 2)), -- 1.23 CAST("xyz" AS Uint64) IS NULL, -- true, because it failed CAST(-1 AS Uint16) IS NULL, -- true, negative to unsigned CAST([-1, 0, 1] AS List), -- [null, 0, 1] --Optional item type: failed item to null. CAST(["3.14", "bad", "42"] AS List), -- [3.14, 42] --Non-optional item type: failed item deleted. CAST(255 AS Uint8), -- 255 CAST(256 AS Uint8) IS NULL -- true, out of range ``` ### Implicit casting {#implicit-cast} Implicit type casting that occurs in basic operations (`+`, `-`, `*`, `/`, `%`) between different data types. The table cells specify the operation result type, if the operation is possible: #### Numeric types If the numeric types don't match, both arguments are BitCast to the result type before performing the operation. | Type | Int8 | Int16 | Int32 | Int64 | Uint8 | Uint16 | Uint32 | Uint64 | Float | Double | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | **Int8** | — | `Int16` | `Int32` | `Int64` | `Int8` | `Uint16` | `Uint32` | `Uint64` | `Float` | `Double` | | **Int16** | `Int16` | — | `Int32` | `Int64` | `Int16` | `Int16` | `Uint32` | `Uint64` | `Float` | `Double` | | **Int32** | `Int32` | `Int32` | — | `Int64` | `Int32` | `Int32` | `Int32` | `Uint64` | `Float` | `Double` | | **Int64** | `Int64` | `Int64` | `Int64` | — | `Int64` | `Int64` | `Int64` | `Int64` | `Float` | `Double` | | **Uint8** | `Int8` | `Int16` | `Int32` | `Int64` | — | `Uint16` | `Uint32` | `Uint64` | `Float` | `Double` | | **Uint16** | `Uint16` | `Int16` | `Int32` | `Int64` | `Uint16` | — | `Uint32` | `Uint64` | `Float` | `Double` | | **Uint32** | `Uint32` | `Uint32` | `Int32` | `Int64` | `Uint32` | `Uint32` | — | `Uint64` | `Float` | `Double` | | **Uint64** | `Uint64` | `Uint64` | `Uint64` | `Int64` | `Uint64` | `Uint64` | `Uint64` | — | `Float` | `Double` | | **Float** | `Float` | `Float` | `Float` | `Float` | `Float` | `Float` | `Float` | `Float` | — | `Double` | | **Double** | `Double` | `Double` | `Double` | `Double` | `Double` | `Double` | `Double` | `Double` | `Double` | — | #### Date and time types ##### Basic types | Type | Date | Datetime | Timestamp | Interval | TzDate | TzDatetime | TzTimestamp | --- | --- | --- | --- | --- | --- | --- | --- | **Date** | — | `DateTime` | `Timestamp` | — | `TzDate` | `TzDatetime` | `TzTimestamp` | | **Datetime** | — | — | `Timestamp` | — | — | `TzDatetime` | `TzTimestamp` | | **Timestamp** | — | — | — | — | — | — | `TzTimestamp` | | **Interval** | — | — | — | — | — | — | — | | **TzDate** | — | — | — | — | — | `TzDatetime` | `TzTimestamp` | | **TzDatetime** | — | — | — | — | — | — | `TzTimestamp` | | **TzTimestamp** | — | — | — | — | — | — | — | | **Date32** | — | — | — | — | — | — | — | | **Datetime64** | — | — | — | — | — | — | — | | **Timestamp64** | — | — | — | — | — | — | — | | **Interval64** | — | — | — | — | — | — | — | | **TzDate32** | — | — | — | — | — | — | — | | **TzDatetime64** | — | — | — | — | — | — | — | | **TzTimestamp64** | — | — | — | — | — | — | — | ##### Extended types | Type | Date32 | Datetime64 | Timestamp64 | Interval64 | TzDate32 | TzDatetime64 | TzTimestamp64 | --- | --- | --- | --- | --- | --- | --- | --- | **Date** | `Date32` | `DateTime64` | `Timestamp64` | — | `TzDate32` | `TzDatetime64` | `TzTimestamp64` | | **Datetime** | — | `Datetime64` | `Timestamp64` | — | — | `TzDatetime64` | `TzTimestamp64` | | **Timestamp** | — | — | `Timestamp64` | — | — | — | `TzTimestamp64` | | **Interval** | — | — | — | — | — | — | — | | **TzDate** | — | — | — | — | `TzDate32` | `TzDatetime64` | `TzTimestamp64` | | **TzDatetime** | — | — | — | — | — | `TzDatetime64` | `TzTimestamp64` | | **TzTimestamp** | — | — | — | — | — | — | `TzTimestamp64` | | **Date32** | — | `DateTime64` | `Timestamp64` | — | `TzDate32` | `TzDatetime64` | `TzTimestamp64` | | **Datetime64** | — | — | `Timestamp64` | — | — | `TzDatetime64` | `TzTimestamp64` | | **Timestamp64** | — | — | — | — | — | — | `TzTimestamp64` | | **Interval64** | — | — | — | — | — | — | — | | **TzDate32** | — | — | — | — | — | `TzDatetime64` | `TzTimestamp64` | | **TzDatetime64** | — | — | — | — | — | — | `TzTimestamp64` | | **TzTimestamp64** | — | — | — | — | — | — | — | ## Data types accepting NULL Any typed data in YQL, including table columns, can be either non-nullable (guaranteed value) or nullable (empty value denoted as `NULL`). Data types that can include `NULL` values are called *optional* or, in SQL terms, *nullable*. Optional data types in the [text format](https://ytsaurus.tech/docs/en/type_string.md) use the question mark at the end (for example, `String?`) or the notation `Optional<...>`. The following operations are most often performed on optional data types: * [IS NULL](https://ytsaurus.tech/docs/syntax/expressions.md#is-null): Matching an empty value * [COALESCE](https://ytsaurus.tech/docs/builtins/basic.md#coalesce): Leave the filled values unchanged and replace `NULL` with the default value that follows * [UNWRAP](https://ytsaurus.tech/docs/builtins/basic.md#optional-ops): Extract the value of the original type from the optional data type, `T?` is converted to `T` * [JUST](https://ytsaurus.tech/docs/builtins/basic#optional-ops): Add optionality to the current type, `T` is converted to `T?`. * [NOTHING](https://ytsaurus.tech/docs/builtins/basic.md#optional-ops): Create an empty value with the specified type. `Optional` (nullable) isn't a property of a data type or column, but a container type where [containers](https://ytsaurus.tech/docs/en/containers.md) can be arbitrarily nested into each other. For example, a column with the type `Optional>` can accept 4 values: `NULL` of the whole container, `NULL` of the inner container, `TRUE`, and `FALSE`. The above-declared type differs from `List>`, because it uses `NULL` as an empty list, and you can't put more than one non-null element in it. In addition, `Optional>` type values are returned as results when searching by the key in the `Dict(k,v)` dictionary with `Optional` type values. Using this type of result data, you can distinguish between a `NULL` value in the dictionary and a situation when the key is missing. #### Example ```yql $dict = {"a":1, "b":null}; $found = $dict["b"]; select if($found is not null, unwrap($found), -1); ``` Result: ```text # column0 null ``` ## Logical and arithmetic operations with NULL {#null_expr} The `NULL` literal has a separate singular `Null` type and can be implicitly converted to any optional type (for example, the nested type `Optional...>>`). In ANSI SQL, `NULL` means "an unknown value", that's why logical and arithmetic operations involving `NULL` or empty `Optional` have certain specifics. #### Examples ```yql SELECT True OR NULL, -- Just(True) (works the same way as True OR ) False AND NULL, -- Just(False) True AND NULL, -- NULL (more precise than Nothing – ) NULL OR NOT NULL, -- NULL (all NULLs are "different") 1 + NULL, -- NULL (Nothing) — the result of adding 1 together with -- an unknown Int value) 1 == NULL, -- NULL (the result of adding 1 together with an unknown Int value) (1, NULL) == (1, 2), -- NULL (composite elements are compared by component -- through `AND`) (2, NULL) == (1, 3), -- Just(False) (expression is equivalent to 2 == 1 AND NULL == 3) ``` # Containers YQL supports container types to define complex data structures organized in various ways. Values of container types can be passed to YQL queries as input parameters or returned from YQL queries as columns of the set of results. | Type | Declaration,
example | Description | | ------------ | ---------------- | ------------- | | List | `List`,
`List` | A variable-length sequence consisting of same-type elements. | | Dictionary | `Dict`,
`Dict` | Set of key-value pairs with a fixed type of keys and values. | | Set | `Set`,
`Set` | A set of elements with a fixed type is a special case of a dictionary with the `Void` value type. | | Tuple | `Tuple`,
`Tuple` | Set of unnamed fixed-length elements with types specified for all elements. | | Structure | `Struct`,
`Struct` | A set of named fields with specified value types, fixed at query start (must be data-independent). | | Stream | `Stream`,
`Stream` | Single-pass iterator by same-type values, not serializable | | Variant on tuple | `Variant`,
`Variant` | A tuple known to have exactly one element filled | | Variant on structure | `Variant`,
`Variant` | A structure known to have exactly one element filled | | Enumeration | `Enum`,
`Enum` | A container with exactly one enumeration element selected and defined only by its name. | If necessary, you can nest containers in any combination, for example, `List>`. In certain contexts, [optional values](https://ytsaurus.tech/docs/en/optional.md) can also be considered a container type (`Optional`) that behaves like a list of length 0 or 1. To create literals of list containers, dictionary containers, set containers, tuple containers, or structure containers, you can use the [operator notation](https://ytsaurus.tech/docs/builtins/basic.md#containerliteral). To create a variant literal over a tuple or structure, use the function [Variant](https://ytsaurus.tech/docs/builtins/basic.md#variant). To create an enumeration literal, use the function [Enum](https://ytsaurus.tech/docs/builtins/basic.md#enum). To access the container elements, use a [dot or square brackets](https://ytsaurus.tech/docs/syntax/expressions.md#items-access), depending on the container type. # Special data types | Type | Description | | ----- | ----- | | `Callable` | A callable value that can be executed by passing arguments in parentheses in YQL SQL syntax or using the `Apply` function with the s-expressions syntax. | | `Resource` | Resource is an opaque pointer to a resource you can pass between the user defined functions (UDF). The type of the returned and accepted resource is declared inside a function using a string label. When passing a resource, YQL checks for label matching to prevent passing of resources between incompatible functions. If the labels mismatch, a type error occurs. | | `Tagged` | Tagged is the option to assign an application name to any other type. | | `Generic` | The data type used for data types. | | `Unit` | Unit is the data type used for non-enumerable entities (data sources and data sinks, atoms, etc.). | | `Null` | Void is a singular data type with the only possible null value. It's the type of the `NULL` literal and can be converted to any `Optional` type. | | `Void` | Void is a singular data type with the only possible `"null"` value. | | `EmptyList` | A singular data type with the only possible [] value. It's the type of the `[]` literal and can be converted to any `List` type. | | `EmptyDict` | A singular data type with the only possible `{}` value. It's a type of the `{}` literal and can be converted to any `Dict` or `Set` type. | ## Linear types Most YQL types are immutable, meaning expressions return new values instead of modifying existing ones. This approach, which is common in functional programming languages, allows for more aggressive optimizations such as eliminating common subexpressions or caching results. However, in some scenarios, this may lead to slower query execution. For example, when changing a value in a list or map, you need to either return its complete copy or use persistent data structures, which also introduces additional overhead. Linear types offer a different approach: unlike immutable output values, linear type values can't be reused. Similar to a relay baton, they are passed from their point of creation to their point of consumption, where they are converted into regular immutable values. Linear types were added in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Linear types are described by a single parameter, type T, and come in two variants: statically verifiable `Linear` and runtime verifiable `DynamicLinear`. Statically verifiable types are more efficient but are limited in their composition capabilities. The `T` parameter of a linear type typically uses the `Resource` type. In this case, user-defined functions (UDFs) can pass data between each other with guaranteed protection against reuse in another query, allowing for more efficient implementations. Linear types are non-serializable, meaning they can't be read from or written to tables and can only be used within expressions. Functions that accept or return linear types are categorized into three classes: * Producers: Functions where a linear type appears in the output but not in the parameters. * Transformers: Functions where a linear type appears both in the parameters and in the output. * Consumers: Functions where a linear type appears in the parameters but not in the output. A producer must accept at least one parameter containing a dependent expression because it's possible to generate multiple independent values of linear types from the input. We recommend creating and consuming linear types within the [`Block`](https://ytsaurus.tech/docs/builtins/basic.md#block) function, which allows passing an anonymous dependent expression as a `lambda` parameter. ### Validation of static linear types (`Linear`) * Validation of `linear` types is the final stage of optimization. If the optimizer successfully eliminates reuse of the expression, no error occurs. * A `linear` type can't be a `lambda` parameter. * A `linear` type can't be returned by a `lambda`. * A `linear` type can be used either by itself or within a `Struct/Tuple` field (without nesting). In this case, if possible, the optimizer monitors individual usage of `Struct/Tuple` fields via the field access operator (dot). To use values of linear types within other container types, such as lists, use a `DynamicLinear` type. ### Validation of dynamic linear types (`DynamicLinear`) * `FromDynamicLinear` or a user-defined function (UDF) can extract a value of this type, but only once. Attempts to extract the same value again result in a query execution error. # Rules for type casting using the operator [CAST](https://ytsaurus.tech/docs/syntax/expressions.md#cast). ## Rules for casting primitive data types * When casting primitive data types, some of the source information may be discarded unless contained in the target type. For example: * The `Float`/`Double` fractional part, when casting to integer types. * The `Datetime`/`Timestamp` time, when casting to `Date`. * The timezone, when casting from timezone types to date/time types without a timezone. * If, in a certain combination of the source and target type, casting can't be performed for all possible values of the source type, then, if the casting fails, `CAST` returns `NULL`. In such cases, one `Optional` level is added to the return value type, unless already present. For example, the constructs: `CAST("3.14" AS Float?)` and `CAST("3.14" AS Float)` are fully equivalent and return `Float?`. * If casting is possible for all values of the source type, then adding '?' works like `Just` above: `CAST(3.14 AS Utf8?)` is the same as `Just(CAST(3.14 AS Utf8))`. All primitive type combinations for which `CAST` is possible are described [here](https://ytsaurus.tech/docs/en/primitive.md). ## Rules for casting containers ### Rules for Optional * If a higher `Optional` level is set for the target type than for the source type, it's same as adding `Just` on top of `CAST` with a lower `Optional` level. * If the source type has a higher level of `Optional` for the source type, then `NULL` at any level higher than the target level results in `NULL`. * At equal levels of `Optional`, the `NULL` value preserves the same level. ```yql SELECT CAST(1 AS Int32?), -- same as Just(1) CAST(Just(2/1) AS Float??), -- [2] CAST(Just(3/0) AS Float??) IS NULL; -- false: the result of Just(NULL) ``` ### Rules for List/Dict * To create a list, `CAST` is applied to each item in the source list to cast it to the target type. * If the target item type is non-optional and `CAST` on the item might fail, then such casting is discarded. In this case, the resulting list might be shorter or even empty if every casting failed. * For dictionaries, the casting is totally similar to lists, with `CAST` being applied to keys and values. ```yql SELECT CAST([-1, 0, 1] AS List), -- [null, 0, 1] CAST(["3.14", "bad", "42"] AS List), -- [3.14, 42] CAST({-1:3.14, 7:1.6} AS Dict), -- {7: "1.6"} CAST({-1:3.14, 7:1.6} AS Dict); -- {7: "1.6", null:"3.14"} ``` ### Rules for Struct/Tuple * A structure or tuple is created by applying `CAST` to each item of the source type to cast it to an item with the same name or target type index. * If some field is missing in the target type, it's simply discarded. * If some field is missing in the source value type, then it can be added only if it's optional and accepts the `NULL` value. * If some field is non-optional in the target type, but its casting might fail, then `CAST` adds Optional to the structure or tuple level and might return `NULL` for the entire result. ```yql SELECT CAST((-1, 0, 1) AS Tuple), -- (null, 0, "1") CAST((-2, 0) AS Tuple), -- null CAST((3, 4) AS Tuple), -- (3, "4") type Tuple? CAST(("4",) AS Tuple), -- (4, null) CAST((5, 6, null) AS Tuple); -- (5,) elements are removed. SELECT -- One field is removed and one is added: ("three":null, "two": "42") CAST(<|one:"8912", two:42|> AS Struct). ``` ### Rules for Variant * A variant with a specific name or index is cast to a variant with the same name or index. * If casting of a variant might fail and the type of this variant is non-optional, then `CAST` adds Optional to the top level and can return `NULL`. * If some variant is missing in the target type, then `CAST` adds Optional to the top level and returns `NULL` for such a value. ### Nested containers * All of the above rules are applied recursively for nested containers. # Text representation of data types ## Introduction {#intro} Since YQL is a strongly typed language, the data type is important for many of its aspects. To make data type management easy, YQL has a data type definition convention in text format. It's mentioned in many places in the documentation. There's also a library that provides functions for building a data type based on a text description (for example, when manually defining the signature for the called value) or for serializing the data type into a string for debugging purposes. Functions for working with data types [are described in the article](https://ytsaurus.tech/docs/builtins/types.md). The format for textual data type presentation is described below. ## General conventions {#rules} * [Primitive data types](https://ytsaurus.tech/docs/en/primitive.md) are represented in text format simply by referencing their name. * A complex data type is composed of other data types. If you depict this structure as a tree, it has [primitive data types](https://ytsaurus.tech/docs/en/primitive.md) as leaves and [containers](https://ytsaurus.tech/docs/en/containers.md) as other nodes. You may treat [special data types](https://ytsaurus.tech/docs/en/special.md) as exceptions, because they can function as both. * The text representation repeats the structure of this tree from the root to the leaves: each node of the tree specifies the name of the current data type, and proceeding to a deeper level is denoted by different types of brackets. * Feel free to use spaces and line breaks if they improve readability. * If the ID contains something else except the Latin letters and numbers, put it in single quotes and use C-escaping. ## Containers {#containers} * Use angle brackets to specify the types of container elements. Example: `List`. * If a container can hold multiple heterogeneous elements, they are listed inside angle brackets with a comma. Example: `Tuple`. * If a container can hold named elements, use comma-separated name-type pairs with a colon in-between instead of comma-separated data types. Example: `Struct`. * The underlying `Variant` container type is chosen based on the presence of names in arguments. Example: `Variant` is a variant on tuple, `Variant` is a variant on structure. ## Types that allow NULL {#optional} * They are called `Optional` in YQL terms, or nullable in the classic SQL terms. * Formally, this type is a container. So, you may declare it as `Optional<...>`, but the shortcut notation of a question mark suffix is usually used instead. Example: `String?`. ## Called values {#callable} * The basic form of the called values looks as follows: `(arg1, arg2, ...) -> result`. Example of a signature description for a function receiving two strings and returning a number: `(String, String) -> Int64`. * The called values can return the called values: in this case, they make up a chain of the required length. Example: `(String, String) -> (String, String) -> Int64`. * Optional arguments must have the `Optional` type at the top level and be enclosed in square brackets. Example: `(String, [String?, Double?]) -> Int64`. * The arguments of the called values can contain flags. Currently, the only possible flag is `AutoMap`. It means that if NULL is passed to this argument, the result must also be set to NULL without running the function. Example: `(String{Flags: AutoMap}) -> Int64`. * Use this particular format if you need `Optional>`, because the trailing question mark refers to the result of the called value. ## Resources {#resources} * Unlike containers, a resource isn't parameterized by the element type (it's a pointer in memory and YQL knows nothing about its contents). Instead, a resource is parameterized by a string label that can safeguard against passing resources between incompatible functions. Example: `Resource`. # List of articles on YQL syntax * [Lexical structure](https://ytsaurus.tech/docs/en/lexer.md) * [Expressions](https://ytsaurus.tech/docs/en/expressions.md) * [USE](https://ytsaurus.tech/docs/en/use.md) * [SELECT](https://ytsaurus.tech/docs/en/select/index.md) * [VALUES](https://ytsaurus.tech/docs/en/values.md) * [CREATE TABLE](https://ytsaurus.tech/docs/en/create_table.md) * [DROP TABLE](https://ytsaurus.tech/docs/en/drop_table.md) * [INSERT](https://ytsaurus.tech/docs/en/insert_into.md) * [REPLACE](https://ytsaurus.tech/docs/en/replace_into.md) * [GROUP BY](https://ytsaurus.tech/docs/en/group_by.md) * [JOIN](https://ytsaurus.tech/docs/en/join.md) * [COMBINE](https://ytsaurus.tech/docs/en/combine.md) * [WINDOW](https://ytsaurus.tech/docs/en/window.md) * [MATERIALIZE](https://ytsaurus.tech/docs/en/materialize.md) * [FLATTEN](https://ytsaurus.tech/docs/en/flatten.md) * [ACTION](https://ytsaurus.tech/docs/en/action.md) * [SUBQUERY](https://ytsaurus.tech/docs/en/subquery.md) * [DISCARD](https://ytsaurus.tech/docs/en/discard.md) * [INTO RESULT](https://ytsaurus.tech/docs/en/into_result.md) * [PROCESS](https://ytsaurus.tech/docs/en/process.md) * [REDUCE](https://ytsaurus.tech/docs/en/reduce.md) * [PRAGMA](https://ytsaurus.tech/docs/en/pragma/index.md) * [EXPORT and IMPORT](https://ytsaurus.tech/docs/en/export_import.md) # Lexical structure An application written in YQL language is a valid UTF-8 text consisting of _commands_ (statement) separated by semicolons (`;`). The last semicolon can be omitted. Each command is a sequence of _tokens_ that are valid for this command. Tokens can be _keywords_, _IDs_, _literals_, and so on. Tokens are separated by whitespace characters (space, tab, line feed) or _comments_. The comment is not a part of the command and is syntactically equivalent to a space character. ## Syntax compatibility modes {#lexer-modes} Two syntax compatibility modes are supported: * Advanced C++ (default) * ANSI SQL ANSI SQL mode is enabled through a special comment `--!ansi-lexer` that must be placed at the start of the program. Specifics of interpretation of lexical elements in different compatibility modes are described below. ## Comments {#comments} The following types of comments are supported: * Single-line comment: starts with `--` (two minus characters _following one another_) and continues to the end of the line * Multiline comment: starts with `/*` and ends with `*/` ```sql SELECT 1; -- A single-line comment /* Some multi-line comment */ ``` In C++ syntax compatibility mode (default), a multiline comment ends with the _nearest_ `*/`. The ANSI SQL syntax compatibility mode accounts for nesting of multiline comments: ```sql --!ansi_lexer SELECT * FROM T; /* this is a comment /* this is a nested comment, without ansi_lexer it raises an error */ */ ``` ## Keywords and identifiers {#keywords-and-ids} **Keywords** are tokens that have a fixed value in the YQL language. Examples of keywords: `SELECT`, `INSERT`, `FROM`, `ACTION`, and so on. Keywords are case-insensitive, that is, `SELECT` and `SeLEcT` are equivalent to each other. The list of keywords is not fixed and is going to expand as the language develops. A keyword can't contain numbers and begin or end with an underscore. **Identifiers** are tokens that identify the names of tables, columns, and other objects in YQL. Identifiers in YQL are always case-sensitive. An identifier can be written in the body of the program without any special formatting, if the identifier: * Is not a keyword * Begins with a Latin letter or underscore * Is followed by a Latin letter, an underscore, or a number ```sql SELECT my_column FROM my_table; -- my_column and my_table are identifiers ``` To record a random identifier to the program's body, the identifier is enclosed in back ticks: ```sql SELECT `column with space` from T; SELECT * FROM `my_dir/my_table` ``` IDs in backticks are never interpreted as keywords: ```sql SELECT `select` FROM T; -- select - Column name in the T table ``` When using backticks, you can use the standard C escaping: ```sql SELECT 1 as `column with\n newline, \x0a newline and \` backtick `; ``` In ANSI SQL syntax compatibility mode, arbitrary IDs can also be enclosed in double quotes. To include a double quote in a quoted ID, use two double quotes: ```sql --!ansi_lexer SELECT 1 as "column with "" double quote"; -- column name will be: column with " double quote ``` ## SQL hints {#sql-hints} SQL hints are special settings with which a user can modify a query execution plan (for example, enable/disable specific optimizations or force the JOIN execution strategy). Unlike [PRAGMA](https://ytsaurus.tech/docs/en/pragma/index.md), SQL hints act locally – they are linked to a specific point in the YQL query (normally, after the keyword) and affect only the corresponding statement or even a part of it. SQL hints are a set of settings "name-value list" and defined inside special comments — comments with SQL hints must have `+` as the first character: ```sql --+ Name1(Value1 Value2 Value3) Name2(Value4) ... ``` An SQL hint name must be comprised of ASCII alphanumeric characters and start with a letter. Hint names are case insensitive. A hint name must be followed by a custom number of space-separated values. A value can be a custom set of characters. If there's a space or parenthesis in a set of characters, single quotation marks must be used: ```sql --+ foo('value with space and paren)') ``` ```sql --+ foo('value1' value2) -- equivalent to --+ foo(value1 value2) ``` To escape a single quotation within a value, double it: ```sql --+ foo('value with single quote '' inside') ``` If there're two or more hints with the same name in the list, the latter is used: ```sql --+ foo(v1 v2) bar(v3) foo() -- equivalent to --+ bar(v3) foo() ``` Unknown SQL hint names (or syntactically incorrect hints) never result in errors, they're simply ignored: ```sql --+ foo(value1) bar(value2 baz(value3) -- due to a missing closing parenthesis in bar, is equivalent to --+ foo(value1) ``` Thanks to this behavior, previous valid YQL queries with comments that look like hints remain intact. Syntactically correct SQL hints in a place unexpected for YQL result in a warning: ```sql -- presently, hints after SELECT are not supported SELECT /*+ foo(123) */ 1; -- warning 'Hint foo will not be used' ``` What's important is that SQL hints are hints for an optimizer, so: * Hints never affect search results. * As YQL optimizers improve, a situation is possible when a hint becomes outdated and is ignored (for example, the algorithm based on a given hint completely changes or the optimizer becomes so sophisticated that it can be expected to choose the best solution, so some manual settings are likely to interfere). ## String literals {#string-literals} A string literal (constant) is expressed as a sequence of characters enclosed in single quotes. Inside a string literal, you can use the C-style escaping rules: ```yql SELECT 'string with\n newline, \x0a newline and \' backtick '; ``` In the C++ syntax compatibility mode (default), you can use double quotes instead of single quotes: ```yql SELECT "string with\n newline, \x0a newline and \" backtick "; ``` In ASNI SQL compatibility mode, double quotes are used for IDs, and the only escaping that can be used for string literals is a pair of single quotes: ```sql --!ansi_lexer SELECT 'string with '' quote'; -- result: string with ' quote ``` Based on string literals, simple literals can be obtained. ### Multi-line string literals {#multiline-string-literals} A multiline string literal is expressed as an arbitrary set of characters enclosed in double at signs `@@`: ```yql $text = @@some multiline text@@; SELECT LENGTH($text); ``` If you need to use double at signs in your text, duplicate them: ```yql $text = @@some multiline with double at: @@@@ text@@; SELECT $text; ``` ### Typed string literals {#typed-string-literals} * For string literals, including [multi-string](https://ytsaurus.tech/docs/en/#multiline-string-literals) ones, the `String` type is used by default. * You can use the following suffixes to explicitly control the literal type: * `u` — `Utf8`; * `y` — `Yson`; * `j` — `Json`. **Example:** ```yql SELECT "foo"u, '[1;2]'y, @@{"a":null}@@j; ``` ## Numeric literals {#literal-numbers} * Integer literals have the default type `Int32`, if they fit within the Int32 range. Otherwise, they automatically expand to `Int64`. * You can use the following suffixes to explicitly control the literal type: * `l`: `Int64`. * `s`: `Int16`. * `t`: `Int8`. * Add the suffix `u` to convert a type to its corresponding unsigned type: * `ul`: `Uint64`. * `u`: `Uint32`. * `us`: `Uint16`. * `ut`: `Uint8`. * You can also use hexadecimal, octal, and binary format for integer literals using the prefixes `0x`, `0o` and `0b`, respectively. You can arbitrarily combine them with the above-mentioned suffixes. * Floating point literals have the `Double` type by default, but you can use the suffix `f` to narrow it down to `Float`. ```sql SELECT 123l AS `Int64`, 0b01u AS `Uint32`, 0xfful AS `Uint64`, 0o7ut AS `Uint8`, 456s AS `Int16`, 1.2345f AS `Float`; ``` # Expressions ## String concatenation {#concatenation} Executed using the binary operator `||`. As with other binary operators, if the data on either side is `NULL`, the result is also `NULL`. Don't confuse this operator with a logical "or": in SQL, it's denoted by the `OR` keyword. It's also not worth doing concatenation using `+`. #### Examples ```yql SELECT "fo" || "o"; ``` ## Matching a string by pattern {#check-match} `REGEXP` and `RLIKE` are aliases and offer a short way to record for a [Re2::Grep](https://ytsaurus.tech/docs/udf/list/re2.md#match) call. `MATCH` — similar to [Re2::Match](https://ytsaurus.tech/docs/udf/list/re2.md#match). `LIKE` works as follows: * Patterns can include two special characters: * `%`: Zero or more of any characters. * `_`: Exactly one of any character. All other characters are literals that represent themselves. * As opposed to `REGEXP`, `LIKE` must be matched exactly. For example, to search a substring, add `%` at the beginning and end of the pattern. * `ILIKE` is a case-insensitive version of `LIKE`. * If `LIKE` is applied to the key column of the sorted table and the pattern doesn't start with a special character, filtering by prefix drills down directly to the cluster level, which in some cases lets you avoid the full table scan. This optimization is disabled for `ILIKE`. * To escape special characters, specify the escaped character after the pattern using the `ESCAPE '?'` keyword. Instead of `?` you can use any character except `%`, `_`, and `\`. For example, if you use a question mark as an escaping symbol, then `?%`, `?_` and `??` expressions will match their second character in the template: percent, underscore, and question mark, respectively. The escape character is undefined by default. The most popular way to use the `LIKE` and `REGEXP` keywords is to filter a table using the statements with the `WHERE` clause. However, there are no restrictions on using templates in this context: you can use them in most of contexts involving strings, for example, with concatenation by using `||`. #### Examples ```yql SELECT * FROM my_table WHERE string_column REGEXP '\\d+'; -- requires a second slash because all -- standard string literals in SQL -- may accept C-escaped strings. ``` ```yql SELECT string_column LIKE '___!_!_!_!!!!!!' ESCAPE '!' -- searches for a string of exactly 9 symbols: -- 3 random, -- then 3 underscores -- and 3 exclamation marks FROM my_table; ``` ```yql SELECT * FROM my_table WHERE key LIKE 'foo%bar'; -- if there's table sorting by key, it only scans keys, -- starting with foo, and then among them -- leaves only those that end with bar ``` ## Operators ### Arithmetic operators {#math-operators} The operators `+`, `-`, `*`, `/`, `%` are defined for [primitive data types](https://ytsaurus.tech/docs/types/primitive.md) that are variations of numbers. For the Decimal data type, bankers rounding is used (to the nearest even integer). #### Examples ```yql SELECT 2 + 2; ``` ```yql SELECT 0.0 / 0.0; ``` ### Comparison operators {#comparison-operators} `=`, `==`, `!=`, `<>`, `>`, `<` operators are defined for: * Primitive data types except Yson and Json. * Tuples and structures with the same set of fields. No order is defined for structures, but you can check for (non-)equality. Tuples are compared element-by-element left to right. #### Examples ```yql SELECT 2 > 1; ``` ### Logical operators {#logic-operators} Use the operators `AND`, `OR`, `XOR` for logical operations on Boolean values (`Bool`). #### Examples ```yql SELECT 3 > 0 AND false; ``` ### Bitwise operators {#bit-operators} Bitwise operations on numbers: * `&`, `|`, `^`: AND, OR, and XOR, respectively. Don't confuse bitwise operations with the related keywords. The keywords `AND`, `OR`, and `XOR` are used for *Boolean values only*, but not for numbers. * ` ~ ` — negation. * `<<`, `>>`: Left or right shifts. * `|<<`, `>>|`: Circular left or right shifts. #### Examples ```yql SELECT key << 10 AS key, ~value AS value FROM my_table; ``` ### Precedence and associativity of operators {#operator-priority} Operator precedence determines the order of evaluation of an expression that contains different operators. For example, the expression `1 + 2 * 3` is evaluated as `1 + (2 * 3)` because the multiplication operator has a higher precedence than the addition operator. Associativity determines the order of evaluating expressions containing operators of the same type. For example, the expression `1 + 2 + 3` is evaluated as `(1 + 2) + 3` because the addition operator is left-associative. On the other hand, the expression `a ?? b ?? c` is evaluated as `a ?? (b ?? c)` because the `??` operator is right-associative The table below shows precedence and associativity of YQL operators. The operators in the table are listed in descending order of precedence. | Priority | Operator | Description | Associativity | | --- | --- | --- | --- | | 1 | `a[], a.foo, a()` | Accessing a container item, calling a function | Left | | 2 | `+a, -a, ~a, NOT a` | Unary operators: plus, minus, bitwise and logical negation | Right | | 3 | `a\|\|b` | [String concatenation](https://ytsaurus.tech/docs/en/#concatenation) | Left | | 4 | `a*b, a/b, a%b` | Multiplication, division, remainder of division | Left | | 5 | `a+b, a-b` | Addition/Subtraction | Left | | 6 | `a ?? b` | Operator notation for [NVL/COALESCE](https://ytsaurus.tech/docs/builtins/basic.md#coalesce) | Right | | 7 | `a<>b, a\|<>\|b,` `a\|b, a^b, a&b` | Shift operators and logical bit operators | Left | | 8 | `a=b, a>b` | Comparison | Left | | 9 | `a IN b` | Occurrence of an element in a set | Left | | 9 | `a==b, a=b, a!=b, a<>b,` `a is (not) distinct from b` | Comparison for (non-)equality | Left | | 10 | `a XOR b` | Logical XOR | Left | | 11 | `a AND b` | Logical AND | Left | | 12 | `a OR b` | Logical OR | Left | ## IS \[NOT\] NULL {#is-null} Matching an empty value (`NULL`). Since `NULL` is a special value that [equals nothing](https://ytsaurus.tech/docs/types/optional.md#null_expr), regular [comparison operators](https://ytsaurus.tech/docs/en/#comparison-operators) are not suitable for this task. #### Examples ```yql SELECT key FROM my_table WHERE value IS NOT NULL; ``` ## IS \[NOT\] DISTINCT FROM {#is-distinct-from} Comparing of two values. Unlike regular [comparison operators](https://ytsaurus.tech/docs/en/#comparison-operators), `NULL` values are considered equal to each other. Comparison is done according to the following rules: 1. `IS DISTINCT FROM`/`IS NOT DISTINCT FROM` operators are defined for those (and only those) arguments for which `!=` and `=` operators are defined. 2. The result of `IS NOT DISTINCT FROM` is equal to the logical negation of the `IS DISTINCT FROM` result for these arguments. 3. If the result of the `==` operator is not equal to `NULL` for some arguments, then it is equal to the result of the `IS NOT DISTINCT FROM` operator for the same arguments. 4. If both arguments are empty `Optional's` or `NULL's`, then the value of `IS NOT DISTINCT FROM` is `True`. 5. The result of `IS NOT DISTINCT FROM` for an empty `Optional` or `NULL` and a non-empty `Optional` or non-`Optional` value is `False`. For values of composite types, these rules are used recursively. ## BETWEEN {#between} Checking whether a value is in a range. Syntax: `expr [NOT] BETWEEN [ASYMMETRIC | SYMMETRIC] expr AND expr`. * `BETWEEN` and `BETWEEN ASYMMETRIC` are equivalent, `x BETWEEN a AND b` is equivalent to `a <= x AND x <= b`. * `BETWEEN SYMMETRIC` automatically repositions the arguments so that the range is non-empty, `x BETWEEN SYMMETRIC a AND b` is equivalent to `(x BETWEEN a AND b) OR (x BETWEEN b AND a)`. * `NOT` inverts the check result. #### Examples ```yql SELECT * FROM my_table WHERE key BETWEEN 10 AND 20; ``` ```yql SELECT * FROM my_table WHERE key NOT BETWEEN SYMMETRIC 20 AND 10; ``` ## IN {#in} Checking whether a value is inside of a set of values. It's logically equivalent to a chain of equality comparisons using `OR` but implemented more efficiently. {% note warning %} Unlike the same keyword in Python, in YQL `IN` **DOES NOT** involve searching for a substring in a string. You can use [String::Contains](https://ytsaurus.tech/docs/udf/list/string.md) or [LIKE / REGEXP](https://ytsaurus.tech/docs/en/#check-match) described above to search for a substring. {% endnote %} You can specify the [hint](https://ytsaurus.tech/docs/en/lexer.md#sql-hints) `COMPACT` right after `IN`. If `COMPACT` is not specified, then `IN` with a subquery is executed as a relevant `JOIN` (`LEFT SEMI` for `IN` and `LEFT ONLY` for `NOT IN`). The presence of `COMPACT` forces an in-memory execution strategy: a hash table is built in memory based on the content of the right part of `IN`, and then the left part is filtered based on the table. The `COMPACT` hint must be used with care. Because the hash table is built in memory, the query may throw an error if the right part of `IN` contains a lot of large and/or different elements. Because there's a limit on the byte size of queries (approx. 1 MB) in YQL, to get a larger list of values you should attach them to the query via URL and use the [ParseFile](https://ytsaurus.tech/docs/builtins/basic.md#parsefile) function. #### Examples ```yql SELECT column IN (1, 2, 3) FROM my_table; ``` ```yql SELECT * FROM my_table WHERE string_column IN ("a", "b", "c"); ``` ```yql $foo = AsList(1, 2, 3); SELECT 1 IN $foo; ``` ```yql $values = (SELECT column + 1 FROM table); SELECT * FROM my_table WHERE -- filtering by in-memory hash table based on the table column1 IN /*+ COMPACT() */ $values AND -- with subsequent LEFT ONLY JOIN with other_table column2 NOT IN (SELECT other_column FROM other_table); ``` ## AS {#as} Can be used in the following scenarios: * Adding a short name (alias) for columns or tables within the query. * Using named arguments in function calls. * If you perform explicit casting to specify the target type, see [CAST](https://ytsaurus.tech/docs/en/#cast). #### Examples ```yql SELECT key AS k FROM my_table; ``` ```yql SELECT t.key FROM my_table AS t; ``` ```yql SELECT MyFunction(key, 123 AS my_optional_arg) FROM my_table; ``` ## CAST {#cast} Tries to cast the value to the specified type. The attempt may fail and return `NULL`. When used with numbers, it may lose precision or most significant bits. For lists and dictionaries, it can either delete or replace with `NULL` the elements whose conversion failed. For structures and tuples, it deletes elements that are omitted in the target type. To learn more about conversions, see [here](https://ytsaurus.tech/docs/types/cast.md). For the Decimal parameter data type, two additional arguments are specified: * The total number of decimal digits (up to 35, inclusive). * The number of decimal digits after the decimal point (out of the total number, i.e. strictly no more than the previous argument). #### Examples ```yql SELECT CAST("12345" AS Double), -- 12345.0 CAST(1.2345 AS Uint8), -- 1 CAST(12345 AS String), -- "12345" CAST("1.2345" AS Decimal(5, 2)), -- 1.23 CAST("xyz" AS Uint64) IS NULL, -- true, because it failed CAST(-1 AS Uint16) IS NULL, -- true, negative to unsigned CAST([-1, 0, 1] AS List), -- [null, 0, 1] --Optional item type: failed item to null. CAST(["3.14", "bad", "42"] AS List), -- [3.14, 42] --Non-optional item type: failed item deleted. CAST(255 AS Uint8), -- 255 CAST(256 AS Uint8) IS NULL -- true, out of range ``` ## BITCAST {#bitcast} Performs a bitwise conversion of an integer value to the specified integer type. The conversion is always successful, but may lose precision or high-order bits. #### Examples ```yql SELECT BITCAST(100000ul AS Uint32), -- 100000 BITCAST(100000ul AS Int16), -- -31072 BITCAST(100000ul AS Uint16), -- 34464 BITCAST(-1 AS Int16), -- -1 BITCAST(-1 AS Uint16); -- 65535 ``` ## CASE {#case} Conditional expressions and branching. It's similar to `if`, `switch` and ternary operators in the imperative programming languages. If the result of the `WHEN` expression is `true`, the value of the `CASE` expression becomes the result following the condition, and the rest of the `CASE` expression isn't calculated. If the condition is not met, all the `WHEN` clauses that follow are checked. If none of the `WHEN` conditions is fulfilled, the result recorded in the `ELSE` sentence becomes the `CASE` value. The `ELSE` branch is mandatory in the `CASE` expression. Expressions in `WHEN` are checked sequentially, from top to bottom. Since its syntax is quite sophisticated, it's often more convenient to use the built-in [IF](https://ytsaurus.tech/docs/builtins/basic.md#if) function. #### Examples ```yql SELECT CASE WHEN value > 0 THEN "positive" ELSE "negative" END FROM my_table; ``` ```yql SELECT CASE value WHEN 0 THEN "zero" WHEN 1 THEN "one" ELSE "not zero or one" END FROM my_table; ``` ## Named expressions {#named-nodes} Complex queries may be sophisticated, containing lots of nested levels and/or repeating parts. In YQL, you can use named expressions to assign a name to an arbitrary expression or subquery. Named expressions can be referenced in other expressions or subqueries. In this case, the original expression/subquery is actually substituted at point of use. A named expression is defined as follows: ```antlr = | ; ``` Here `` consists of a $ character and an arbitrary non-empty identifier (for example, `$foo`). If the expression on the right is a tuple, you can automatically unpack it by specifying several named expressions separated by commas on the left: ```antlr , , ... = ; ``` In this case, the number of expressions must match the tuple size. Each named expression has a scope. It starts immediately after defining the named expression and ends at the end of the closest enclosing name scope (for example, at the end of the query or at the end of a [lambda function](https://ytsaurus.tech/docs/en/#lambda) body, [ACTION](https://ytsaurus.tech/docs/en/action.md#define-action), [SUBQUERY](https://ytsaurus.tech/docs/en/subquery.md#define-subquery), or [EVALUATE FOR](https://ytsaurus.tech/docs/en/action.md#evaluate-for) cyclic path). Redefining a named expression with the same name hides the previous expression from the current scope. If the named expression has never been used, a warning is issued. To avoid such a warning, use the underscore as the first character in the ID (for example, `$_foo`). `$_` named expression is called an anonymous named expression and processed in a special way. It works as if `$_` was automatically replaced with `$_`. Anonymous named expressions are convenient when you don't need the expression value. For example, to fetch the second element from a tuple of three elements, you can write: ```yql $_, $second, $_ = AsTuple(1, 2, 3); select $second; ``` An attempt to reference an anonymous named expression results in an error: ```yql $_ = 1; select $_; --- error: Unable to reference anonymous name $_ export $_; --- error: Can not export anonymous name $_ ``` Also, you can't import a named expression using an anonymous alias: ```yql import utils symbols $sqrt as $_; --- error: Can not import anonymous name $_ ``` Anonymous argument names are also supported for [lambda functions](https://ytsaurus.tech/docs/en/#lambda), [ACTION](https://ytsaurus.tech/docs/en/action.md#define-action), [SUBQUERY](https://ytsaurus.tech/docs/en/subquery.md#define-subquery) and in [EVALUATE FOR](https://ytsaurus.tech/docs/en/action.md#evaluate-for). {% note info %} If named expression substitution results in completely identical subgraphs in the query execution graph, the graphs are combined to execute a subgraph only once. {% endnote %} #### Examples ```yql $multiplier = 712; SELECT a * $multiplier, -- $multiplier is 712 b * $multiplier, (a + b) * $multiplier FROM abc_table; ``` ```yql $intermediate = ( SELECT value * value AS square, value FROM my_table ); SELECT a.square * b.value FROM $intermediate AS a INNER JOIN $intermediate AS b ON a.value == b.square; ``` ```yql $a, $_, $c = AsTuple(1, 5u, "test"); -- tuple unpacking SELECT $a, $c; ``` ```yql $x, $y = AsTuple($y, $x); -- swap expression values ``` ## Table expressions {#table-contexts} A table expression is an expression that returns a table. Table expressions in YQL are as follows: * Subqueries: `(SELECT key, subkey FROM T)` * [named subqueries](https://ytsaurus.tech/docs/en/#named-nodes): `$foo = SELECT * FROM T;` (use of a `$foo` named subquery is a table expression) * [subquery templates](https://ytsaurus.tech/docs/en/subquery.md#define-subquery): `DEFINE SUBQUERY $foo($name) AS ... END DEFINE;` (`$foo("InputTable")` call is a table expression). Semantics of a table expression depends on the context where it is used. In YQL, table expressions can be used in the following contexts: * Table context: after [FROM](https://ytsaurus.tech/docs/en/select/from.md). In this case, table expressions work as expected: for example, `$input = SELECT a, b, c FROM T; SELECT * FROM $input` returns a table with three columns. The table context also appears after [UNION [ALL]](https://ytsaurus.tech/docs/en/select/operators.md#union), [INTERSECT [ALL]](https://ytsaurus.tech/docs/en/select/operators.md#intersect), [EXCEPT [ALL]](https://ytsaurus.tech/docs/en/select/operators.md#except), [JOIN](https://ytsaurus.tech/docs/en/join.md#join), [PROCESS](https://ytsaurus.tech/docs/en/process.md#process), and [REDUCE](https://ytsaurus.tech/docs/en/reduce.md#reduce). * Vector context: after [IN](https://ytsaurus.tech/docs/en/#in). In this context, the table expression must contain exactly one column (the name of this column doesn't affect the expression result in any way). A table expression in a vector context is typed as a list (the type of the list element is the same as the column type in this case). Example: `SELECT * FROM T WHERE key IN (SELECT k FROM T1)`; * A scalar context arises *in all the other cases*. As in vector contexts, a table expression must contain exactly one column, but the value of the table expression is a scalar, that is, an arbitrarily selected value of this column (if no rows are returned, the result is `NULL`). Example: `$count = SELECT COUNT(*) FROM T; SELECT * FROM T ORDER BY key LIMIT $count / 2`. The order of rows in a table context, the order of elements in a vector context, and the rule for selecting a value from a scalar context (if multiple values are returned), aren't defined. This order also cannot be affected by `ORDER BY`: `ORDER BY` without `LIMIT` is ignored in table expressions with a warning, and `ORDER BY` with `LIMIT` defines a set of elements rather than the order within that set. There is an exception to this rule. A named expression containing [PROCESS](https://ytsaurus.tech/docs/en/process.md#process), when used in scalar context, behaves as if it existed in table context: ```yql $input = SELECT 1 AS key, 2 AS value; $process = PROCESS $input; SELECT FormatType(TypeOf($process)); -- $process used in scalar context, -- but the result of SELECT in this case is List> SELECT $process[0].key; -- returns 1 SELECT FormatType(TypeOf($input)); -- error, $input in scalar context must contain one column ``` {% note warning "Attention!" %} A common error is to use an expression in a scalar context rather than a table context or vector context. For example: ```yql $dict = SELECT key, value FROM T1; DEFINE SUBQUERY $merge_dict($table, $dict) AS SELECT * FROM $table LEFT JOIN $dict USING(key); END DEFINE; SELECT * FROM $merge_dict("Input", $dict); -- $dict here is used in scalar context. -- error, exactly one column is expected in scalar context ``` A correct notation in this case is: ```yql DEFINE SUBQUERY $dict() AS SELECT key, value FROM T1; END DEFINE; DEFINE SUBQUERY $merge_dict($table, $dict) AS SELECT * FROM $table LEFT JOIN $dict() USING(key); -- use of the table expression $dict() -- (subquery template call) in table context END DEFINE; SELECT * FROM $merge_dict("Input", $dict); -- $dict is a subquery template (not a table expression) -- passed as a table expression argument ``` {% endnote %} ## Lambda functions {#lambda} Let you combine multiple expressions into a single callable value. List arguments in round brackets, following them by the arrow and lambda function body. The body of the lambda consists of an expression enclosed in parentheses or of curly brackets enclosing the optional chain of expressions and assigns [named expressions](https://ytsaurus.tech/docs/en/#named-nodes) and the output of a call after the `RETURN` keyword in the last expression. The scope for the lambda body: first the local named expressions, then arguments, then named expressions defined above by the lambda function at the top level of the query. Only use pure expressions inside the lambda body (those might also be other lambdas, possibly passed through arguments). But you can't use [SELECT](https://ytsaurus.tech/docs/en/select/index.md), [INSERT INTO](https://ytsaurus.tech/docs/en/insert_into.md) and other top-level statements inside lambdas. One or more of the last lambda parameters can be marked with a question mark as optional: if they haven't been specified when calling lambda, they are assigned the `NULL` value. #### Examples ```yql $f = ($y) -> { $prefix = "x"; RETURN $prefix || $y; }; $g = ($y) -> ("x" || $y); $h = ($x, $y?) -> ($x + ($y ?? 0)); SELECT $f("y"), $g("z"), $h(1), $h(2, 3); -- "xy", "xz", 1, 5 ``` ```yql -- if the lambda output is calculated as a single expression, you can use a more compact syntax variant: $f = ($x, $_) -> ($x || "suffix"); -- the second argument isn't used by SELECT $f("prefix_", "whatever"); ``` ## Accessing containers {#items-access} For accessing the values inside containers: * `Struct<>`, `Tuple<>` and `Variant<>`, use a **dot**. The set of keys (for the tuple and the corresponding variant — indexes) is known at the query compilation time. The key is **validated** before beginning the query execution. * `List<>` and `Dict<>`, use **square brackets**. The set of keys (set of indexes for keys) is known only at the query execution time. The key is **not validated** before beginning the query execution. If no value is found, an empty value (NULL) is returned [Description and list of available containers](https://ytsaurus.tech/docs/types/containers.md). When using this syntax to access containers within table columns, be sure to specify the full column name, including the table name or table alias separated by a dot (see the first example below). #### Examples ```yql SELECT t.struct.member, t.tuple.7, t.dict["key"], t.list[7] FROM my_table AS t; ``` ```yql SELECT Sample::ReturnsStruct().member; ``` # ACTION ## DEFINE ACTION {#define-action} Specifies a named action that is a parameterizable block of multiple top-level expressions. **Syntax** 1. `DEFINE ACTION`: action definition. 1. [Name of the action](https://ytsaurus.tech/docs/en/expressions.md#named-nodes), as part of which the declared action is further available for the call. 1. The values of parameter names are listed in parentheses. 1. `AS` keyword. 1. List of top-level expressions. 1. `END DEFINE`: The marker of the last expression inside the action. One or more of the last parameters can be marked with a question mark `?` as optional. If they are omitted during the call, they will be assigned the `NULL` value. ## DO {#do} Executes an `ACTION` with the specified parameters. **Syntax** 1. `DO`: Executing an action. 1. The named expression for which the action is defined. 1. The values to be used as parameters are listed in parentheses. `EMPTY_ACTION`: An action that does nothing. {% note info "Note" %} In large queries, you can declare actions in separate files and connect them to the main query using [EXPORT](https://ytsaurus.tech/docs/en/export_import.md#export) + [IMPORT](https://ytsaurus.tech/docs/en/export_import.md#import) to get multiple logical parts that are easier to navigate than one long text. An important nuance: the `USE my_cluster;` directive in the import query does not affect behavior of actions declared in other files. {% endnote %} **Example** ```yql DEFINE ACTION $hello_world($name, $suffix?) AS $name = $name ?? ($suffix ?? "world"); SELECT "Hello, " || $name || "!"; END DEFINE; DO EMPTY_ACTION(); DO $hello_world(NULL); DO $hello_world("John"); DO $hello_world(NULL, "Earth"); ``` ## BEGIN .. END DO {#begin} Performing an action without declaring it (anonymous action). **Syntax** 1. `BEGIN`; 1. List of top-level expressions. 1. `END DO`. An anonymous action can't include any parameters. **Example** ``` DO BEGIN SELECT 1; SELECT 2 -- here and in the previous example of ';' you can omit END DO before END ``` ## EVALUATE IF {#evaluate-if} `EVALUATE IF`: Executing an action depending on the condition. It's followed by: 1. Condition. 2. [DO](https://ytsaurus.tech/docs/en/#do) with the name and parameters of the action or an anonymous action. 3. An optional `ELSE` followed by the second `DO` for a situation where the condition is not met. ## EVALUATE FOR {#evaluate-for} `EVALUATE FOR`: Executing an action for each item in the list. It's followed by: 1. A [named expression](https://ytsaurus.tech/docs/en/expressions.md#named-nodes) applied to each next element in the list. 2. `IN` keyword. 3. The above-declared named expression applied to the list the action is executed on. 4. [DO](https://ytsaurus.tech/docs/en/#do) with the name and parameters of an action or an anonymous action. In the parameters, you can use both the current element from the first paragraph and any named expressions declared above, including the list itself. 5. An optional `ELSE` followed by the second `DO` for the situation when the list is empty. **Examples** ```yql DEFINE ACTION $hello() AS SELECT "Hello!"; END DEFINE; DEFINE ACTION $bye() AS SELECT "Bye!"; END DEFINE; EVALUATE IF RANDOM(0) > 0.5 DO $hello() ELSE DO $bye(); EVALUATE IF RANDOM(0) > 0.1 DO BEGIN SELECT "Hello!"; END DO; EVALUATE FOR $i IN AsList(1, 2, 3) DO BEGIN SELECT $i; END DO; ``` ```yql -- copy the table $input to $count new tables $count = 3; $input = "my_input"; $inputs = ListReplicate($input, $count); $outputs = ListMap( ListFromRange(0, $count), ($i) -> { RETURN "tmp/out_" || CAST($i as String) } ); $pairs = ListZip($inputs, $outputs); DEFINE ACTION $copy_table($pair) as $input = $pair.0; $output = $pair.1; INSERT INTO $output WITH TRUNCATE SELECT * FROM $input; END DEFINE; EVALUATE FOR $pair IN $pairs DO $copy_table($pair) ELSE DO EMPTY_ACTION(); -- you can skip specifying this ELSE, -- default value implies doing nothing ``` {% note info "Note" %} Note that `EVALUATE` is run before the operation starts. Please also note that in `EVALUATE` you can't use [anonymous tables](https://ytsaurus.tech/docs/en/select/temporary_table.md). {% endnote %} # CREATE TABLE The table is created automatically at the first [INSERT INTO](https://ytsaurus.tech/docs/en/insert_into.md), as specified by the [USE](https://ytsaurus.tech/docs/en/use.md) operator of database. The scheme is defined automatically. # CREATE VIEW `CREATE VIEW` creates an independent [view](https://ytsaurus.tech/docs/en/select/view.md). YQL over YTsaurus supports two types of views: independent views, which can request to arbitrary tables within the cluster, and views that are linked to a specific table. ## Availability `CREATE VIEW` is available starting from the language version [2025.05](https://ytsaurus.tech/docs/changelog/2025.05.md). ## Syntax ```yql CREATE VIEW [IF NOT EXISTS] [cluster.]`//path/to/view` AS DO BEGIN END DO ``` This query will create an independent view on the `cluster` (or on the current cluster if not specified) at the path `//path/to/view`. The view consists of the block of commands ``. The `` can include only commands that are allowed in [DEFINE SUBQUERY](https://ytsaurus.tech/docs/en/subquery.md#define-subquery). The `USE` command or explicit cluster specification for tables cannot be used — within a view, you can only request to tables located on the same cluster as the view itself. Unlike `DEFINE SUBQUERY`, commands in `CREATE VIEW` must not use any values from outside the `CREATE VIEW` body. If any object already exists at the path `//path/to/view`, the view creation will fail. If the `IF NOT EXISTS` modifier is specified, no error is raised, but the `CREATE VIEW` command does not create a view (completes with no result). {% note warning "Attention" %} Creating a view and using it within the same YQL-query is not supported yet. When creating views, please note that the [language version](https://ytsaurus.tech/docs/changelog/index.md#langver-desc) for the view will be determined by the query in which the view is used, not by the query in which it was created. The same rule applies to [libraries](https://ytsaurus.tech/docs/en/export_import.md). {% endnote %} ## Example ```yql USE cluster; $now = CurrentUtcDate(); CREATE VIEW `//home/yql/tutorial/users_with_birth_year` AS DO BEGIN $now = CurrentUtcDate(); -- cannot use named expressions from outside $year_of_birth = ($age) -> (DateTime::GetYear(DateTime::ShiftYears($now, -(cast($age as Int32) ?? 0)))); SELECT $year_of_birth(age) AS birth_year, t.* WITHOUT age FROM `//home/yql/tutorial/users` AS t; END DO; ``` For information on how to use the created view, see the section [SELECT VIEW](https://ytsaurus.tech/docs/en/select/view.md). ## See also * [DROP VIEW](https://ytsaurus.tech/docs/en/drop_view.md) # COMMIT By default, the entire YQL query is executed within a single transaction, and independent parts inside it are executed in parallel, if possible. Using the `COMMIT;` keyword you can add a barrier to the execution process to delay execution of expressions that follow until all the preceding expressions have completed. To commit in the same way automatically after each expression in the query, you can use `PRAGMA autocommit;`. ## Examples ```yql INSERT INTO result1 SELECT * FROM my_table; INSERT INTO result2 SELECT * FROM my_table; COMMIT; -- result2 will already include the SELECT contents from the second line: INSERT INTO result3 SELECT * FROM result2; ``` # COMBINE Groups rows from two input tables by a common key and applies a UDF or [lambda function](https://ytsaurus.tech/docs/en/expressions.md#lambda) to each group. Unlike [JOIN](https://ytsaurus.tech/docs/en/join.md), `COMBINE` does not build a Cartesian product of matching rows: the function receives all rows with the same key as two lists and can implement arbitrary matching logic. ## Syntax {#syntax} ```yql COMBINE input1 AS alias1 [PRESORT presort_expression1 [ASC | DESC], ...] WITH input2 AS alias2 [PRESORT presort_expression2 [ASC | DESC], ...] ON alias1.key_expression = alias2.key_expression [AND ...] USING function(item_expression1, item_expression2) ``` ## Availability {#availability} `COMBINE` is available starting with language version [2026.02](https://ytsaurus.tech/docs/changelog/2026.02.md). ## Description {#description} The `ON` clause contains one or more equality conditions joined with `AND`. Multiple predicates produce a composite tuple key, while one predicate produces a scalar key. For every key present in at least one input, `COMBINE` calls the function with three arguments: 1. The common key from the `ON` clause. 2. A list of values of the first `USING` argument for rows from the first input. 3. A list of values of the second `USING` argument for rows from the second input. If a key occurs in only one input, the list for the other input is empty. At the group level, `COMBINE` therefore has `FULL JOIN` semantics. Use `TableRow()` to pass the entire row. Other expressions can select columns or calculate a value. The optional `PRESORT` clause specifies row order within a group; without it, element order is undefined. The function in `USING` can return the same types as [PROCESS](https://ytsaurus.tech/docs/en/process.md): a structure, optional structure, list, or stream of structures. The result is converted into a flat table. {% note info "Note" %} `COMBINE` is useful when rows with the same key must be processed together without the row multiplication of a regular join, for example when matching time intervals. {% endnote %} ## Examples {#examples} ```yql $count_rows = ($key, $left_rows, $right_rows) -> { RETURN <| key: $key, left_count: ListLength($left_rows), right_count: ListLength($right_rows) |>; }; COMBINE my_table1 AS L WITH my_table2 AS R ON L.key = R.key USING $count_rows(TableRow(), TableRow()); ``` ```yql $zip_rows = ($key, $left_rows, $right_rows) -> { RETURN <| key: $key.0, subkey: $key.1, rows: ListZipAll($left_rows, $right_rows) |>; }; COMBINE my_table1 AS L PRESORT L.timestamp WITH my_table2 AS R PRESORT R.timestamp ON L.key = R.key AND L.subkey = R.subkey USING $zip_rows(TableRow(), TableRow()); ``` # DISCARD Calculates [SELECT](https://ytsaurus.tech/docs/en/select/index.md), [REDUCE](https://ytsaurus.tech/docs/en/reduce.md) or [PROCESS](https://ytsaurus.tech/docs/en/process.md), but returns no result to the client or to the table. It can't be set at the same time as [INTO RESULT](https://ytsaurus.tech/docs/en/into_result.md). It's good to combine it with `Ensure` to check the final calculation result against the user's criteria. #### Examples ```yql DISCARD SELECT 1; ``` ```yql INSERT INTO result_table WITH TRUNCATE SELECT * FROM my_table WHERE value % 2 == 0; COMMIT; DISCARD SELECT Ensure( 0, -- will discard result anyway COUNT(*) > 1000, "Too small result table, got only " || CAST(COUNT(*) AS String) || " rows" ) FROM result_table; ``` # DROP TABLE Deletes the specified table. Search for the table by name in the database specified by the [USE](https://ytsaurus.tech/docs/en/use.md) operator. If there is no such table, an error is returned. ### Examples ```yql DROP TABLE my_table; ``` # DROP VIEW `DROP VIEW` deletes an independent [view](https://ytsaurus.tech/docs/en/select/view.md). ## Syntax ```yql DROP VIEW [IF EXISTS] [cluster.]`//path/to/view`; ``` If `//path/to/view` is not a view, the command fails with an error. An attempt to drop a non-existent view also fails with an error unless the `IF EXISTS` modifier is specified. ## Availability `DROP VIEW` is available starting from language version [2025.05](https://ytsaurus.tech/docs/changelog/2025.05.md). ## Examples ```yql DROP VIEW `//path/to/view1`; -- drops the view on the current cluster DROP VIEW IF EXISTS cluster.`//path/to/view2`; -- explicit cluster specification, completes without error if `//path/to/view2` does not exist ``` ## See also * [CREATE VIEW](https://ytsaurus.tech/docs/en/create_view.md) ## GROUP BY Group the `SELECT` results by the values of the specified columns or expressions. The aggregate functions (`COUNT`, `MAX`, `MIN`, `SUM`, `AVG`) are often used together with `GROUP BY` to perform calculations in each group. **Syntax** ```sql SELECT -- In SELECT, you can use: column1, -- key columns specified in GROUP BY key_n, -- named expressions specified in GROUP BY column1 + key_n, -- random non-aggregate derived functions Aggr_Func1( column2 ), -- aggregate functions containing any columns in arguments, Aggr_Func2( key_n + column2 ), -- including named expressions specified in GROUP BY ... FROM table GROUP BY column1, column2, ..., AS key_n -- When grouping by expression, it can be assigned a name via AS, -- that may be used in SELECT ``` Querying the `SELECT * FROM table GROUP BY k1, k2, ...` type returns all the columns listed in GROUP BY, which is equivalent to the `SELECT DISTINCT k1, k2, ... query FROM table`. An asterisk can also be used as an argument for the `COUNT` aggregate function. `COUNT(*)` means "the count of rows in the group". {% note info %} Aggregate functions ignore `NULL` in their arguments, except for `COUNT`. {% endnote %} YQL also supports factories of aggregate functions implemented using the `AGGREGATION_FACTORY` and `AGGREGATE_BY` functions. **Examples** ```sql SELECT key, COUNT(*) FROM my_table GROUP BY key; ``` ```sql SELECT double_key, COUNT(*) FROM my_table GROUP BY key + key AS double_key; ``` ```sql SELECT double_key, -- OK: key column COUNT(*) AS group_size, -- OK: COUNT(*) SUM(key + subkey) AS sum1, -- OK: aggregate function CAST(SUM(1 + 2) AS String) AS sum2, -- OK: aggregate function with constant argument SUM(SUM(1) + key) AS sum3, -- ERROR: nested aggregations are not allowed key AS k1, -- ERROR: use of non-key Key column without aggregation key * 2 AS dk1, -- ERROR in YQL: use of non-key Key column without aggregation FROM my_table GROUP BY key * 2 AS double_key, subkey as sk, ``` {% note warning "Attention!" %} Specifying a name for a column or expression in `GROUP BY .. AS foo` it is an extension on top of YQL. This name becomes visible in `WHERE` despite the fact that filtration by `WHERE` is performed[before](https://ytsaurus.tech/docs/en/select/index.md#selectexec) grouping. For example, if the `T` table includes two columns, `foo` and `bar`, then the query `SELECT` foo FROM T WHERE foo > 0 GROUP BY bar AS foo would actually filter data by the `bar` column from the source table. {% endnote %} ## GROUP BY ... SessionWindow() {#session-window} YQL supports grouping by session. To standard expressions in `GROUP BY`, you can add a special `SessionWindow` function: ```sql SELECT user, session_start, SessionStart() AS same_session_start, -- the same as session_start COUNT(*) AS session_size, SUM(value) AS sum_over_session, FROM my_table GROUP BY user, SessionWindow(, ) AS session_start ``` The following happens in this case: 1) The input table is partitioned by the grouping keys specified in `GROUP BY`, ignoring SessionWindow (in this case, it's based on `user`). If `GROUP BY` includes nothing more than SessionWindow, then the input table gets into one partition 2) Each partition is split into disjoint subsets of rows (sessions). For this, the partition is sorted in the ascending order of the `time_expr` expression. The session limits are drawn between neighboring items of the partition, that differ in their `time_expr` values by more than `timeout_expr` 3) The sessions obtained in this way are the final partitions on which aggregate functions are calculated. The SessionWindow() key column (in the example, it's `session_start`) has the value "the minimum `time_expr` in the session". Also, if SessionWindow() is present in `GROUP BY`, you can use the special `SessionStart` aggregate function. An extended version of SessionWindow with four arguments is also supported: `SessionWindow(, , , )` Where: * `` is an expression based on which the original partition is sorted * `` is a lambda function for initializing the session calculation status. It has the signature `(TableRow())->State`. It's called once for the first (following the sorting order) element of the source partition * `` is a lambda function for refreshing the session calculation state and defining session limits. It has the signature `(TableRow(), State)->Tuple`. It's called for every item of the source partition, except the first one. The new value of state is calculated based on the current row of the table and the previous state. If the first item in the return tuple is `True`, then a new session starts from the _current_ row. The new session key is obtained by applying `` to the second tuple element. * ``: A lambda function for calculating the session key (the "value" of SessionWindow() that is also accessible via SessionStart()). The function has the signature `(TableRow(), State)->SessionKey`. It's called in the first partition element (after ``) and on those elements for which `` returned`True` as the first tuple element. It's worth noting that to start a new session, `` must return a value that is different from the previous session key. Sessions having the same keys are not merged. For example, if `` sequentially returns `0, 1, 0, 1`, they will be four different sessions. Using the extended version of SessionWindow, you can, for example, do the following: divide a partition into sessions, as in the SessionWindow use case with two arguments, but with the maximum session length limited by a certain constant: **Example** ```sql $max_len = 1000; -- maximum session length $timeout = 100; -- timeout (timeout_expr in a simplified variant of SessionWindow) $init = ($row) -> (AsTuple($row.ts, $row.ts)); -- session state - tuple of 1) values of temporary ts column in the first session row and 2) in the current row $update = ($row, $state) -> { $is_end_session = $row.ts - $state.0 > $max_len OR $row.ts - $state.1 > $timeout; $new_state = AsTuple(IF($is_end_session, $row.ts, $state.0), $row.ts); return AsTuple($is_end_session, $new_state); }; $calculate = ($row, $state) -> ($row.ts); SELECT user, session_start, SessionStart() AS same_session_start, -- the same as session_start COUNT(*) AS session_size, SUM(value) AS sum_over_session, FROM my_table GROUP BY user, SessionWindow(ts, $init, $update, $calculate) AS session_start ``` You can use SessionWindow in GROUP BY only once. ## ROLLUP, CUBE, and GROUPING SETS {#rollup} The results of calculating the aggregate function as subtotals for the groups and overall totals over individual columns or whole table. **Syntax** ```sql SELECT c1, c2, -- columns based on which grouping is performed AGGREGATE_FUNCTION(c3) AS outcome_c -- (SUM, AVG, MIN, MAX, COUNT) aggregate function FROM table_name GROUP BY GROUP_BY_EXTENSION(c1, c2) -- GROUP BY: ROLLUP, CUBE, or GROUPING SETS extension ``` * `ROLLUP` groups the column values in the order they are listed in the arguments (strictly from left to right), generates subtotals for each group and the overall total. * `CUBE` groups the values for every possible combination of columns, generates the subtotals for each group and the overall total. * `GROUPING SETS` sets the groups for subtotals. You can combine `ROLLUP`, `CUBE` and `GROUPING SETS`, separating them by commas. ### GROUPING {#grouping} The values of columns not used in calculations are replaced with `NULL` in the subtotal. In the overall total, the values of all columns are replaced by `NULL`. `GROUPING`: A function that allows you to distinguish the source `NULL` values from the `NULL` values added while calculating subtotals and overall totals. `GROUPING` returns a bit mask: * `0`: If `NULL` is used for the original empty value. * `1`: If `NULL` is added for a subtotal or overall total. **Example** ```sql SELECT column1, column2, column3, CASE GROUPING( column1, column2, column3, ) WHEN 1 THEN "Subtotal: column1 and column2" WHEN 3 THEN "Subtotal: column1" WHEN 4 THEN "Subtotal: column2 and column3" WHEN 6 THEN "Subtotal: column3" WHEN 7 THEN "Grand total" ELSE "Individual group" END AS subtotal, COUNT(*) AS rows_count FROM my_table GROUP BY ROLLUP( column1, column2, column3 ), GROUPING SETS( (column2, column3), (column3) -- if you added (column2) as well, then -- summing up these ROLLUP and GROUPING SETS would give the result -- similar to CUBE ) ; ``` ## DISTINCT {#distinct} Applying aggregate functions only to distinct values of the column. {% note info %} Applying `DISTINCT` to calculated values is not currently implemented. For this purpose, use a [subquery](https://ytsaurus.tech/docs/en/select/from.md) or the clause `GROUP BY ... AS ...`. {% endnote %} **Example** ```sql SELECT key, COUNT(DISTINCT value) AS count -- top 3 keys based on the number of unique values FROM my_table GROUP BY key ORDER BY count DESC LIMIT 3. ``` You can also use `DISTINCT` to fetch unique rows using [`SELECT DISTINCT`](https://ytsaurus.tech/docs/en/select/distinct.md). ## COMPACT The presence of [SQL hint](https://ytsaurus.tech/docs/en/lexer.md#sql-hints) `COMPACT` right after the `GROUP` keyword allows for more effective aggregation in cases where the query author knows beforehand that no aggregation key produces large amounts of data (more than a gigabyte or millions of rows). If this assumption fails to materialize, then the operation may fail with Out of Memory error or start running much slower compared to the non-COMPACT version. Unlike regular GROUP BY, Map-side combiner and additional Reduce for each field with [DISTINCT](https://ytsaurus.tech/docs/en/#distinct) aggregation are disabled. **Example:** ```yql SELECT key, COUNT(DISTINCT value) AS count -- top 3 keys based on the number of unique values FROM my_table GROUP /*+ COMPACT() */ BY key ORDER BY count DESC LIMIT 3; ``` ## HAVING {#having} Filtering a `SELECT` based on the aggregate function calculation results. The syntax is similar to the [`WHERE`](https://ytsaurus.tech/docs/en/select/where.md) clause. **Example** ```yql SELECT key FROM my_table GROUP BY key HAVING COUNT(value) > 100; ``` # EXPORT and IMPORT A mechanism for putting part of the query into a separate attached file. To use the mechanism, you need to set the following pragma: * [PRAGMA Library](https://ytsaurus.tech/docs/en/pragma/file.md#library) — marks the attached file as available for import. ## EXPORT `EXPORT $my_symbol1, $my_symbol2, ...;` lists the names of named expressions in the library that are available for import. ## IMPORT `IMPORT my_library SYMBOLS $my_symbol1, $my_symbol2, ...;` makes the listed named expressions available for further use. {% note info %} [Lambdas](https://ytsaurus.tech/docs/en/expressions.md#lambda), [actions](https://ytsaurus.tech/docs/en/action.md), [named subqueries](https://ytsaurus.tech/docs/en/subquery.md), constant values, and expressions can be transferred to the library, but __subqueries or aggregate functions__ cannot be. {% endnote %} {% note warning %} The file linked by the [PRAGMA Library](https://ytsaurus.tech/docs/en/pragma/file.md#library) must be attached to the query. __You can't use [PRAGMA File](https://ytsaurus.tech/docs/en/pragma/file.md#file) for this purpose__. {% endnote %} ## Example my_lib.sql: ```yql $Square = ($x) -> { RETURN $x * $x; }; $Sqrt = ($x) -> { RETURN Math::Sqrt($x); }; -- Aggregation functions created using -- AggregationFactory, convenient to transfer to the library $Agg_sum = AggregationFactory("SUM"); $Agg_max = AggregationFactory("MAX"); EXPORT $Square, $Sqrt, $Agg_sum, $Agg_max; ``` Query: ```yql PRAGMA Library("my_lib.sql"); IMPORT my_lib SYMBOLS $Square, $Sqrt, $Agg_sum, $Agg_max; SELECT $Square(2), -- 4 $Sqrt(4); -- 2 SELECT AGGREGATE_BY(x, $Agg_sum), -- 5 AGGREGATE_BY(x, $Agg_max) -- 3 FROM ( SELECT 2 AS x UNION ALL SELECT 3 AS x ) ``` # FLATTEN ## FLATTEN BY {#flatten-by} Converts rows in the source table using vertical unpacking of [containers](https://ytsaurus.tech/docs/types/containers.md) of variable length (lists or dictionaries). For example: * Source table: |[a, b, c]|1| | --- | --- | |[d]|2| |[]|3| * The table resulting from `FLATTEN BY` on the left column: |a|1| | --- | --- | |b|1| |c|1| |d|2| #### Example ```yql $sample = AsList( AsStruct(AsList('a','b','c') AS value, CAST(1 AS Uint32) AS id), AsStruct(AsList('d') AS value, CAST(2 AS Uint32) AS id), AsStruct(AsList() AS value, CAST(3 AS Uint32) AS id) ); SELECT value, id FROM as_table($sample) FLATTEN BY (value); ``` This conversion can be convenient in the following cases: * When it is necessary to output statistics by cells from a container column (for example, via [`GROUP BY`](https://ytsaurus.tech/docs/en/group_by.md)). * When the cells in a container column store IDs from another table that you want to join with [`JOIN`](https://ytsaurus.tech/docs/en/join.md). #### Syntax * `FLATTEN BY` is specified after `FROM`, but before `GROUP BY`, if `GROUP BY` is present in the query. * The type of the result column depends on the type of the source column: | Container type | Result type | Comments | | --- | --- | --- | | `List` | `X` | List cell type | | `Dict` | `Tuple` | Tuple of two elements containing key-value pairs | | `Optional` | `X` | The result is almost equivalent to the clause `WHERE foo IS NOT NULL`, but the `foo` column type is changed to `X` | * By default, the result column replaces the source column. Use `FLATTEN BY foo AS bar` to keep the source container. As a result, the source container is still available as `foo` and the output container is available as `bar`. * To build a Cartesian product of multiple container columns, use the clause `FLATTEN BY (a, b, c)`. Parentheses are mandatory to avoid grammar conflicts. * Inside `FLATTEN BY`, you can only use column names from the input table. To apply `FLATTEN BY` to the calculation result, use a subquery. * In `FLATTEN BY` you can use both columns and arbitrary named expressions (unlike columns, `AS` is required in this case). To avoid grammatical ambiguities of the expression after `FLATTEN BY`, make sure to use parentheses with the following: `... FLATTEN BY (ListSkip(col, 1) AS col) ...` * If the source column had nested containers, for example, `List`, `FLATTEN BY` unpacks only the outer level. To completely unpack the nested containers, use a subquery. {% note info %} `FLATTEN BY` interprets [optional data types](https://ytsaurus.tech/docs/types/optional.md) as lists of length 0 or 1. The table rows with `NULL` are skipped, and the column type changes to a similar non-optional type. `FLATTEN BY` makes only one conversion at a time, so use `FLATTEN LIST BY` or `FLATTEN OPTIONAL BY` on optional containers, for example, `Optional>`. {% endnote %} ## Applying FLATTEN BY to YSON {#flatten-by-apply-yson} To apply `FLATTEN BY` to YSON, you first need to transform it into one of the container types discussed above. For instance, you can do this using the [UDF module of the same name](https://ytsaurus.tech/docs/udf/list/yson.md) from the list of presets. ### Specifying the container type {#flatten-by-specific-type} To specify the type of container to convert to, you can use: * `FLATTEN LIST BY` For `Optional>`, `FLATTEN LIST BY` will unpack the list, treating `NULL` as an empty list. * `FLATTEN DICT BY` For `Optional>`, `FLATTEN DICT BY` will unpack the dictionary, interpreting `NULL` as an empty dictionary. * `FLATTEN OPTIONAL BY` To filter the `NULL` values without serialization, specify the operation by using `FLATTEN OPTIONAL BY`. #### Examples ```yql SELECT t.item.0 AS key, t.item.1 AS value, t.dict_column AS original_dict, t.other_column AS other FROM my_table AS t FLATTEN DICT BY dict_column AS item; ``` ```yql SELECT * FROM ( SELECT AsList(1, 2, 3) AS a, AsList("x", "y", "z") AS b ) FLATTEN LIST BY (a, b); ``` ```yql SELECT * FROM ( SELECT "1;2;3" AS a, AsList("x", "y", "z") AS b ) FLATTEN LIST BY (String::SplitToList(a, ";") as a, b); ``` ### Analogues of FLATTEN BY in other DBMS {#flatten-other-dmb} * PostgreSQL: `unnest` * Hive: `LATERAL VIEW` * MongoDB: `unwind` * Google BigQuery: `FLATTEN` * ClickHouse: `ARRAY JOIN / arrayJoin` ## FLATTEN COLUMNS {#flatten-columns} Transforms each column of type `Struct` into individual columns, one for each field within the struct. The names of the new columns are the names of the fields from the original struct columns. Columns that are not structs remain unchanged. - Only one level of the struct is flattened. - The original struct columns are not included in the result; their names are not used anywhere. - All column names in the resulting table (including names from struct fields in the original columns and names of non-struct columns) must be unique; name conflicts result in an error. #### Example ```yql SELECT x, y, z, not_struct FROM ( SELECT AsStruct( 1 AS x, "foo" AS y), AsStruct( false AS z), 1 as not_struct, ) FLATTEN COLUMNS; ``` # INSERT INTO Adds rows to the table. If the target table already exists and is not sorted, the operation `INSERT INTO` adds rows at the end of the table. For a sorted table, YQL attempts to preserve sorting by starting a sorted merge. Search for the table by name in the database set by the [USE](https://ytsaurus.tech/docs/en/use.md) operator. ## Description `INSERT INTO` lets you perform the following operations: * Adding constant values using [`VALUES`](https://ytsaurus.tech/docs/en/values.md). ```yql INSERT INTO my_table (Column1, Column2, Column3, Column4) VALUES (345987,'ydb', 'Apple region', 1414); COMMIT; ``` ```yql INSERT INTO my_table (Column1, Column2) VALUES ("foo", 1), ("bar", 2); ``` * Saving the results of a `SELECT` statement. ```yql INSERT INTO my_table SELECT SourceTableColumn1 AS MyTableColumn1, "Empty" AS MyTableColumn2, SourceTableColumn2 AS MyTableColumn3 FROM source_table; ``` ## Using modifiers Write operation can be performed with one or more modifiers. The modifier is specified after the `WITH` keyword after the table name: `INSERT INTO ... WITH SOME_HINT`. For example, to clear existing data from the table before executing a write operation, just add the `WITH TRUNCATE` modifier: ```yql INSERT INTO my_table WITH TRUNCATE SELECT key FROM my_table_source; ``` The following rules apply: - If the modifier has a value, it's specified after the `=` sign: `INSERT INTO ... WITH SOME_HINT=value`. - If you need to specify multiple modifiers, they must be enclosed in parentheses: `INSERT INTO ... WITH (SOME_HINT1=value, SOME_HINT2, SOME_HINT3=value)`. ## Writing to dynamic tables You can only write data to sorted dynamic tables, and they must already exist before you run the query. `INSERT INTO` statements can only be used with the `WITH TRUNCATE` modifier, which implies that any data in the table will be completely overwritten. To insert data without clearing the table, use [REPLACE INTO](https://ytsaurus.tech/docs/en/replace_into.md) instead. ### Full list of supported write modifiers (for static tables) * `TRUNCATE` — Clears existing data from the table. * `COMPRESSION_CODEC=codec_name` — Writes data with the specified compression codec. This takes priority over the `yt.PublishedCompressionCodec` pragma. For the list of supported values, see [Compression](https://ytsaurus.tech/docs/en/user-guide/storage/compression#compression_codecs). * `ERASURE_CODEC=erasure_name` — Writes data with the specified erasure codec. This takes priority over the `yt.PublishedErasureCodec` pragma. For the list of supported values, see [Replication and erasure coding](https://ytsaurus.tech/docs/en/user-guide/storage/replication#erasure). * `EXPIRATION=value` — Sets the expiration period for the table being created. The value can be set as a `duration` or as a `timestamp` in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. Takes priority over the `yt.ExpirationDeadline`/`yt.ExpirationInterval` pragmas. This modifier can only be set together with `TRUNCATE`. * `REPLICATION_FACTOR=factor` — Sets the replication factor for the table being created. The value must be in the [1, 10] range. * `USER_ATTRS=yson` — Sets user attributes that will be added to the table. Attributes must be set as a YSON dictionary. * `MEDIA=yson` — Sets the media. This takes priority over the `yt.PublishedMedia` pragma. For the list of supported values, see [Media](https://ytsaurus.tech/docs/en/user-guide/storage/media). * `PRIMARY_MEDIUM=medium_name` — Sets the primary medium. This takes priority over the `yt.PublishedPrimaryMedium` pragma. * `KEEP_META` — Saves the current schema, user attributes, and storage attributes (codecs, replications, media) of the target table. This modifier can only be set together with `TRUNCATE`. {% note info %} We do not recommend using system attributes in `USER_ATTRS`, as applying them to written data can lead to unpredictable or unreliable results. {% endnote %} ### Full list of supported write modifiers (for dynamic tables) * `TRUNCATE` — Clears existing data from the table; mandatory for `INSERT INTO`. * `USER_ATTRS=yson` — Sets user attributes that will be added to the table. Attributes must be set as a YSON dictionary. #### Examples ```yql INSERT INTO my_table WITH (TRUNCATE, EXPIRATION="15m") SELECT key FROM my_table_source; INSERT INTO my_table WITH USER_ATTRS="{attr1=value1; attr2=value2;}" SELECT key FROM my_table_source; ``` # REPLACE INTO Writes rows to a dynamic table. For existing keys, it overwrites entire rows; for new keys, it adds values to the table. To use `REPLACE INTO`, the following conditions must be met: - The target table must exist. - The target table must be a sorted dynamic table. - The target table must be mounted. - Data being written must be unique by key. - Data sorting by key is not required. YQL can automatically sort unsorted data using the `sort` operation. ## Description `REPLACE INTO` supports the same operations as [INSERT INTO](https://ytsaurus.tech/docs/en/insert_into.md), but with one crucial difference: `REPLACE INTO` allows inserting only into dynamic tables. You can use it when the data you're inserting only partially overwrites the existing records, and you don't want to wipe them with `INSERT INTO WITH TRUNCATE`. ## Using modifiers The logic for using modifiers in the same as with [INSERT INTO](https://ytsaurus.tech/docs/en/insert_into.md). ### Full list of supported write modifiers * `USER_ATTRS=yson` — Sets user attributes that will be added to the table. Any existing attributes will be overwritten. Attributes must be set as a YSON dictionary. {% note info %} We do not recommend using system attributes in `USER_ATTRS`, as applying them to written data can lead to unpredictable or unreliable results. {% endnote %} #### Examples ```yql REPLACE INTO my_dynamic_table SELECT key FROM my_table_source; REPLACE INTO my_dynamic_table WITH USER_ATTRS="{attr1=value1; attr2=value2;}" SELECT key FROM my_table_source; ``` # INTO RESULT Lets you specify the user label for [SELECT](https://ytsaurus.tech/docs/en/select/index.md), [PROCESS](https://ytsaurus.tech/docs/en/process.md), or [REDUCE](https://ytsaurus.tech/docs/en/reduce.md). It can't be used along with [DISCARD](https://ytsaurus.tech/docs/en/discard.md). ## Examples ```yql SELECT 1 INTO RESULT foo; ``` ```yql SELECT * FROM my_table WHERE value % 2 == 0 INTO RESULT `Name of the result`. ``` # JOIN It lets you combine multiple data sources (subqueries or tables) by equality of values in the specified columns or expressions (the `JOIN` keys). **Syntax** ```sql SELECT ... FROM table_1 -- first JOIN step: JOIN table_2 -- left subquery -- entries in table_1 -- right subquery -- entries in table_2 -- next JOIN step: JOIN table_n -- left subquery -- JOIN result in the previous step -- right subquery -- entries in table_n -- JOIN can include the following steps ... WHERE ... ``` At each JOIN step, rules are used to establish correspondences between rows in the left and right data subqueries, creating a new subquery that includes every combination of rows that meet the JOIN conditions. {% note warning "Attention!" %} Since columns in YQL are identified by their names, and you can't have two columns with the same name in the subquery, `SELECT * FROM ... JOIN ...` can't be executed if there are columns with identical names in the joined tables. {% endnote %} ## Types of join (Join_Type) * `INNER` (default): Rows from joined subqueries that don't match any rows on the other side won't be included in the result. * `LEFT`: If there's no value in the right subquery, it adds a row to the result with column values from the left subquery, using `NULL` in columns from the right subquery * `RIGHT`: If there's no value in the left subquery, it adds the row to the result, including column values from the right subquery, but using `NULL` in columns from the left subquery * `FULL` = `LEFT` + `RIGHT` * `LEFT/RIGHT SEMI`: One side of the subquery is a whitelist of keys, its values are not available. The result includes columns from one table only, no Cartesian product is created. * `LEFT/RIGHT ONLY`: Subtracting the sets by keys (blacklist). It's almost the same as adding `IS NULL` to the key on the opposite side in the regular `LEFT/RIGHT` JOIN, but with no access to values: the same as `SEMI` JOIN. * `CROSS`: A full Cartesian product of two tables without specifying key columns and no explicit `ON/USING`. * `EXCLUSION`: Both sides minus the intersection. ![](https://ytsaurus.tech/docs/en/_assets/join-YQL-06.png) {% note info %} `NULL` is a special value to denote nothing. Hence, `NULL` values on both sides are not treated as equal to each other. This eliminates ambiguity in some types of `JOIN` and avoids a giant Cartesian product otherwise created. {% endnote %} ## Conditions for joining (Join_Condition) For `CROSS JOIN`, no join condition is specified. The result includes the Cartesian product of the left and right subquery, meaning it combines everything with everything. The number of rows in the resulting subquery is the product of the number of rows in the left and right subqueries. For any other JOIN types, specify the condition using one of the two methods: 1. `USING (column_name)`. Used if both the left and right subqueries share a column whose equality of values is a join condition. 2. `ON (equality_conditions)`. Lets you set a condition of equality for column values or expressions over columns in the left and right subqueries or use several such conditions combined by `and`. **Examples:** ```sql SELECT a.value as a_value, b.value as b_value FROM a_table AS a FULL JOIN b_table AS b USING (key); ``` ```sql SELECT a.value as a_value, b.value as b_value FROM a_table AS a FULL JOIN b_table AS b ON a.key = b.key; ``` ```sql SELECT a.value as a_value, b.value as b_value, c.column2 FROM a_table AS a CROSS JOIN b_table AS b LEFT JOIN c_table AS c ON c.ref = a.key and c.column1 = b.value; ``` To make sure no full scan of the right joined table is required, a secondary index can be applied to the columns included in the Join condition. Accessing a secondary index should be specified explicitly in `JOIN table_name VIEW index_name AS table_alias` format. For example, creating an index to use in the Join condition: ```yql ALTER TABLE b_table ADD INDEX b_index_ref GLOBAL ON(ref); ``` Using the created index: ```yql SELECT a.value as a_value, b.value as b_value FROM a_table AS a INNER JOIN b_table VIEW b_index_ref AS b ON a.ref = b.ref; ``` If the statement filters data in addition to `JOIN`, we recommend wrapping the criteria that would return `true` for most of the rows in the `LIKELY(...)` function call. If your assumption that positive values prevail for the criteria is correct, such a hint might speed up your subquery. `LIKELY` can be useful when the predicate calculation is a resource-intensive operation and JOIN significantly reduces the number of rows. In front of any data source for `JOIN`, you can add the `ANY` keyword to suppress duplicate `JOIN` keys on the given side. In this case, only one row is left from the set of rows with the same `JOIN` key value (no matter which one, that's why the keyword is called `ANY`). This syntax is different from the one accepted in [ClickHouse], where `ANY` is placed before `JOIN` type and only functions for the right side. Request ```yql $t1 = AsList( AsStruct("1" AS key, "v111" AS value), AsStruct("2" AS key, "v121" AS value), AsStruct("2" AS key, "v122" AS value), AsStruct("3" AS key, "v131" AS value), AsStruct("3" AS key, "v132" AS value)); $t2 = AsList( AsStruct("2" AS key, "v221" AS value), AsStruct("2" AS key, "v222" AS value), AsStruct("3" AS key, "v231" AS value), AsStruct("3" AS key, "v232" AS value), AsStruct("4" AS key, "v241" AS value)); SELECT a.key, a.value, b.value FROM ANY AS_TABLE($t1) AS a JOIN ANY AS_TABLE($t2) AS b ON a.key == b.key; ``` results in: | a.key | a.value | b.value | | --- | --- | --- | | "3" | "v131" | "v231" | | "2" | "v121" | "v221" | а без `ANY` выдал бы: | a.key | a.value | b.value | | --- | --- | --- | | "3" | "v131" | "v231" | | "3" | "v131" | "v232" | | "3" | "v132" | "v231" | | "3" | "v132" | "v232" | | "2" | "v121" | "v221" | | "2" | "v121" | "v222" | | "2" | "v122" | "v221" | | "2" | "v122" | "v222" | ## JOIN execution strategies in YTsaurus ### Introduction The following `JOIN` syntax is supported in standard SQL: ```yql SELECT ... FROM T1 JOIN T2 ON F(T1, T2); ``` where `F(T1, T2)` is a custom predicate depending on columns of both tables `T1, T2`. YQL only supports a special case where the `F` predicate is separable, i.e. looks as follows: ```yql SELECT ... FROM T1 JOIN T2 ON F1(T1) = G1(T2) AND F2(T1) = G2(T2) AND ...; ``` Such predicate structure allows for effective `JOIN` implementation within the map-reduce concept. Just like in standard SQL, several `JOIN`s can be created in a single SELECT in YQL: ```yql SELECT ... FROM T1 JOIN T2 ON F1(T1) = G1(T2) AND F2(T1) = G2(T2) AND ... JOIN T3 ON H1(T1,T2) = J1(T3) AND H2(T1,T2) = J2(T3) AND ...; ``` Currently, such `JOIN`s are performed consecutively, strictly in the order specified in the query. The only exception is the StarJoin strategy. ### Calculating JOIN keys `JOIN` execution starts when the keys are calculated and their values are saved to a separate column. This transformation takes place as early as the SQL parser level and is common for all backends (YDB, YTsaurus, DQ, etc.) In practice, the query ```yql SELECT ... FROM T1 JOIN T2 ON F1(T1) = G1(T2) AND F2(T1) = G2(T2) AND ...; ``` is converted to ```yql SELECT ... FROM ( SELECT t.*, F1(...) as _yql_join_key1, F2(...) as _yql_join_key2, ... FROM T1 as t ) as t1 JOIN ( SELECT t.*, G1(...) as _yql_join_key1, G2(...) as _yql_join_key2, ... FROM T2 as t ) as t2 ON t1._yql_join_key1 = t2._yql_join_key1 AND t1._yql_join_key2 = t2._yql_join_key2 AND ...; ``` ### Converting keys to a simple common type This stage is already specific for YTsaurus. It converts key columns from both sides to an identical simple type. Request ```yql SELECT ... FROM T1 as t1 JOIN T2 as t2 ON t1.key1 = t2.key1 AND t1.key2 = t2.key2 AND ...; ``` is converted to ```yql SELECT ... FROM ( SELECT t.*, CastToCommonKeyType(key1) as _yql_join_key1, CastToCommonKeyType(key2) as _yql_join_key2, FROM T1 as t ) as t1 JOIN ( SELECT t.*, CastToCommonKeyType(key1) as _yql_join_key1, CastToCommonKeyType(key2) as _yql_join_key2, FROM T2 as t ) as t2 ON t1._yql_join_key1 = t2._yql_join_key1 AND t1._yql_join_key2 = t2._yql_join_key2 AND ...; ``` Conversion to a common type is necessary for the correct map-reduce operation with related but different key types. For example, `Optional` will be the common type for `Int32` and `Uint32` key types. If no conversion into a common type is performed, and initial columns are left as key for map-reduce operations, then YtsaurusYTsaurus will deem -1 and 4294967295 keys to be identical. This conversion is not always necessary. For example, Int32 and Optional key types operate correctly. Additionally, composite key types (everything more sophisticated than Optional from [simple type](https://ytsaurus.tech/docs/types/primitive.md))) after changing to common type are also converted to a string: ```yql if ( YQL::HasNulls(casted_key), -- if null null occurs somewhere in a key, -- then the value is converted to a string-type null (null in SQL is not equal to any value, including itself) StablePickle(casted_key), -- otherwise the value is serialized to string presentation ) ``` This conversion is needed because composite key types aren't supported as reduce operation keys in YTsaurus. Therefore, after all conversions we obtain keys of pair-wise identical simple type (with precision down to Optional) at both `JOIN` sides. ### Basic JOIN strategy (aka CommonJoin) The basic `JOIN` strategy is selected when other `JOIN` strategies cannot be applied due to some other reason. This strategy supports all `JOIN` types, including `CROSS JOIN`, and is implemented through a single MapReduce operation. The following happens at the map stage: 1) converting keys to a simple common type 2) if the ANY modifier is present, identical keys are "thinned" by a separate window filter. Strings with identical keys are detected at a window of a certain size (hundreds of megabytes) and duplicates are filtered out 3) nulls are processed in keys. For `INNER JOIN`, the nulls from both sides are filtered out, and for `LEFT/RIGHT/FULL JOIN` strings with null keys, they are passed to a separate output table immediately from the map stage From the map stage, strings with an identical key are passed to a single YTsaurus reduce job where `JOIN` actually takes place. If necessary, the resulting table from the reduce stage is merged with output tables from the map stage by using a separate YTsaurus Merge operation. To perform `CROSS JOIN` (where there are no keys), an identical 0 key is assigned to all strings of both output tables at the map stage. ### LookupJoin strategy This strategy is triggered when one of the tables is sorted by `JOIN` keys and the second one has a very small size (less than ~900 strings). {% note info %} Hereinafter, a table is called _sorted by JOIN keys_ if the list of `JOIN` keys acts as a sorting keys prefix for a certain order of `JOIN` keys. For example, a table with `JOIN` `b, a` keys and sorting by `a, b, c` is sorted by the `JOIN` keys. {% endnote %} LookupJoin is supported for the following JOIN types: * `INNER` (small table can be from any side) * `LEFT SEMI` (small table from the left) * `RIGHT SEMI` (small table from the right) The LookupJoin strategy is implemented through a single Map operation on a big table, with a small table loaded into the memory. Key types don't have to match. The small table's keys are casted into the big table's key type. Unlike the MapJoin strategy (see below), in LookupJoin `JOIN` keys' values from the small table are passed to the ranges `setting` in the big table's YPath. Thus, only the strings with `JOIN` keys that are present in the small table are read out from the big table. LookupJoin is the most effective `JOIN` strategy. However, it imposes the strictest conditions on `JOIN` types (they must be "filtering" based on the big table) and on the small table's size (keys must "fit in" the maximum permitted number of `ranges` in YPath). Moreover, `ANY` on the big table's side is not supported in LookupJoin. (PRAGMA) settings for the strategy: | Name | Description | | --- | --- | | [yt.LookupJoinLimit](https://ytsaurus.tech/docs/en/pragma/yt.md#ytlookupjoinlimit) | Small table's maximum size in bytes (up to 10M) | | [yt.LookupJoinMaxRows](https://ytsaurus.tech/docs/en/pragma/yt.md#ytlookupjoinmaxrows) | Small table's maximum size in strings (up to 1,000) | The LookupJoin strategy gets disabled when any of these values is set to 0. ### SortedJoin strategy (aka MergeJoin) This strategy is triggered when both tables are sorted by `JOIN` keys. Here, `JOIN` keys must match in terms of types with precision down to Optional at the upper level. If only one table is sorted and the other's size is under `yt.JoinMergeUnsortedFactor * `, the SortedJoin strategy is selected as well and the unsorted table is sorted out a separate YTsaurus operation. [`yt.JoinMergeUnsortedFactor`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytjoinmergeunsortedfactor) is set to `0.2` by default. The SortedJoin strategy supports all `JOIN` types except `CROSS JOIN` and is implemented through a single Reduce operation. Here, reduce with foreign tables mode is used where possible. Moreover, when `JOIN` keys are unique, the `enable_key_guarantee = false` setting is additionally enabled. The SortedJoin strategy can be forcibly selected through an [SQL hint](https://ytsaurus.tech/docs/en/lexer.md#sql-hints): ```yql SELECT * FROM T1 AS a JOIN /*+ merge() */ T2 AS b ON a.key = b.key; ``` In this case (where necessary) 1) `JOIN` keys will be converted to common type 2) both tables will be sorted by `JOIN` keys (PRAGMA) settings for the strategy: | Name | Description | | --- | --- | | [`yt.JoinMergeUnsortedFactor`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytjoinmergeunsortedfactor) | See above | | [`yt.JoinMergeTablesLimit`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytjoinmergetableslimit) | Maximum number of `JOIN` input tables (when using [RANGE, CONCAT](https://ytsaurus.tech/docs/en/select/concat.md), etc.) | | [`yt.JoinMergeUseSmallAsPrimary`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytjoinmergeusesmallasprimary) | Influences the primary table selection when executing a Reduce operation | | [`yt.JoinMergeForce`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytjoinmergeforce) | Forces the selection of the SortedJoin strategy for all `JOIN` in a query | `yt.JoinMergeTablesLimit` set to 0 disables the SortedJoin strategy. ### MapJoin strategy This strategy triggers when one of the input tables is sufficiently small (its size doesn't exceed [`yt.MapJoinLimit`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytmapjoinlimit)). In this case, a smaller table is loaded into the memory (as a dictionary with `JOIN` keys), and Map is executed thereafter for the big table. This strategy supports all `JOIN` types (including `CROSS`). However, it isn't selected if `ANY` is on the bigger side. An unique feature of MapJoin is that this strategy is available for earlier selection. In other words, when a smaller input table is already calculated and subject to size limitations and the big table isn't yet ready. In this case, MapJoin can be selected immediately, and there's a chance that the Map operation for the big table will "glue to" the Map operation (for example, filter) preparing this big table. There is also a sharded MapJoin variant: the small table is split into [`yt.MapJoinShardCount`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytmapjoinshardcount) parts (each one cannot exceed `yt.MapJoinLimit`), and each part is in parallel and on its own `JOIN`with the big table through a Map operation. Then all obtained parts are joined through YTsaurus Merge. Sharded MapJoin is only possible for certain `JOIN` types (`INNER`,`CROSS`, and `LEFT SEMI`, provided that the small table to the right is unique). (PRAGMA) settings for the strategy: | Name | Description | | --- | --- | | [`yt.MapJoinLimit`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytmapjoinlimit) | Maximum size of the smaller `JOIN` side presentation in the memory for which the MapJoin strategy is selected | | [`yt.MapJoinShardCount`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytmapjoinshardcount) | Maximum number of shards | | [`yt.MapJoinShardMinRows`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytmapjoinshardminrows) | Minimum number of strings in one shard | `yt.MapJoinLimit` set to 0 disables the MapJoin strategy. ### StarJoin strategy This strategy makes it possible to perform several subsequent `JOIN`s at once through a single Reduce operation. This strategy is possible when dictionary tables are subsequently appended to one ("main") table through `INNER JOIN` or `LEFT JOIN`, provided that: 1) identical keys are used in all `JOIN`s from the main table 2) all tables are sorted by `JOIN` keys 3) dictionary tables are also unique in terms of `JOIN` keys The StarJoin strategy can be forced using an [SQL hint](https://ytsaurus.tech/docs/en/lexer.md#sql-hints): ```yql SELECT ... FROM T1 AS a JOIN /*+ star() */ T2 AS b ON a.key = b.key JOIN T3 AS c ON a.key = c.key ``` When you specify the hint: - as the main table the _left side_ of the JOIN is forced for which the hint is specified - an additional sorting by keys is inserted for all inputs where necessary - in cases where the StarJoin strategy is not applicable, a warning with the reason is issued (PRAGMA) settings for the strategy: | Name | Description | | --- | --- | | [`yt.JoinEnableStarJoin`](https://ytsaurus.tech/docs/en/pragma/yt.md#ytjoinenablestarjoin) | Enables/disables StarJoin strategy selection (enabled by default) | ### Strategy selection sequence When `JOIN` is executed, strategies are tried in a certain sequence, and the first suitable one is selected. The sequence is as follows: 1) StarJoin 2) LookupJoin 3) OrderedJoin 4) MapJoin 5) CommonJoin (always possible) # MATERIALIZE Materializes the specified source or expression on the current or specified cluster. When an expression is materialized, its type must be a list of structures. The cluster is taken from the `ON` clause or, if it is omitted, from the [USE](https://ytsaurus.tech/docs/en/use.md) statement. The materialized source preserves all columns and sort order. It also creates a barrier that prevents optimizers from merging computations across the materialization boundary. ## Syntax {#syntax} ```yql MATERIALIZE -- a table name, named expression, or nested SELECT INTO $ -- parameter used to reference the materialized result ON -- cluster where the source is materialized (optional) WITH -- additional modifiers (optional) ``` ## Availability {#availability} `MATERIALIZE` is available starting with language version [2026.02](https://ytsaurus.tech/docs/changelog/2026.02.md). In Query Tracker, select language version 2026.02 or later to use `MATERIALIZE`. ## Modifiers {#modifiers} A modifier follows the `WITH` keyword. Its value is separated with `=`. Enclose multiple modifiers in parentheses: `WITH (SOME_HINT1=value, SOME_HINT2)`. The `prune_unused_columns` modifier removes columns that are not used by consumers from the materialized source. Systems that perform materialization may support additional modifiers. ## Examples {#examples} ```yql USE cluster; MATERIALIZE (SELECT 1 AS a, 2 AS b) INTO $materialized; SELECT * FROM $materialized; ``` ```yql USE cluster; $input = SELECT key, value FROM my_table ORDER BY key; MATERIALIZE $input INTO $materialized ON another_cluster; SELECT * FROM another_table AS a JOIN $materialized AS b USING key; ``` The materialized source in the second example preserves sorting by `key`, so the `JOIN` strategy may take this sorting into account. ```yql USE cluster; $input = SELECT a, b, c, d FROM my_table; MATERIALIZE $input INTO $materialized WITH prune_unused_columns; SELECT a, b FROM $materialized; SELECT c FROM $materialized; ``` After optimization, the last example materializes only the `a`, `b`, and `c` columns. # PRAGMA ## Definition Pragmas are special directives that override the query execution settings. For example, they can be used to select a strategy for joining tables, configure the error logging level, or specify which pool to perform query operations in. Pragmas can affect query execution speed, resource allocation, and semantics. The pragmas apply only within the current query. For more information, see the [pragma scope](https://ytsaurus.tech/docs/en/global.md#pragmascope). For the full list of available settings, [see below](https://ytsaurus.tech/docs/en/#pragmas). ## Syntax `PRAGMA x.y = "z";` or `PRAGMA x.y("z", "z2", "z3");`: * `x`: (optional) The category of the setting. * `y`: The name of the setting. * `z`: (optional for flags) The value of the setting. The following suffixes are acceptable: * `Kb`, `Mb`, `Gb`: For data amounts. * `s`, `m`, `h`, `d`: For time values. For [dynamic YT pragmas](https://ytsaurus.tech/docs/en/yt.md), you can revert the settings values to their default states using `PRAGMA my_pragma = default`. Please note that you can't reset the settings for other pragmas. {% note info %} The pragma name is case‑insensitive and ignores underscores. The following entries are identical and will be interpreted the same way: `PRAGMA mypragma`, `PRAGMA MyPrAgMA`, `PRAGMA my_pragma`. {% endnote %} ## Examples ```yql PRAGMA AutoCommit; ``` ```yql PRAGMA TablePathPrefix = "home/yql"; ``` ```yql PRAGMA Warning("disable", "1101"); ``` ## Available pragmas {#pragmas} Getting diagnostic information from YQL as an additional result of a query. * [Global](https://ytsaurus.tech/docs/en/global.md) * [Yson](https://ytsaurus.tech/docs/en/yson.md) * [Working with files](https://ytsaurus.tech/docs/en/file.md) * [YT pragmas](https://ytsaurus.tech/docs/en/yt.md) * Debug and service — documentation will be added soon # Global ## Scope {#pragmascope} Unless otherwise specified, a global pragma affects all the subsequent expressions up to the end of the module where it's used. If necessary and logically possible, you can change the value of this setting several times within a given query to make it different at different execution steps. There are also special scoped pragmas with the scope defined by the same rules as the scope of [named expressions](https://ytsaurus.tech/docs/expressions.md#named-nodes). Their scope is limited to certain SQL blocks, including ACTION, SUBQUERY, and lambda. Unlike scoped pragmas, global pragmas can only be used in the global scope (not inside [DEFINE ACTION](https://ytsaurus.tech/docs/action.md#define-action) and [DEFINE SUBQUERY](https://ytsaurus.tech/docs/subquery.md#define-subquery)). ## AutoCommit {#autocommit} | Value type | By default | | --- | --- | | Flag | false | Automatically perform [COMMIT](https://ytsaurus.tech/docs/commit.md) after each expression. ## TablePathPrefix {#table-path-prefix} | Value type | By default | | --- | --- | | String | — | Add the specified prefix to the cluster table paths. Works on the same principle as merging paths in a file system: supports references to the parent catalog `..` and doesn't require adding a slash to the right. For example, ```yql PRAGMA TablePathPrefix = "home/yql"; SELECT * FROM test; ``` The prefix is not added if the table name is an absolute path (starts with /). ## UDF {#udf} | Value type | Default value | Static/
dynamic | | --- | --- | --- | | String | — | Static | | String, the name of the prefix appended to all modules | "" | Static | Importing all UDFs from the specified library. For the pragma to work, the library must be attached to the query using the ![ico](https://ytsaurus.tech/_images/qt-ui-attach.png%20=30x30) icon. Please note that the library must be shared library (.so) and compiled for Linux x64. When setting a prefix, it's appended before the names of all loaded modules, e.g. CustomPrefixIp::IsIPv4 instead of Ip::IsIPv4. Setting the prefix lets you use different versions of the same UDF. ## RuntimeLogLevel {#runtime-log-level} | Value type | Default value | Static/
dynamic | | --- | --- | --- | | String, one of the following: `Trace`, `Debug`, `Info`, `Notice`, `Warn`, `Error`, or `Fatal` | `Info` | Static | Lets you change the computation logging level (for example, for UDFs) at query runtime or at the UDF signature declaration stage. ## UseTablePrefixForEach {#use-table-prefix-for-each} | Value type | By default | | --- | --- | | Flag | false | `EACH`/`PARTITION_LIST` use [TablePathPrefix](https://ytsaurus.tech/docs/en/#table-path-prefix) for each list item. ## Warning {#warning} | Value type | By default | | --- | --- | | 1. Action
2. Warning code or "*" | — | Action: * `disable`: Disable. * `error`: Treat as an error. * `default`: Revert to the default behavior. The warning code is returned with the text itself (it's displayed on the right side of the web interface). Examples: `PRAGMA Warning("error", "*");` `PRAGMA Warning("disable", "1101");` `PRAGMA Warning("default", "4503");` In this case, all the warnings are treated as errors, except for warnings `1101` (will be disabled) and `4503` (will be processed by default, that is, remain a warning). Since warnings may be added in new YQL releases, use `PRAGMA Warning("error", "*");` with caution (at least cover such queries with autotests). [List of warning and error codes](https://github.com/ytsaurus/ytsaurus/blob/main/yql/essentials/core/issue/protos/issue_id.proto) ## Greetings {#greetings} | Value type | By default | | --- | --- | | Text | — | Issue the specified text as the query's Info message. Example: `PRAGMA Greetings("It's a good day!");` ## WarningMsg {#warningmsg} | Value type | By default | | --- | --- | | Text | — | Issue the specified text as the query's Warning message. Example: `PRAGMA WarningMsg("Attention!");` ## DqEngine {#dqengine} | Value type | By default | | --- | --- | | disable/auto/force string | "auto" | When set to "auto", it enables a new compute engine. Computing is made, whenever possible, without creating map/reduce operations. The "force" value unconditionally routes computations to the new engine. ## SimpleColumns {#simplecolumns} `SimpleColumns` / `DisableSimpleColumns` | Value type | By default | | --- | --- | | Flag | true | If using `SELECT foo.* FROM ... AS foo`, delete the `foo.` prefix from the names of the resulting columns. It also works for [JOIN](https://ytsaurus.tech/docs/join.md), but then it may crash if there's a name conflict (which can be resolved through [WITHOUT](https://ytsaurus.tech/docs/select/without.md) and renaming columns). For JOIN in SimpleColumns mode, an implicit Coalesce is made for key columns: the query `SELECT * FROM T1 AS a JOIN T2 AS b USING(key)` in the SimpleColumns mode works same as `SELECT a.key ?? b.key AS key, ... FROM T1 AS a JOIN T2 AS b USING(key)` ## CoalesceJoinKeysOnQualifiedAll `CoalesceJoinKeysOnQualifiedAll` / `DisableCoalesceJoinKeysOnQualifiedAll` | Value type | By default | | --- | --- | | Flag | true | Controls implicit Coalesce for the key `JOIN` columns in the SimpleColumns mode. If the flag is set, key columns are coalesced if there is at least one expression in the format `foo.*` or `*` in SELECT: for example, `SELECT a.* FROM T1 AS a JOIN T2 AS b USING(key)`. If the flag isn't set, JOIN keys are coalesced only if there is an asterisk '*' after `SELECT`. ## StrictJoinKeyTypes `StrictJoinKeyTypes` / `DisableStrictJoinKeyTypes` | Value type | By default | | --- | --- | | Flag | false | If the flag is set, [JOIN](https://ytsaurus.tech/docs/join.md) will require a strict match of key types. By default, `JOIN` preconverts keys to a shared type, which might result in performance degradation. `StrictJoinKeyTypes` is a [scoped](https://ytsaurus.tech/docs/en/#pragmascope) setting. ## AnsiInForEmptyOrNullableItemsCollections | Value type | By default | | --- | --- | | Flag | false | This pragma brings the behavior of the `IN` operator in accordance with the standard when there's `NULL` in the left or right side of `IN`. The behavior of `IN` when on the right side there is a Tuple with elements of different types also changed. Examples: `1 IN (2, 3, NULL) = NULL (was Just(False))` `NULL IN () = Just(False) (was NULL)` `(1, null) IN ((2, 2), (3, 3)) = Just(False) (was NULL)` For more information about the `IN` behavior when operands include NULLs, see [here](https://ytsaurus.tech/docs/expressions.md#in). You can explicitly select the old behavior by specifying the pragma `DisableAnsiInForEmptyOrNullableItemsCollections`. If no pragma is set, you'll see a warning, and the original variant will be used. ## AnsiRankForNullableKeys | Value type | By default | | --- | --- | | Flag | false | Aligns the ``RANK`/`DENSE_RANK` behavior with the standard if there are optional types in the window sort keys or in the argument of such window functions. It means that: * The result type is always Uint64 rather than Uint64?; * NULLs in keys are treated as equal to each other (the current implementation returns NULL). You can explicitly select the old behavior by specifying the pragma `DisableAnsiRankForNullableKeys`. If no pragma is set, you'll see a warning, and the original variant will be used. ## AnsiCurrentRow | Value type | By default | | --- | --- | | Flag | false | Aligns the implicit setting of a window frame with the standard if there is [ORDER BY](https://ytsaurus.tech/docs/select/order_by.md). If `AnsiCurrentRow` is not set, the `(ORDER BY key)` window is equivalent to `(ORDER BY key ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`. The standard requires that such window behaves as `(ORDER BY key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)`. The difference is in `CURRENT ROW` interpretation. In `ROWS` mode, `CURRENT ROW` is interpreted literally: the current string in the partition. And in `RANGE` mode, the end of the `CURRENT ROW` frame means "the last row in the partition with the sorting key equal to the current row". ## OrderedColumns {#orderedcolumns} `OrderedColumns` / `DisableOrderedColumns` Preserve the [order of columns](https://ytsaurus.tech/docs/select/index.md#orderedcolumns) to `SELECT` / `JOIN` / `UNION [ALL]` / `INTERSECT [ALL]` / `EXCEPT [ALL]` and save it when writing the results. The order of columns is undefined by default. ## PositionalUnionAll {#positionalunionall} Enable the standard columnar execution for [UNION [ALL]](https://ytsaurus.tech/docs/select/operators.md#union), [INTERSECT [ALL]](https://ytsaurus.tech/docs/select/operators.md#intersect), and [EXCEPT [ALL]](https://ytsaurus.tech/docs/select/operators.md#except). This automatically enables [ordered columns](https://ytsaurus.tech/docs/en/#orderedcolumns). ## RegexUseRe2 | Value type | By default | | --- | --- | | Flag | false | Use Re2 UDF instead of Pcre for executing `REGEX`,`MATCH`,`RLIKE`, and SQL operators. Re2 UDF supports correct processing of Unicode symbols, unlike Pcre UDF, which is used by default. ## ClassicDivision | Value type | By default | | --- | --- | | Flag | true | In the classical version, the result of integer division remains integer (by default). If disabled, the result is always Double. `ClassicDivision` is a [scoped](https://ytsaurus.tech/docs/en/#pragmascope) setting. ## CheckedOps | Value type | By default | | --- | --- | | Flag | false | When enabled: if aggregate functions SUM/SUM_IF, binary operations `+`, `-`, `*`, `/`, `%`, or unary operation `-` on integers result in an overflow beyond the range of the target argument or result type, return `NULL`. When disabled: overflow isn't checked. Has no effect on floating point or `Decimal` numbers. `CheckedOps` is a [scoped](https://ytsaurus.tech/docs/en/#pragmascope) setting. ## UnicodeLiterals `UnicodeLiterals`/`DisableUnicodeLiterals` | Value type | By default | | --- | --- | | Flag | false | When the mode is enabled, string literals without suffixes like "foo"/'bar'/@@multiline@@ will have the `Utf8` type. If disabled, then `String`. `UnicodeLiterals` is a [scoped](https://ytsaurus.tech/docs/en/#pragmascope) setting. ## WarnUntypedStringLiterals `WarnUntypedStringLiterals`/`DisableWarnUntypedStringLiterals` | Value type | By default | | --- | --- | | Flag | false | When the mode is enabled, string literals without a suffix like "foo"/'bar'/@@multiline@@ will prompt a warning. It can be suppressed if you explicitly select the `s` suffix for the `String` type or the `u` suffix for the `Utf8` type. `WarnUntypedStringLiterals` is a [scoped](https://ytsaurus.tech/docs/en/#pragmascope) setting. ## SimplePg `SimplePg`/`DisableSimplePg` | Value type | By default | | --- | --- | | Flag | false | When the mode is enabled, all functions from the [SimplePg module](https://ytsaurus.tech/udf/list/simple_pg.md) are imported into the global function space. When the mode is disabled, these functions require the `SimplePg::` prefix to be called. `SimplePg` is a [scoped](https://ytsaurus.tech/docs/en/#pragmascope) setting. ## AllowDotInAlias | Value type | By default | | --- | --- | | Flag | false | Enable dot in names of result columns. This behavior is disabled by default, since the further use of such columns in `JOIN` is not fully implemented. ## WarnUnnamedColumns | Value type | By default | | --- | --- | | Flag | false | Generate a warning if a column name (in the format `column[0-9]+`) was automatically generated for an unnamed expression in `SELECT`. ## GroupByLimit | Value type | By default | | --- | --- | | Positive number | 32 | Increasing the limit on the number of groups in [GROUP BY](https://ytsaurus.tech/docs/group_by.md). ## GroupByCubeLimit | Value type | By default | | --- | --- | | Positive number | 5 | Increasing the limit on the number of dimensions in [GROUP BY](https://ytsaurus.tech/docs/group_by.md#rollup-cube-group-sets). Use this option with care: the computational complexity of the query grows exponentially with the number of dimensions. ## CostBasedOptimizer {#costbasedoptimizer} | Value type | Default | | --- | --- | | String: `disable` / `pg` / `native` | disable | Enables the cost-based optimizer for `JOIN` order. It reorders inputs to minimize plan cost based on table statistics and applies only when there are more than two inputs. * `disable`: Optimization is disabled. * `native`: Native optimizer, recommended for YT. * `pg`: Optimizer based on PostgreSQL algorithms. This is a static pragma: its value is fixed before the query optimization stage. # Yson Management of Yson UDF default behavior. To learn more, see the [documentation](https://ytsaurus.tech/udf/list/yson.md), in particular, [Yson::Options](https://ytsaurus.tech/udf/list/yson.md#ysonoptions). The Yson pragma affects all subsequent expressions up to the end of the module in which it occurs. ## yson.AutoConvert | Value type | By default | | --- | --- | | Flag | false | Automatic conversion of values to the required data type in all Yson UDF calls, including implicit calls. ## yson.Strict | Value type | By default | | --- | --- | | Flag | true | Strict mode control in all Yson UDF calls, including implicit calls. Empty or `"true"` value enables strict mode. If the value is `"false"`, strict mode is disabled. ## yson.DisableStrict | Value type | By default | | --- | --- | | Flag | false | An inverted version of `yson.Strict`. Empty or `"true"` value disables strict mode. If the value is `"false"`, strict mode is enabled. # Working with files ## File | Value type | Default value | Static/
dynamic | | --- | --- | --- | | Two or three string arguments — alias, URL, and optional token name | — | Static | Attach a file to the query by URL. For attaching files you can use the built-in functions [FilePath and FileContent](https://ytsaurus.tech/builtins/basic.md#filecontent). This `PRAGMA` is a universal alternative to attaching files using built-in mechanisms of web or console clients. Usage example: ```yql -- Attach a file from the Cypress. PRAGMA File('file.txt', 'yt:///tmp/my-file.txt'); SELECT "Content of " || FilePath("file.txt") || ":\n" || FileContent("file.txt"); ``` YQL reserves the right to cache files at the URL for an indefinite period, hence, if there is a significant change in the content behind it, we strongly recommend to modify the URL by adding or modifying dummy parameters. If the token name is specified, its value will be used to access the target system. ## FileOption | Value type | Default value | Static/
dynamic | |-------------------------------------------------|--------------|--------------------------------| | Three string arguments: alias, key, value | — | Static | Set the option by the specified key for the specified file to the specified value. The file with this alias should already be declared through [PRAGMA File](https://ytsaurus.tech/docs/en/#file) or attached to the query. Currently supported options: | Key | Value range | Description | |-------------------------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------| | `bypass_artifact_cache` | `true`/`false` | Manages [caching](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options#fajly) | Example: ```yql PRAGMA FileOption("", "bypass_artifact_cache", "true"); ``` ## Library | Value type | Default value | Static/
dynamic | | --- | --- | --- | | One or two arguments: file name and optional URL | — | Static | Treat the specified attached file as a library from which you can do [IMPORT](https://ytsaurus.tech/docs/export_import.md). The syntax type for the library is determined from the file extension: * `.sql`: For the YQL dialect of SQL (recommended). * `.yqls`: For s-expressions. Example with a file attached to the query: ```yql PRAGMA library("a.sql"); IMPORT a SYMBOLS $x; SELECT $x; ``` If the URL is specified, the library is downloaded from the URL rather than from the previously attached file as in the following example: ```yql PRAGMA library("a.sql","https://raw.githubusercontent.com/ytsaurus/ytsaurus/refs/heads/main/yt/docs/code-examples/yql/pragma-library-example"); IMPORT a SYMBOLS $x; SELECT $x; ``` In this case, you can use text parameter value substitution in the URL: ```yql DECLARE $_ver AS STRING; -- "pragma-library-example" PRAGMA library("a.sql","https://raw.githubusercontent.com/ytsaurus/ytsaurus/refs/heads/main/yt/docs/code-examples/yql/{$_ver}"); IMPORT a SYMBOLS $x; SELECT $x; ``` ## Package | Value type | Default value | Static/
dynamic | | --- | --- | --- | | Two or three arguments: package name, URL, and optional token | — | Static | Attach a hierarchical set of files to the query by URL, treating them as a package with the specified name, an interrelated set of libraries. The package name is expected in ``project_name.package_name`` format; package libraries can then be used to make [IMPORT](https://ytsaurus.tech/docs/export_import.md) with a module name like ``pkg.project_name.package_name.maybe.nested.module.name``. Example for a package with a flat hierarchy consisting of two libraries, foo.sql and bar.sql: ```yql PRAGMA package("project.package", "/path/to/package"); IMPORT pkg.project.package.foo SYMBOLS $foo; IMPORT pkg.project.package.bar SYMBOLS $bar; SELECT $foo, $bar; ``` In this case, you can use text parameter value substitution in the URL: ```yql DECLARE $_path AS STRING; -- "path" PRAGMA package("project.package", "{$_path}/to/package"); IMPORT pkg.project.package.foo SYMBOLS $foo; IMPORT pkg.project.package.bar SYMBOLS $bar; SELECT $foo, $bar; ``` ## OverrideLibrary | Value type | Default value | Static/
dynamic | | --- | --- | --- | | One argument: file name | — | Static | Interpret the specified attached file as a library and override one of the package libraries with it. The file name should be in the format ``project_name/package_name/maybe/nested/module/name.EXTENSION``, [PRAGMA Library](https://ytsaurus.tech/docs/en/#library) extensions are supported. Example: ```yql PRAGMA package("project.package", "/path/to/package"); PRAGMA override_library("project/package/maybe/nested/module/name.sql"); IMPORT pkg.project.package.foo SYMBOLS $foo; SELECT $foo; ``` {% note warning %} For PRAGMA `Package`, only links to directories on YTsaurus clusters are supported. {% endnote %} # YT Pragmas YT pragmas are a namespace for pragmas that configure YTsaurus‑specific parameters of YQL queries. ## Syntax YT pragma names include the `yt` prefix: ```yql PRAGMA yt. = ''; ``` {% note warning %} Pass the values of all YT pragmas as strings, in quotes, regardless of type. Use either single `'...'` or double `"..."` quotes. For example: `PRAGMA yt.QueryCacheUseExpirationTimeout = 'true';` {% endnote %} ## Scope and features By scope, YT pragmas can be divided into static and dynamic ones. **Static pragmas**: - You initialize them once at the earliest stage of query processing. - They affect all expressions in the current module where they’re declared. - If you specify a static pragma multiple times in a query, only its last set value applies. **Dynamic pragmas**: - You initialize them at the query execution stage, after optimization and execution plan building. - They remain in effect until the next identical pragma or until the end of the query. - Only for a dynamic pragma can you reset its value to the default by assigning `default`. {% note info %} All pragmas that affect query optimizers are static, because the values of dynamic pragmas aren’t computed at this stage yet. {% endnote %} ## Per‑cluster support {#settings} Some pragmas support a special operating mode — _per‑cluster_. This mode lets you set different pragma values for different clusters in a single query. For example, you can specify: “Run the query on cluster A with settings X, and on cluster B with settings Y.” This is useful in distributed queries where different clusters require different execution conditions. The _per‑cluster_ mode is available for all dynamic pragmas and for some static ones — in the documentation below, such pragmas are explicitly marked with the “per‑cluster” label. ### How to use By default, YT pragmas are written with the `yt` prefix — this means the setting applies to the current cluster where the query runs. To apply the same pragma to another cluster with a different value, replace `yt` with the cluster’s name. For example, let’s set different temporary directories for two clusters in a query: ```yql PRAGMA yt.TmpFolder = "//tmp/my_folder"; -- current cluster PRAGMA cluster_2.TmpFolder = "//tmp/other_folder"; -- cluster cluster_2 ... -- query body ``` As a result, when the query runs, temporary files will be saved in `//tmp/my_folder` on the current cluster and in `//tmp/other_folder` on the `cluster_2` cluster. {% note warning %} You can’t use the `yt` prefix and a cluster name at the same time. Entries such as `PRAGMA cluster_2.yt.TmpFolder` or `PRAGMA yt.cluster_2.TmpFolder` are invalid and will result in an error. {% endnote %} ## yt.Annotations {#annotations} This lets you set arbitrary structured information related to the operation. It’s useful for searching and identifying operations in the archive (you can search it via the [API](https://ytsaurus.tech/api/python/userdoc.md#operation_and_job_info_commands)). For details, see [Operation settings](https://ytsaurus.tech/user-guide/data-processing/operations/operations-options) section. | Possible values | Default value | Type | | --- | --- | --- | | String representation of a [YSON Map](https://ytsaurus.tech/user-guide/storage/formats.md#yson) | — | Dynamic | #### Signature {#annotations-signature} ```yql PRAGMA yt.Annotations = '{ "name" = "login"; "time" = "20.02.2002"; }'; ``` #### Result {#annotations-result} When you search for an operation in the archive, the information set in the Map will be visible. ## yt.Auth {#auth} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | Use authentication data other than the default. ## yt.AutoMerge / yt.TemporaryAutoMerge / yt.PublishedAutoMerge {#auto-merge} | Possible values | Default value | Type | | --- | --- | --- | | String: relaxed / economy / disabled | relaxed | Dynamic | Manage the YTsaurus setting [with the same name](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/automerge) that helps reduce quota consumption for the number of chunks. `yt.TemporaryAutoMerge` applies to all YT operations except merge inside a YtPublish node. `yt.PublishedAutoMerge` applies only to merge inside a YtPublish node (if it runs there). `yt.AutoMerge` sets this setting’s value simultaneously for all YTsaurus operations in the query. ## yt.BatchListFolderConcurrency {#batch-list-folder-concurrency} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 5 | Static | Set the number of concurrent directory listing operations. ## yt.BinaryExpirationInterval {#binary-expiration-interval} | Possible values | Default value | Type | | --- | --- | --- | | Time interval with support for `s/m/h/d` suffixes | — | Static | This lets you manage the [TTL of cached binary artifacts](https://ytsaurus.tech/docs/en/user-guide/storage/cypress#TTL). It works only together with `yt.BinaryTmpFolder`. Each use of a binary artifact in a query extends its TTL. ## yt.BinaryTmpFolder {#binary-tmp-folder} | Possible values | Default value | Type | | --- | --- | --- | | Path on the cluster | — | Static | Set a separate path on the cluster where the query’s binary artifacts (UDF and job binary) will be cached. Artifacts are saved in the directory root with a name equal to the artifact’s MD5. Saving and using artifacts in this directory happens outside a transaction, even if the query includes the `yt.ExternalTx` pragma. ## yt.BufferRowCount {#buffer-row-count} | Possible values | Default value | Type | | --- | --- | --- | | Number, not less than 1 | — | Dynamic | The maximum number of records that can be buffered by JobProxy. ## yt.ColumnGroupMode {#column-group-mode} | Possible values | Default value | Type | | --- | --- | --- | | String: disable / single / perusage | disable | Static | Set the mode for computing column groups for the query’s intermediate tables. In `disable` mode, column groups aren’t used. In `single` mode, one group is created for all table columns. In `perusage` mode, granular column groups are created based on their consumers. All columns in one group are used simultaneously by one or more consumers. For example, if an intermediate table has columns [a, b, c, d, e, f] and two operations use column selections [a, b, c, d] and [c, d, e, f] respectively, the table will have three column groups: [a, b], [c, d], and [e, f]. If the intermediate table is used to publish to an output table (i.e., the consumer is a YtPublish node), column groups aren’t applied, except when you explicitly set the [column_groups modifier](https://ytsaurus.tech/docs/insert_into.md#hints). In the latter case, the intermediate table uses the modifier’s column groups. ## yt.CombineCoreLimit {#combine-core-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, not less than 1M | 128M | Static | Set the memory buffer size for running a CombineCore node. ## yt.CommonJoinCoreLimit {#common-join-core-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 128M | Static | Set the memory buffer size for running a CommonJoinCore node (it runs in a job when you choose the common JOIN strategy). ## `yt.ConvertDynamicTablesToStatic` | Value type | Default | Static /
Dynamic | | --- | --- | --- | | String: disable / join / all | disable; Starting from [2026.02](https://ytsaurus.tech/changelog/2026.02.md) — join | Static | Add preliminary conversion of dynamic tables to static tables. In `join` mode, only tables that are inputs to joins are converted. This lets you use the map join strategy over such tables. In `all` mode, all dynamic tables in the query are converted to static tables. In `disable` mode, no conversion happens. ## yt.CoreDumpPath {#core-dump-path} | Possible values | Default value | Type | | --- | --- | --- | | Path on the cluster | — | Static, [per‑cluster](https://ytsaurus.tech/docs/en/*per-cluster) | This lets you save a [coredump](https://en.wikipedia.org/wiki/Core_dump) from failed MapReduce operation jobs to a separate table. ## yt.DataSizePerJob / yt.DataSizePerMapJob {#data-size-per-job} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 2G | Dynamic | Manage the splitting of MapReduce operations into jobs: the larger the number, the fewer jobs. For computationally intensive jobs, it’s recommended to decrease the value, and for jobs that quickly scan a lot of data (in particular, user_sessions), increase it. You can use K, M, and G suffixes to specify values in kilobytes, megabytes, and gigabytes, respectively. ## yt.DataSizePerPartition {#data-size-per-partition} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 1G | Dynamic | Manage the size of partitions in MapReduce operations. You can use K, M, and G suffixes to specify values in kilobytes, megabytes, and gigabytes, respectively. ## yt.DataSizePerSortJob {#data-size-per-sort-job} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | - | Dynamic | Manage the splitting of sort jobs in MapReduce operations. You can use K, M, and G suffixes to specify values in kilobytes, megabytes, and gigabytes, respectively. ## yt.DefaultCalcMemoryLimit {#default-calc-memory-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 1G | Static | This sets the memory limit for computations that aren't related to table access. You can use the K, M, and G suffixes to specify values in kilobytes, megabytes, and gigabytes, respectively. ## yt.DefaultLocalityTimeout {#default-locality-timeout} | Possible values | Default value | Type | | --- | --- | --- | | Time interval with support for `s/m/h/d` suffixes | — | Dynamic | This sets the `locality_timeout` setting in the operation spec (the setting isn't documented yet). ## yt.DefaultMapSelectivityFactor {#default-map-selectivity-factor} | Possible values | Default value | Type | | --- | --- | --- | | Positive floating-point number | — | Dynamic | See the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/mapreduce). ## yt.DefaultMaxJobFails {#default-max-job-fails} | Possible values | Default value | Type | | --- | --- | --- | | Positive integer | 5 | Static | The number of failed MapReduce jobs at which query retries stop and the query is considered unsuccessful. ## yt.DefaultMemoryDigestLowerBound {#default-memory-digest-lower-bound} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number between 0.0 and 1.0, inclusive | — | Dynamic | This sets the `user_job_memory_digest_lower_bound` setting in the operation spec. You can read about the setting in the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/scheduler/memory-digest#nastrojki-digest). ## yt.DefaultMemoryLimit {#default-memory-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 512M | Dynamic | The job memory limit, in bytes, requested when MapReduce operations are started. You can use the K, M, and G suffixes to specify values in kilobytes, megabytes, and gigabytes, respectively. ## yt.DefaultMemoryReserveFactor {#default-memory-reserve-factor} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number between 0.0 and 1.0, inclusive | — | Dynamic | See the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options#memory_reserve_factor). ## yt.DefaultOperationWeight {#default-operation-weight} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number | 1.0 | Dynamic | This sets the weight for all MapReduce operations you start within the selected compute pool. ## yt.Description {#description} | Possible values | Default value | Type | | --- | --- | --- | | String representation of a YSON map | — | Dynamic | This sets the information that's displayed in the web interface on the operation page. See the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options). ## yt.DisableJobSplitting {#disable-job-splitting} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Dynamic | Use this to prevent the YTsaurus Scheduler from adaptively splitting long-running user jobs further. ## yt.DisableOptimizers {#disable-optimizers} | Possible values | Default value | Type | | --- | --- | --- | | String with a list of optimizers separated by any of these characters: comma, semicolon, space, or `\|` | — | Static | This disables the specified optimizers. ## yt.DockerImage {#docker-image} | Possible values | Default value | Type | | --- | --- | --- | | Path to the Docker image | — | Dynamic | You can specify a Docker image to create the environment where your user jobs will run. ## yt.DontForceTransformForInputTables {#dont-force-transform-for-input-tables} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Static | This disables forced data transformation for user tables with storage settings (`erasure_codec`, `compression_codec`, `primary_medium`, `media`, column groups) that differ from the default settings. Forced transformation is applied to input tables if they're used to write to output tables only via the YtMerge operation. ## yt.ErasureCodecCpu {#erasure-codec-cpu} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number, at least 1.0 | 1.0 | Dynamic | This is a multiplier for estimating CPU consumption when processing tables compressed with an erasure codec. It affects how MapReduce operations are split into jobs. ## yt.EvaluationTableSizeLimit {#evaluation-table-size-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, no more than 10M | 1M | Static | This sets the maximum total size of tables used at the evaluation stage. ## yt.ExpirationDeadline / yt.ExpirationInterval {#expiration-deadline} | Possible values | Default value | Type | | --- | --- | --- | | ExpirationDeadline: timestamp in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. ExpirationInterval: time interval with support for `s/m/h/d` suffixes, during which the node must have no access requests. | — | Dynamic | This lets you manage the [TTL for tables created by the operation](https://ytsaurus.tech/docs/en/user-guide/storage/cypress#TTL). ## yt.ExternalTx {#external-tx} This pragma is used to specify an external [transaction](https://ytsaurus.tech/user-guide/storage/transactions.md) in which context the query will run (for example, if the transaction was created via [CLI](https://ytsaurus.tech/api/cli/commands.md#start-tx)). This may be required to perform multiple actions atomically—for example, two YQL queries, or a YQL query and table modifications. This pragma also lets you read intermediate data in transactions (for example, in a long operation with temporary files). {% note info %} Directories for the query are created within the specified transaction. This can lead to conflicts when two queries with different ExternalTx values try to write data to a directory that doesn't yet exist. {% endnote %} | Possible values | Default value | Type | | --- | --- | --- | | Transaction ID (string) | — | Static, [per-cluster](https://ytsaurus.tech/docs/en/*per-cluster) | #### Example {#external-tx-example} ```yql PRAGMA yt.ExternalTx = 'a3d149bd-674dfa1-fa68f314-2c22562'; -- Transaction ID (you can view it via CLI) ``` #### Result {#external-tx-result} The query will run within the specified transaction. #### Notes and limitations {#external-tx-limitations} Specifying the pragma without a query body will cause an error. ## yt.ExtraTmpfsSize {#extra-tmpfs-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | — | Dynamic | This lets you increase the tmpfs size in addition to the total size of all explicitly used files (specified in megabytes). This can be useful if you create new local files from a UDF. This setting is ignored without [UseTmpfs](https://ytsaurus.tech/docs/en/#use-tmpfs). ## yt.FileCacheTtl {#file-cache-ttl} | Possible values | Default value | Type | | --- | --- | --- | | Time interval with support for `s/m/h/d` suffixes | 7d | Static | This lets you manage the TTL for the YTsaurus [file cache](https://ytsaurus.tech/faq/temp.md). A value of 0 disables TTL for the file cache. ## yt.FolderInlineDataLimit {#folder-inline-data-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 100K | Static | This sets the maximum data size for an inline list obtained as a result of the Folder computation. If the size is larger, a temporary file will be used. ## yt.FolderInlineItemsLimit {#folder-inline-items-limit} | Possible values | Default value | Type | | --- | --- | --- | | Positive integer | 100 | Static | This sets the maximum number of items in an inline list obtained as a result of the Folder computation. If the number is larger, a temporary file will be used. ## yt.ForceJobSizeAdjuster {#force-job-size-adjuster} | Possible values | Default value | Type | | --- | --- | --- | | Flag | true | Dynamic | This sets the `"force_job_size_adjuster"` option in the operation settings. ## yt.HybridDqExecution {#hybrid-dq-execution} | Possible values | Default value | Type | | --- | --- | --- | | Flag | true | Static | This enables hybrid query execution via DQ. ## yt.IgnoreTypeV3 {#ignore-type-v} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Static | When tables with a type_v3 schema are read, all fields with complex types are exposed in the query as Yson fields. Complex types include all non-data types and data types with more than one level of optionality. ## yt.IgnoreWeakSchema {#ignore-weak-schema} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Static | This ignores the weak table schema (generated by sorting a schema-less table by a set of fields). Together with `yt.InferSchema`, this lets you infer the schema from the data for such tables. ## yt.IgnoreYamrDsv {#ignore-yamr-dsv} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Static | This ignores `_format=yamred_dsv` if it’s specified in the input table’s metadata. ## yt.InferSchema / yt.ForceInferSchema {#infer-schema} | Possible values | Default value | Type | | --- | --- | --- | | Number from 1 to 1000 | — | Static | This infers the data schema from the content of the first rows of the tables. If you specify the PRAGMA without a value, it means one first row. If you specify several rows and different data types appear for a column, they’re expanded up to Yson. InferSchema enables schema inference only for tables that don’t have a schema specified in the metadata at all. With ForceInferSchema, the data schema from the metadata is ignored, except for the list of key columns for sorted tables. In addition to the detected columns, a dictionary column _other (row by row) is generated with the values of columns that weren’t present in the first row but were found later. This lets you use [WeakField](https://ytsaurus.tech/builtins/basic.md#weakfield) on such tables. Because of the wide range of possible issues, this mode isn’t recommended and is disabled by default. ## yt.InferSchemaTableCountThreshold {#infer-schema-table-count-threshold} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 50 | Static | If the number of tables for which the schema is inferred from the content exceeds the specified value, schema inference runs as a separate operation on YTsaurus, which can be significantly faster. ## yt.IntermediateAccount {#intermediate-account} | Possible values | Default value | Type | | --- | --- | --- | | Account name in YTsaurus | intermediate | Dynamic | This lets you use your own account for intermediate data within a fused MapReduce operation. By default, a shared account is used, which might overflow at an inconvenient moment. If you specify [PRAGMA yt.TmpFolder](https://ytsaurus.tech/docs/en/#tmp-folder), the account specified on the temporary directory is used by default instead of the shared one. ## yt.IntermediateDataMedium {#intermediate-data-medium} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Dynamic | This sets the medium used for intermediate data in operations (Sort, MapReduce). For details, see the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/sort). ## yt.IntermediateReplicationFactor {#intermediate-replication-factor} | Possible values | Default value | Type | | --- | --- | --- | | Number from 1 to 10 | — | Dynamic | This is the replication factor for intermediate data. ## yt.JobEnv {#job-env} | Possible values | Default value | Type | | --- | --- | --- | | String representation of a YSON dictionary | — | Dynamic | This sets environment variables for the map and reduce jobs of the operations. The keys in the dictionary set the names of the environment variables, and the values in the dictionary set the values of the environment variables. ## yt.JoinAllowColumnRenames {#join-allow-column-renames} | Possible values | Default value | Type | | --- | --- | --- | | Flag | true | Static | This enables the use of column renaming when executing the Ordered JOIN strategy (the [rename_columns](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options#path_attributes) attribute is used). If the option is disabled, the Ordered JOIN strategy is selected only when the column names on the left and right match. ## yt.CostBasedOptimizerPartial {#yt.costbasedoptimizerpartial} This controls when the [cost-based optimizer](https://ytsaurus.tech/docs/en/global.md#costbasedoptimizer) starts for queries with multiple `JOIN`s. The pragma lets you avoid waiting for all inputs to be materialized and preserve computation parallelism if a full recalculation of the join order isn’t required. | Value type | Default | Static /
dynamic | | --- | --- | --- | | Non-negative number | 0 | Static | The value determines how many `JOIN` inputs must be ready and materialized to disk before reordering: * `0` — wait for all inputs and perform full reordering. This mode gives the optimizer the most information but reduces parallelism and can interfere with operation merging. * `1` — don’t wait for inputs and optimize only the already ready subtrees. This mode preserves parallelism but may change a smaller part of the plan. * `N > 1` — experimental mode: start optimization when at least `N` inputs are ready. #### Example {#cost-based-optimizer-partial-example} ```yql PRAGMA CostBasedOptimizer = 'native'; PRAGMA yt.CostBasedOptimizerPartial = '1'; PRAGMA yt.ExtendedStatsMaxChunkCount = '10000'; ``` #### Result {#cost-based-optimizer-partial-result} The cost-based optimizer will reorder the ready `JOIN` subtrees without waiting for the materialization of the other inputs. Extended statistics will be requested only for tables whose total number of chunks on the cluster doesn’t exceed 10,000. #### Notes and limitations {#cost-based-optimizer-partial-limitations} * The pragma works only when [`CostBasedOptimizer`](https://ytsaurus.tech/docs/en/global.md#costbasedoptimizer) is enabled. * Values greater than `1` enable experimental mode: the threshold semantics and plan construction may change in future YQL versions. For persistent queries, it’s recommended to use `0` or `1`. * The partial mode preserves more parallelism but may build a less optimal `JOIN` order than full reordering after all inputs are ready. ## yt.ExtendedStatsMaxChunkCount {#yt.extendedstatsmaxchunkcount} This limits the collection of extended column-wise statistics for the cost-based optimizer. Use this pragma so that getting statistics for tables with a large number of chunks doesn’t delay query optimization. | Value type | Default | Static /
dynamic | | --- | --- | --- | | Positive number | — | Static | The value sets the maximum total number of chunks of input tables on one cluster: * If the number of chunks doesn’t exceed the threshold, YQL requests extended statistics. * If the threshold is exceeded, statistics aren’t requested and the optimizer uses less accurate estimates. * `0` removes the limit on the number of chunks. * If the pragma isn’t set, extended statistics aren’t requested. #### Example {#extended-stats-max-chunk-count-example} ```yql PRAGMA CostBasedOptimizer = 'native'; PRAGMA yt.ExtendedStatsMaxChunkCount = '10000'; ``` #### Result {#extended-stats-max-chunk-count-result} For input tables with a total number of chunks not exceeding 10,000 on the cluster, the optimizer will get extended column-wise statistics. For larger sets of tables, optimization will continue without it. #### Notes and limitations {#extended-stats-max-chunk-count-limitations} * The pragma affects only the collection of statistics for the cost-based optimizer and doesn’t limit the reading of the tables themselves. * The value `0` may lead to a long time to get statistics for very large tables. * The recommended initial value is `10000`; you should change it considering the number of chunks in the input tables and the acceptable optimization time. ## yt.JoinCollectColumnarStatistics {#join-collect-columnar-statistics} | Possible values | Default value | Type | | --- | --- | --- | | String: disable / sync / async | async | Static | This controls the use of column-wise statistics for accurate estimation of JOIN inputs and selection of the corresponding strategy. Async enables the asynchronous collection mode for column-wise statistics. ## yt.JoinColumnarStatisticsFetcherMode {#join-columnar-statistics-fetcher-mode} | Possible values | Default value | Type | | --- | --- | --- | | String: from_nodes / from_master / fallback | fallback | Static | This controls the mode for requesting column-wise statistics for accurate estimation of JOIN inputs from YTsaurus. The from_nodes mode provides an accurate estimate but may not meet timeouts for large tables. The from_master mode works very fast but gives coarse statistics. The fallback mode works as a combination of the previous two. ## yt.JoinMergeForce {#join-merge-force} | Possible values | Default value | Type | | --- | --- | --- | | Flag | - | Static | This forces the selection of the Ordered JOIN strategy. If the flag is set to True, the Ordered JOIN strategy is selected even if one or both sides of the JOIN aren’t sorted. In this case, the unsorted sides are sorted beforehand. The limits on the maximum size of the unsorted table (see `yt.JoinMergeUnsortedFactor`) are ignored in this case. ## yt.JoinMergeReduceJobMaxSize {#join-merge-reduce-job-max-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 8G | Static | This is the maximum allowed size of the Reduce job when a small table is selected as the primary in the Ordered JOIN strategy. If the resulting size exceeds the specified value, the Reduce operation is repeated with the larger table as the primary. ## yt.JoinMergeTablesLimit {#join-merge-tables-limit} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 64 | Static | This is the total allowed number of tables on the left and right to enable the Ordered JOIN strategy. Setting the value to 0 disables this strategy completely. ## yt.JoinMergeUnsortedFactor {#join-merge-unsorted-factor} | Possible values | Default value | Type | | --- | --- | --- | | Positive floating-point number | 0.2 | Static | This is the minimum ratio of the size of the unsorted side of the JOIN to the sorted side for its additional sorting and selection of the Ordered JOIN strategy. ## yt.JoinMergeUseSmallAsPrimary {#join-merge-use-small-as-primary} | Possible values | Default value | Type | | --- | --- | --- | | Flag | - | Static | This explicitly controls the selection of the primary table in the Reduce operation for the Ordered JOIN strategy. If set to true, the smaller side is always selected as the primary table. If the flag value is false, the larger side is selected, except in the case of unique keys on the larger side. Selecting the larger table as the primary is safe even if it contains monster keys, but it works slower. If this pragma isn’t set, the primary is selected automatically based on the maximum size of the resulting jobs (see yt.JoinMergeReduceJobMaxSize). ## yt.LayerPaths {#layer-paths} | Possible values | Default value | Type | | --- | --- | --- | | String with a list of paths to porto layers, separated by any of the following characters: comma, semicolon, space, or `\|` | — | Dynamic | Allows you to specify a sequence of porto layers used to create the environment in which custom jobs will run. ## yt.LLVMMemSize {#llvm-mem-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 256M | Dynamic | Set the fixed memory size required for LLVM code compilation in jobs. ## yt.LLVMPerNodeMemSize {#llvm-per-node-mem-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 10K | Dynamic | Set the memory size per computation graph node required for LLVM code compilation in jobs. ## yt.LookupJoinLimit {#lookup-join-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, no more than 10M | 1M | Static | You can use a table as a Map in the Lookup JOIN strategy if its size does not exceed the minimum of `yt.LookupJoinLimit` and `yt.EvaluationTableSizeLimit`. ## yt.LookupJoinMaxRows {#lookup-join-max-rows} | Possible values | Default value | Type | | --- | --- | --- | | Number, no more than 1000 | 900 | Static | This is the maximum number of rows in a table that can act as a Map in the Lookup JOIN strategy. ## yt.MapJoinLimit {#map-join-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 2048M | Static | This sets the size limit for the smaller table in a JOIN that triggers the Map-side strategy (creating an in-memory Map from the smaller table and using it in the Map over the larger table). If you set the value to 0, you can completely disable this strategy. ## yt.MapJoinShardCount {#map-join-shard-count} | Possible values | Default value | Type | | --- | --- | --- | | Number from 1 to 10 | 4 | Static | The map-side JOIN strategy can be executed in sharded mode: the smaller side is split into N shards (where N is less than or equal to the value of this PRAGMA), each of which is joined with the larger side independently and in parallel, and the concatenation of the JOIN results for all shards is considered the final JOIN result. ## yt.MapJoinShardMinRows {#map-join-shard-min-rows} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 1 | Static | This sets the minimum number of records per shard in the Map-side JOIN strategy. ## yt.MapLocalityTimeout {#map-locality-timeout} | Possible values | Default value | Type | | --- | --- | --- | | Time interval with support for `s/m/h/d` suffixes | — | Dynamic | Set the `map_locality_timeout` setting in the operation specification (this setting is not yet documented). ## yt.MaxColumnGroups {#max-column-groups} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 64 | Static | Set the maximum number of column groups for the intermediate query table. If the calculated number of groups exceeds this limit, no groups are created for this table. ## yt.MaxExtraJobMemoryToFuseOperations {#max-extra-job-memory-to-fuse-operations} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 2G | Static | This is the maximum memory consumption allowed for jobs after the optimizers merge operations. ## yt.MaxInputTables {#max-input-tables} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 1000 | Static | This limits the number of tables that you can pass as input to each specific MapReduce operation. ## yt.MaxInputTablesForSortedMerge {#max-input-tables-for-sorted-merge} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 100 | Static | This limits the number of tables that you can pass as input to a sorted merge operation. ## yt.MaxJobCount {#max-job-count} | Possible values | Default value | Type | | --- | --- | --- | | Positive integer | 16384 | Dynamic | The maximum number of jobs within a single YTsaurus operation. Applies only to single-stage map, reduce, merge, and similar operations. If both [`yt.DataSizePerJob`](https://ytsaurus.tech/docs/en/#data-size-per-sort-job) and `yt.MaxJobCount` are specified, jobs are partitioned based on [`yt.DataSizePerJob`](https://ytsaurus.tech/docs/en/#data-size-per-sort-job). Even if the resulting value `N` exceeds `yt.MaxJobCount`, `N` jobs will be started, while `yt.MaxJobCount` will only determine whether jobs are split after their number reaches a certain threshold. ## yt.MaxKeyWeight {#max-key-weight} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, up to 256K | 16K | Dynamic | Increase the limit on the maximum length of table keys in YTsaurus by which the table is sorted. ## yt.MaxOutputTables {#max-output-tables} | Possible values | Default value | Type | | --- | --- | --- | | Number from 1 to 100 | 50 | Static | This limits the number of output tables for each specific MapReduce operation. ## yt.MaxReplicationFactorToFuseOperations {#max-replication-factor-to-fuse-operations} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number, not less than 1.0 | 20.0 | Static | This is the maximum data replication factor allowed after the optimizers merge operations. ## yt.MaxRowWeight {#max-row-weight} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, up to 128M | 16M | Dynamic | Increase the limit on the maximum row length in a yt table. ## yt.MaxSpeculativeJobCountPerTask {#max-speculative-job-count-per-task} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | — | Dynamic | Specifies the number of speculatively executed jobs in YTsaurus operations. By default, the YTsaurus cluster settings are used. ## yt.MinColumnGroupSize {#min-column-group-size} | Possible values | Default value | Type | | --- | --- | --- | | Positive number, not less than 2 | 2 | Static | Set the minimum size of a column group. If the calculated group contains fewer columns than the pragma value, the group is not created. ## yt.MinLocalityInputDataWeight {#min-locality-input-data-weight} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | — | Dynamic | Set the `min_locality_input_data_weight` setting in the operation specification (this setting is not yet documented). ## yt.UseQLFilter {#yt.useqlfilter} Pass the compatible part of the `WHERE` condition to YTsaurus via [`input_query`](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options#common_options). YTsaurus uses `min`/`max` statistics to avoid reading chunks and blocks where the condition is definitely false. This pragma is useful for selective reading of large static tables with a strict schema. | Value type | Default | Static /
dynamic | | --- | --- | --- | | Flag | false | Static | #### Example {#use-ql-filter-example} ```yql PRAGMA yt.UseQLFilter; SELECT key AS key, value AS value FROM `//path/to/table` WHERE key >= 1000 AND key < 2000; ``` #### Result {#use-ql-filter-result} Compatible comparisons from `WHERE` are passed to `input_query`. If the table statistics prove that a chunk or block contains no `key` values within the specified range, YTsaurus skips it without reading from disk. The rest of the condition continues to be evaluated using YQL. #### Notes and limitations {#use-ql-filter-limitations} * You can use numeric types, `Bool`, `String`, `Utf8`, and their `Optional` variants. * You can use comparisons `<`, `<=`, `>`, `>=`, `==`, `!=` between a column and a constant expression, as well as `AND`, `OR`, `NOT`, `EXISTS`, `COALESCE`. * The table must have a strict schema. * Dynamic tables are not supported. * The pragma does not apply to tables with a custom schema or columns defined via `WITH SCHEMA` or `WITH COLUMNS`. ## yt.MinPublishedAvgChunkSize {#min-published-avg-chunk-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | — | Static | If the average chunk size in the resulting output table is smaller than the specified setting, an additional YTsaurus Merge operation runs to enlarge the chunks to the specified size. The value 0 has a special meaning — in this case, merge always runs and enlarges the chunks to 1G. If the table uses a compression codec, the output chunk size may differ from the specified value by the compression ratio. Essentially, this pragma sets the data size per merge job. After compression, the output size may be significantly smaller. In this case, increase the pragma value by the expected compression ratio. ## yt.MinTempAvgChunkSize {#min-temp-avg-chunk-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | — | Static | This setting works the same as `yt.MinPublishedAvgChunkSize`, but it applies to intermediate temporary tables. ## yt.NetworkProject {#network-project} | Possible values | Default value | Type | | --- | --- | --- | | String | `yt.StaticNetworkProject` | Dynamic | Set the use of the specified network project in the jobs for regular operations in the request. ## yt.NightlyCompress {#nightly-compress} | Possible values | Default value | Type | | --- | --- | --- | | Flag | - | Dynamic | Controls the background table compression process to reduce the amount of space occupied by tables. Setting the value to `true` sets the table's `@force_nightly_compress` attribute to `true`. Setting the value to `false` sets the table's `@nightly_compression_settings` attribute with the child value `enabled` to `false`. The setting applies only to tables that are newly created by a YQL request (and to tables that are overwritten using [INSERT INTO ... WITH TRUNCATE](https://ytsaurus.tech/docs/en/insert_into)). The setting doesn't apply to temporary tables. ## yt.OmitInaccessibleRows {#omit-inaccessible-rows} Manage the behavior when reading tables with [row-level ACL](https://ytsaurus.tech/docs/en/user-guide/storage/row-level-security) (RLS). By default, reading a table with row-level ACL set results in an authorization error if the user doesn't have the `full_read` permission. The `yt.OmitInaccessibleRows` pragma changes this behavior: when enabled, rows without access are skipped, and the request completes successfully. Only the rows allowed by the RLS predicate are included in the result. | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Static | #### Example {#omit-inaccessible-rows-example} ```yql PRAGMA yt.OmitInaccessibleRows = "true"; SELECT * FROM `//path/to/table_with_rls`; ``` #### Result {#omit-inaccessible-rows-result} The request returns only the rows that are accessible to the current user according to the row-level ACL. Rows that aren't accessible are skipped without an error. #### Limitations {#omit-inaccessible-rows-restrictions} - You can't specify `row_index` in `ranges` when reading a table with row-level ACL — the request will result in an error. The row indexes in `ranges` are counted relative to the physical rows on the disk, not the rows accessible to the user. For example, `//path/to/table[:#100]` will return up to 100 rows from the disk, some of which may be inaccessible and will be filtered out. - RLS isn't supported for dynamic tables — any read operation will result in an error. ## yt.OperationReaders {#operation-readers} | Possible values | Default value | Type | | --- | --- | --- | | A string with a list of logins separated by any of the following characters: comma, semicolon, space, or ` | ` | Dynamic | Allow you to grant read permissions for the MapReduce operations created in YTsaurus to users other than the owner of the YQL operation. ## yt.OperationSpec {#operation-spec} | Possible values | Default value | Type | | --- | --- | --- | | String representation of a YSON map | — | Dynamic | Set a map of operation settings. This lets you specify settings that don't have equivalents as pragmas. Settings specified through specialized pragmas have higher priority and overwrite the values in this map. ## yt.OptimizeFor {#optimize-for} | Possible values | Default value | Type | | --- | --- | --- | | String: lookup / scan | scan | Dynamic | Manage the `optimize_for` attribute on the tables that are created. ## yt.Owners {#owners} Allow you to grant access to manage [MapReduce operations](https://ytsaurus.tech/user-guide/data-processing/operations/mapreduce.md) in YTsaurus (cancel, pause, run-job-shell, etc.) to users other than the one who launched the query. | Possible values | Default value | Type | | --- | --- | --- | | A string with a list of logins separated by any of the following characters: `,`, `;`, ` `, or `\|` | — | Dynamic | #### Example {#owners-example} ```yql PRAGMA yt.Owners = 'ivanov petrov'; -- user logins separated by a space ``` #### Result {#owners-result} The specified users will be able to manage the MapReduce operations. ## yt.ParallelOperationsLimit {#parallel-operations-limit} | Possible values | Default value | Type | | --- | --- | --- | | A number, at least 1 | 16 | Static | Set the maximum number of YTsaurus operations that can run simultaneously within a request. ## yt.Pool {#pool} Use this to override the compute pool, which by default is the current user's login or was set by the `yt.StaticPool` pragma. | Value type | Default value | Type | | --- | --- | --- | | String | The value of the `yt.StaticPool` pragma — if it was set earlier; the current user's login — if `yt.StaticPool` wasn't set | Dynamic | #### Signature {#pool-signature} ```yql PRAGMA yt.StaticPool = ''; PRAGMA yt.Pool = ''; ``` #### Result {#pool-result} After you specify the `yt.StaticPool` pragma, the request will run in pool ``, and after the `yt.Pool` pragma — in ``. ## yt.PoolTrees {#pool-trees} | Possible values | Default value | Type | | --- | --- | --- | | A string with a list of tree names separated by any of the following characters: comma, semicolon, space, or ` | ` | Dynamic | Enables selecting non-default pool trees. ## yt.PrimaryMedium {#primary-medium} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Dynamic | Set the [primary medium in YTsaurus](https://ytsaurus.tech/docs/en/user-guide/storage/media#primary) for Published and Temporary tables and for intermediate data in operations. This is equivalent to setting the `yt.IntermediateDataMedium`, `yt.PublishedPrimaryMedium`, and `yt.TemporaryPrimaryMedium` pragmas at the same time. ## yt.PublishedCompressionCodec / yt.TemporaryCompressionCodec {#published-compression-codec} | Possible values | Default value | Type | | --- | --- | --- | | String, see the [documentation](https://ytsaurus.tech/docs/en/user-guide/storage/compression) | zstd_5 | Dynamic | Set the compression settings for tables created via YQL. Published tables are the tables specified in [INSERT INTO](https://ytsaurus.tech/docs/insert_into.md); all other tables are considered Temporary. The codec specified as Temporary is also used for intermediate data within a single YTsaurus operation, for example, a merged MapReduce. ## yt.PublishedErasureCodec / yt.TemporaryErasureCodec {#published-erasure-codec} | Possible values | Default value | Type | | --- | --- | --- | | String, see the [documentation](https://ytsaurus.tech/docs/en/user-guide/storage/replication#erasure) | none | Dynamic | Erasure coding is always disabled by default. To enable it, use the recommended lrc_12_2_2 value. The difference between Published and Temporary is the same as for [CompressionCodec](https://ytsaurus.tech/docs/en/#published-compression-codec). ## yt.PublishedMedia / yt.TemporaryMedia {#published-media} | Possible values | Default value | Type | | --- | --- | --- | | String representation of a YSON map | — | Dynamic | Set the `@media` attribute on the tables that are newly created. If present, this attribute specifies [the media in YTsaurus](https://ytsaurus.tech/docs/en/user-guide/storage/media#naznachenie-mediuma) where the table's chunks will be stored. Published tables are the tables specified in [INSERT INTO](https://ytsaurus.tech/docs/insert_into.md); all other tables are considered Temporary. ## yt.PublishedPrimaryMedium / yt.TemporaryPrimaryMedium {#published-primary-medium} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Dynamic | Set the `@primary_medium` attribute on the tables that are newly created. If present, this attribute specifies [the primary medium in YTsaurus](https://ytsaurus.tech/docs/en/user-guide/storage/media#primary) where the chunks will be written. By default, YTsaurus sets the primary medium to `"default"`. Published tables are the tables specified in [INSERT INTO](https://ytsaurus.tech/docs/insert_into.md); all other tables are considered Temporary. ## yt.PublishedReplicationFactor / yt.TemporaryReplicationFactor {#published-replication-factor} | Possible values | Default value | Type | | --- | --- | --- | | A number from 1 to 10 | — | Dynamic | Set the replication factor for tables created via YQL. Published tables are the tables specified in [INSERT INTO](https://ytsaurus.tech/docs/insert_into.md); all other tables are considered Temporary. ## yt.PythonCpu {#python-cpu} | Possible values | Default value | Type | | --- | --- | --- | | A floating-point number, at least 1.0 | 4.0 | Dynamic | A multiplier used to estimate CPU consumption by [Python UDFs](https://ytsaurus.tech/udf/python.md). It affects how MapReduce operations are split into jobs. ## yt.QueryCacheChunkLimit {#query-cache-chunk-limit} Use this pragma to control how tables are written to the cache, depending on the number of chunks in the table: with the `concatenate` command (as‑is) or with the `merge` command (with chunk merging). | Possible values | Default | Type | | --- | --- | --- | | A string containing the number of chunks — `` | `'0'` | Dynamic | #### Example { #query-cache-chunk-limit-example } ```yql PRAGMA yt.QueryCacheChunkLimit = '100000'; ``` #### Result { #query-cache-chunk-limit-result } If the number of chunks in the table is less than the set limit, the table is written to the cache with the `concatenate` command as‑is. If the number of chunks exceeds the set limit, the table is written to the cache using the `merge` command (with chunk merging). So, with the default value of `0`, tables are written to the cache using the `merge` command. #### Notes and limitations { #query-cache-chunk-limit-constraint } The `concatenate` command — writing as‑is — runs faster than `merge`, which needs time to merge the chunks and then write them. You’ll get better performance when caching tables with a relatively small number of chunks by using `concatenate`. ## yt.QueryCacheIgnoreTableRevision {#query-cache-ignore-table-revision} Use this pragma to avoid flushing the cache when data in a table changes. This speeds up debugging complex queries on large, mutable tables when the query logic doesn’t depend on data changes. By default, the system computes a hash from table metadata based on several parameters, including the revision number: YTsaurus `revision`. The revision is a non‑negative integer that increases each time the table changes. For more details about the `revision` attribute, see the [Metadata Tree](https://ytsaurus.tech/docs/en/user-guide/storage/cypress#time_attributes) section. With this pragma, you can control cache flushing: you can include or exclude the revision number when calculating the hash from the table metadata. | Possible values | Default | Type | | --- | --- | --- | | A string containing `true` or `false` | `false` | Static | #### Example { #query-cache-ignore-table-revision-example } ```yql PRAGMA yt.QueryCacheIgnoreTableRevision = 'true'; ``` #### Result { #query-cache-ignore-table-revision-result } If you set the flag to `true`, the YTsaurus revision number is excluded when calculating the hash from the table metadata, and the Query Cache isn’t flushed when the content of the input tables changes. In this case, the system will not invalidate the Query Cache when the contents of the input tables change. #### Notes and limitations { #query-cache-ignore-table-revision-constraint } {% note warning %} Don’t use this pragma in production. Use it only when debugging complex queries to reduce their execution time. {% endnote %} ## yt.QueryCacheMode {#query-cache-mode} The pragma controls caching of MapReduce operation results in YTsaurus. The cache stores results of previous operations: if the same operation has already run in a previous request, it won’t be launched again. Instead, YTsaurus will take the ready result from the cache. This speeds up requests that contain computations identical to those in other requests. Query Cache is also useful when you’re debugging or making relatively small changes to a query: in this case, the results of most operations are taken from the cache. | Possible values | Default | Type | | --- | --- | --- | | `'disable'` / `'readonly'` / `'refresh'` / `'normal'` | `'normal'` | Static | #### Example { #query-cache-mode-example } ```yql PRAGMA yt.QueryCacheMode = 'disable'; ``` #### Result { #query-cache-mode-result } Depending on the selected mode, YTsaurus will use, ignore, or update the cache: - `disable` — the cache is disabled. YTsaurus doesn’t check the cache and doesn’t write results to it. This mode is suitable for production where data changes frequently: it saves resources and avoids unnecessary read and write operations to the cache. - `readonly` — read‑only mode. YTsaurus takes the result from the cache if it exists, but doesn’t save new results to it. This mode is good for testing or debugging: you can use old results and avoid cluttering the cache with new entries. - `refresh` — write‑only mode. YTsaurus saves results to the cache but doesn’t read from it. Use this mode to refresh the cache: run a query without reading from the cache and save its result for future use. - `normal` — the default mode. YTsaurus both reads from and writes to the cache. This mode is especially useful during development and debugging when you run the same query multiple times to speed up iterations. In `normal` and `refresh` modes, the operation results are additionally saved at the path `///query_cache/`, where: - `tmp_folder` — the temporary directory. By default, it’s `tmp/`; you can set it using the [yt.TmpFolder](https://ytsaurus.tech/docs/en/#tmp-folder) pragma. - `hash` — the hash of the significant metadata and data of the input tables and the logical program that run in the operation. {% note info %} In `normal` mode, when you start a MapReduce operation, the system looks for the cache at the path `///query_cache/`. If the result is not in the cache, YTsaurus checks active operations from other queries. They may already include a similar operation that will produce the required result. If that operation has not yet written the result to the cache, the new query waits for it to complete. The system then retrieves the result from the cache. Keep in mind that reading from the cache can sometimes take longer than starting an operation from scratch. This can happen when the operation that computes the result and writes it to the cache has fewer resources than the operation that was started later and is waiting to read the result from the cache. In this case, the waiting query will run more slowly than if it were started from scratch. {% endnote %} ## yt.QueryCacheTtl {#query-cache-ttl} Use this pragma to set the storage time for tables created by an operation in the query cache — [TTL (Time to Live)](https://ytsaurus.tech/docs/en/user-guide/storage/cypress) — in the directory `/query_cache/`, where `tmp_folder` is the temporary directory. By default, it’s `tmp/`; you can set it using the [yt.TmpFolder](https://ytsaurus.tech/docs/en/#tmp-folder) pragma. | Possible values | Default | Type | | --- | --- | --- | | A string containing a time interval in the specified format: a number and a suffix `s/m/h/d` (seconds, minutes, hours, days) | `'7d'` | Static | #### Example { #query-cache-ttl-example } ```yql PRAGMA yt.QueryCacheTtl = '3h'; ``` #### Result { #query-cache-ttl-result } If you explicitly specify a value in the pragma, the Query Cache will be cleared after the set interval. The interval is counted from the moment the table is created in the query cache or from the moment the table was last used (see the [yt.QueryCacheUseExpirationTimeout](https://ytsaurus.tech/docs/en/#query-cache-use-expiration-timeout) pragma). If you don’t set an interval, the Query Cache will be cleared automatically after 7 days (by default). ## yt.QueryCacheUseExpirationTimeout {#query-cache-use-expiration-timeout} This pragma defines the mode for counting the TTL interval for tables in the query cache. | Possible values | Default | Type | | --- | --- | --- | | A string containing `true` or `false` | `false` | Static | #### Example { #query-cache-use-expiration-timeout-example } ```yql PRAGMA yt.QueryCacheUseExpirationTimeout = 'true'; ``` #### Result { #query-cache-use-expiration-timeout-result } With the default value of `false`, the TTL is counted from the moment the table is created in the query cache. If you set the value to `true`, the TTL is counted from the moment the table was last used. #### Notes and limitations { #query-cache-use-expiration-timeout-constraint } {% note warning %} Use this pragma only together with the [yt.QueryCacheTtl](https://ytsaurus.tech/docs/en/#query-cache-ttl) pragma: without specifying a TTL interval, it has no effect. {% endnote %} ## yt.ReduceLocalityTimeout {#reduce-locality-timeout} | Possible values | Default | Type | | --- | --- | --- | | A time interval with support for the `s/m/h/d` suffixes | — | Dynamic | This pragma sets the `reduce_locality_timeout` setting in the operation specification (the setting isn’t documented yet). ## yt.ReleaseTempData {#release-temp-data} | Possible values | Default | Type | | --- | --- | --- | | A string: `immediate` / `finish` / `never` | immediate | Static | Use this pragma to control when to delete temporary objects (for example, tables) that are created during query execution: - `immediate` — delete the objects as soon as they’re no longer needed. - `finish` — delete them at the end of the entire YQL query execution. - `never` — never delete them. ## yt.SamplingIoBlockSize {#sampling-io-block-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | — | Dynamic | Set the minimum block size for coarse-grained sampling. ## yt.ScriptCpu {#script-cpu} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number, not less than 1.0 | 1.0 | Dynamic | Multiplier used to estimate CPU consumption by script UDFs (including [Python UDF](https://ytsaurus.tech/udf/python.md). It affects how MapReduce operations are split into jobs. You can override it with specialized pragmas `yt.PythonCpu` / `yt.JavascriptCpu` for a specific UDF type. ## yt.SortLocalityTimeout {#sort-locality-timeout} | Possible values | Default value | Type | | --- | --- | --- | | Time interval with support for `s/m/h/d` suffixes | — | Dynamic | Set the `sort_locality_timeout` setting in the operation specification (this setting is not documented yet). ## yt.StartedBy {#started-by} | Possible values | Default value | Type | | --- | --- | --- | | String representation of a YSON map | — | Dynamic | Set a map that describes the client through which the operation was started. See the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options). ## yt.StaticPool {#static-pool} Use this to override the compute pool, which by default is the current user's login. You can set only one new value for `yt.StaticPool`. If you need to override the value for the next query, set it using the dynamic pragma `yt.Pool`. | Value type | Default value | Type | | --- | --- | --- | | String | Current user's login | Static, [per-cluster](https://ytsaurus.tech/docs/en/*per-cluster) | #### Signature {#static-pool-signature} ```yql PRAGMA yt.StaticPool = ''; ``` #### Result {#static-pool-result} After you set the pragma, the query will run in the `` pool. #### Notes and limitations {#static-pool-limitations} - Specifying a non-existent pool will cause a query execution error. - If you specify `yt.StaticPool` multiple times, the last pragma value is used for all queries. For example, both queries written after the pragma with the `pool_1` value and queries after the pragma with `pool_2` will run in `pool_2`: ```yql PRAGMA yt.StaticPool = ''; PRAGMA yt.StaticPool = ''; ``` ## yt.SuspendIfAccountLimitExceeded {#suspend-if-account-limit-exceeded} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Dynamic | See the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options#common_options). ## yt.SwitchLimit {#switch-limit} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, not less than 1M | 128M | Static | Set the memory buffer size for executing the Switch node. ## yt.TableContentCompressLevel {#table-content-compress-level} | Possible values | Default value | Type | | --- | --- | --- | | Positive number, up to and including 11 | 8 | Dynamic | Configures the compression level for table content delivered via a file (when yt.TableContentDeliveryMode="file"). ## yt.TableContentDeliveryMode {#table-content-delivery-mode} | Possible values | Default value | Type | | --- | --- | --- | | String: native / file | native | Dynamic | If you set the value to `native`, the table content is delivered to jobs using the native YTsaurus mechanisms. If you set the value to `file`, the table content is first downloaded on the YQL server and then delivered to jobs as a regular file. ## yt.TableContentMaxChunksForNativeDelivery {#table-content-max-chunks-for-native-delivery} | Possible values | Default value | Type | | --- | --- | --- | | Positive number, up to and including 1000 | 1000 | Static | Set the maximum number of chunks in a table for delivering it to jobs using the native YTsaurus mechanisms. If this number is exceeded, the table is delivered via a file. ## yt.TableContentMaxInputTables {#table-content-max-input-tables} | Possible values | Default value | Type | | --- | --- | --- | | Positive number, up to and including 1000 | 1000 | Static | Set the maximum number of tables for delivering them to jobs using the native YTsaurus mechanisms. If this number is exceeded, a preliminary merge is inserted. ## yt.TableContentMinAvgChunkSize {#table-content-min-avg-chunk-size} | Possible values | Default value | Type | | --- | --- | --- | | Bytes | 1GB | Static | Set the minimum average chunk size in a table for delivering it to jobs using the native YTsaurus mechanisms. A preliminary merge is inserted for chunks that are not large enough. ## yt.TableContentTmpFolder {#table-content-tmp-folder} | Possible values | Default value | Type | | --- | --- | --- | | Path on the cluster | — | Dynamic | The directory where temporary files will be stored for tables delivered via a file (when yt.TableContentDeliveryMode="file"). If you don't set it, the standard YTsaurus file cache is used. ## yt.TableContentUseSkiff {#table-content-use-skiff} | Possible values | Default value | Type | | --- | --- | --- | | Flag | true | Dynamic | Enable the Skiff format for delivering the table to operation jobs. ## yt.TablesTmpFolder {#tables-tmp-folder} | Possible values | Default value | Type | | --- | --- | --- | | String | `//tmp/yql/` | Static, [per-cluster](https://ytsaurus.tech/docs/en/*per-cluster)| Set the directory for storing [temporary tables](https://ytsaurus.tech/faq/temp.md). This has priority over `yt.TmpFolder`. ## yt.TempTablesTtl {#temp-tables-ttl} | Possible values | Default value | Type | | --- | --- | --- | | Time interval with support for `s/m/h/d` suffixes | — | Static | Use this to manage the TTL for [temporary tables](https://ytsaurus.tech/faq/temp.md). It affects tables with the full result; other temporary tables are unconditionally deleted when the query finishes, regardless of this pragma. ## yt.TentativePoolTrees {#tentative-pool-trees} | Possible values | Default value | Type | | --- | --- | --- | | String with a list of tree names separated by any of the following characters: comma, semicolon, space, or `\|` | — | Dynamic | Allows operations to be distributed cautiously across non-default pool trees. ## yt.TentativeTreeEligibilityMaxJobDurationRatio {#tentative-tree-eligibility-max-job-duration-ratio} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number | — | Dynamic | Takes effect only when the `yt.TentativePoolTrees` pragma is set. Specifies the acceptable job slowdown factor in the alternative pool tree. ## yt.TentativeTreeEligibilityMinJobDuration {#tentative-tree-eligibility-min-job-duration} | Possible values | Default value | Type | | --- | --- | --- | | Milliseconds | — | Dynamic | Takes effect only when the `yt.TentativePoolTrees` pragma is set. Specifies the minimum average job duration in the alternative pool tree. ## yt.TentativeTreeEligibilitySampleJobCount {#tentative-tree-eligibility-sample-job-count} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | — | Dynamic | Takes effect only when the `yt.TentativePoolTrees` pragma is set. Specifies the number of jobs in the sample. ## yt.TmpFolder {#tmp-folder} This pragma is used to specify a directory for storing temporary tables and files. For more details, see the [Temporary data](https://ytsaurus.tech/faq/temp.md) section. | Possible values | Default value | Type | | --- | --- | --- | | Directory path (string) | Current user's directory — `//tmp/yql/` | Static, [per-cluster](https://ytsaurus.tech/docs/en/*per-cluster) | #### Example {#tmp-folder-example} ```yql PRAGMA yt.TmpFolder = '//tmp/yql/ivanov/folder'; ``` #### Result {#tmp-folder-result} Temporary tables and files will be saved in the specified directory. ## yt.TopSortMaxLimit {#top-sort-max-limit} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 1000 | Static | This is the maximum LIMIT value used in combination with ORDER BY that triggers the TopSort optimization. ## yt.TopSortRowMultiplierPerJob {#top-sort-row-multiplier-per-job} | Possible values | Default value | Type | | --- | --- | --- | | Number not less than 1 | 10 | Static | The value is calculated as `LIMIT * yt.TopSortRowMultiplierPerJob`. ## yt.TopSortSizePerJob {#top-sort-size-per-job} | Possible values | Default value | Type | | --- | --- | --- | | Bytes, not less than 1 | 128M | Static | You set the expected data volume per job in a TopSort operation. ## yt.UseColumnarStatistics {#use-columnar-statistics} | Possible values | Default value | Type | | --- | --- | --- | | String: disable / auto / force / 0 (=disable) / 1 (=force) | force | Dynamic | This enables the use of columnar statistics to accurately estimate job sizes when you run operations on tables with columnar selections. See the [documentation](https://ytsaurus.tech/docs/en/user-guide/data-processing/operations/operations-options#common_options). In auto mode, the system automatically disables the use of statistics for operations that have tables with `optimize_for=lookup` as input. ## yt.UseDefaultTentativePoolTrees {#use-default-tentative-pool-trees} | Possible values | Default value | Type | | --- | --- | --- | | Flag | — | Dynamic | You set the value of the `use_default_tentative_pool_trees` option in the operation specification. ## yt.UseNativeYtTypes {#use-native-yt-types} | Possible values | Default value | Type | | --- | --- | --- | | Flag | true | Static | Enables writing complex-type values to tables using native support for complex types in YTsaurus. ## yt.UserSlots {#user-slots} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | Unlimited | Dynamic | This sets an upper limit on the number of jobs that can run in parallel within a MapReduce operation. ## yt.UseSkiff {#use-skiff} | Possible values | Default value | Type | | --- | --- | --- | | Flag | true | Dynamic | This enables the Skiff format for input/output in operation jobs. ## yt.UseTmpfs {#use-tmpfs} | Possible values | Default value | Type | | --- | --- | --- | | Flag | false | Dynamic | This mounts [tmpfs](https://en.wikipedia.org/wiki/Tmpfs) to the `_yql_tmpfs` folder in the MapReduce job sandbox. We don’t recommend using this. [*per-cluster]: You can configure all dynamic and some static pragmas so that their effect applies only to a specific cluster. For more details, see the [per-cluster support](https://ytsaurus.tech/docs/en/#settings) section. # PROCESS Convert the input table using a UDF or a [lambda function](https://ytsaurus.tech/docs/en/expressions.md#lambda) that is applied sequentially to each input row and can create zero, one, or multiple output strings per input string (similar to Map in terms of MapReduce). The parameters of the function call after the `USING` keyword explicitly specify the values of columns from which to pass values for each input row and in which order. You can use functions that return the result of one of three composite types derived from `OutputType` (supported `OutputType` options are described below): * `OutputType`: Each input row must always have an output row with the schema determined by the structure type. * `OutputType?`: The function may skip rows, returning empty values (`TUnboxedValue()` in C++, `None` in Python, or `null` in JavaScript). * `Stream` or `List`: An option to return multiple rows. Regardless of the option selected above, the result is converted to a flat table with columns defined by the `OutputType` type. As `OutputType`, you can use one of the types: * `Struct<...>`: `PROCESS` has exactly one output with entries of a given structure that is a flat table with columns corresponding to the fields `Struct<...>` * `Variant,...>`: `PROCESS` will have the number of outputs equal to the number of alternatives in `Variant`. The entries of each output are represented by a flat table with columns based on fields from the relevant variant. In this case, you can access the set of `PROCESS` outputs as a `Tuple` of lists that can be unpacked into separate [named expressions](https://ytsaurus.tech/docs/en/expressions.md#named-nodes) and used independently. In the list of function arguments after the `USING` keyword, you can pass one of the two special named expressions: * `TableRow()`: The entire current row in the form of a structure. * `TableRows()`: A lazy iterator by strings, in terms of the types `Stream>`. In this case, the output type of the function can only be `Stream` or `List`. {% note info %} After executing `PROCESS`, within the same query, on the resulting table (or tables), you can perform [SELECT](https://ytsaurus.tech/docs/en/select/index.md), [REDUCE](https://ytsaurus.tech/docs/en/reduce.md), other `PROCESS` and so on, depending on the intended result. {% endnote %} The `USING` keyword and the function are optional: if you omit them, the source table is returned. This can be convenient for using a [subquery template](https://ytsaurus.tech/docs/en/subquery.md). In `PROCESS` you can pass multiple inputs (the input here means a table, a [range of tables](https://ytsaurus.tech/docs/en/select/concat.md), a subquery, a [named expression](https://ytsaurus.tech/docs/en/expressions.md#named-nodes)), separated by commas. To the function from `USING`, you can only pass in this case special named expressions `TableRow()` or `TableRows()` that will have the following type: * `TableRow()`: A `Variant` where each element has an entry structure type from the relevant input. For each input row in the Variant, the element corresponding to the occurrence ID for this row is non-empty * `TableRows()`: A lazy iterator by Variants, in terms of the types `Stream>`. The alternative has the same semantics as for `TableRow()` After `USING` in `PROCESS` you can optionally specify `ASSUME ORDER BY` with a list of columns. The result of such a `PROCESS` statement is treated as sorted, but without actually running a sort. Sort check is performed at the query execution stage. It supports setting the sort order using the keywords `ASC` (ascending order) and `DESC` (descending order). Expressions are not supported in `ASSUME ORDER BY`. ## Examples ```yql PROCESS my_table USING MyUdf::MyProcessor(value) ``` ```yql $udfScript = @@ def MyFunc(my_list): return [(int(x.key) % 2, x) for x in my_list] @@; -- The function returns an iterator of Variants $udf = Python3::MyFunc(Callable<(Stream>) -> Stream, Struct<...>>>>, $udfScript ); -- The output of the PROCESS produces a tuple of lists $i, $j = (PROCESS my_table USING $udf(TableRows())); SELECT * FROM $i; SELECT * FROM $j; ``` ```yql $udfScript = @@ def MyFunc(stream): for r in stream: yield {"alt": r[0], "key": r[1].key} @@; -- The function accepts an iterator of Variants as input $udf = Python::MyFunc(Callable<(Stream, Struct<...>>>) -> Stream>>, $udfScript ); PROCESS my_table1, my_table2 USING $udf(TableRows()); ``` # REDUCE Groups the input by the specified key columns, then passes the current keys and a lazy iterator over their corresponding values from the remaining columns to the specified UDF for processing. Similarly to `PROCESS`, the UDF can return an arbitrary number of result rows per call and also return `Variants` to create multiple outputs. In terms of MapReduce, it's very similar to Reduce. Keywords that can follow: * `PRESORT` (optional) — specifies the sequence inside each group. Syntax is similar to that of [ORDER BY](https://ytsaurus.tech/docs/en/select/order_by.md). * `ON` (required) — specifies key columns. * `USING` or `USING ALL` (required) — UDF call. See the detailed rules below. Rules for passing UDF arguments: * If `TableRows()` is specified as a UDF argument, then the UDF must accept one argument: a lazy iterator over strings, with the type `Stream>`. In this case, the output function type can only be `Stream` or `List`. It's guaranteed that the data in the input iterator is grouped by the key and, if necessary, sorted according to the `PRESORT` section. With `TableRows()`, you can only use `USING ALL`. * With `USING`: * The UDF must accept two arguments: the current key is passed to the first argument, and a lazy iterator with values corresponding to this key is passed to the second argument. * With `USING ALL`: * The UDF must accept one argument: a lazy iterator over `Tuples`, where the first item in the tuple is a key, and the second item is a lazy iterator with values corresponding to this key. * The key to be passed to the UDF follows the rule below. If there is only one key column, then only its value is used in the key. If there are multiple columns (columns are listed similarly to `GROUP BY` separated by commas), then the key is a `Tuple` with values from the listed columns in the specified order. * When you call `REDUCE` from a query, only the expression whose values will be passed as iterator items follows the UDF name in parentheses (the second UDF argument for `USING` or the second item of the tuple for `USING ALL`). The result is built in the same way as for [PROCESS](https://ytsaurus.tech/docs/en/process.md). You can also use the `TableRow()` keyword to get the whole string as a structure. In `REDUCE`, you can pass multiple inputs (the input here means a table, a [range of tables](https://ytsaurus.tech/docs/en/select/concat.md), a subquery, a [named expression](https://ytsaurus.tech/docs/en/expressions.md#named-nodes)), separated by commas. All inputs must have the key columns of the matching type specified in `ON`. To the function from `USING` in this case, you can only pass a special named expression `TableRow()`. The second argument (or the second element of the tuple for `USING ALL`) will include a lazy iterator over variants with a populated element that corresponds to the occurrence ID for the current entry. After `USING`, in `REDUCE` you can optionally specify `ASSUME ORDER BY` with a list of columns. The result of such a `REDUCE` statement is treated as sorted, but without actually running a sort. Sort check is performed at the query execution stage. It supports setting the sort order using the keywords `ASC` (ascending order) and `DESC` (descending order). Expressions are not supported in `ASSUME ORDER BY`. ## Examples ```yql REDUCE my_table ON key, subkey USING MyUdf::MyReducer(TableRow()); ``` ```yql REDUCE my_table ON key, subkey USING ALL MyUdf::MyStreamReducer(TableRow()); -- MyUdf::MyStreamReducer accepts the idle list of tuples as input (key, list of records for keys) ``` ```yql REDUCE my_table PRESORT LENGTH(subkey) DESC ON key USING MyUdf::MyReducer( AsTuple(subkey, value) ); ``` ```yql REDUCE my_table ON key USING ALL MyUdf::MyFlatStreamReducer(TableRows()); -- MyUdf::MyFlatStreamReducer accepts a single idle list of records as input ``` ```yql -- The function returns an alternative $udf = Python::MyReducer(Callable<(String, Stream>) -> Variant, Struct<...>>>, $udfScript ); -- We get the tuple of lists as output of REDUCE $i, $j = (REDUCE my_table ON key USING $udf(TableRow())); SELECT * FROM $i; SELECT * FROM $j; ``` ```yql $script = @@ def MyReducer(key, values): state = None, 0 for name, last_visit_time in values: if state[1] < last_visit_time: state = name, last_visit_time return { 'region':key, 'last_visitor':state[0], } @@; $udf = Python::MyReducer(Callable<( Int64?, Stream> ) -> Struct< region:Int64?, last_visitor:String? >>, $script ); REDUCE hahn.`home/yql/tutorial/users` ON region USING $udf((name, last_visit_time)); ``` ```yql -- The function accepts a key and an alternatives iterator as input $udf = Python::MyReducer(Callable<(String, Stream,Struct<...>>>) -> Struct<...>>, $udfScript ); REDUCE my_table1, my_table2 ON key USING $udf(TableRow()); ``` # SELECT overview ## Definition Returns the result of evaluating the expressions specified after `SELECT`. It can be used in combination with other operations to obtain other effect. Examples: ```yql SELECT "Hello, world!"; ``` ```yql SELECT 2 + 2; ``` ## SELECT execution procedure {#selectexec} The `SELECT` query result is calculated as follows: * Determine the set of input tables by evaluating the [FROM](https://ytsaurus.tech/docs/en/from.md) clauses. * Apply [SAMPLE](https://ytsaurus.tech/docs/en/sample.md)/[TABLESAMPLE](https://ytsaurus.tech/docs/en/sample.md) to input tables. * Execute [FLATTEN COLUMNS](https://ytsaurus.tech/docs/flatten.md#flatten-columns) or [FLATTEN BY](https://ytsaurus.tech/docs/flatten.md); aliases set in `FLATTEN BY` become visible after this point. * Execute every [JOIN](https://ytsaurus.tech/docs/join.md). * Add to (or replace in) the data the columns listed in [GROUP BY ... AS ...](https://ytsaurus.tech/docs/group_by.md) (executed after `WHERE` starting with version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#group-by-expr-alias-where)). * [WHERE](https://ytsaurus.tech/docs/en/where.md) is executed. All data that don't match the predicate are filtered out. * [GROUP BY](https://ytsaurus.tech/docs/group_by.md) is executed and aggregate function values are calculated. * [HAVING](https://ytsaurus.tech/docs/group_by.md#having) is filtered. * [window function](https://ytsaurus.tech/docs/window.md) values are calculated. * Evaluate expressions in `SELECT`. * Assign names set by aliases to expressions in `SELECT`. * top-level [DISTINCT](https://ytsaurus.tech/docs/en/distinct.md) is applied to columns obtained this way. * All subqueries in the [UNION [ALL]](https://ytsaurus.tech/docs/en/operators.md#union), [INTERSECT [ALL]](https://ytsaurus.tech/docs/en/operators.md#intersect), and [EXCEPT [ALL]](https://ytsaurus.tech/docs/en/operators.md#except) operators are processed following the same logic and then joined, intersected, or excluded, respectively. The current implementation doesn't support combining `INTERSECT [ALL]`, `EXCEPT [ALL]`, and `UNION [ALL]`, nor does it allow using more than one `INTERSECT [ALL]` or `EXCEPT [ALL]` operator within a single `SELECT` statement. * sorting based on [ORDER BY](https://ytsaurus.tech/docs/en/order_by.md) is performed. * [OFFSET and LIMIT](https://ytsaurus.tech/docs/en/limit_offset.md) are applied to the obtained result. ## Column order in YQL {#orderedcolumns} The standard SQL is sensitive to the order of columns in projections (that is, in `SELECT`). While the order of columns must be preserved in the query results or when writing data to a new table, some SQL constructs use this order. This is also true for the [UNION [ALL]](https://ytsaurus.tech/docs/en/operators.md#union), [INTERSECT [ALL]](https://ytsaurus.tech/docs/en/operators.md#intersect), and [EXCEPT [ALL]](https://ytsaurus.tech/docs/en/operators.md#except) operators and the positional [ORDER BY](https://ytsaurus.tech/docs/en/order_by.md) (ORDER BY ordinal). The column order is ignored in YQL by default: * The order of columns in the output tables and query results is undefined * The data schema of `UNION [ALL]`, `INTERSECT [ALL]`, and `EXCEPT [ALL]` results is output by column names rather than positions If you enable `PRAGMA OrderedColumns;`, the order of columns is preserved in the query results and is derived from the order of columns in the input tables using the following rules: * `SELECT`: an explicit column enumeration dictates the result order. * `SELECT` with an asterisk (`SELECT * FROM ...`) inherits the order from its input. * the column order after [JOIN](https://ytsaurus.tech/docs/join.md): first, the columns from the left side, then the columns from the right side. If the order for one of the sides included in the `JOIN` output is not defined, the order of output columns is not defined either. * Column order of `UNION [ALL]`, `INTERSECT [ALL]`, and `EXCEPT [ALL]` depends on [execution mode](https://ytsaurus.tech/docs/en/operators.md#positional-mode). * the column order for [AS_TABLE](https://ytsaurus.tech/docs/en/from_as_table.md) is not defined. ## Combining queries {#combining-queries} You can join the results of multiple `SELECT` queries (or subqueries) using the `UNION` and `UNION ALL` keywords. ```yql query1 UNION [ALL] query2 (UNION [ALL] query3 ...) ``` Joining more than two queries is interpreted as a left-associative operation, meaning ```yql query1 UNION query2 UNION ALL query3 ``` is interpreted as ```yql (query1 UNION query2) UNION ALL query3 ``` You can find the intersection of two `SELECT` queries (or subqueries) using the `INTERSECT` and `INTERSECT ALL` keywords. ```yql query1 INTERSECT query2 ``` Queries containing multiple `INTERSECT [ALL]` operators aren't allowed. ```yql query1 INTERSECT query2 INTERSECT query3 ... -- error query1 INTERSECT query2 INTERSECT ALL query3 ... -- error ``` You can exclude one `SELECT` query (or subquery) from another using the `EXCEPT` and `EXCEPT ALL` keywords. ```yql query1 EXCEPT query2 ``` Queries containing multiple `EXCEPT [ALL]` operators aren't allowed. ```yql query1 EXCEPT query2 EXCEPT query3 -- error query1 EXCEPT query2 EXCEPT ALL query3 -- error ``` Combining different `UNION`, `INTERSECT`, or `EXCEPT` operators within a single query isn't allowed. ```yql query1 UNION query2 INTERSECT query3 -- error query1 UNION query2 EXCEPT query3 -- error query1 INTERSECT query2 EXCEPT query3 -- error ``` This behavior is independent of whether `DISTINCT` / `ALL` is present. ```yql query1 UNION ALL query2 INTERSECT ALL query3 -- error query1 UNION ALL query2 EXCEPT ALL query3 -- error query1 INTERSECT ALL query2 EXCEPT ALL query3 -- error ``` {% note warning %} Only `UNION` and `UNION ALL` can be used multiple times. Using more than one `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, or `EXCEPT ALL` operator isn't allowed. {% endnote %} If `ORDER BY/LIMIT/DISCARD/INTO RESULT` is present in the combined subqueries, the following rules apply: * `ORDER BY/LIMIT/INTO RESULT` is only permitted after the last subquery. * `DISCARD` is allowed only before the first subquery. * The specified operators influence the result of `UNION [ALL]`, `INTERSECT [ALL]`, or `EXCEPT [ALL]` rather than the subquery. * To apply an operator to a subquery, enclose the subquery in parentheses. ## Accessing multiple tables in one query In standard SQL, [UNION ALL](https://ytsaurus.tech/docs/en/operators.md#union-all) is used to execute a query across tables that bundle outputs of two or more `SELECT`s. This is not very convenient for the use case of running the same query on multiple tables (for example, if they contain data for different dates). To make this more convenient, in YQL `SELECT` statements, after `FROM`, you can specify not only a table or a subquery, but also call built-in functions letting you combine data from multiple tables. The following functions are defined for these purposes: ```CONCAT(`table1`, `table2`, `table3` VIEW view_name, ...)``` combines all the tables listed in the arguments. `EACH($list_of_strings)` or `EACH($list_of_strings VIEW view_name)` combines all tables whose names are listed in the list of strings. Optionally, you can pass multiple lists in separate arguments similar to `CONCAT`. {% note warning %} All of the above functions don't guarantee the order of the table union. The list of tables is calculated **before** executing the query. Therefore, the tables created during the query execution won't be included in the function results. {% endnote %} By default, the schemas of all participating tables are merged based on [UNION ALL](https://ytsaurus.tech/docs/en/operators.md#union-all) rules. If you don't want to merge schemas, use functions with the `_STRICT` suffix (for example, `CONCAT_STRICT`) — they will work the same but treat any difference in table schemas as an error. All arguments of the functions described above can be declared separately using [named expressions](https://ytsaurus.tech/docs/expressions.md#named-nodes). In this case, you can also use simple expressions in them by implicitly calling [EvaluateExpr](https://ytsaurus.tech/builtins/basic.md#evaluate_expr_atom). To get the name of the source table from which you originally obtained each row, use T[ablePath()](https://ytsaurus.tech/builtins/basic.md#tablepath). #### Examples ```yql USE some_cluster; SELECT * FROM CONCAT( `table1`, `table2`, `table3`); ``` ```yql USE some_cluster; $indices = ListFromRange(1, 4); $tables = ListMap($indices, ($index) -> { RETURN "table" || CAST($index AS String); }); SELECT * FROM EACH($tables); -- identical to the previous example. ``` ## Clauses supported in SELECT queries * [FROM](https://ytsaurus.tech/docs/en/from.md) * [FROM AS_TABLE](https://ytsaurus.tech/docs/en/from_as_table.md) * [FROM SELECT](https://ytsaurus.tech/docs/en/from_select.md) * [DISTINCT](https://ytsaurus.tech/docs/en/distinct.md) * [UNIQUE DISTINCT](https://ytsaurus.tech/docs/en/unique_distinct_hints.md) * [UNION INTERSECT EXCEPT](https://ytsaurus.tech/docs/en/operators.md) * [WITH](https://ytsaurus.tech/docs/en/with.md) * [WITHOUT](https://ytsaurus.tech/docs/en/without.md) * [WHERE](https://ytsaurus.tech/docs/en/where.md) * [ORDER BY](https://ytsaurus.tech/docs/en/order_by.md) * [ASSUME ORDER BY](https://ytsaurus.tech/docs/en/assume_order_by.md) * [LIMIT OFFSET](https://ytsaurus.tech/docs/en/limit_offset.md) * [SAMPLE](https://ytsaurus.tech/docs/en/sample.md) * [TABLESAMPLE](https://ytsaurus.tech/docs/en/sample.md) * [CONCAT](https://ytsaurus.tech/docs/en/concat.md) # Explicitly created temporary (anonymous) tables In complex multiphase queries, it can be useful to explicitly create a physical temporary table in order to manually affect the query process. To do this, you can use the table name starting with `@`. Such tables are called anonymous to distinguish them from temporary tables created by YT operations. Each such name within the query is replaced, at execution time, by a globally unique path to a table in a temporary directory. Such temporary tables are automatically deleted upon completion of the query, following the same rules as implicitly created tables. This feature lets you ignore conflicts in paths to temporary tables between parallel operations, and also avoid deleting them explicitly when the query completes. ## Examples ```yql INSERT INTO @my_temp_table SELECT * FROM my_input_table ORDER BY value; COMMIT; SELECT * FROM @my_temp_table WHERE value = "123" UNION ALL SELECT * FROM @my_temp_table WHERE value = "456"; ``` Temporary table names can use [named expressions](https://ytsaurus.tech/docs/expressions.md#named-nodes). ```yql $tmp_name = "my_temp_table"; INSERT INTO @$tmp_name SELECT 1 AS one, 2 AS two, 3 AS three; COMMIT; SELECT * FROM @$tmp_name; ``` ## FROM {#from} Data source for `SELECT`. The argument can accept the table name, the result of another `SELECT`, or a [named expression](https://ytsaurus.tech/docs/expressions.md#named-nodes). Between `SELECT` and `FROM`, list the comma-separated column names from the source (or `*` to select all columns). The table is searched by name in the database specified by the operator [USE](https://ytsaurus.tech/docs/use.md). #### Examples ```yql SELECT key FROM my_table; ``` ```yql SELECT * FROM (SELECT value FROM my_table); ``` ```yql $table_name = "my_table"; SELECT * FROM $table_name; ``` ## FROM AS_TABLE {#as-table} Accessing named expressions as tables using the `AS_TABLE` function. `AS_TABLE($variable)` lets you use the value of `$variable` as the data source for the query. In this case, the variable `$variable` must have the type `List>`. #### Example ```yql $data = AsList( AsStruct(1u AS Key, "v1" AS Value), AsStruct(2u AS Key, "v2" AS Value), AsStruct(3u AS Key, "v3" AS Value)); SELECT Key, Value FROM AS_TABLE($data); ``` ## FROM ... SELECT ... {#from-select} An inverted format, first specifying the data source and then the operation. #### Examples ```yql FROM my_table SELECT key, value; ``` ```yql FROM a_table AS a JOIN b_table AS b USING (key) SELECT *; ``` # Listing the contents of a directory in a cluster Specified as the `FOLDER` function in [FROM](https://ytsaurus.tech/docs/en/from.md). Arguments: 1. The path to the directory. 2. An optional string with a list of meta attributes separated by a semicolon. The result is a table with three fixed columns: 1. **Path** (`String`): The full name of the table. 2. **Type** (`String`): The node type (table, map_node, file, document, and others). 3. **Attributes** (`Yson`) is a Yson dictionary with meta attributes ordered in the second argument. Recommendations for use: * To get only the list of tables, you must add `...WHERE Type == "table"`. Then you can add more conditions if you want using the [AGGREGATE_LIST](https://ytsaurus.tech/builtins/aggregation.md#aggregate-list) aggregate function from the Path column to get only the list of paths and pass them to [EACH](https://ytsaurus.tech/docs/en/concat.md). * Because the Path column is returned in the same format as the [TablePath()](https://ytsaurus.tech/builtins/basic.md#tablepath) function's output, you can use it for the table's JOIN attributes for its rows. * We recommend that you work with the Attributes column using [Yson UDF](https://ytsaurus.tech/udf/list/yson.md). {% note warning %} Use FOLDER with attributes containing large values with caution (`schema` could be one of those). The query containing FOLDER in a folder with a large number of tables and a heavy attribute may cause a severe load on the YT wizard. {% endnote %} ## Examples ```yql USE hahn; $table_paths = ( SELECT AGGREGATE_LIST(Path) FROM FOLDER("my_folder", "schema;row_count") WHERE Type = "table" AND Yson::GetLength(Attributes.schema) > 0 AND Yson::LookupInt64(Attributes, "row_count") > 0 ); SELECT COUNT(*) FROM EACH($table_paths); ``` # Recursive directory traversal in a cluster An iterator for traversing the target cluster tree, with the ability to accumulate state, typically in the form of a list of paths to tables. Custom functions are called for the descendants of each node to accumulate state and select attributes and nodes for further traversal. Specified as the `WalkFolders` function in [FROM](https://ytsaurus.tech/docs/en/from.md). Returns a single `State` column with the same type as `InitialState`. Required: 1. **Path**: Path to the original directory. Optional: 2. **InitialState** (`Persistable`). The state must be of any serializable type (for example, `Callable` or `Resource` can't be used). By default, `ListCreate(String)` is used Optional named: 3. **RootAttributes** (`String`): Semicolon-separated string listing the desired meta attributes (for example, `"schema;row_count"`). `""` by default. 4. **PreHandler**: Lambda function called for the list of descendants of the current directory after the List operation (before resolving the links). Accepts a list of nodes, the current state, and the current traversal depth, returning the next state. Signature: ```(List>, TypeOf(InitialState), Int32) -> TypeOf(InitialState)``` TypeOf(InitialState) is the returned type of InitialState. Default implementation: `($nodes, $state, $level) -> ($state)` 5. **ResolveHandler**: Lambda function called after `PreHandler`. Accepts a list of descendant links of the current directory, the current state, a list of requested attributes for the ancestor directory, and the current traversal depth. Returns `Tuple>, TypeOf(InitialState)>`, a tuple with a list of links to traverse with the requested meta attributes, along with the next state. If a link is broken, WalkFolders ignores it: you don't need to check for this in the handler Signature: ```(List>, TypeOf(InitialState), List, Int32) -> Tuple>, TypeOf(InitialState)>``` Default implementation: ```yql -- resolve each link, requesting the same attributes that we requested from their ancestor ($nodes, $state, $rootAttrList, $level) -> { $linksToVisit = ListMap($nodes, ($node) -> (($node.Path, $rootAttrList))); return ($linksToVisit, $state); } ``` 6. **DiveHandler**: Lambda function called after `ResolveHandler`. Accepts a list of descendant directories of the current directory, the current state, a list of requested attributes for the ancestor directory, and the current traversal depth. Returns `Tuple>, TypeOf(InitialState)>`, a tuple with a list of directories to traverse with the requested meta attributes after processing the current directory, along with the next state. The returned paths are queued for traversal. Signature: ```(List>, TypeOf(InitialState), List, Int32) -> Tuple>, TypeOf(InitialState)>``` Default implementation: ```yql -- traverse each subdirectory, requesting the same attributes that we requested from their ancestor ($nodes, $state, $rootAttrList, $level) -> { $nodesToDive = ListMap($nodes, ($node) -> (($node.Path, $rootAttrList))); return ($nodesToDive, $state); } ``` 7. **PostHandler**: Lambda function called after `DiveHandler`. Accepts a list of descendants of the current directory after resolving the links, the current state, and the current traversal depth. Signature: ```(List>, TypeOf(InitialState), Int32) -> TypeOf(InitialState)``` Default implementation: `($nodes, $state, $level) -> ($state)` {% note warning %} * **WalkFolders can create a significant load on the master.** Exercise caution when using WalkFolders with attributes containing large values (`schema` could be one of those) or when traversing a subtree with significant size or depth. Directory listing requests within a single WalkFolders call can be executed in parallel. When requesting attributes with large values, **reduce** the number of simultaneous requests by using the [yt.BatchListFolderConcurrency](https://ytsaurus.tech/docs/pragma/yt.md#ytbatchlistfolderconcurrency) pragma. * Handlers are executed via [EvaluateExpr](https://ytsaurus.tech/builtins/basic.md#evaluate_expr_atom). There is a limit on the number of YQL AST nodes. You can't use very large containers within State. This restriction can be circumvented by running multiple WalkFolders calls and combining the results. Or you can serialize the new state into a string without intermediate deserialization (for example, using JSON/Yson lines). * Because of parallel directory listing calls, the traversal order of the nodes in the tree doesn't follow a depth-first search (DFS) pattern * InitialState is used to infer handler types. It must be specified explicitly: for example, as `ListCreate(String)` rather than `[]`. {% endnote %} Recommendations for use: * We recommend using [Yson UDF](https://ytsaurus.tech/udf/list/yson.md) to work with the Attributes column * The listing result for each directory is cached so you can quickly traverse the same subtree again in another WalkFolders call within the same request, if needed ## Examples Recursively collect the paths of all tables starting from `initial_folder`: ```yql $postHandler = ($nodes, $state, $level) -> { $tables = ListFilter($nodes, ($x)->($x.Type = "table")); return ListExtend($state, ListExtract($tables, "Path")); }; SELECT State FROM WalkFolders(`initial_folder`, $postHandler AS PostHandler); ``` Recursively find the last table created in `initial_folder`: ```yql $extractTimestamp = ($node) -> { $creation_time_str = Yson::LookupString($node.Attributes, "creation_time"); RETURN DateTime::MakeTimestamp(DateTime::ParseIso8601($creation_time_str)); }; $postHandler = ($nodes, $maxTimestamp, $_) -> { $tables = ListFilter($nodes, ($node) -> ($node.Type == "table")); RETURN ListFold( $tables, $maxTimestamp, ($table, $maxTimestamp) -> (max_of($extractTimestamp($table), $maxTimestamp)) ); }; $initialTimestamp = CAST(0ul AS Timestamp); SELECT * FROM WalkFolders(`initial_folder`, $initialTimestamp, "creation_time" AS RootAttributes, $postHandler AS PostHandler); ``` Recursively collect the paths of all tables two levels deep from `initial_folder` ```yql $diveHandler = ($nodes, $state, $attrList, $level) -> { $paths = ListExtract($nodes, "Path"); $pathsWithReqAttrs = ListMap($paths, ($x) -> (($x, $attrList))); $nextToVisit = IF($level < 2, $pathsWithReqAttrs, []); return ($nextToVisit, $state); }; $postHandler = ($nodes, $state, $level) -> { $tables = ListFilter($nodes, ($x)->($x.Type = "table")); return ListExtend($state, ListExtract($tables, "Path")); }; SELECT State FROM WalkFolders(`initial_folder`, $diveHandler AS DiveHandler, $postHandler AS PostHandler); ``` Collect the paths from all nodes within `initial_folder` without traversing its subdirectories ```yql $diveHandler = ($_, $state, $_, $_) -> { $nextToVisit = []; RETURN ($nextToVisit, $state); }; $postHandler = ($nodes, $state, $_) -> { $tables = ListFilter($nodes, ($x) -> ($x.Type = "table")); RETURN ListExtend($state, ListExtract($tables, "Path")); }; SELECT State FROM WalkFolders(`initial_folder`, $diveHandler AS DiveHandler, $postHandler AS PostHandler); ``` Recursively collect the paths of all broken links (links with non-existent destination paths) from `initial_folder`. ```yql $resolveHandler = ($list, $state, $attrList, $_) -> { $broken_links = ListFilter($list, ($link) -> (Yson::LookupBool($link.Attributes, "broken"))); $broken_links_target_paths = ListNotNull( ListMap( $broken_links, ($link) -> (Yson::LookupString($link.Attributes, "target_path")) ) ); $nextState = ListExtend($state, $broken_links_target_paths); -- WalkFolders ignores broken links during resolution $paths = ListTake(ListExtract($list, "Path"), 1); $pathsWithReqAttrs = ListMap($paths, ($x) -> (($x, $attrList))); RETURN ($pathsWithReqAttrs, $nextState); }; SELECT State FROM WalkFolders(`initial_folder`, $resolveHandler AS ResolveHandler, "target_path" AS RootAttributes); ``` Recursively build a Yson that includes a `Type`, a `Path`, and an `Attributes` map containing node attributes (`creation_time` and a custom `foo` attribute) for each node from `initial_folder`. ```yql -- If you need to accumulate a very large state in a single request, you can store it as a string to circumvent the limit on the number of nodes during the Evaluate call. $saveNodesToYsonString = ($list, $stateStr, $_) -> { RETURN $stateStr || ListFold($list, "", ($node, $str) -> ($str || ToBytes(Yson::SerializeText(Yson::From($node))) || "\n")); }; $serializedYsonNodes = SELECT State FROM WalkFolders("//home/yql", "", "creation_time;foo" AS RootAttributes, $saveNodesToYsonString AS PostHandler); SELECT ListMap(String::SplitToList($serializedYsonNodes, "\n", true AS SkipEmpty), ($str) -> (Yson::Parse($str))); ``` Pagination of WalkFolders results. Skipping the first 200 paths, collect 100 paths from `initial_folder`: ```yql $skip = 200ul; $take = 100ul; $diveHandler = ($nodes, $state, $reqAttrs, $_) -> { $paths = ListExtract($nodes, "Path"); $pathsWithReqAttrs = ListMap($paths, ($x) -> (($x, $reqAttrs))); $_, $collectedPaths = $state; -- complete the traversal if we've reached the required number of nodes $nextToVisit = IF( ListLength($collectedPaths) > $take, [], $pathsWithReqAttrs ); return ($nextToVisit, $state); }; $postHandler = ($nodes, $state, $_) -> { $visited, $collectedPaths = $state; $paths = ListExtract($nodes, "Path"); $itemsToTake = IF( $visited < $skip, 0, $take - ListLength($collectedPaths) ); $visited = $visited + ListLength($paths); return ($visited, ListExtend($collectedPaths, ListTake($paths, $itemsToTake))); }; $initialState = (0ul, ListCreate(String)); $walkFoldersRes = SELECT * FROM WalkFolders(`initial_folder`, $initialState, $diveHandler AS DiveHandler, $postHandler AS PostHandler); $_, $paths = Unwrap($walkFoldersRes); SELECT $paths; ``` # DISTINCT Selecting unique rows. {% note info %} Applying `DISTINCT` to calculated values is not currently implemented. For this purpose, use a subquery or the clause [`GROUP BY ... AS ...`](https://ytsaurus.tech/docs/group_by.md). {% endnote %} ## Example ```yql SELECT DISTINCT value -- only unique values from the table FROM my_table; ``` The `DISTINCT` keyword can also be used to apply [aggregate functions](https://ytsaurus.tech/builtins/aggregation.md) only to distinct values. For more information, see the documentation for [GROUP BY](https://ytsaurus.tech/docs/group_by.md). # UNIQUE DISTINCT hints Directly after `SELECT`, it is possible to add [SQL hints](https://ytsaurus.tech/docs/lexer.md#sql-hints) `unique` or `distinct`, which declare that this projection generates data containing unique values in the specified set of columns of a row-based or columnar table. This can be used to optimize subsequent subqueries executed on this projection, or for writing to table meta-attributes during `INSERT`. * Columns are specified in the hint values, separated by spaces. * If the set of columns is not specified, uniqueness applies to the entire set of columns in this projection. * `unique` - indicates unique or `null` values. According to the SQL standard, each null is unique: `NULL = NULL` -> `NULL`. * `distinct` - indicates completely unique values including null: `NULL IS DISTINCT FROM NULL` -> `FALSE`. * Multiple sets of columns can be specified in several hints for a single projection. * If the hint contains a column that is not in the projection, it will be ignored. # Operations with subquery results: UNION, INTERSECT, EXCEPT ## Joining (UNION) {#union} Joins the results of multiple `SELECT` queries (or subqueries), removing any duplicates. Its behavior is identical to executing `UNION ALL` followed by `SELECT DISTINCT *`. See [UNION ALL](https://ytsaurus.tech/docs/en/#union-all) for detailed information about its behavior. ```yql SELECT key FROM T1 UNION SELECT key FROM T2; ``` You can explicitly specify the `DISTINCT` keyword, though it won't affect the execution result. ```yql SELECT key FROM T1 UNION DISTINCT SELECT key FROM T2; ``` #### Example ```yql SELECT * FROM (VALUES (1, 2)) AS t(x, y) UNION SELECT * FROM (VALUES (1, 2)) AS t(x, y); ``` ``` x y 1 2 ``` ## Joining with duplicates (UNION ALL) {#union-all} Joins the results of multiple `SELECT` queries (or subqueries) without removing duplicates. ```yql SELECT key FROM T1 UNION ALL SELECT key FROM T2; ``` #### Example ```yql SELECT * FROM (VALUES (1, 2)) AS t(x, y) UNION ALL SELECT * FROM (VALUES (1, 2)) AS t(x, y); ``` ``` x y 1 2 1 2 ``` ## Intersection (INTERSECT) {#intersect} Finds the intersection of the results of two `SELECT` queries (or subqueries), removing any duplicates. Its behavior is identical to executing `INTERSECT ALL` followed by `SELECT DISTINCT *`. See [INTERSECT ALL](https://ytsaurus.tech/docs/en/#intersect-all) for detailed information about its behavior. ```yql SELECT key FROM T1 INTERSECT SELECT key FROM T2; ``` You can explicitly specify the `DISTINCT` keyword, though it won't affect the execution result. ```yql SELECT key FROM T1 INTERSECT DISTINCT SELECT key FROM T2; ``` #### Example ```yql SELECT * FROM (VALUES (1), (1), (1), (2)) AS t(x) INTERSECT SELECT * FROM (VALUES (1), (1)) AS t(x); ``` ``` x 1 ``` ## Intersection with duplicates (INTERSECT ALL) {#intersect-all} Finds the intersection of the results of two `SELECT` queries (or subqueries) without removing duplicates. ```yql SELECT key FROM T1 INTERSECT ALL SELECT key FROM T2; ``` The query returns a table of duplicate rows that occur in both source tables. The number of duplicate rows equals the minimum number of occurrences in either of the source tables. #### Example ```yql SELECT * FROM (VALUES (1), (1), (1), (2)) AS t(x) INTERSECT ALL SELECT * FROM (VALUES (1), (1)) AS t(x); ``` ``` x 1 1 ``` ## Exclusion (EXCEPT) {#except} Excludes the results of one `SELECT` query (or subquery) from another. ```yql SELECT key FROM T1 EXCEPT SELECT key FROM T2; ``` You can explicitly specify the `DISTINCT` keyword, though it won't affect the execution result. ```yql SELECT key FROM T1 EXCEPT DISTINCT SELECT key FROM T2; ``` The query returns a table of unique rows that occur in the first table but not in the second. #### Example ```yql SELECT * FROM (VALUES (1), (1), (1), (2)) AS t(x) EXCEPT SELECT * FROM (VALUES (1)) AS t(x); ``` ``` x 2 ``` ## Exclusion with duplicates (EXCEPT ALL) {#except-all} Excludes the results of one `SELECT` query (or subquery) from another, taking duplicates (the number of occurrences) into account. ```yql SELECT key FROM T1 EXCEPT ALL SELECT key FROM T2; ``` The query returns a table of duplicate rows that occur more frequently in the first table than in the second. The number of duplicate rows equals the difference in their occurrences between the first and second source tables. #### Example ```yql SELECT * FROM (VALUES (1), (1), (1), (2)) AS t(x) EXCEPT ALL SELECT * FROM (VALUES (1)) AS t(x); ``` ``` x 2 1 1 ``` ## "By name" and "by position" modes {#positional-mode} The operations above can be launched in two modes: by column names (the default mode) and by column positions (corresponds to the ANSI SQL standard and is enabled by the [PRAGMA](https://ytsaurus.tech/docs/pragma/global.md#positionalunionall)). ### By column names In the "by name" mode, the output of the resulting data schema uses the following rules: * The resulting table includes all columns that were found in at least one of the input tables. * if a column wasn't present in any input table, it's automatically assigned [optional data type](https://ytsaurus.tech/types/optional.md) (that permits `NULL` value); * If a column in different input tables had different types, then the shared type (the broadest one) is output. * If a column in different input tables had a heterogeneous type, for example, string and numeric, an error is raised. The order of output columns in this mode is equal to the largest common prefix of the order of inputs, followed by all other columns in the alphabetic order. If the largest common prefix is empty (for example, if the order isn't specified for one of the inputs), then the output order is undefined. #### Example In "by name" mode, which is the default, the result of executing this query will be a selection with three columns: `x`, `y`, and `z`: ```yql SELECT 1 AS x UNION ALL SELECT 2 AS y UNION ALL SELECT 3 AS z; ``` ``` x y z 1 2 3 ``` ### By column positions In the "by position" mode, the output of the resulting data schema uses the following rules: * All inputs must have equal number of columns. * The order of columns must be defined for all inputs. * The names of the resulting columns must match the names of columns in the first table. * The type of the resulting columns is output as a common (widest) type of input column types having the same positions. The order of the output columns in this mode is the same as the order of columns in the first input. #### Example When `PRAGMA PositionalUnionAll;` is enabled, the following query returns only the `x` column: ```yql PRAGMA PositionalUnionAll; SELECT 1 AS x UNION ALL SELECT 2 AS y UNION ALL SELECT 3 AS z; ``` ``` x 1 2 3 ``` If the column order is undefined (for example, when using `AS_TABLE`), the query fails: ```yql PRAGMA PositionalUnionAll; SELECT 1 AS x, 2 as y UNION ALL SELECT * FROM AS_TABLE([<|x:3, y:4|>]); ``` ``` Input #1 does not have ordered columns... ``` # Accessing multiple tables in one query In standard SQL, [UNION ALL](https://ytsaurus.tech/docs/select/operators.md#union-all) is used to execute a query across tables that bundle outputs of two or more `SELECT`s. This is not very convenient for the use case of running the same query on multiple tables (for example, if they contain data for different dates). To make this more convenient, in YQL `SELECT` statements, after `FROM`, you can specify not only a table or a subquery, but also call built-in functions letting you combine data from multiple tables. The following functions are defined for these purposes: ```CONCAT(`table1`, `table2`, `table3` VIEW view_name, ...)``` combines all the tables listed in the arguments. ```EACH($list_of_strings) or EACH($list_of_strings VIEW view_name)``` combines all tables whose names are listed in the list of strings. Optionally, you can pass multiple lists in separate arguments similar to `CONCAT`. ```RANGE(`prefix`, `min`, `max`, `suffix`, `view`)``` combines a range of tables. Arguments: * The prefix is the folder to search tables in. It's specified without a trailing slash. This is the only required argument. If it's the only one specified, all tables in the folder are used. * min, max: These two arguments specify a range of table names included. The range is inclusive on both ends. If no range is specified, all tables in the prefix folder are used. The names of tables or folders located in the folder specified in prefix are lexicographically matched with the `[min, max]` range rather than concatenated, so make sure to specify the range without leading slashes. * suffix is the name of the table. It's expected without a leading slash. If no suffix is specified, then the `[min, max]` arguments specify a range of table names. If the suffix is specified, then the `[min, max]` arguments specify a range of folders hosting a table with the name specified in the suffix argument. ```LIKE(`prefix`, `pattern`, `suffix`, `view`)``` and ```REGEXP(`prefix`, `pattern`, `suffix`, `view`)``` — the pattern argument is set in a format similar to the same-name binary operator: [LIKE](https://ytsaurus.tech/docs/expressions.md#like) and [REGEXP](https://ytsaurus.tech/docs/expressions.md#regexp). ```FILTER(`prefix`, `callable`, `suffix`, `view`)```: The `callable` argument must be a callable expression with the `(String)->Bool` signature that will be called for each table/subdirectory in the prefix folder. The query will only include those tables for which the callable value returned `true`. [Lambda functions](https://ytsaurus.tech/docs/expressions.md#lambda) are most convenient to use as callable values. ```PARTITION_LIST($list_of_structs)``` — merges all tables listed in the list of structures and adds new columns to these tables. The structure must have a `TablePath` field (table path). The `TablePath` field must be of type `String` or `Utf8`. An optional `TableView` field with the type `Optional` or `Optional` is also supported. If the `TableView` field is not null, the corresponding [VIEW](https://ytsaurus.tech/docs/select/view.md) is applied to the table. All other fields of the structure are added as additional columns to the corresponding table. First, `VIEW`/automatic unpacking of protobuf is applied, and then columns from `PARTITION_LIST` are added. The added columns replace columns with the same name from the table. The `PARTITION_LIST` function is available starting from version [2025.04](https://ytsaurus.tech/changelog/2025.04.md). ```PARTITIONS(`prefix`, `pattern`, `view`)``` — merges tables that match the `pattern` template and adds parts of the paths corresponding to the template components to the table as columns. Arguments: * `prefix` — directory for searching tables, specified without a trailing slash; * `pattern` — template that will be applied to table paths without the initial `prefix`. The template can contain the following components: * `*` — matches 0 or more path characters not including `/`. The behaviour is similar to the behaviour of `*` in the Unix shell; * `${name:type}` — matches the part of the path component corresponding to the specified type and creates an additional column in the table with the type `type` and the name `name`. If the type is not specified, `String` is implied; * all other characters match themselves; * `view` — optional argument. Specifies the VIEW for all tables that match the pattern. The `PARTITIONS` function is available starting from version [2025.04](https://ytsaurus.tech/changelog/2025.04.md). Examples of using these functions are provided below. {% note warning %} All of the above functions don't guarantee the order of the table union. The list of tables is calculated **before** executing the query. Therefore, the tables created during the query execution won't be included in the function results. For the `CONCAT`/`RANGE`/`LIKE`/`REGEXP`/`FILTER`/`PARTITIONS` functions, the [TablePathPrefix](https://ytsaurus.tech/docs/pragma/global.md#table-path-prefix) is automatically added to the `prefix` argument if `prefix` is not an absolute path. For the `EACH` and `PARTITION_LIST` functions, this does not happen. In order for the [TablePathPrefix](https://ytsaurus.tech/docs/pragma/global.md#table-path-prefix) to apply to them as well, you need to enable the PRAGMA [UseTablePrefixForEach](https://ytsaurus.tech/docs/pragma/global.md#use-table-prefix-for-each). Columns added via `PARTITIONS`/`PARTITION_LIST` replace columns with the same names from the original table. It is recommended to use unique names to avoid conflicts. {% endnote %} By default, the schemas of all participating tables are merged based on [UNION ALL](https://ytsaurus.tech/docs/en/operators.md#union-all) rules. If you don't want to merge schemas, use functions with the `_STRICT` suffix (for example, `CONCAT_STRICT`) — they will work the same but treat any difference in table schemas as an error. To specify a cluster for the joined tables, add it before the function name. All arguments of the functions described above can be declared separately using [named expressions](https://ytsaurus.tech/docs/expressions.md#named-nodes). In this case, you can also use simple expressions in them by implicitly calling [EvaluateExpr](https://ytsaurus.tech/builtins/basic.md#evaluate_expr_atom). To get the name of the source table from which you originally obtained each row, use T[ablePath()](https://ytsaurus.tech/builtins/basic.md#tablepath). ## Examples ```yql USE some_cluster; SELECT * FROM CONCAT( `table1`, `table2`, `table3`); ``` ```yql USE some_cluster; $indices = ListFromRange(1, 4); $tables = ListMap($indices, ($index) -> { RETURN "table" || CAST($index AS String); }); SELECT * FROM EACH($tables); -- identical to the previous example. ``` ```yql USE some_cluster; SELECT * FROM RANGE(`my_folder`); ``` ```yql SELECT * FROM some_cluster.RANGE( -- You can specify the cluster before the name of the `my_folder`, `from_table`, `to_table`) function; ``` ```yql USE some_cluster; SELECT * FROM RANGE( `my_folder`, `from_folder`, `to_folder`, `my_table`); ``` ```yql USE some_cluster; SELECT * FROM RANGE( `my_folder`, `from_table`, `to_table`, ``, `my_view`); ``` ```yql USE some_cluster; SELECT * FROM LIKE( `my_folder`, "2017-03-%" ); ``` ```yql USE some_cluster; SELECT * FROM REGEXP( `my_folder`, "2017-03-1[2-4]?" ); ``` ```yql $callable = ($table_name) -> { return $table_name > "2017-03-13"; }; USE some_cluster; SELECT * FROM FILTER( `my_folder`, $callable ); ``` ```yql $part_list = AsList( AsStruct("path/to/table1" as TablePath, "view1" as TableView, 1 as idx, "1" as idx_str), AsStruct("path/to/table2" as TablePath, 2 as idx, "2" as idx_str), ); SELECT * FROM some_cluster.PARTITION_LIST($part_list); -- The query above is equivalent to -- SELECT 1 as idx, "1" as idx_str, t1.* WITHOUT idx, idx_str FROM some_cluster.`path/to/table1` AS t1 -- UNION ALL -- SELECT 2 as idx, "2" as idx_str, t2.* WITHOUT idx, idx_str FROM some_cluster.`path/to/table2` AS t2 ``` ```yql -- Let's assume that the //home/daily_log directory has the following structure: //home/daily_logs///event_log use some_cluster; pragma yt.EarlyPartitionPruning; SELECT company, event_id, count(*) as events_count FROM PARTITIONS(`//home/daily_logs`, "${event_date:Date}/${company}/event_log") -- With yt.EarlyPartitionPruning, YQL will analyze the predicate in WHERE and exclude all tables older than 2025-01-01. -- Metadata will not be loaded for excluded tables. WHERE event_date >= Date("2025-10-01") OR event_id = 123 AND event_date >= Date("2025-01-01") GROUP BY company, event_id; ``` # Data views (VIEW) YQL supports two types of data views: * Linked to specific tables. * Independent, that might use an arbitrary number of tables within the cluster. Both of the views are non-materialized. It means that they are substituted into the calculation graph at each use. Accessing a `VIEW`: * Views linked to a table require a special syntax: ```[cluster.]`path/to/table` VIEW view_name```; * Views that aren't linked to tables, look like regular tables from the user perspective. If the meta attributes of the table specify an automatic UDF call to convert raw data into a structured set of columns, you can access raw data using a special `raw` view like this: ```[cluster.]`path/to/table` VIEW raw```. ## Examples ```yql USE some_cluster; SELECT * FROM my_table VIEW my_view; ``` # WITH It's set after the data source in `FROM` and is used for additional hints for tables. You can't use hints for subqueries and [named expressions](https://ytsaurus.tech/docs/expressions.md#named-nodes). The following values are supported: * `INLINE`: Hints that the table contents is small and you need to use its in-memory view to process the query. The actual size of the table is not controlled in this case, and if it's large, the query might fail with an out-of-memory error. * `UNORDERED`: Suppresses original table sorting. * `SCHEMA` type: Hints that the specified table schema must be used entirely, ignoring the schema in the metadata. * `COLUMNS` type: Hints that the specified types should be used for columns whose names match the table's column names in the metadata, as well as which columns are additionally present in the table. When setting the `SCHEMA` and `COLUMNS` hints, the type must be a [structure](https://ytsaurus.tech/types/containers.md). If you use the `SCHEMA` hint, then with the table functions like [EACH](https://ytsaurus.tech/docs/en/concat.md#each) you can use an empty list of tables that is treated as an empty table with columns defined in the `SCHEMA`. ## Examples ```yql SELECT key FROM my_table WITH INLINE; ``` ```yql SELECT key, value FROM my_table WITH SCHEMA Struct; ``` ```yql SELECT key, value FROM my_table WITH COLUMNS Struct; ``` ```yql SELECT key, value FROM EACH($my_tables) WITH SCHEMA Struct>; ``` # WITHOUT Excluding columns from the result of `SELECT *`. The `IF EXISTS` modifier does not throw an error for missing columns. ## Examples ```yql SELECT * WITHOUT foo, bar FROM my_table; SELECT * WITHOUT IF EXISTS foo, bar FROM my_table; ``` ```yql PRAGMA simplecolumns; SELECT * WITHOUT t.foo FROM my_table AS t CROSS JOIN (SELECT 1 AS foo) AS v; ``` # WHERE Filtering rows in the `SELECT` result based on a condition in tables. ## Example ```yql SELECT key FROM my_table WHERE value > 0; ``` # ORDER BY Sorting the `SELECT` result using a comma-separated list of sorting criteria. As a criteria, you can use a column value or an expression on columns. Ordering by column sequence number is not supported (`ORDER BY N` where `N` is a number). Each criteria can be followed by the sorting direction: - `ASC`: Sorting in the ascending order. Applied by default. - `DESC`: Sorting in the descending order. Multiple sorting criteria will be applied left-to-right. ## Example ```yql SELECT key, string_column FROM my_table ORDER BY key DESC, LENGTH(string_column) ASC; ``` You can also use `ORDER BY` for [window functions](https://ytsaurus.tech/docs/window.md). # ASSUME ORDER BY Checking that the `SELECT` result is sorted by the value in the specified column or multiple columns. The result of such a `SELECT` statement is treated as sorted, but without actually running a sort. Sort check is performed at the query execution stage. As in case of `ORDER BY`, it supports setting the sort order using the keywords `ASC` (ascending order) and `DESC` (descending order). Expressions are not supported in `ASSUME ORDER BY`. ## Examples ```yql SELECT key || "suffix" as key, -CAST(subkey as Int32) as subkey FROM my_table ASSUME ORDER BY key, subkey DESC; ``` # LIMIT and OFFSET `LIMIT`: limits the output to the specified number of rows. By default, the output is not restricted. `OFFSET`: specifies the offset from the beginning (in rows). By default, it's zero. ## Examples ```yql SELECT key FROM my_table LIMIT 7; ``` ```yql SELECT key FROM my_table LIMIT 7 OFFSET 3; ``` ```yql SELECT key FROM my_table LIMIT 3, 7; -- equivalent to the previous example ``` # TABLESAMPLE and SAMPLE Building a random sample from the data source specified in `FROM`. `TABLESAMPLE` is part of the SQL standard and works as follows: * The operating mode is specified: * `BERNOULLI` means "slowly, straightforwardly going through all the data, but in a truly random way". * `SYSTEM` uses knowledge about the physical data storage of data to avoid full data scans, but somewhat sacrificing randomness of the sample. The data is split into sufficiently large blocks, and the whole data blocks are sampled. For applied calculations on sufficiently large tables, the result may well be consistent. * The size of the random sample is indicated as a percentage after the operating mode, in parentheses. * Optionally, it can be followed by the `REPEATABLE` keyword and an integer in parentheses to be used as a seed for a pseudorandom number generator. `SAMPLE` is a shorter alias without sophisticated settings and sample size specified as a fraction. It currently corresponds to the `BERNOULLI` mode. ## Examples ```yql SELECT * FROM my_table TABLESAMPLE BERNOULLI(1.0) REPEATABLE(123); -- one percent of the table ``` ```yql SELECT * FROM my_table TABLESAMPLE SYSTEM(1.0); -- about one percent of the table ``` ```yql SELECT * FROM my_table SAMPLE 1.0 / 3; -- one-third of the table ``` # TABLESAMPLE and SAMPLE Building a random sample from the data source specified in `FROM`. `TABLESAMPLE` is part of the SQL standard and works as follows: * The operating mode is specified: * `BERNOULLI` means "slowly, straightforwardly going through all the data, but in a truly random way". * `SYSTEM` uses knowledge about the physical data storage of data to avoid full data scans, but somewhat sacrificing randomness of the sample. The data is split into sufficiently large blocks, and the whole data blocks are sampled. For applied calculations on sufficiently large tables, the result may well be consistent. * The size of the random sample is indicated as a percentage after the operating mode, in parentheses. * Optionally, it can be followed by the `REPEATABLE` keyword and an integer in parentheses to be used as a seed for a pseudorandom number generator. `SAMPLE` is a shorter alias without sophisticated settings and sample size specified as a fraction. It currently corresponds to the `BERNOULLI` mode. ## Examples ```yql SELECT * FROM my_table TABLESAMPLE BERNOULLI(1.0) REPEATABLE(123); -- one percent of the table ``` ```yql SELECT * FROM my_table TABLESAMPLE SYSTEM(1.0); -- about one percent of the table ``` ```yql SELECT * FROM my_table SAMPLE 1.0 / 3; -- one-third of the table ``` # Subquery templates ## DEFINE SUBQUERY {#define-subquery} `DEFINE SUBQUERY` lets you declare a subquery template that is a parameterizable block of several top-level expressions (statements), and then use it repeatedly in the `FROM` section of [SELECT](https://ytsaurus.tech/docs/en/select/index.md) statements or as input data in [PROCESS](https://ytsaurus.tech/docs/en/process.md)/[REDUCE](https://ytsaurus.tech/docs/en/reduce.md) with parameters. As opposed to [actions](https://ytsaurus.tech/docs/en/action.md), the subquery template must end with the `SELECT`/`PROCESS`/`REDUCE` statement whose result is the subquery's return value. Keep in mind that you can't use the top-level `SELECT`/`PROCESS`/`REDUCE` statement and the modifying expressions (for example, `INSERT`) more than once. After `DEFINE SUBQUERY`, specify: 1. [A named expression](https://ytsaurus.tech/docs/en/expressions.md#named-nodes) that will be used to access the declared template further in the query. 2. The round brackets contain a list of named expressions you can use to access parameters inside the subquery template. 3. `AS` keyword. 4. The list of top-level expressions. 5. `END DEFINE` acts as a marker of the last expression inside the subquery template. One or more of the last subquery parameters can be marked with a question as optional: unless they haven't been specified when calling subquery, they will be assigned the `NULL` value. {% note info %} In large queries, you can use separate files for action definition and include them to the main query using [EXPORT](https://ytsaurus.tech/docs/en/export_import.md#export) + [IMPORT](https://ytsaurus.tech/docs/en/export_import.md#import) so that instead of one long text you can have several logical parts that are easier to navigate. An important nuance: the `USE my_cluster`; directive in the import query doesn't affect behavior of actions declared in other subquery files. {% endnote %} Even if the list of parameters in the subquery template definition is empty, when using it in `FROM`, specify the parentheses `()`. This may help to limit the scope of named expressions used in only one subquery. In some cases, instead of `DEFINE SUBQUERY` it's more convenient to use an equivalent [lambda function](https://ytsaurus.tech/docs/en/expressions.md#lambda). In this case, the lambda function must accept, as the first argument, the special object called `world` that passes dependencies to make certain PRAGMA or COMMIT statements visible at the query template's point of use. Also, make sure to pass this object as the first argument along with the other arguments (if any) to other query templates, if you use them in your lambda function. The return value of the lambda function must have the structure list type (output table) or a list of variants over a tuple of structures (multiple output tables). In the latter case, the following unpacking is usually used at the query template's point of use: ```yql $out1, $out2 = PROCESS $mySubquery($myParam1, $myParam2); -- next we use $out1 and $out2 as separate tables. ``` #### Examples ```yql DEFINE SUBQUERY $hello_world($name, $suffix?) AS $name = $name ?? ($suffix ?? "world"); SELECT "Hello, " || $name || "!"; END DEFINE; SELECT * FROM $hello_world(NULL); -- Hello, world! SELECT * FROM $hello_world("John"); -- Hello, John! SELECT * FROM $hello_world(NULL, "Earth"); -- Hello, Earth! ``` ```yql DEFINE SUBQUERY $dup($x) AS SELECT * FROM $x(1) -- apply the passed query template with one argument UNION ALL SELECT * FROM $x(2); -- ... and with other argument END DEFINE; DEFINE SUBQUERY $sub($n) AS SELECT $n * 10; END DEFINE; SELECT * FROM $dup($sub); -- pass the query template $sub as a parameter -- Result: -- 10 -- 20 ``` ```yql /* Hide the named expressions $a and $b inside a separate scope */ DEFINE SUBQUERY $clean() AS $a = 10; $b = $a * $a; SELECT $a AS a, $b AS b; END DEFINE; SELECT * FROM $clean(); -- a: 10, b: 100 ``` ```yql USE my_cluster; DEFINE SUBQUERY $input() as SELECT * FROM `home/yql/tutorial/users`; END DEFINE; DEFINE SUBQUERY $myProcess1($nestedQuery, $lambda) AS PROCESS $nestedQuery() -- the parentheses () are mandatory here USING $lambda(TableRow()); END DEFINE; $myProcess2 = ($world, $nestedQuery, $lambda) -> { -- If you use ListFlatMap or YQL::OrderedFlatMap, you get an Ordered YT Map operation return YQL::FlatMap($nestedQuery($world), $lambda); }; -- With such use, the implementations of $myProcess1 and $myProcess2 are identical SELECT * FROM $myProcess1($input, ($x) -> { RETURN AsList($x, $x) }); SELECT * FROM $myProcess2($input, ($x) -> { RETURN AsList($x, $x) }); ``` ```yql USE my_cluster; DEFINE SUBQUERY $runPartition($table) AS $paritionByAge = ($row) -> { $recordType = TypeOf($row); $varType = VariantType(TupleType($recordType, $recordType)); RETURN If($row.age % 2 == 0, Variant($row, "0", $varType), Variant($row, "1", $varType), ); }; PROCESS $table USING $paritionByAge(TableRow()); END DEFINE; -- Unpacking two results $i, $j = (PROCESS $runPartition("home/yql/tutorial/users")); SELECT * FROM $i; SELECT * FROM $j; ``` ## SubqueryExtend, SubqueryUnionAll, SubqueryMerge, SubqueryUnionMerge {#subquery-extend} {#subquery-unionall} {#subquery-merge} {#subquery-unionmerge} Combining the subquery templates: `SubqueryExtend`, `SubqueryUnionAll`, `SubqueryMerge`, `SubqueryUnionMerge`. These functions combine the results of one or more subquery templates passed by arguments. The number of parameters in such subquery templates must be the same. * `SubqueryExtend` requires matching of subquery schemas. * `SubqueryUnionAll` follows the same rules as [ListUnionAll](https://ytsaurus.tech/docs/builtins/list.md#ListUnionAll). * `SubqueryMerge` uses the same constraints as `SubqueryExtend` and also outputs a sorted result if all subqueries have the same sort order. * `SubqueryUnionMerge` uses the same constraints as `SubqueryUnionAll` and also outputs a sorted result if all subqueries have the same sort order. #### Examples ```yql DEFINE SUBQUERY $sub1() as SELECT 1 as x; END DEFINE; DEFINE SUBQUERY $sub2() as SELECT 2 as x; END DEFINE; $s = SubqueryExtend($sub1,$sub2); PROCESS $s(); ``` ## SubqueryExtendFor, SubqueryUnionAllFor, SubqueryMergeFor, SubqueryUnionMergeFor {#subquery-extend-for} {#subquery-unionall-for} {#subquery-merge-for} {#subquery-unionmerge-for} Combining subquery templates after substituting list items: `SubqueryExtendFor`, `SubqueryUnionAllFor`, `SubqueryMergeFor`, `SubqueryUnionMergeFor`. The functions take the following arguments: * A non-empty list of values. * A subquery template that must have exactly one parameter. They substitute each item from the list into the subquery template as a parameter and then combine the obtained subqueries. * `SubqueryExtendFor` requires matching of subquery schemas. * `SubqueryUnionAllFor` follows the same rules as [ListUnionAll](https://ytsaurus.tech/docs/builtins/list.md#ListUnionAll). * `SubqueryMergeFor` uses the same constraints as `SubqueryExtendFor` and also outputs a sorted result if all subqueries have the same sort order. * `SubqueryUnionMergeFor` uses the same constraints as `SubqueryUnionAllFor` and also outputs a sorted result if all subqueries have the same sort order. #### Examples ```yql DEFINE SUBQUERY $sub($i) as SELECT $i as x; END DEFINE; $s = SubqueryExtendFor([1,2,3],$sub); PROCESS $s(); ``` ## SubqueryOrderBy, SubqueryAssumeOrderBy Adding sorting to the `SubqueryOrderBy` template or indicating the presence of this `SubqueryAssumeOrderBy`. The functions take the following arguments: * A subquery template without parameters. * A list of pairs (`string` indicating the column name and `Boolean` value: `True` for sorting in ascending order or `False` for sorting in descending order). And they build a new query template without parameters where sorting is performed or a comment on the use of sorting is added to the result. To use the resulting query template, call the `PROCESS` function, since, when using a `SELECT`, sorting is ignored. #### Examples ```yql DEFINE SUBQUERY $sub() as SELECT * FROM (VALUES (1,'c'), (1,'a'), (3,'b')) AS a(x,y); end define; $sub2 = SubqueryOrderBy($sub, [('x',false), ('y',true)]); PROCESS $sub2(); ``` # USE Specifying the "database". As a rule, one of the YTsaurus clusters is used as a database. This database will be used by default to search for tables whenever the database hasn't been specified explicitly. If the query doesn't include `USE`, then the cluster must be specified at the beginning of the table path in the format ``` cluster.`path/to/table` ``` , for example ``` cluster.`home/yql/test` ```. Backticks are used to automatically escape special characters (in this case, slashes). Usually the cluster name is specified explicitly, but you can use an expression for it. In this case, `USE` must have the notation ```USE yt:$cluster_name```, where `$cluster_name` is the [named expression](https://ytsaurus.tech/docs/en/expressions.md#named-nodes) of the `String` type. Alternatively, you can specify the cluster right at the beginning of the table path in the format ``` yt:$cluster_name.`path/to/table` ```. In this case, `USE` itself can be used inside [actions](https://ytsaurus.tech/docs/en/action.md) or [subquery templates](https://ytsaurus.tech/docs/en/subquery.md). The current cluster value is inherited in the declarations of nested actions or subqueries. The influence area of `USE` stops after the end of the action or subquery template in which it was declared. #### Examples ```yql USE ; ``` {% note info "Note" %} `USE` **doesn't** guarantee that a query will be executed on the specified cluster. A query can be executed on another cluster if no input data are used in it (e.g. ```USE foo; SELECT 2 + 2;```) or if a full path to a table in another cluster is specified (e.g. ``` USE foo; SELECT * FROM bar.`path/to/table`; ```). {% endnote %} # Basic VALUES syntax in YQL ## VALUES as a top-level operator It lets you create a table from specified values. For example, this statement creates a table of k columns and n rows: ```yql VALUES (expr_11, expr_12, ..., expr_1k), (expr_21, expr_22, ..., expr_2k), .... (expr_n1, expr_n2, ..., expr_nk); ``` This statement is totally equivalent to the following one: ```yql SELECT expr_11, expr_12, ..., expr_1k UNION ALL SELECT expr_21, expr_22, ..., expr_2k UNION ALL .... SELECT expr_n1, expr_n2, ..., expr_nk; ``` #### Example ```yql VALUES (1,2), (3,4); ``` ## VALUES after FROM VALUES can also be used in a subquery, after [FROM](https://ytsaurus.tech/docs/en/select/from.md). For example, the following two queries are equivalent: ```yql VALUES (1,2), (3,4); SELECT * FROM (VALUES (1,2), (3,4)); ``` In all examples above, column names are assigned by YQL as `column0 ... columnN`. To assign arbitrary column names, you can use the following construct: ```yql SELECT * FROM (VALUES (1,2), (3,4)) as t(x,y); ``` In this case, the columns will get the names `x`, `y`. # OVER, PARTITION BY, and WINDOW Window functions were introduced in the SQL:2003 standard and expanded in the SQL:2011 standard. They let you run calculations on a set of table rows that are related to the current row in some way. Unlike [aggregate functions](https://ytsaurus.tech/docs/builtins/aggregation.md), using window functions doesn't cause rows to become grouped into a single output row: the number of rows in the results table always matches that in the source table. If a query contains both aggregate and window functions, grouping is performed and aggregate function values are calculated first. The calculated values of aggregate functions can be used as window function arguments (but not the other way around). The order for calculating window functions in relation to other query elements is described in the [SELECT](https://ytsaurus.tech/docs/en/select/index.md) section. ## Syntax {#syntax} General syntax for calling a window function is as follows ```yql function_name([expression [, expression ...]]) OVER (window_definition) ``` or ```yql function_name([expression [, expression ...]]) OVER window_name ``` Here, `window_name` (*window name*) is an arbitrary ID that is unique within the query and `expression` is an arbitrary expression that contains no window function calls. In the query, each window name must be mapped to the *window definition* (`window_definition`): ```yql SELECT F0(...) OVER (window_definition_0), F1(...) OVER w1, F2(...) OVER w2, ... FROM my_table WINDOW w1 AS (window_definition_1), ... w2 AS (window_definition_2) ; ``` Here, the `window_definition` is written as ```antlr [ PARTITION BY (expression AS column_identifier | column_identifier) [, ...] ] [ ORDER BY expression [ASC | DESC] ] [ frame_definition ] ``` You can set an optional *frame definition* (`frame_definition`) in one of the following ways: * ```ROWS frame_begin``` * ```ROWS BETWEEN frame_begin AND frame_end``` * ```RANGE frame_begin``` * ```RANGE BETWEEN frame_begin AND frame_end``` {% note info %} `RANGE` mode is available starting with language version 2026.01. In Query Tracker, select language version 2026.01 or later to use `RANGE` frames. {% endnote %} The *frame start* (`frame_begin`) and *frame end* (`frame_end`) are set one of the following ways: * ```UNBOUNDED PRECEDING``` * ```offset PRECEDING``` * ```CURRENT ROW``` * ```offset FOLLOWING``` * ```UNBOUNDED FOLLOWING``` Here, *frame offset* (`offset`) is a non-negative literal. If the frame end isn't set, `CURRENT ROW` is assumed. In `ROWS` mode, the offset is always an integer. In `RANGE` mode with an offset, `ORDER BY` must contain exactly one column; the offset type is determined by that column and must support addition, subtraction, and comparison with it. Numeric columns, including `Decimal`, use a numeric offset; date and time columns use `Interval` or `Interval64`. PostgreSQL types are supported similarly. `NaN` and `Inf` values are not allowed. There should be no window function calls in any of the expressions inside the window definition. ## Calculation algorithm ### Partitioning {#partition} If `PARTITION BY` is set, the source table rows are grouped into *partitions*, which are then handled independently of each other. If `PARTITION BY` isn't set, all rows in the source table are put in the same partition. If `ORDER BY` is set, it determines the order of rows in a partition. Both in `PARTITION BY` and [GROUP BY](https://ytsaurus.tech/docs/en/group_by.md) you can use aliases and [SessionWindow](https://ytsaurus.tech/docs/en/group_by.md#session-window). If `ORDER BY` is omitted, the order of rows in the partition is undefined. ### Frame {#frame} The `frame_definition` specifies a set of partition rows that fall into the *window frame* associated with the current row. In `ROWS` mode, the window frame contains rows with the specified offsets relative to the current row in the partition. For example, `ROWS BETWEEN 3 PRECEDING AND 5 FOLLOWING` contains three preceding rows, the current row, and five following rows. In `RANGE` mode with an offset, the range is based on values of the single `ORDER BY` column rather than a number of rows. For example, `RANGE BETWEEN 3 PRECEDING AND 5 FOLLOWING` includes rows whose value is between “current minus 3” and “current plus 5”. `RANGE CURRENT ROW` includes all rows with the same sort value. `UNBOUNDED PRECEDING` and `UNBOUNDED FOLLOWING` have the same meaning as in `ROWS` mode. The set of rows in the window frame may change depending on which row is the current one. For example, for the first row in the partition, the ROWS `BETWEEN 3 PRECEDING AND 1 PRECEDING` window frame will have no rows. Setting `UNBOUNDED PRECEDING` as the frame start means "from the first partition row" and `UNBOUNDED FOLLOWING` as the frame end — "up to the last partition row". Setting `CURRENT ROW` means "from/to the current row". If no `frame_definition` is specified, a set of rows to be included in the window frame depends on whether there is `ORDER BY` in the `window_definition`. Namely, if there is `ORDER BY`, then `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` is implicitly assumed. If none, then `ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING`. Further, depending on the specific window function, it's calculated either based on the set of rows in the partition or the set of rows in the window frame. [List of available window functions](https://ytsaurus.tech/docs/builtins/window.md) ```yql SELECT ts, AVG(value) OVER w AS moving_avg FROM my_table WINDOW w AS ( ORDER BY ts RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING ); ``` #### Examples ```yql SELECT COUNT(*) OVER w AS rows_count_in_window, some_other_value -- access to the current row FROM `my_table` WINDOW w AS ( PARTITION BY partition_key_column ORDER BY int_column ); ``` ```yql SELECT LAG(my_column, 2) OVER w AS row_before_previous_one FROM `my_table` WINDOW w AS ( PARTITION BY partition_key_column ); ``` ```yql SELECT -- AVG (like all aggregate functions used as window functions) -- calculated on a window frame AVG(some_value) OVER w AS avg_of_prev_current_next, some_other_value -- access to the current row FROM my_table WINDOW w AS ( PARTITION BY partition_key_column ORDER BY int_column ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING ); ``` ```yql SELECT -- LAG doesn't depend on the window frame position LAG(my_column, 2) OVER w AS row_before_previous_one FROM my_table WINDOW w AS ( PARTITION BY partition_key_column ORDER BY my_column ); ``` ## Implementation specifics * Functions on `ROWS/RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING` or `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` run in O(partition size) without extra memory. `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` requires memory to buffer rows with equal `ORDER BY` values. * For a `ROWS/RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING` frame, a `COMPACT` [hint](https://ytsaurus.tech/docs/en/lexer.md#sql-hints) can be specified after `PARTITION` to select the in-memory strategy. For example: `PARTITION /*+ COMPACT() */ BY key` or `PARTITION /*+ COMPACT() */ BY ()` (if `PARTITION BY` was initially absent). If the `COMPACT` hint is specified, this requires additional memory equal to O(partition size), but then no extra `JOIN` operation is made. * If the window frame doesn't start with `UNBOUNDED PRECEDING`, calculating window functions on this window requires additional memory equal to O(the maximum number of rows from the window boundaries to the current row), while the computation time is equal to O(number_of_partition_rows * window_size). * For a frame starting with `UNBOUNDED PRECEDING` and ending with `N`, where `N` is neither `CURRENT ROW` nor `UNBOUNDED FOLLOWING`, O(N) extra memory is required and computation takes O(partition size). * The `LEAD(expr, N)` and `LAG(expr, N)` functions always require O(N) of RAM. If possible, change `ROWS/RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING` to `ROWS/RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` by reversing the `ORDER BY` sorting order. In terms of MapReduce, window functions are physically executed through Reduce, with `PARTITION BY` keys, which can mean lengthy execution for big sections, as well as a strict 200 GB per section limit for the main clusters YTsaurus. # Classic SQL constructs not supported yet ## NATURAL JOIN {#natural-join} An alternative is to explicitly list the matching columns on both sides. ## NOW() / CURRENT_TIME() {#now} An alternative is to use the functions [CurrentUtcDate, CurrentUtcDatetime and CurrentUtcTimestamp](https://ytsaurus.tech/docs/builtins/basic.md#current-utc). # Built-in YQL functions - [Basic](https://ytsaurus.tech/docs/en/basic.md) - [Aggregate](https://ytsaurus.tech/docs/en/aggregation.md) - [Window](https://ytsaurus.tech/docs/en/window.md) - [For working with lists](https://ytsaurus.tech/docs/en/list.md) - [For working with dicts](https://ytsaurus.tech/docs/en/dict.md) - [For working with structures](https://ytsaurus.tech/docs/en/struct.md) - [For working with types](https://ytsaurus.tech/docs/en/types.md) - [For working with code generation](https://ytsaurus.tech/docs/en/codegen.md) - [For working with JSON](https://ytsaurus.tech/docs/en/json.md) - [C++ libraries](https://ytsaurus.tech/docs/udf/list/index.md) # Basic built-in functions Below are the general-purpose functions. For specialized functions, there are separate articles: [aggregate](https://ytsaurus.tech/docs/en/aggregation.md), [window](https://ytsaurus.tech/docs/en/window.md), as well as for [lists](https://ytsaurus.tech/docs/en/list.md), [dictionaries](https://ytsaurus.tech/docs/en/dict.md), [structures](https://ytsaurus.tech/docs/en/struct.md), [data types](https://ytsaurus.tech/docs/en/types.md), and [code generation](https://ytsaurus.tech/docs/en/codegen.md). ## COALESCE {#coalesce} Goes through the arguments from left to right and returns the first non-empty argument found. For the result to be guaranteed non-empty (not [optional type](https://ytsaurus.tech/docs/types/optional.md)), the rightmost argument must be of this type (a literal is often used). If there is one argument, returns it without change. #### Signature ```yql COALESCE(T?, ..., T)->T COALESCE(T?, ..., T?)->T? ``` Enables you to pass potentially empty values to functions that cannot process them. You can use the `??` operator as a shorter alternative. The `NVL` alias is also supported. #### Examples ```yql SELECT COALESCE( maybe_empty_column, "it's empty!" ) FROM my_table; ``` ```yql SELECT maybe_empty_column ?? "it's empty!" FROM my_table; ``` ```yql SELECT NVL( maybe_empty_column, "it's empty!" ) FROM my_table; ``` All three examples above are equivalent. ## LENGTH {#length} Returns the length of the string in bytes. This function is also available under the `LEN` name . #### Signature ```yql LENGTH(T)->Uint32 LENGTH(T?)->Uint32? ``` #### Examples ```yql SELECT LENGTH("foo"); ``` ```yql SELECT LEN("bar"); ``` {% note info %} You can use the [Unicode::GetLength](https://ytsaurus.tech/docs/udf/list/unicode.md) function to calculate the length of a string in Unicode characters. To get the number of items in a list, use the [ListLength](https://ytsaurus.tech/docs/en/list.md#listlength) function. {% endnote %} ## SUBSTRING {#substring} Returns a substring. #### Signature ```yql Substring(String[, Uint32? [, Uint32?]])->String Substring(String?[, Uint32? [, Uint32?]])->String? ``` Mandatory arguments: * Source string; * Position: The offset from the beginning of the string in bytes (integer) or `NULL` meaning "from the beginning". Optional arguments: * Substring length: The number of bytes starting from the specified position (an integer, or the default `NULL` meaning "up to the end of the source string"). Indexing starts from zero. If the specified position and length are beyond the string, returns an empty string. If the input string is optional, the result is also optional. #### Examples ```yql SELECT SUBSTRING("abcdefg", 3, 1); -- d ``` ```yql SELECT SUBSTRING("abcdefg", 3); -- defg ``` ```yql SELECT SUBSTRING("abcdefg", NULL, 3); -- abc ``` ## FIND {#find} Finding the position of a substring in a string. #### Signature ```yql Find(String, String[, Uint32?])->Uint32? Find(String?, String[, Uint32?])->Uint32? Find(Utf8, Utf8[, Uint32?])->Uint32? Find(Utf8?, Utf8[, Uint32?])->Uint32? ``` Mandatory arguments: * Source string; * The substring being searched for. Optional arguments: * A position in bytes to start the search with (an integer or `NULL` by default that means "from the beginning of the source string"). Returns the first substring position found or `NULL` (meaning that the desired substring hasn't been found starting from the specified position). #### Examples ```yql SELECT FIND("abcdefg_abcdefg", "abc"); -- 0 ``` ```yql SELECT FIND("abcdefg_abcdefg", "abc", 1); -- 8 ``` ```yql SELECT FIND("abcdefg_abcdefg", "abc", 9); -- null ``` ## RFIND {#rfind} Reverse finding the position of a substring in a string, from the end to the beginning. #### Signature ```yql RFind(String, String[, Uint32?])->Uint32? RFind(String?, String[, Uint32?])->Uint32? RFind(Utf8, Utf8[, Uint32?])->Uint32? RFind(Utf8?, Utf8[, Uint32?])->Uint32? ``` Mandatory arguments: * Source string; * The substring being searched for. Optional arguments: * A position in bytes to start the search with (an integer or `NULL` by default, meaning "from the end of the source string"). Returns the first substring position found or `NULL` (meaning that the desired substring hasn't been found starting from the specified position). #### Examples ```yql SELECT RFIND("abcdefg_abcdefg", "bcd"); -- 9 ``` ```yql SELECT RFIND("abcdefg_abcdefg", "bcd", 8); -- 1 ``` ```yql SELECT RFIND("abcdefg_abcdefg", "bcd", 0); -- null ``` ## StartsWith, EndsWith {#starts_ends_with} Checking for a prefix or suffix in a string. #### Signatures ```yql StartsWith(T str, U prefix)->Bool[?] EndsWith(T str, U suffix)->Bool[?] ``` Mandatory arguments: * Source string; * The substring being searched for. Arguments must be of the `String`/`Utf8` (or optional `String`/`Utf8`) or string PostgreSQL (`PgText`/`PgBytea`/`PgVarchar`) type. The function outputs an optional Bool (except when both arguments are non-optional — in this case, it returns a Bool). #### Examples ```yql SELECT StartsWith("abc_efg", "abc") AND EndsWith("abc_efg", "efg"); -- true ``` ```yql SELECT StartsWith("abc_efg", "efg") OR EndsWith("abc_efg", "abc"); -- false ``` ```yql SELECT StartsWith("abcd", NULL); -- null ``` ```yql SELECT EndsWith(NULL, Utf8("")); -- null ``` ```yql SELECT StartsWith("abc_efg"u, "abc"p) AND EndsWith("abc_efg", "efg"pv); -- true ``` ## IF {#if} Checks the condition: `IF(condition_expression, then_expression, else_expression)`. It's a simplified alternative for [CASE WHEN ... THEN ... ELSE ... END](https://ytsaurus.tech/docs/syntax/expressions.md#case). #### Signature ```yql IF(Bool, T, T)->T IF(Bool, T)->T? ``` You may omit the `else_expression` argument. In this case, if the condition is false (`condition_expression` returned `false`), an empty value is returned with the type corresponding to `then_expression` and allowing for `NULL`. Hence, the result will have an [optional data type](https://ytsaurus.tech/docs/types/optional.md). #### Examples ```yql SELECT IF(foo > 0, bar, baz) AS bar_or_baz, IF(foo > 0, foo) AS only_positive_foo FROM my_table; ``` ## NANVL {#nanvl} Replaces the values `NaN` (not a number) in the expressions that have the type `Float`, `Double`, or [Optional](https://ytsaurus.tech/docs/types/optional.md). #### Signature ```yql NANVL(Float, Float)->Float NANVL(Double, Double)->Double ``` Arguments: 1. The expression where you want to make a replacement. 2. The value to replace `NaN`. If one of the arguments is `Double`, the result `isDouble`, otherwise, it's `Float`. If one of the arguments is `Optional`, then the result is `Optional`. #### Examples ```yql SELECT NANVL(double_column, 0.0) FROM my_table; ``` ## Random... {#random} Generates a pseudorandom number: * `Random()`: A floating point number (Double) from 0 to 1. * `RandomNumber()`: An integer from the complete Uint64 range. * `RandomUuid()` — [Uuid version 4](https://tools.ietf.org/html/rfc4122#section-4.4). #### Signatures ```yql Random(T1[, T2, ...])->Double RandomNumber(T1[, T2, ...])->Uint64 RandomUuid(T1[, T2, ...])->Uuid ``` No arguments are used for random number generation: they are only needed to control the time of the call. A new random number is returned at each call. Therefore: * If Random is called again within a **same query** and with a same set of arguments, the same set of random numbers is returned. Keep in mind that we mean the arguments themselves (i.e., the text between parentheses) rather than their values. * Calling of Random with the same set of arguments in **different** queries returns different sets of random numbers. {% note warning %} If Random is used in [named expressions](https://ytsaurus.tech/docs/syntax/expressions.md#named-nodes), one-time calculation isn't guaranteed for it. Depending on the optimizers and the execution environment, it can be calculated either once or many times. To guarantee one-time calculation in this case, materialize your named expression into a table. {% endnote %} Use cases: * `SELECT RANDOM(1);`: Get one random value for the entire query and use it multiple times (to get multiple random values, you can pass various constants of any type). * `SELECT RANDOM(1) FROM table;`: The same random number for each row in the table. * `SELECT RANDOM(1), RANDOM(2) FROM table;`: Two random numbers for each row of the table, all the numbers in each of the columns are the same. * `SELECT RANDOM(some_column) FROM table;`: Different random numbers for each row in the table. * `SELECT RANDOM(some_column), RANDOM(some_column) FROM table;`: Different random numbers for each row of the table, but two identical numbers within the same row. * `SELECT RANDOM(some_column), RANDOM(some_column + 1) FROM table;` or `SELECT RANDOM(some_column), RANDOM(other_column) FROM table;`: Two columns, with different numbers in both. #### Examples ```yql SELECT Random(key) -- [0, 1) FROM my_table; ``` ```yql SELECT RandomNumber(key) -- [0, Max) FROM my_table; ``` ```yql SELECT RandomUuid(key) -- Uuid version 4 FROM my_table; ``` ```yql SELECT RANDOM(column) AS rand1, RANDOM(column) AS rand2, -- same as rand1 RANDOM(column, 1) AS randAnd1, -- different from rand1/2 RANDOM(column, 2) AS randAnd2 -- different from randAnd1 FROM my_table; ``` ## Udf {#udf} Builds a `Callable` object by the specified function name and optional `external user types`, `RunConfig`, and `TypeConfig`. * `Udf(Foo::Bar)`: The `Foo::Bar` function without any extra parameters. * `Udf(Foo::Bar)(1, 2, 'abc')` — Calling the UDF `Foo::Bar`. * `Udf(Foo::Bar, Int32, @@{"device":"AHCI"}@@ as TypeConfig")(1, 2, 'abc')`: Calling the `Foo::Bar` user-defined function with an additional `Int32` type and a specified `TypeConfig`. * `Udf(Foo::Bar, "1e9+7" as RunConfig")(1, 'extended' As Precision)`: Calling the `Foo::Bar` user-defined function with the specified `RunConfig` and named parameters. * `Udf(Foo::Bar, $parent as Depends)`: Calling the `Foo::Bar` user-defined function indicating the dependency of the calculation on a given node (starting with version [2025.03](https://ytsaurus.tech/docs/changelog/2025.03.md)). Additional settings may be passed as named arguments whose values are strings or numeric literals: * `Cpu`: A positive factor describing the CPU consumed by the UDF. The default is "1". A larger value requires greater parallelism. For example: "4.5". * `ExtraMem`: Additional memory in bytes. The default is "0". Specify either a non-negative integer number of bytes or a string with a `K`, `M`, or `G` suffix, such as "512K", "2048M", or "1G". #### Signatures ```yql Udf(Callable[, T1, T2, ..., T_N][, V1 as TypeConfig][,V2 as RunConfig]])->Callable ``` Where `T1`, `T2`, etc. are the custom `external` types. #### Examples ```yql $IsoParser = Udf(DateTime2::ParseIso8601); SELECT $IsoParser("2022-01-01"); ``` ```yql SELECT Udf(Unicode::IsUtf)("2022-01-01") ``` ```yql $config = @@{ "name":"MessageFoo", "meta": "..." }@@; SELECT Udf(Protobuf::TryParse, $config As TypeConfig)("") ``` ```yql SELECT Udf(Foo::Bar, 4.5 AS Cpu, 100000000 AS ExtraMem)(1); ``` ```yql SELECT Udf(Foo::Bar, "4.5" AS Cpu, "100M" AS ExtraMem)(1); ``` ## CurrentUtc... {#current-utc} `CurrentUtcDate()`, `CurrentUtcDatetime()` and `CurrentUtcTimestamp()`: Getting the current date and/or time in UTC. The result data type is specified at the end of the function name. #### Signatures ```yql CurrentUtcDate(...)->Date CurrentUtcDatetime(...)->Datetime CurrentUtcTimestamp(...)->Timestamp ``` The arguments are optional and work the same as [RANDOM](https://ytsaurus.tech/docs/en/#random). #### Examples ```yql SELECT CurrentUtcDate(); ``` ```yql SELECT CurrentUtcTimestamp(TableRow()) FROM my_table; ``` ## CurrentTz... {#current-tz} `CurrentTzDate()`, `CurrentTzDatetime()`, and `CurrentTzTimestamp()`: Get the current date and/or time in the [IANA time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) specified in the first argument. The result data type is specified at the end of the function name. #### Signatures ```yql CurrentTzDate(String, ...)->TzDate CurrentTzDatetime(String, ...)->TzDatetime CurrentTzTimestamp(String, ...)->TzTimestamp ``` The arguments that follow are optional and work the same as [RANDOM](https://ytsaurus.tech/docs/en/#random). #### Examples ```yql SELECT CurrentTzDate("Europe/Moscow"); ``` ```yql SELECT CurrentTzTimestamp("Europe/Moscow", TableRow()) FROM my_table; ``` ## AddTimezone Adding the time zone information to the date/time in UTC. In the result of `SELECT` or after `CAST`, a `String` will be subject to the time zone rules used to calculate the time offset. #### Signature ```yql AddTimezone(Date, String)->TzDate AddTimezone(Date?, String)->TzDate? AddTimezone(Datetime, String)->TzDatetime AddTimezone(Datetime?, String)->TzDatetime? AddTimezone(Timestamp, String)->TzTimestamp AddTimezone(Timestamp?, String)->TzTimestamp? ``` Arguments: 1. Date: the type is `Date`/`Datetime`/`Timestamp`. 2. [IANA name of the time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Result type: `TzDate`/`TzDatetime`/`TzTimestamp`, depending on the input data type. #### Examples ```yql SELECT AddTimezone(Datetime("2018-02-01T12:00:00Z"), "Europe/Moscow"); ``` ## RemoveTimezone Removing the time zone data and converting the value to date/time in UTC. #### Signature ```yql RemoveTimezone(TzDate)->Date RemoveTimezone(TzDate?)->Date? RemoveTimezone(TzDatetime)->Datetime RemoveTimezone(TzDatetime?)->Datetime? RemoveTimezone(TzTimestamp)->Timestamp RemoveTimezone(TzTimestamp?)->Timestamp? ``` Arguments: 1. Date: the type is `TzDate`/`TzDatetime`/`TzTimestamp`. Result type: `Date`/`Datetime`/`Timestamp`, depending on the input data type. #### Examples ```yql SELECT RemoveTimezone(TzDatetime("2018-02-01T12:00:00,Europe/Moscow")); ``` ## Version {#version} `Version()` returns a string describing the current version of the node processing the query. In some cases, such as during rolling cluster upgrades, it might return different strings depending on which node is processing the query. The function doesn't accept any arguments. #### Examples ```yql SELECT Version(); ``` ## CurrentLanguageVersion {#current-language-version} `CurrentLanguageVersion()` returns a string describing the current version (if defined) of the language selected for the current query or an empty string. #### Examples ```yql SELECT CurrentLanguageVersion(); ``` ## MAX_OF, MIN_OF, GREATEST, and LEAST {#max-min} Returns the minimum or maximum among N arguments. Those functions let you replace the SQL standard statement `CASE WHEN a < b THEN a ELSE b END` that would be too verbose for N more than two. #### Signatures ```yql MIN_OF(T[,T,...})->T MAX_OF(T[,T,...})->T ``` The argument types must be mutually castable and accept `NULL`. `GREATEST` is a synonym for `MAX_OF` and `LEAST` is a synonym for `MIN_OF`. #### Examples ```yql SELECT MIN_OF(1, 2, 3); ``` ## AsTuple, AsStruct, AsList, AsDict, AsSet, AsListStrict, AsDictStrict and AsSetStrict {#as-container} Creates containers of the appropriate types. [Operator writing](https://ytsaurus.tech/docs/en/#containerliteral) of container literals is also available. Features: * Container items are passed through arguments, so the number of items in the resulting container is equal to the number of passed arguments, except when the dict keys are repeated. * In `AsTuple` and `AsStruct`, can be invoked without arguments and arguments can have different types. * Field names in `AsStruct` are set via `AsStruct(field_value AS field_name)`. * You need at least one argument to create a list if you need to output item types. To create an empty list with the specified item type, use the [ListCreate](https://ytsaurus.tech/docs/en/list.md#listcreate) function. You can create an empty list as an `AsList()` invocation without arguments. In this case, this expression will have the `EmptyList` type. * You need at least one argument to create a dict if you need to output item types. To create an empty dict with the specified item type, use the [DictCreate](https://ytsaurus.tech/docs/en/dict.md#dictcreate) function. You can create an empty dict as an `AsDict()` invocation without arguments. In this case, this expression will have the `EmptyDict` type. * You need at least one argument to create a set if you need to output item types. To create an empty set with the specified item type, use the [SetCreate](https://ytsaurus.tech/docs/en/dict.md#setcreate) function. You can create an empty set as an `AsSet()` invocation without arguments. In this case, this expression will have the `EmptyDict` type. * `AsList` outputs the general type of list items. A type error is generated if there are incompatible types. * `AsDict` outputs the general types of keys and values separately. A type error is generated if there are incompatible types. * `AsSet` outputs the general types of keys. A type error is generated if there are incompatible types. * `AsListStrict`, `AsDictStrict`, and `AsSetStrict` require the same type for arguments. * In `AsDict` and `AsDictStrict`, `Tuple` of two items (key and value) is expected as arguments. If keys are repeated, only the value for the first key remains in the dict. * In `AsSet` and `AsSetStrict`, keys are expected as arguments. #### Examples ```yql SELECT AsTuple(1, 2, "3") AS `tuple`, AsStruct( 1 AS a, 2 AS b, "3" AS c ) AS `struct`, AsList(1, 2, 3) AS `list`, AsDict( AsTuple("a", 1), AsTuple("b", 2), AsTuple("c", 3) ) AS `dict`, AsSet(1, 2, 3) AS `set` ``` ## Container literals {#containerliteral} For some containers, an operator format of their literal values is possible: * Tuple: `(value1, value2...)`. * Structure: `<|name1: value1, name2: value2...|>`. * List: `[value1, value2,...]`. * Dict: `{key1: value1, key2: value2...}`. * Set: `{key1, key2...}`. An insignificant trailing comma is allowed in all cases. This comma is mandatory for a tuple with one item: `(value1,)`. For field names in the structure literal, you can use an expression that can be counted in evaluation time, for example, string literals, as well as identifiers (including in backticks). For a list inside, the [AsList](https://ytsaurus.tech/docs/en/#as-container) function is used, for a dict — the [AsDict](https://ytsaurus.tech/docs/en/#as-container) function, for a set — the [AsSet](https://ytsaurus.tech/docs/en/#as-container) function, for a tuple — the [AsTuple](https://ytsaurus.tech/docs/en/#as-container) function, and for a structure — the [AsStruct](https://ytsaurus.tech/docs/en/#as-container) function. #### Examples ```yql $name = "computed " || "member name"; SELECT (1, 2, "3") AS `tuple`, <| `complex member name`: 2.3, b: 2, $name: "3", "inline " || "computed member name": false |> AS `struct`, [1, 2, 3] AS `list`, { "a": 1, "b": 2, "c": 3, } AS `dict`, {1, 2, 3} AS `set` ``` ## Variant {#variant} `Variant()` creates a variant value over a tuple or structure. #### Signature ```yql Variant(T, String, Type>)->Variant<...> ``` Arguments: * Value * String with a field name or tuple index * Variant type #### Example ```yql $var_type = Variant; SELECT Variant(6, "foo", $var_type) as Variant1Value, Variant(false, "bar", $var_type) as Variant2Value; ``` ## AsVariant {#asvariant} `AsVariant()` creates a value of a [variant on a structure](https://ytsaurus.tech/docs/types/containers.md) with one field. This value can be implicitly converted to any variant over a structure that has a matching data type for this field name and might include more fields with other names. #### Signature ```yql AsVariant(T, String)->Variant ``` Arguments: * Value * A string with the field name #### Example ```yql SELECT AsVariant(6, "foo") as VariantValue ``` ## Visit, VisitOrDefault {#visit} Processes possible values of a variant represented by a structure or tuple using provided handler functions for each field or item. #### Signature ```yql Visit(Variant, K1->R AS key1, K2->R AS key2, ...)->R Visit(Variant, K1->R, K2->R, ...)->R VisitOrDefault(Variant{Flags:AutoMap}, R, [K1->R, [K2->R, ...]])->R VisitOrDefault(Variant{Flags:AutoMap}, R, [K1->R AS key1, [K2->R AS key2, ...]])->R ``` #### Arguments * When the variant is represented by a structure: accepts the variant as the positional argument and one named argument (handler) for each field of the structure. * When the variant is represented by a tuple: accepts the variant itself and one handler for each item in the tuple as positional arguments. * The `VisitOrDefault` modification accepts an additional positional argument (the second one), which is the default value and which enables you to omit certain handlers. #### Example ```yql $vartype = Variant; $handle_num = ($x) -> { return 2 * $x; }; $handle_flag = ($x) -> { return If($x, 200, 10); }; $handle_str = ($x) -> { return Unwrap(CAST(LENGTH($x) AS Int32)); }; $visitor = ($var) -> { return Visit($var, $handle_num AS num, $handle_flag AS flag, $handle_str AS str); }; SELECT $visitor(Variant(5, "num", $vartype)), -- 10 $visitor(Just(Variant(True, "flag", $vartype))), -- Just(200) $visitor(Just(Variant("somestr", "str", $vartype))), -- Just(7) $visitor(Nothing(OptionalType($vartype))), -- Nothing(Optional) $visitor(NULL) -- NULL ; ``` ## VariantItem {#variantitem} Returns the value of a homogeneous variant (a variant containing fields or items of the same type). #### Signature ```yql VariantItem(Variant{Flags:AutoMap})->K VariantItem(Variant{Flags:AutoMap})->K ``` #### Example ```yql $vartype1 = Variant; SELECT VariantItem(Variant(7, "num2", $vartype1)), -- 7 VariantItem(Just(Variant(5, "num1", $vartype1))), -- Just(5) VariantItem(Nothing(OptionalType($vartype1))), -- Nothing(Optional) VariantItem(NULL) -- NULL ; ``` ## Way {#way} Returns an active field (active index) of a variant over structure (tuple). #### Signature ```yql Way(Variant{Flags:AutoMap})->Utf8 Way(Variant{Flags:AutoMap})->Uint32 ``` #### Example ```yql $vr = Variant(1, "0", Variant); $vrs = Variant(1, "a", Variant); SELECT Way($vr); -- 0 SELECT Way($vrs); -- "a" ``` ## DynamicVariant {#dynamic_variant} Creates an instance of a homogeneous variant (a variant containing fields or items of the same type), where the variant index or field can be set dynamically. If the index or field name does not exist, returns `NULL`. The reverse function is [VariantItem](https://ytsaurus.tech/docs/en/#variantitem). #### Signature ```yql DynamicVariant(item:T,index:Uint32?,Variant)->Optional> DynamicVariant(item:T,index:Utf8?,Variant)->Optional> ``` #### Example ```yql $dt = Int32; $tvt = Variant<$dt,$dt>; SELECT ListMap([(10,0u),(20,2u),(30,NULL)],($x)->(DynamicVariant($x.0,$x.1,$tvt))); -- [0: 10,NULL,NULL] $dt = Int32; $svt = Variant; SELECT ListMap([(10,'x'u),(20,'z'u),(30,NULL)],($x)->(DynamicVariant($x.0,$x.1,$svt))); -- [x: 10,NULL,NULL] ``` ## Enum {#enum} `Enum()` creates an enumeration value. #### Signature ```yql Enum(String, Type>)->Enum<...> ``` Arguments: * A string with the field name * Enumeration type #### Example ```yql $enum_type = Enum; SELECT Enum("Foo", $enum_type) as Enum1Value, Enum("Bar", $enum_type) as Enum2Value; ``` ## AsEnum {#asenum} `AsEnum()` creates an [enumeration](https://ytsaurus.tech/docs/types/containers.md) value with one element. This value can be implicitly cast to any enumeration containing such a name. #### Signature ```yql AsEnum(String)->Enum<'tag'> ``` Arguments: * A string with the name of an enumeration item #### Example ```yql SELECT AsEnum("Foo"); ``` ## AsTagged, Untag {#as-tagged} Wraps a value in [Tagged data type](https://ytsaurus.tech/docs/types/special.md) with the specified tag preserving the physical data type. `Untag`: Reverse operation. #### Signature ```yql AsTagged(T, tagName:String)->Tagged AsTagged(T?, tagName:String)->Tagged? Untag(Tagged)->T Untag(Tagged?)->T? ``` Mandatory arguments: 1. Value of an arbitrary type. 2. Tag name. Returns a copy of the value from the first argument with the specified tag in the data type. Examples of use cases: * Returning to the client to display media files of base64-encoded strings in the web interface. * Protecting at the boundaries of the UDF call against the transfer of incorrect values. * Additional clarifications at the returned column type level. ## TablePath {#tablepath} Access to the current table name, which might be needed when using [CONCAT](https://ytsaurus.tech/docs/syntax/select/concat.md#concat), [RANGE](https://ytsaurus.tech/docs/syntax/select/concat.md#range), and other related mechanisms. #### Signature ```yql TablePath()->String ``` No arguments. Returns a string with the full path or an empty string and warning when used in an unsupported context (for example, when working with a subquery or a range of 1000+ tables). {% note info %} The [TablePath](https://ytsaurus.tech/docs/en/#tablepath), [TableName](https://ytsaurus.tech/docs/en/#tablename), and [TableRecordIndex](https://ytsaurus.tech/docs/en/#tablerecordindex) functions do no support temporary and anonymous tables (they return an empty string or 0 for [TableRecordIndex](https://ytsaurus.tech/docs/en/#tablerecordindex)). These functions are calculated when the `SELECT` projection is [executed](https://ytsaurus.tech/docs/syntax/select/index.md#selectexec), and the current table might already be temporary at that point. To avoid such a situation, create a subquery for calculating these functions, as shown in the second example below. {% endnote %} #### Examples ```yql SELECT TablePath() FROM CONCAT(table_a, table_b); ``` ```yql SELECT key, tpath_ AS path FROM (SELECT a.*, TablePath() AS tpath_ FROM RANGE(`my_folder`) AS a) WHERE key IN $subquery; ``` ## TableName {#tablename} Get the table name based on the table path. You can obtain the path using the [TablePath](https://ytsaurus.tech/docs/en/#tablepath) function or as the `Path` column when using the table function [FOLDER](https://ytsaurus.tech/docs/syntax/select/folder.md). #### Signature ```yql TableName()->String TableName(String)->String TableName(String, String)->String ``` Optional arguments: * Path to the table, `TablePath()` is used by default (see also its limitations). * Specifying the system ("yt") whose rules are used to determine the table name. You need to specify the system only if [USE](https://ytsaurus.tech/docs/syntax/use.md) doesn't specify the current cluster. #### Examples ```yql USE hahn; SELECT TableName() FROM CONCAT(table_a, table_b); ``` ```yql SELECT TableName(Path, "yt") FROM cluster.FOLDER(folder_name); ``` ## TableRecordIndex {#tablerecordindex} Access to the current sequence number of a row in the physical source table, **starting from 1** (depends on the storage implementation). #### Signature ```yql TableRecordIndex()->Uint64 ``` No arguments. When used together with [CONCAT](https://ytsaurus.tech/docs/syntax/select/concat.md#concat), [RANGE](https://ytsaurus.tech/docs/syntax/select/concat.md#range), and other related mechanisms, numbering is reset for each input table. If used in an incorrect context, it returns 0. #### Example ```yql SELECT TableRecordIndex() FROM my_table; ``` ## TableRow, JoinTableRow {#tablerow} Getting the entire table row as a structure. No arguments. `JoinTableRow` in case of `JOIN` always returns a structure with table prefixes. #### Signature ```yql TableRow()->Struct ``` #### Example ```yql SELECT TableRow() FROM my_table; ``` ## FileContent and FilePath {#file-content-path} You can attach arbitrary named files to your query both in the console and web interfaces. With these functions, you can use the name of the attached file to get its contents or the path in the sandbox, and then use it as you like in the query. #### Signatures ```yql FilePath(String)->String FileContent(String)->String ``` The `FileContent` and `FilePath` argument is a string with an alias. #### Examples ```yql SELECT "Content of " || FilePath("my_file.txt") || ":\n" || FileContent("my_file.txt"); ``` ## FolderPath {#folderpath} Getting the path to the root of a directory with several "attached" files with the common prefix specified. #### Signature ```yql FolderPath(String)->String ``` The argument is a string with a prefix among aliases. See also [PRAGMA File](https://ytsaurus.tech/docs/syntax/pragma/file.md#file) and [PRAGMA Folder](https://ytsaurus.tech/docs/syntax/pragma/file.md#folder). #### Examples ```yql PRAGMA File("foo/1.txt", "http://url/to/somewhere"); PRAGMA File("foo/2.txt", "http://url/to/somewhere/else"); PRAGMA File("bar/3.txt", "http://url/to/some/other/place"); SELECT FolderPath("foo"); -- The directory at the return path will -- include the files 1.txt and 2.txt downloaded by the above links ``` ## ParseFile Get a list of values from the attached text file. You can use it in combination with [IN](https://ytsaurus.tech/docs/syntax/expressions.md#in) and attaching the file via URL (instructions on how to attach files in the web interface and in the client). Only one file format is supported: one value per line. For something more sophisticated, you'd currently need to write a small UDF in [Python](https://ytsaurus.tech/docs/udf/python.md). #### Signature ```yql ParseFile(String, String)->List ``` Two required arguments: 1. List cell type: only strings and numeric types are supported. 2. The name of the attached file. {% note info %} The return value is a lazy list. For repeat use, wrap it in the function [ListCollect](https://ytsaurus.tech/docs/en/list.md#listcollect) {% endnote %} #### Examples ```yql SELECT ListLength(ParseFile("String", "my_file.txt")); ``` ```yql SELECT * FROM my_table WHERE int_column IN ParseFile("Int64", "my_file.txt"); ``` ## WeakField {#weakfield} Fetches a table column from a strong schema, if it is in a strong schema, or from the `_other` and `_rest` fields. If the value is missing, it returns `NULL`. Syntax: `WeakField([
.], [, ])`. The default value is used only if the column is missing in the data schema. To use the default value in any case, use [COALESCE](https://ytsaurus.tech/docs/en/#coalesce). #### Examples ```yql SELECT WeakField(my_column, String, "no value"), WeakField(my_table.other_column, Int64) FROM my_table; ``` ## Ensure... {#ensure} Checking for the user conditions: * `Ensure()`: Checking whether the predicate is true at query execution. * `EnsureType()`: Checking that the expression type exactly matches the specified type. * `EnsureConvertibleTo()`: A soft check of the expression type (with the same rules as for implicit type conversion). If the check fails, the entire query fails. #### Signatures ```yql Ensure(T, Bool, String)->T EnsureType(T, Type, String)->T EnsureConvertibleTo(T, Type, String)->T ``` Arguments: 1. An expression that will result from a function call if the check is successful. It's also checked for the data type in the corresponding functions. 2. Ensure uses a Boolean predicate that is checked for being `true`. The other functions use the data type that can be obtained using the [relevant functions](https://ytsaurus.tech/docs/en/types.md) or a string literal with the [text description of the type](https://ytsaurus.tech/docs/types/type_string.md). 3. An optional string with an error comment to be included in the overall error message when the query is complete. The data itself can't be used for type checks, since the data check is performed at query validation (or can be an arbitrary expression in the case of Ensure). To check the conditions based on the final calculation result, it's convenient to combine Ensure with [DISCARD SELECT](https://ytsaurus.tech/docs/syntax/discard.md). Ensure calculation is not guaranteed if the program result does not depend on its return value. In particular, don't use singular types, such as `Null`,`Void`,`EmptyList`,`EmptyDict`, or empty `Struct`/`Tuple`, as the first Ensure argument. #### Examples ```yql SELECT Ensure( value, value < 100, "value out or range" ) AS value FROM my_table; ``` ```yql SELECT EnsureType( value, TypeOf(other_value), "expected value and other_value to be of same type" ) AS value FROM my_table; ``` ```yql SELECT EnsureConvertibleTo( value, Double?, "expected value to be numeric" ) AS value FROM my_table; ``` ## AssumeStrict {#assumestrict} #### Signature ```yql AssumeStrict(T)->T ``` The `AssumeStrict` function returns its argument. Using this function is a way to tell the YQL optimizer that the expression in the argument is *strict*, i.e. free of runtime errors. Most of the built-in YQL functions and operators are strict, but there are exceptions like [Unwrap](https://ytsaurus.tech/docs/en/#optional-ops) and [Ensure](https://ytsaurus.tech/docs/en/#ensure). Besides that, a non-strict expression is considered a UDF call. If you are sure that no runtime errors actually occur when computing the expression, we recommend using `AssumeStrict`. #### Example ```yql SELECT * FROM T1 AS a JOIN T2 AS b USING(key) WHERE AssumeStrict(Unwrap(CAST(a.key AS Int32))) == 1; ``` In this example, we assume that all values of the `a.key` text column in table `T1` are valid numbers, so Unwrap does not cause an error. If there is `AssumeStrict`, the optimizer will be able to perform filtering first and then JOIN. Without `AssumeStrict`, such optimization is not performed: the optimizer must take into account the situation when there are non-numeric values in the `a.key` column which are filtered out by `JOIN`. ## Likely {#likely} #### Signature ```yql Likely(Bool)->Bool Likely(Bool?)->Bool? ``` The `Likely` function returns its argument. This function is a hint for the optimizer that tells it that in most cases its argument will be `True`. For example, if such a function is used in `WHERE`, it means that the filter is weekly selective. #### Example ```yql SELECT * FROM T1 AS a JOIN T2 AS b USING(key) WHERE Likely(a.amount > 0) -- This condition is almost always true ``` When you use `Likely`, the optimizer won't attempt to perform filtering before `JOIN`. ## WithSideEffects, WithSideEffectsMode {#side_effects} #### Signature ```yql WithSideEffects(T)->T WithSideEffectsMode(T, mode:string)->T ``` The functions are available starting with version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). The `WithSideEffects` or `WithSideEffectsMode` function returns its first argument. The function serves as a requirement for the optimizer and marks the internal expression (usually a UDF call) as the one containing side effects. A side effect is: * The impact of a function call on another function beyond passing data in arguments — for example, through a global state. * Observed behavior beyond YQL during query execution — for example, reading/writing data in external systems. Not a side effect: * UDF counters and logs available in the ABI. In other words, optimizations that change the values of these counters or log entries due to a different number of UDF calls are allowed. * Query execution error. The optimizer should not remove the calculation of this expression and should generally not change the number of its calculations if it depends, for example, on each table row. Possible mode values: * `General`: All side effects are allowed; common subexpressions can't be deleted. * `SemilatticeRT`: Only idempotent and commutative side effects are allowed; the expression result can be calculated once and reused, allowing for the removal of common subexpressions. * `None`: Side effects are not allowed beyond the expression. Using the `WithSideEffects` function is equivalent to calling `WithSideEffectsMode` with the mode set to `General`. A typical example of a `General` side effect is executing `UPDATE` to another system and returning the number of modified entries as the expression result. A typical example of a `SemilatticeRT` side effect is executing `UPSERT` to a table without non-key columns (which is an idempotent and commutative action) and returning the number of sent entries as the expression result. #### Example ```yql SELECT WithSideEffects(MyModule::Func(...)) FROM table ``` ## ToDynamicLinear #### Signature ```yql ToDynamicLinear(Linear)->DynamicLinear ``` Added in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). `ToDynamicLinear` converts a value from a static [linear](https://ytsaurus.tech/docs/types/linear.md) type to a dynamic one. ## FromDynamicLinear #### Signature ```yql FromDynamicLinear(DynamicLinear)->Linear ``` Added in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). `FromDynamicLinear` converts a value from a dynamic [linear](https://ytsaurus.tech/docs/types/linear.md) type to a static one. ## LinearDestroy #### Signature ```yql LinearDestroy(T, [Linear...])->T ``` Added in version [2025.05](https://ytsaurus.tech/docs/changelog/2025.05.md). This function returns its first parameter, consuming 0 or more [linear](https://ytsaurus.tech/docs/types/linear.md) type values listed after the first parameter. ## AsErased, PeekErased {#type_erasure} #### Signature {#type-erasure-signature} ```yql AsErased(T)->Resource<_Erased> PeekErased(Resource<_Erased>, type U)->Optional ``` Available starting with version [2026.02](https://ytsaurus.tech/docs/changelog/2026.02.md). In Query Tracker, select language version 2026.02 or later to use these functions. `AsErased` erases a value's type by converting any input type to a fixed type. A [linear](https://ytsaurus.tech/docs/types/linear.md) type cannot be passed to it, but `DynamicLinear` can be used. `PeekErased` returns the original value when its type exactly matches the requested type; otherwise, it returns an empty `Optional`. Type erasure can be used to build recursive data and functions: ```yql $erased = TypeOf(AsErased(NULL)); $nodeType = Struct, right: Optional<$erased>>; $makeNode = ($value, $left, $right) -> { RETURN CAST(<| value: $value, left: IF($left IS NOT NULL, AsErased($left)), right: IF($right IS NOT NULL, AsErased($right)) |> AS $nodeType); }; ``` ```yql $e = AsErased(1); SELECT PeekErased($e, Int32), -- 1 PeekErased($e, String); -- NULL ``` ## Block #### Signature ```yql Block(lambda((dependsOnArgument)->T))->T ``` Added in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). `Block` executes a lambda with one parameter and returns its output value. The type of this parameter is undefined because it should only be used as a dependent node. Dependent nodes are nodes used to control the computation of non-deterministic functions, such as [Random](https://ytsaurus.tech/docs/en/#random), or functions that create values of [linear](https://ytsaurus.tech/docs/types/linear.md) types. #### Example ```yql SELECT Block(($arg)->{ $dict = ToMutDict({'key1':123}, $arg); -- используем зависимый узел при создании значения линейного типа $dict = MutDictInsert($dict, 'key2', 456); return FromMutDict($dict); }); -- {'key1':123, 'key2': 456} ``` ## EvaluateExpr, EvaluateAtom {#evaluate_expr_atom} Evaluate an expression before the start of the main calculation and input its result to the query as a literal (constant). In many contexts where the standard SQL would only expect a constant (for example in table names, number of rows in [LIMIT](https://ytsaurus.tech/docs/syntax/select/limit_offset.md), and so on), this functionality is automatically activated implicitly. EvaluateExpr can be used where the grammar already expects an expression. For example, you can use it to: * Round the current time to days, weeks, or months and add the time to the query. This will ensure [valid query caching](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytquerycachemode), although normally, when you use [current-time functions](https://ytsaurus.tech/docs/en/#current-utc), caching is disabled completely. * Run a heavy calculation with a small result once per query instead of once per job. You can use EvaluateAtom to create an [atom](https://ytsaurus.tech/docs/types/special.md) dynamically. However, since atoms are mainly controlled from a lower s-expressions level, it is generally not recommended to use this function directly. The only argument for both functions is the expression for calculation and substitution. Restrictions: * The expression must not trigger MapReduce operations. #### Examples ```yql $now = CurrentUtcDate(); SELECT EvaluateExpr( DateTime::MakeDate(DateTime::StartOfWeek($now) ) ); ``` ## Literals of primitive types {#data-type-literals} For primitive types, you can create literals based on string literals. #### Syntax `( [, ] )` Unlike `CAST("myString" AS MyType)`: * The check for literal's castability to the desired type occurs at validation. * The result is non-optional. For the `Date`, `Datetime`, `Timestamp`, and `Interval` data types, literals are supported only in the format conforming to [ISO 8601](https://ru.wikipedia.org/wiki/ISO_8601). `Interval` has the following differences from the standard: * It supports the negative sign for shifts to the past. * Microseconds can be expressed as fractional parts of seconds. * You can't use units of measurement exceeding one week. * The options with the beginning/end of the interval and with repetitions, are not supported. For the `TzDate`, `TzDatetime`, `TzTimestamp` data types, literals are also set in the format conforming to [ISO 8601](https://ru.wikipedia.org/wiki/ISO_8601), but instead of the optional Z suffix, they specify the [IANA name of the time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), separated by a comma (for example, GMT or Europe/Moscow). For the Decimal parameter data type, two additional arguments are specified: * The total number of decimal digits (up to 35, inclusive). * The number of decimal digits after the decimal point (out of the total number, i.e. strictly no more than the previous argument). #### Examples ```yql SELECT Bool("true"), Uint8("0"), Int32("-1"), Uint32("2"), Int64("-3"), Uint64("4"), Float("-5"), Double("6"), Decimal("1.23", 5, 2), -- up to 5 decimal points, 2 of which follow a comma String("foo"), Utf8("hello"), Yson("[3;%false]"), Json(@@{"a":1,"b":null}@@), Date("2017-11-27"), Datetime("2017-11-27T13:24:00Z"), Timestamp("2017-11-27T13:24:00.123456Z"), Interval("P1DT2H3M4.567890S"), TzDate("2017-11-27,Europe/Moscow"), TzDatetime("2017-11-27T13:24:00,America/Los_Angeles"), TzTimestamp("2017-11-27T13:24:00.123456,GMT"), Uuid("f9d5cc3f-f1dc-4d9c-b97e-766e57ca4ccb"); ``` ## Access to the metadata of the current operation {#metadata} When you run YQL operations via the web interface or HTTP API, you get access to the following data: * `CurrentOperationId()`: The private ID of the operation. * `CurrentOperationSharedId()`: The public ID of the operation. * `CurrentAuthenticatedUser()`: The username of the current user. #### Signatures ```yql CurrentOperationId()->String CurrentOperationSharedId()->String CurrentAuthenticatedUser()->String ``` No arguments. If this data is missing, for example, when you run operations in the embedded mode, the functions return an empty string. #### Examples ```yql SELECT CurrentOperationId(), CurrentOperationSharedId(), CurrentAuthenticatedUser(); ``` ## ToBytes and FromBytes {#to-from-bytes} Conversion of [primitive data types](https://ytsaurus.tech/docs/types/primitive.md) to a string with their binary representation and back. Numbers are represented in the [little endian](https://en.wikipedia.org/wiki/Endianness#Little-endian) encoding. #### Signatures ```yql ToBytes(T)->String ToBytes(T?)->String? FromBytes(String, Type)->T? FromBytes(String?, Type)->T? ``` #### Examples ```yql SELECT ToBytes(123), -- "\u0001\u0000\u0000\u0000" FromBytes( "\xd2\x02\x96\x49\x00\x00\x00\x00", Uint64 ); -- 1234567890ul ``` ## ByteAt {#byteat} Getting the byte value in the string by the index from its beginning. If the index is invalid, `NULL` is returned. #### Signature ```yql ByteAt(String, Uint32)->Uint8 ByteAt(String?, Uint32)->Uint8? ByteAt(Utf8, Uint32)->Uint8 ByteAt(Utf8?, Uint32)->Uint8? ``` Arguments: 1. String: `String` or `Utf8`. 2. Index: `Uint32`. #### Examples ```yql SELECT ByteAt("foo", 0), -- 102 ByteAt("foo", 1), -- 111 ByteAt("foo", 9); -- NULL ``` ## ...Bit {#bitops} `TestBit()`, `ClearBit()`, `SetBit()`, and `FlipBit()`: Test, clear, set, or flip a bit in an unsigned number by the specified bit sequence number. #### Signatures ```yql TestBit(T, Uint8)->Bool TestBit(T?, Uint8)->Bool? TestBit(String, Uint8)->Bool? TestBit(String?, Uint8)->Bool? ClearBit(T, Uint8)->T ClearBit(T?, Uint8)->T? SetBit(T, Uint8)->T SetBit(T?, Uint8)->T? FlipBit(T, Uint8)->T FlipBit(T?, Uint8)->T? ``` Arguments: 1. An unsigned number to perform the desired operation on. `TestBit` is also implemented for strings (see the description below). 2. Bit number. `TestBit` returns `true/false`. Other functions return a copy of their first argument with the corresponding transformation performed. `TestBit` for string arguments works as follows: 1. Based on the second argument, the function selects the corresponding byte *from the beginning of the string*. 2. In that byte, it then selects the corresponding low-order bit. #### Examples ```yql SELECT TestBit(1u, 0), -- true TestBit('ax', 12) -- true (second byte, fourth bit) SetBit(8u, 0); -- 9 ``` ## Abs {#abs} Absolute value of the number. #### Signature ```yql Abs(T)->T Abs(T?)->T? ``` #### Examples ```yql SELECT Abs(-123); -- 123 ``` ## Just {#optional-ops} `Just()`: Change the data type of the value to an [optional](https://ytsaurus.tech/docs/types/optional.md) type derived from the current data type (for example, `T` becomes `T?`). #### Signature ```yql Just(T)->T? ``` #### Examples ```yql SELECT Just("my_string"); -- String? ``` ## AsOptional {#asoptional} Available starting with version [2026.01](https://ytsaurus.tech/docs/changelog/2026.01.md). Allows the supplied value to contain `NULL`. If the type is already optional, a PostgreSQL type, or `NULL`, the value is returned unchanged. Unlike [Just](https://ytsaurus.tech/docs/en/#optional-ops), it does not add another nesting level to an already optional value. ```yql SELECT AsOptional(42), -- Just(42) AsOptional(NULL), -- NULL AsOptional(Just(42)), -- Just(42) AsOptional(1p); -- 1p ``` ## Unwrap {#unwrap} `Unwrap()`: Converting the [optional](https://ytsaurus.tech/docs/types/optional.md) value of the data type to the corresponding non-optional type, raising a runtime error if the data is `NULL`. This means that `T?` becomes `T`. If the value is not [optional](https://ytsaurus.tech/docs/types/optional.md), the function returns its first argument without change. #### Signature ```yql Unwrap(T?)->T Unwrap(T?, Utf8)->T Unwrap(T?, String)->T ``` Arguments: 1. Value to be converted. 2. An optional string with a comment for the error text. The reverse operation is [Just](https://ytsaurus.tech/docs/en/#optional-ops). #### Examples ```yql $value = Just("value"); SELECT Unwrap($value, "Unexpected NULL for $value"); ``` ## Nothing {#nothing} `Nothing()`: Create an empty value for the specified [Optional](https://ytsaurus.tech/docs/types/optional.md) data type. #### Signature ```yql Nothing(Type)->T? ``` #### Examples ```yql SELECT Nothing(String?); -- A NULL value with the String? type ``` [Learn more about ParseType and other functions for data types](https://ytsaurus.tech/docs/en/types.md). ## Callable {#callable} Create a callable value with the given signature from a lambda function. Usually used to place callable values in containers. #### Signature ```yql Callable(TypeT>>, lambda)->Callable<(...)->T> ``` Arguments: 1. Type. 2. Lambda function. #### Examples ```yql $lambda = ($x) -> { RETURN CAST($x as String) }; $callables = AsTuple( Callable(Callable<(Int32)->String>, $lambda), Callable(Callable<(Bool)->String>, $lambda), ); SELECT $callables.0(10), $callables.1(true); ``` ## Pickle, Unpickle {#pickle} `Pickle()` and `StablePickle()` serialize an arbitrary object into a sequence of bytes, if possible. Typical non-serializable objects are Callable and Resource. The serialization format is not versioned and can be used within a single query. For the Dict type, the StablePickle function pre-sorts the keys, and for Pickle, the order of dictionary elements in the serialized representation isn't defined. `Unpickle()` is the inverse operation (deserialization), where with the first argument being the data type of the result and the second argument is the string with the result of `Pickle()` or `StablePickle()`. #### Signatures ```yql Pickle(T)->String StablePickle(T)->String Unpickle(Type, String)->T ``` #### Examples ```yql SELECT * FROM my_table WHERE Digest::MurMurHash32( Pickle(TableRow()) ) % 10 == 0; -- from practical viewpoint, it's better to use TABLESAMPLE $buf = Pickle(123); SELECT Unpickle(Int32, $buf); ``` ## StaticMap Transforms a structure or tuple by applying a lambda function to each item. #### Signature ```yql StaticMap(Struct<...>, lambda)->Struct<...> StaticMap(Tuple<...>, lambda)->Tuple<...> ``` Arguments: * Structure or tuple. * Lambda for processing items. Result: a structure or tuple with the same number and naming of items as in the first argument, and with item data types determined by lambda results. #### Examples ```yql SELECT * FROM ( SELECT StaticMap(TableRow(), ($item) -> { return CAST($item AS String); }) FROM my_table ) FLATTEN COLUMNS; -- converting all columns to rows ``` ## StaticZip Merges structures or tuples element-by-element. All arguments (one or more) must be either structures with the same set of fields or tuples of the same length. The result will be a structure or tuple, respectively. Each item of the result is a tuple comprised of items taken from arguments. #### Signature ```yql StaticZip(Struct, Struct)->Struct StaticZip(Tuple, Tuple)->Tuple ``` #### Examples ```yql $one = <|k1:1, k2:2.0|>; $two = <|k1:3.0, k2:4|>; -- element-wise addition of two structures SELECT StaticMap(StaticZip($one, $two), ($tuple)->($tuple.0 + $tuple.1)) AS sum; ``` ## StaticFold, StaticFold1 {#staticfold} ```yql StaticFold(obj:Struct/Tuple, initVal, updateLambda) StaticFold1(obj:Struct/Tuple, initLambda, updateLambda) ``` Static left-associative fold over a structure or tuple. Tuples are folded from the lower index to the higher one; for structures, the order is not guaranteed. - `obj`: Object whose elements are to be folded. - `initVal`: *(for StaticFold)* Initial fold state. - `initLambda`: *(for StaticFold1)* Function that produces an initial fold state based on the first element. - `updateLambda`: Function that updates a state (accepts the next object element and the previous state in the arguments). `StaticFold(<|key_1:$el_1, key_2:$el_2, ..., key_n:$el_n|>, $init, $f)` transforms into fold: ```yql $f($el_n, ...$f($el_2, $f($init, el_1))...) ``` `StaticFold1(<|key_1:$el_1, key_2:$el_2, ..., key_n:$el_n|>, $f0, $f)`: ```yql $f($el_n, ...$f($el_2, $f($f0($init), el_1))...) ``` `StaticFold1(<||>, $f0, $f)` returns `NULL`. The same logic applies to tuples. ## AggregationFactory {#aggregationfactory} Create the [aggregate function](https://ytsaurus.tech/docs/en/aggregation.md) factory to separate the process of describing how to aggregate data and which data to apply it to. Arguments: 1. A string in quotation marks that is the name of an aggregate function, for example, ["MIN"](https://ytsaurus.tech/docs/en/aggregation.md#min). 2. Optional aggregate function parameters that do not depend on the data. For example, the percentile value in [PERCENTILE](https://ytsaurus.tech/docs/en/aggregation.md#percentile). The resulting factory can be used as a second parameter of the [AGGREGATE_BY](https://ytsaurus.tech/docs/en/aggregation.md#aggregateby) function. If the aggregate function works on two columns instead of one, for example, [MIN_BY](https://ytsaurus.tech/docs/en/aggregation.md#minby), `Tuple` of two values is passed as a first argument in [AGGREGATE_BY](https://ytsaurus.tech/docs/en/aggregation.md#aggregateby). For more information, see the description of this aggregate function. #### Examples ```yql $factory = AggregationFactory("MIN"); SELECT AGGREGATE_BY(value, $factory) AS min_value -- apply MIN aggregation to value column FROM my_table; ``` ## AggregateTransformInput {#aggregatetransform} `AggregateTransformInput()` transforms the [aggregate function](https://ytsaurus.tech/docs/en/aggregation.md) factory, for example, the one obtained via the [AggregationFactory](https://ytsaurus.tech/docs/en/#aggregationfactory) function, into another factory in which the specified transformation of input items is performed before the aggregation starts. Arguments: 1. Aggregate function factory. 2. Lambda function with one argument which transforms an input item. #### Examples ```yql $f = AggregationFactory("sum"); $g = AggregateTransformInput($f, ($x) -> (cast($x as Int32))); $h = AggregateTransformInput($f, ($x) -> ($x * 2)); SELECT ListAggregate([1,2,3], $f); -- 6 SELECT ListAggregate(["1","2","3"], $g); -- 6 SELECT ListAggregate([1,2,3], $h); -- 12 ``` ## AggregateTransformOutput {#aggregatetransformoutput} `AggregateTransformOutput()` transforms the [aggregate function](https://ytsaurus.tech/docs/en/aggregation.md) factory, for example, the one obtained via the [AggregationFactory](https://ytsaurus.tech/docs/en/#aggregationfactory) function, into another factory in which the specified transformation of the result is performed after the aggregation is completed. Arguments: 1. Aggregate function factory. 2. Lambda function with one argument which transforms the result. #### Examples ```yql $f = AggregationFactory("sum"); $g = AggregateTransformOutput($f, ($x) -> ($x * 2)); SELECT ListAggregate([1,2,3], $f); -- 6 SELECT ListAggregate([1,2,3], $g); -- 12 ``` ## AggregateFlatten {#aggregateflatten} Adapts the [aggregate function](https://ytsaurus.tech/docs/en/aggregation.md) factory, for example, the one obtained via the [AggregationFactory](https://ytsaurus.tech/docs/en/#aggregationfactory) function, so that it performs aggregation on input items — lists. This operation is similar to [FLATTEN LIST BY](https://ytsaurus.tech/docs/syntax/flatten.md): each item in the list is aggregated. Arguments: 1. Aggregate function factory. #### Examples ```yql $i = AggregationFactory("AGGREGATE_LIST_DISTINCT"); $j = AggregateFlatten($i); SELECT AggregateBy(x, $j) from ( SELECT [1,2] as x union all SELECT [2,3] as x ); -- [1, 2, 3] ``` # Aggregate functions ## COUNT {#count} #### Signature ```yql COUNT(*)->Uint64 COUNT(T)->Uint64 COUNT(T?)->Uint64 ``` Counting the number of rows in the table (if `*` or constant is specified as the argument) or non-empty values in a table column (if the column name is specified as an argument). Like other aggregate functions, it can be combined with [GROUP BY](https://ytsaurus.tech/docs/syntax/group_by.md) to get statistics on the parts of the table that correspond to the values in the columns being grouped. Use the [DISTINCT](https://ytsaurus.tech/docs/syntax/group_by.md#distinct) modifier to count the number of unique values. #### Types - **Item**: `T` - **SerializedState**: `Uint64` - **Result**: `Uint64` #### Examples ```yql SELECT COUNT(*) FROM my_table; ``` ```yql SELECT key, COUNT(value) FROM my_table GROUP BY key; ``` ```yql SELECT COUNT(DISTINCT value) FROM my_table; ``` ## MIN and MAX {#min-max} #### Signature ```yql MIN(T?)->T? MIN(T)->T? MAX(T?)->T? MAX(T)->T? ``` Minimum or maximum value. As an argument, you may use an arbitrary computable expression with a comparable result. #### Types - **Item**: `T` - **SerializedState**: `T` - **Result**: `T` #### Examples ```yql SELECT MIN(value), MAX(value) FROM my_table; ``` ## SUM {#sum} #### Signature ```yql SUM(Unsigned?)->Uint64? SUM(Signed?)->Int64? SUM(Interval?)->Interval? SUM(Decimal(N, M)?)->Decimal(35, M)? ``` Sum of the numbers. As an argument, you may use an arbitrary computable expression with a numeric result or `Interval` type. Integers are automatically expanded to 64 bits to reduce the risk of overflow. ```yql SELECT SUM(value) FROM my_table; ``` #### Types - **Item**: `T` - **SerializedState**: `WidenIntegral(T)` - **Result**: `WidenIntegral(T)` ## AVG {#avg} #### Signature ```yql AVG(Double?)->Double? AVG(Interval?)->Interval? AVG(Decimal(N, M)?)->Decimal(N, M)? ``` Arithmetic average. As an argument, you may use an arbitrary computable expression with a numeric result or `Interval` type. Integer values and time intervals are automatically converted to Double. #### Types - **Item**: `T` - **SerializedState**: `Tuple` - **Result**: `T` #### Examples ```yql SELECT AVG(value) FROM my_table; ``` ## COUNT_IF {#count-if} #### Signature ```yql COUNT_IF(Bool?)->Uint64 ``` Number of rows for which the expression specified as the argument is true (the expression's calculation result is true). The value `NULL` is equated to `false` (if the argument type is `Bool?`). The function *does not* do the implicit type casting to Boolean for strings and numbers. #### Types - **Item**: `Bool` - **SerializedState**: `Uint64` - **Result**: `Uint64` #### Examples ```yql SELECT COUNT_IF(value % 2 == 1) AS odd_count ``` {% note info %} If you need to count the number of unique values on the rows that meet the condition, then unlike other aggregate functions, you can't use the [DISTINCT](https://ytsaurus.tech/docs/syntax/group_by.md#distinct) modifier, since the arguments contain no values. To get this result, use, inside a subquery, the built-in [IF](https://ytsaurus.tech/docs/en/basic.md#if) function with two arguments (to get `NULL` in ELSE), and apply an outer [COUNT(DISTINCT ...)](https://ytsaurus.tech/docs/en/#count) to its results. {% endnote %} ## SUM_IF and AVG_IF {#sum-if} #### Signature ```yql SUM_IF(Unsigned?, Bool?)->Uint64? SUM_IF(Signed?, Bool?)->Int64? SUM_IF(Interval?, Bool?)->Interval? AVG_IF(Double?, Bool?)->Double? ``` Sum or arithmetic average, but only for the rows that satisfy the condition passed by the second argument. Therefore, `SUM_IF(value, condition)` is a slightly shorter notation for `SUM(IF(condition, value))`, same for `AVG`. The argument's data type expansion is similar to the same-name functions without a suffix. NB. `WidenStateIntegral(T) = if (T is Interval) then Decimal(35, 0) else WidenIntegral(T)` #### SumIf types - **Item**: `T` - **SerializedState**: `WidenStateIntegral(T)` - **Result**: `WidenIntegral(T)` #### AvgIf types - **Item**: `T` - **SerializedState**: `Tuple` - **Result**: `WidenIntegral(T)` #### Examples ```yql SELECT SUM_IF(value, value % 2 == 1) AS odd_sum, AVG_IF(value, value % 2 == 1) AS odd_avg, FROM my_table; ``` When the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) is used, [Tuple](https://ytsaurus.tech/docs/en/#aggregate-by) of the value and predicate is passed as the first `AGGREGATE_BY` argument. ```yql $sum_if_factory = AggregationFactory("SUM_IF"); $avg_if_factory = AggregationFactory("AVG_IF"); SELECT AGGREGATE_BY(AsTuple(value, value % 2 == 1), $sum_if_factory) AS odd_sum, AGGREGATE_BY(AsTuple(value, value % 2 == 1), $avg_if_factory) AS odd_avg FROM my_table; ``` ## SOME {#some} #### Signature ```yql SOME(T?)->T? SOME(T)->T? ``` Get the value for an expression specified as an argument, for one of the table rows. Gives no guarantee of which row is used. This function is similar to [any()](https://clickhouse.tech/docs/en/sql-reference/aggregate-functions/reference/any/) in ClickHouse. The absence of guarantees makes `SOME` computationally cheaper than [MIN and MAX](https://ytsaurus.tech/docs/en/#min-max) often used in similar situations. #### Types - **Item**: `T` - **SerializedState**: `T` - **Result**: `T` #### Examples ```yql SELECT SOME(value) FROM my_table; ``` {% note alert %} When the aggregate function `SOME` is called multiple times, it's **not** guaranteed that all the resulting values are taken from the same row of the source table. To get this guarantee, pack the values into any container and pass it to `SOME`. For example, in case of a structure, you can use [AsStruct](https://ytsaurus.tech/docs/en/basic.md#asstruct). {% endnote %} ## CountDistinctEstimate, HyperLogLog and HLL {#countdistinctestimate} #### Signature ```yql CountDistinctEstimate(T)->Uint64? HyperLogLog(T)->Uint64? HLL(T)->Uint64? ``` Approximate estimate of the number of unique values using the [HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog) algorithm. Logically does the same as [COUNT(DISTINCT ...)](https://ytsaurus.tech/docs/en/#count), but works much faster and with a small margin of error. Arguments: 1. Value for estimation. 2. Accuracy (from 4 to 18 inclusive, the default value is 14). The choice of accuracy enables you to trade off the additional consumption of computing resources and RAM for error reduction. All three functions are currently aliases, but `CountDistinctEstimate` may start using a different algorithm in the future. #### Types - **Item**: `T` - **SerializedState**: `String` - **Result**: `Uint64` #### Examples ```yql SELECT CountDistinctEstimate(my_column) FROM my_table; ``` ```yql SELECT HyperLogLog(my_column, 4) FROM my_table; ``` ## AGGREGATE_LIST {#agg-list} #### Signature ```yql AGGREGATE_LIST(T? [, limit:Uint64])->List AGGREGATE_LIST(T [, limit:Uint64])->List AGGREGATE_LIST_DISTINCT(T? [, limit:Uint64])->List AGGREGATE_LIST_DISTINCT(T [, limit:Uint64])->List ``` Get all column values as a list. In combination with `DISTINCT`, returns only unique values. The optional second parameter sets the maximum number of obtained values. The 0 value means there is no limit. If you know in advance that there are not many unique values, better use the `AGGREGATE_LIST_DISTINCT` aggregate function that builds the same result in memory (which may not be enough for a large number of unique values). The order of elements in the resulting list depends on the implementation and is not set externally. To get an ordered list, sort the result, for example, using [ListSort](https://ytsaurus.tech/docs/en/list.md#listsort). To get a list of several values from one string, *DO NOT* use the `AGGREGATE_LIST` function several times, but place all the desired values in a container, for example, via [AsList](https://ytsaurus.tech/docs/en/basic.md#aslist) or [AsTuple](https://ytsaurus.tech/docs/en/basic.md#astuple) and pass this container in a single `AGGREGATE_LIST` invocation. For example, you can use it in combination with `DISTINCT` and the [String::JoinFromList](https://ytsaurus.tech/docs/udf/list/string.md) function (analog of `','.join(list)` from Python) to print to a string all the values that were seen in the column after applying [GROUP BY](https://ytsaurus.tech/docs/syntax/group_by.md). #### Types - **Item**: `T` - **SerializedState**: `List` - **Result**: `List` #### Examples ```yql SELECT AGGREGATE_LIST( region ), AGGREGATE_LIST( region, 5 ), AGGREGATE_LIST( DISTINCT region ), AGGREGATE_LIST_DISTINCT( region ), AGGREGATE_LIST_DISTINCT( region, 5 ) FROM users ``` ```yql -- Analog of GROUP_CONCAT from MySQL SELECT String::JoinFromList(CAST(AGGREGATE_LIST(region, 2) AS List), ",") FROM users ``` There is also a short form of these functions: `AGG_LIST` and `AGG_LIST_DISTINCT`. {% note alert %} Executed **NOT** in a lazy way, so when you use it, you need to make sure that you get a list of reasonable size, around a thousand items. To be on the safe side, you can use a second optional numeric argument that includes a limit on the number of items in the list. {% endnote %} If the number of items in the list is exceeded, the `Memory limit exceeded` error is returned. ## MAX_BY and MIN_BY {#max-min-by} #### Signature ```yql MAX_BY(T1?, T2)->T1? MAX_BY(T1, T2)->T1? MAX_BY(T1, T2, limit:Uint64)->List? MIN_BY(T1?, T2)->T1? MIN_BY(T1, T2)->T1? MIN_BY(T1, T2, limit:Uint64)->List? ``` Return the value of the first argument for the table row in which the second argument was minimum/maximum. You can optionally specify the third argument N which affects the behavior if the table has multiple rows with the same minimum or maximum value: * If N is not specified, the value of one of the rows will be returned and the rest are discarded. * If N is specified, the list with all values will be returned, but no more than N, all values are discarded after reaching the specified number. When choosing the N value, do not exceed hundreds or thousands to avoid problems with the limited available memory on YTsaurus clusters. If the job necessarily needs all values and their number can be measured in tens of thousands or more, then instead of these aggregate functions use `JOIN` of the original table with a subquery where `GROUP BY + MIN/MAX` is performed on the columns you are interested in. {% note warning %} If the second argument is always `NULL`, the aggregation result is `NULL`. {% endnote %} When the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) is used, `Tuple` of the value and key is passed as the first [AGGREGATE_BY](https://ytsaurus.tech/docs/en/#aggregate-by) argument. #### Types without limit - **Item**: `T1` - **SerializedState**: `Tuple` - **Result**: `T1` #### Types with limit - **Item**: `T1` - **SerializedState**: `Tuple, T2>` - **Result**: `List` #### Examples ```yql SELECT MIN_BY(value, LENGTH(value)), MAX_BY(value, key, 100) FROM my_table; ``` ```yql $min_by_factory = AggregationFactory("MIN_BY"); $max_by_factory = AggregationFactory("MAX_BY", 100); SELECT AGGREGATE_BY(AsTuple(value, LENGTH(value)), $min_by_factory), AGGREGATE_BY(AsTuple(value, key), $max_by_factory) FROM my_table; ``` ## TOP and BOTTOM {#top-bottom} #### Signature ```yql TOP(T?, limit:Uint32)->List TOP(T, limit:Uint32)->List BOTTOM(T?, limit:Uint32)->List BOTTOM(T, limit:Uint32)->List ``` Return the list of maximum/minimum expression values. The first argument is an expression, the second one is a limit on the number of items. #### Examples ```yql SELECT TOP(key, 3), BOTTOM(value, 3) FROM my_table; ``` ```yql $top_factory = AggregationFactory("TOP", 3); $bottom_factory = AggregationFactory("BOTTOM", 3); SELECT AGGREGATE_BY(key, $top_factory), AGGREGATE_BY(value, $bottom_factory) FROM my_table; ``` ## TOP_BY and BOTTOM_BY {#top-bottom-by} #### Signature ```yql TOP_BY(T1, T2, limit:Uint32)->List BOTTOM_BY(T1, T2, limit:Uint32)->List ``` Return the list of values of the first argument for strings with maximum/minimum values of the second argument. The third argument is the limit on the number of items in the list. When the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) is used, `Tuple` of the value and key is passed as the first [AGGREGATE_BY](https://ytsaurus.tech/docs/en/#aggregate-by) argument. In this case, the limit on the number of items is passed as the second argument when creating the factory. #### Types - **Item**: `T1` - **SerializedState**: `Tuple>>` - **Result**: `List` #### Examples ```yql SELECT TOP_BY(value, LENGTH(value), 3), BOTTOM_BY(value, key, 3) FROM my_table; ``` ```yql $top_by_factory = AggregationFactory("TOP_BY", 3); $bottom_by_factory = AggregationFactory("BOTTOM_BY", 3); SELECT AGGREGATE_BY(AsTuple(value, LENGTH(value)), $top_by_factory), AGGREGATE_BY(AsTuple(value, key), $bottom_by_factory) FROM my_table; ``` ## TOPFREQ and MODE {#topfreq-mode} #### Signature ```yql TOPFREQ(T [, num:Uint32 [, bufSize:Uint32]])->List> MODE(T [, num:Uint32 [, bufSize:Uint32]])->List> ``` Getting an **approximate** list of the most frequent column values with an estimate of their number. Return a list of structures with two fields: * `Value`: The found frequent value. * `Frequency`: Estimate of the number of mentions in the table. Mandatory argument: the value itself. Optional arguments: 1. For `TOPFREQ`: The desired number of items in the result. `MODE` is an alias to `TOPFREQ` with 1 in this argument. For `TOPFREQ`, the default value is also 1. 2. The number of items in the used buffer, which enables you to trade off memory consumption for accuracy. The default value is 100. #### Types - **Item**: `T` - **SerializedState**: `Tuple>>` - **Result**: `List>` #### Examples ```yql SELECT MODE(my_column), TOPFREQ(my_column, 5, 1000) FROM my_table; ``` ## STDDEV and VARIANCE {#stddev-variance} #### Signature ```yql STDDEV(Double?)->Double? STDDEV_POPULATION(Double?)->Double? POPULATION_STDDEV(Double?)->Double? STDDEV_SAMPLE(Double?)->Double? STDDEVSAMP(Double?)->Double? VARIANCE(Double?)->Double? VARIANCE_POPULATION(Double?)->Double? POPULATION_VARIANCE(Double?)->Double? VARPOP(Double?)->Double? VARIANCE_SAMPLE(Double?)->Double? ``` Standard variance and dispersion by column. A [one-pass parallel algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm) is used the result of which may differ from that obtained by more common methods that require two passes over the data. By default, sample dispersion and standard variance are calculated. Several write methods are available: * With the `POPULATION` suffix/prefix, for example, `VARIANCE_POPULATION` and `POPULATION_VARIANCE`: Calculates dispersion/standard variance for the general population. * With the `SAMPLE` suffix or without a suffix, for example, `VARIANCE_SAMPLE`, `SAMPLE_VARIANCE`, and `VARIANCE`: Calculates sample dispersion and standard variance. There are also several abbreviated aliases, for example, `VARPOP` or `STDDEVSAMP`. If all passed values are `NULL`, `NULL` is returned. #### Types - **Item**: `Double` - **SerializedState**: `Tuple` - **Result**: `Double` #### Examples ```yql SELECT STDDEV(numeric_column), VARIANCE(numeric_column) FROM my_table; ``` ## CORRELATION and COVARIANCE {#correlation-covariance} #### Signature ```yql CORRELATION(Double?, Double?)->Double? COVARIANCE(Double?, Double?)->Double? COVARIANCE_SAMPLE(Double?, Double?)->Double? COVARIANCE_POPULATION(Double?, Double?)->Double? ``` Correlation and covariance of two columns. Abbreviated versions `CORR` or `COVAR` are also available. For covariance, versions with the `SAMPLE`/`POPULATION` suffix similar to [VARIANCE](https://ytsaurus.tech/docs/en/#stddev-variance) described above are available. Unlike the majority of other aggregate functions, they do not skip `NULL` and count it as 0. When the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) is used, `Tuple` of two values is passed as the first [AGGREGATE_BY](https://ytsaurus.tech/docs/en/#aggregate-by) argument. #### CORRELATION types - **Item**: `Double` - **SerializedState**: `Tuple` - **Result**: `Double` #### COVARIANCE types - **Item**: `Double` - **SerializedState**: `Tuple` - **Result**: `Double` #### Examples ```yql SELECT CORRELATION(numeric_column, another_numeric_column), COVARIANCE(numeric_column, another_numeric_column) FROM my_table; ``` ```yql $corr_factory = AggregationFactory("CORRELATION"); SELECT AGGREGATE_BY(AsTuple(numeric_column, another_numeric_column), $corr_factory) FROM my_table; ``` ## PERCENTILE and MEDIAN {#percentile-median} #### Signature ```yql PERCENTILE(T, Double)->T PERCENTILE(T, Tuple)->Tuple PERCENTILE(T, Struct)->Struct PERCENTILE(T, List)->List MEDIAN(T, [ Double ])->T MEDIAN(T, [ Tuple ])->Tuple MEDIAN(T, [ Struct ])->Struct MEDIAN(T, [ List ])->List ``` Calculating percentiles according to the amortized version of the [TDigest](https://github.com/tdunning/t-digest) algorithm. `MEDIAN(x)` without the second argument is an alias for `PERCENTILE(x, 0.5)`. `MEDIAN` with two arguments is fully equivalent to `PERCENTILE`. `PERCENTILE`/`MEDIAN` accepts an expression of the `T` type as the first argument. The currently supported `T` types are the `Interval` and `Double` types (as well as types that allow implicit conversion to these types; for example, integer types). The second argument can be either a single `Double` (percentile value) or multiple percentile values at once in the form of `Tuple`/`Struct`/`List`. Percentile values should range from 0.0 to 1.0 inclusive. #### Types - **Item**: `T` - **SerializedState**: `String` - **Result**: `T`, `Tuple`, `Struct` or `List` depending on the second argument. #### Examples ```yql SELECT MEDIAN(numeric_column), PERCENTILE(numeric_column, 0.99), PERCENTILE(CAST(string_column as Double), (0.01, 0.5, 0.99)), -- calculating three percentiles at once PERCENTILE(numeric_column, AsStruct(0.01 as p01, 0.5 as median, 0.99 as p99)), -- using the structure, you can give descriptive names to percentile values PERCENTILE(numeric_column, ListFromRange(0.00, 1.05, 0.05)), -- calculating a set of percentiles (from 0.0 to 1.0 inclusive in increments of 0.05) FROM my_table; ``` ## HISTOGRAM {#histogram} #### Signature ```yql HISTOGRAM(Double?)->HistogramStruct? HISTOGRAM(Double?, weight:Double)->HistogramStruct? HISTOGRAM(Double?, intervals:Uint32)->HistogramStruct? HISTOGRAM(Double?, weight:Double, intervals:Uint32)->HistogramStruct? ``` In the description of signatures, HistogramStruct means the result of an aggregate function that is a structure of a certain form. Building an approximate histogram by a numerical expression with automatic selection of baskets. [Auxiliary functions](https://ytsaurus.tech/docs/udf/list/histogram.md) ### Basic settings The limit on the number of baskets can be set using the optional argument, the default value is 100. Note that the additional accuracy costs additional computing resources and may have a negative impact on the query execution time and in extreme cases — on its success. ### Weight support You can specify "weight" for each value involved in building the histogram. To do this, you must pass a weight calculation expression to the aggregate function as the second argument. The default weight value is always `1.0`. If non-standard weights are used, the limit on the number of baskets can be set by the third argument. If two arguments are passed, the meaning of the second argument is determined by its type (integer literal — the limit on the number of baskets, otherwise — weight). ### If you need an accurate histogram 1. You can use the aggregate functions described below with fixed basket grids: [LinearHistogram](https://ytsaurus.tech/docs/en/#linearhistogram) or [LogarithmicHistogram](https://ytsaurus.tech/docs/en/#linearhistogram). 2. You can calculate the basket number for each row and perform [GROUP BY](https://ytsaurus.tech/docs/syntax/group_by.md) on it. When the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) is used, [Tuple](https://ytsaurus.tech/docs/en/#aggregate-by) of the value and weight is passed as the first `AGGREGATE_BY` argument. #### Types - **Item**: `Double` - **SerializedState**: `String` - **Result**: `HistogramStruct` #### Examples ```yql SELECT HISTOGRAM(numeric_column) FROM my_table; ``` ```yql SELECT Histogram::Print( HISTOGRAM(numeric_column, 10), 50 ) FROM my_table; ``` ```yql $hist_factory = AggregationFactory("HISTOGRAM"); SELECT AGGREGATE_BY(AsTuple(numeric_column, 1.0), $hist_factory) FROM my_table; ``` ## LinearHistogram, LogarithmicHistogram and LogHistogram {#linearhistogram} Building a histogram based on an explicitly specified fixed scale of baskets. #### Signature ```yql LinearHistogram(Double?)->HistogramStruct? LinearHistogram(Double? [, binSize:Double [, min:Double [, max:Double]]])->HistogramStruct? LogarithmicHistogram(Double?)->HistogramStruct? LogarithmicHistogram(Double? [, logBase:Double [, min:Double [, max:Double]]])->HistogramStruct? LogHistogram(Double?)->HistogramStruct? LogHistogram(Double? [, logBase:Double [, min:Double [, max:Double]]])->HistogramStruct? ``` Arguments: 1. An expression whose value is used to build a histogram. All subsequent ones are optional. 2. Distance between baskets for `LinearHistogram` or a logarithm base for `LogarithmicHistogram`/`LogHistogram` (these are aliases). In both cases, the default value is 10. 3. Minimum value. The default value is minus infinity. 4. Maximum value. The default value is plus infinity. The result format is completely similar to [adaptive histograms](https://ytsaurus.tech/docs/en/#histogram), which enables you to use the same [set of auxiliary functions](https://ytsaurus.tech/docs/udf/list/histogram.md). If the variation of input values is uncontrollably large, we recommend specifying a minimum and a maximum value to prevent potential drops due to high memory consumption. #### Examples ```yql SELECT LogarithmicHistogram(numeric_column, 2) FROM my_table; ``` ## CDF (cumulative distribution function) {#histogramcdf} You can assign a CDF suffix to each Histogram function type to build a cumulative distribution function. Constructions ```yql SELECT Histogram::ToCumulativeDistributionFunction(Histogram::Normalize(Histogram(numeric_column))) FROM my_table; ``` and ```yql SELECT HistogramCDF(numeric_column) FROM my_table; ``` are completely equivalent. ## BOOL_AND, BOOL_OR and BOOL_XOR {#bool-and-or-xor} #### Signature ```yql BOOL_AND(Bool?)->Bool? BOOL_OR(Bool?)->Bool? BOOL_XOR(Bool?)->Bool? ``` Applying the appropriate logical operation (`AND`/`OR`/`XOR`) to all values of a boolean column or expression. Unlike the majority of aggregate functions, these functions **do not skip** `NULL` during aggregation and use the following rules: - `true AND null == null` - `false OR null == null` For `BOOL_AND`: - For any number of `true` values and at least one `NULL` value, the result is `NULL`. - For at least one `false` value, the result is `false` regardless of whether there are any `NULL` values. For `BOOL_OR`: - For any number of `false` values and at least one `NULL` value, the result is `NULL`. - For at least one `true` value, the result is `true` regardless of whether there are any `NULL` values. For `BOOL_XOR`: - For at least one `NULL` value, the result is `NULL`. Examples of such behavior are given below. The `MIN`/`MAX` or `BIT_AND`/`BIT_OR`/`BIT_XOR` functions can be used for aggregation with `NULL` skips. #### Types - **Item**: `Bool` - **SerializedState**: `Bool` - **Result**: `Bool` #### Examples ```yql $data = [ <|nonNull: true, nonFalse: true, nonTrue: NULL, anyVal: true|>, <|nonNull: false, nonFalse: NULL, nonTrue: NULL, anyVal: NULL|>, <|nonNull: false, nonFalse: NULL, nonTrue: false, anyVal: false|>, ]; SELECT BOOL_AND(nonNull) as nonNullAnd, -- false BOOL_AND(nonFalse) as nonFalseAnd, -- NULL BOOL_AND(nonTrue) as nonTrueAnd, -- false BOOL_AND(anyVal) as anyAnd, -- false BOOL_OR(nonNull) as nonNullOr, -- true BOOL_OR(nonFalse) as nonFalseOr, -- true BOOL_OR(nonTrue) as nonTrueOr, -- NULL BOOL_OR(anyVal) as anyOr, -- true BOOL_XOR(nonNull) as nonNullXor, -- true BOOL_XOR(nonFalse) as nonFalseXor, -- NULL BOOL_XOR(nonTrue) as nonTrueXor, -- NULL BOOL_XOR(anyVal) as anyXor, -- NULL FROM AS_TABLE($data); ``` ## BIT_AND, BIT_OR, and BIT_XOR {#bit-and-or-xor} Applying the appropriate bitwise operation to all values of a numeric column or expression. #### Types - **Item**: `T` - **SerializedState**: `T` - **Result**: `T` #### Examples ```yql SELECT BIT_XOR(unsigned_numeric_value) FROM my_table; ``` ## SessionStart {#session-start} Without arguments. Only allowed if there is [SessionWindow](https://ytsaurus.tech/docs/syntax/group_by.md#session-window) in [GROUP BY](https://ytsaurus.tech/docs/syntax/group_by.md)/[PARTITION BY](https://ytsaurus.tech/docs/syntax/window.md#partition). Returns the value of the `SessionWindow` key column. In case of `SessionWindow` with two arguments — the minimum value of the first argument within a group/partition. In case of the extended variant of `SessionWindoow` — the value of the second tuple item returned by `` when the first tuple item is `True`. ## AGGREGATE_BY and MULTI_AGGREGATE_BY {#aggregate-by} Applying the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) to all column or expression values. The `MULTI_AGGREGATE_BY` function requires a structure, tuple, or list in a column or expression value and applies the factory on an item-by-item basis, placing the result in a container of the same form. If different column or expression values contain lists of different lengths, the resulting list will have the smallest of the lengths of those lists. 1. Column, `DISTINCT` column, or expression. 2. Factory. #### Examples ```yql $count_factory = AggregationFactory("COUNT"); SELECT AGGREGATE_BY(DISTINCT column, $count_factory) as uniq_count FROM my_table; SELECT MULTI_AGGREGATE_BY(nums, AggregationFactory("count")) as count, MULTI_AGGREGATE_BY(nums, AggregationFactory("min")) as min, MULTI_AGGREGATE_BY(nums, AggregationFactory("max")) as max, MULTI_AGGREGATE_BY(nums, AggregationFactory("avg")) as avg, MULTI_AGGREGATE_BY(nums, AggregationFactory("percentile", 0.9)) as p90 FROM my_table; ``` ## RANDOM_SAMPLE and RANDOM_VALUE #### Signature The functions are available starting from version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). ```yql RANDOM_SAMPLE(T?, limit:Uint64)->List RANDOM_SAMPLE(T, limit:Uint64)->List RANDOM_VALUE(T)->T RANDOM_VALUE(T?)->Optional ``` Selects no more than `limit` random values. `RANDOM_VALUE` is equivalent to `RANDOM_SAMPLE` with `limit=1`, except it returns an element instead of a list. The probability of each input value appearing in the resulting list is exactly `limit/count(*)`. {% note info %} `RANDOM_SAMPLE` on an empty table without grouping by keys returns an empty list, not `NULL`. {% endnote %} #### RANDOM_SAMPLE Types - **Item**: `T` - **SerializedState**: `Tuple, Uint64, Uint64>` - **Result**: `List` #### RANDOM_VALUE Types - **Item**: `T` - **SerializedState**: `Tuple, Uint64, Uint64>` - **Result**: `T` #### Examples ```yql SELECT RANDOM_SAMPLE(region, 5) as five_random_regions, RANDOM_VALUE(region) as random_region FROM users ``` {% note warning %} Final results are non-deterministic, which means two runs may or may not produce the same output. {% endnote %} {% note warning %} During execution, the aggregation function keeps `limit` elements in memory. When `limit` is large or elements are large, the computation may fail due to exceeding memory limits. {% endnote %} ## UDAF If the above aggregate functions were not enough for some reason, YQL has a mechanism for describing custom aggregate functions. For more information, [see the article](https://ytsaurus.tech/docs/recipes/udaf.md). # List of window functions in YQL The syntax for calling window functions is detailed in a [separate article](https://ytsaurus.tech/docs/syntax/window.md). ## Aggregate functions {#aggregate-functions} All the [aggregate functions](https://ytsaurus.tech/docs/en/aggregation.md) can also be used as window functions. In this case, each row includes an aggregation result obtained on a set of rows from the [window frame](https://ytsaurus.tech/docs/syntax/window.md#frame). #### Examples ```yql SELECT SUM(int_column) OVER w1 AS running_total, SUM(int_column) OVER w2 AS total, FROM my_table WINDOW w1 AS (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), w2 AS (); ``` ## ROW_NUMBER {#row_number} Row number within a [partition](https://ytsaurus.tech/docs/syntax/window.md#partition). Without arguments. #### Signature ```yql ROW_NUMBER()->Uint64 ``` #### Examples ```yql SELECT ROW_NUMBER() OVER w AS row_num FROM my_table WINDOW w AS (ORDER BY key); ``` ## LAG / LEAD {#lag-lead} Accessing a value from the [partition](https://ytsaurus.tech/docs/syntax/window.md#partition) row that is behind (`LAG`) or ahead (`LEAD`) of the current partition row by a fixed number of rows. The first argument specifies the expression to be accessed, and the second argument specifies the offset in rows. You may omit the offset. By default, the neighbor row is used: the previous or next, respectively (hence, 1 is assumed by default). For the rows having no neighbors at a given distance (for example, `LAG(expr, 3)` in the first and second rows of the partition), `NULL` is returned. #### Signature ```yql LEAD(T[,Int32])->T? LAG(T[,Int32])->T? ``` #### Examples ```yql SELECT int_value - LAG(int_value) OVER w AS int_value_diff FROM my_table WINDOW w AS (ORDER BY key); ``` ```yql SELECT item, odd, LAG(item, 1) OVER w as lag1 FROM ( SELECT item, item % 2 as odd FROM ( SELECT AsList(1, 2, 3, 4, 5, 6, 7) as item ) FLATTEN BY item ) WINDOW w As ( PARTITION BY odd ORDER BY item ); /* Output: item odd lag1 -------------------- 2 0 NULL 4 0 2 6 0 4 1 1 NULL 3 1 1 5 1 3 7 1 5 */ ``` ## FIRST_VALUE / LAST_VALUE Accessing the values from the first and last rows of the [window border](https://ytsaurus.tech/docs/syntax/window.md#frame) (in the order of the window's `ORDER BY`). The only argument is the expression that you need to access. Optionally, `OVER` can be preceded by the additional modifier `IGNORE NULLS`. It changes the behavior of functions to the first or last **non-empty** (i.e., non-`NULL`) value among the window frame rows. The antonym of this modifier is `RESPECT NULLS`: it's the default behavior that can be omitted. #### Signature ```yql FIRST_VALUE(T)->T? LAST_VALUE(T)->T? ``` #### Examples ```yql SELECT FIRST_VALUE(my_column) OVER w FROM my_table WINDOW w AS (ORDER BY key); ``` ```yql SELECT LAST_VALUE(my_column) IGNORE NULLS OVER w FROM my_table WINDOW w AS (ORDER BY key); ``` ## NTH_VALUE Accessing values from the specified row within the [window frame](https://ytsaurus.tech/docs/syntax/window.md#frame) (in the order of the window's `ORDER BY`). The arguments are the expression to access and the row number starting with 1. Optionally, `OVER` can be preceded by the additional modifier `IGNORE NULLS`, which lets you skip rows with NULL in the first argument's value. The antonym of this modifier is `RESPECT NULLS`: it's the default behavior that can be omitted. #### Signature ```yql NTH_VALUE(T,N)->T? ``` #### Examples ```yql SELECT NTH_VALUE(my_column, 2) OVER w FROM my_table WINDOW w AS (ORDER BY key); ``` ```yql SELECT NTH_VALUE(my_column, 3) IGNORE NULLS OVER w FROM my_table WINDOW w AS (ORDER BY key); ``` ## RANK / DENSE_RANK / PERCENT_RANK {#rank} Number the groups of neighboring [partition](https://ytsaurus.tech/docs/syntax/window.md#partition) rows having the same expression value in the argument. `DENSE_RANK` numbers the groups one-by-one, and `RANK` skips `(N - 1)` values, with `N` being the number of rows in the previous group. `PERCENT_RANK` returns the relative rank of the current row: `(RANK - 1)/(number of rows in the partition - 1)`. If there is no argument, it uses the order specified in the `ORDER BY` section in the window definition. If the argument is omitted and `ORDER BY` is not specified, then all rows are considered equal to each other. {% note info %} Passing an argument to `RANK`/`DENSE_RANK`/`PERCENT_RANK` is a non-standard extension in YQL. {% endnote %} #### Signature ```yql RANK([T])->Uint64 DENSE_RANK([T])->Uint64 PERCENT_RANK([T])->Double ``` #### Examples ```yql SELECT RANK(my_column) OVER w FROM my_table WINDOW w AS (ORDER BY key); ``` ```yql SELECT DENSE_RANK() OVER w FROM my_table WINDOW w AS (ORDER BY my_column); ``` ```yql SELECT PERCENT_RANK() OVER w FROM my_table WINDOW w AS (ORDER BY my_column); ``` ## NTILE Distributes the rows of an ordered [partition](https://ytsaurus.tech/docs/syntax/window.md#partition) into a specified number of groups. The groups are numbered starting with 1. For each row, the NTILE function returns the number of the group the row belongs to. #### Signature ```yql NTILE(Uint64)->Uint64 ``` #### Examples ```yql SELECT NTILE(10) OVER w AS group_num FROM my_table WINDOW w AS (ORDER BY key); ``` ## CUME_DIST Returns the relative position (> 0 and <= 1) of a row within a [partition](https://ytsaurus.tech/docs/syntax/window.md#partition). Without arguments. #### Signature ```yql CUME_DIST()->Double ``` #### Examples ```yql SELECT CUME_DIST() OVER w AS dist FROM my_table WINDOW w AS (ORDER BY key); ``` ## SessionState() {#session-state} A non-standard window function `SessionState()` (without arguments) lets you get the session calculation status from [SessionWindow](https://ytsaurus.tech/docs/syntax/group_by.md#session-window) for the current row. It's allowed only if `SessionWindow()` is present in the `PARTITION BY` section in the window definition. # Functions for lists ## ListCreate {#list-create} Construct an empty list. The only argument specifies a string describing the data type of the list cell, or the type itself obtained using [relevant functions](https://ytsaurus.tech/docs/en/types.md). YQL doesn't support lists with an unknown cell type. [Type description format documentation](https://ytsaurus.tech/docs/types/type_string.md). #### Examples ```yql SELECT ListCreate(Tuple); ``` ```yql SELECT ListCreate(OptionalType(DataType("String"))); ``` #### Signature ```yql ListCreate(T)->List ``` ## asList and AsListStrict {#aslist} Construct a list based on one or more arguments. The argument types must be compatible in the case of `AsList` and strictly match in the case of `AsListStrict`. #### Examples ```yql SELECT AsList(1, 2, 3, 4, 5); ``` #### Signature ```yql AsList(T..)->List ``` ## ListLength {#listlength} The count of items in the list. #### Examples ```yql SELECT ListLength(list_column) FROM my_table; ``` #### Signature ```yql ListLength(List)->Uint64 ListLength(List?)->Uint64? ``` ## ListHasItems Check that the list contains at least one item. #### Examples ```yql SELECT ListHasItems(list_column) FROM my_table; ``` #### Signature ```yql ListHasItems(List)->Bool ListHasItems(List?)->Bool? ``` ## ListCollect {#listcollect} Convert a lazy list (which can be built by such functions as [ListFilter](https://ytsaurus.tech/docs/en/#listmap), [ListMap](https://ytsaurus.tech/docs/en/#listmap), [ListFlatMap](https://ytsaurus.tech/docs/en/#listmap)) to an eager list. In contrast to a lazy list, where each new pass re-calculates the list contents, in an eager list the content is built at once by consuming more memory. #### Examples ```yql SELECT ListCollect(list_column) FROM my_table; ``` #### Signature ```yql ListCollect(LazyList)->List ListCollect(LazyList?)->List? ``` ## ListSort, ListSortAsc and ListSortDesc {#listsort} Sort the list. By default, the ascending sorting order is applied (`ListSort` is an alias for `ListSortAsc`). Arguments: 1. List. 2. An optional expression to get the sort key from a list element (it's the element itself by default). #### Examples ```yql SELECT ListSortDesc(list_column) FROM my_table; ``` ```yql $list = AsList( AsTuple("x", 3), AsTuple("xx", 1), AsTuple("a", 2) ); SELECT ListSort($list, ($x) -> { RETURN $x.1; }); ``` {% note info %} The [lambda function](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) was used in the example. {% endnote %} #### Signature ```yql ListSort(List)->List ListSort(List?)->List? ListSort(List, (T)->U)->List ListSort(List?, (T)->U)->List? ``` ## ListExtend and ListExtendStrict {#listextend} Sequentially join lists (concatenation of lists). The arguments can be lists, optional lists, and `NULL`. The types of list elements must be compatible in the case of `ListExtend` and strictly match in the case of `ListExtendStrict`. If at least one of the lists is optional, then the result is also optional. If at least one argument is `NULL`, then the result type is `NULL`. #### Examples ```yql SELECT ListExtend( list_column_1, list_column_2, list_column_3 ) FROM my_table; ``` ```yql $l1 = AsList("a", "b"); $l2 = AsList("b", "c"); $l3 = AsList("d", "e"); SELECT ListExtend($l1, $l2, $l3); -- ["a","b","b","c","d","e"] ``` #### Signature ```yql ListExtend(List..)->List ListExtend(List?..)->List? ``` ## ListUnionAll {#listunionall} Sequentially join lists of structures (concatenation of lists). A field is added to the output list of structures if it exists in at least one source list, but if there is no such field in any list, it is added as NULL. In the case when a field is present in two or more lists, the output field is cast to the common type. If at least one of the lists is optional, then the result is also optional. #### Examples ```yql SELECT ListUnionAll( list_column_1, list_column_2, list_column_3 ) FROM my_table; ``` ```yql $l1 = AsList( <|value:1|>, <|value:2|> ); $l2 = AsList( <|key:"a"|>, <|key:"b"|> ); SELECT ListUnionAll($l1, $l2); -- result: [("value":1),("value":2),("key":"a"),("key":"b")] -- schema: List> ``` #### Signature ```yql ListUnionAll(List>, List>..)->List> ListUnionAll(List>?, List>?..)->List>? ``` ## ListZip and ListZipAll {#listzip} Based on the input lists, build a list of pairs containing the list elements with corresponding indexes (`List>`). The length of the returned list is determined by the shortest list for ListZip and the longest list for ListZipAll. When the shorter list is exhausted, a `NULL` value of a relevant [optional type](https://ytsaurus.tech/docs/types/optional.md). #### Examples ```yql SELECT ListZip(list_column_1, list_column_2, list_column_3), ListZipAll(list_column_1, list_column_2) FROM my_table; ``` ```yql $l1 = AsList("a", "b"); $l2 = AsList(1, 2, 3); SELECT ListZip($l1, $l2); -- [("a",1),("b",2)] SELECT ListZipAll($l1, $l2); -- [("a",1),("b",2),(null,3)] ``` #### Signature ```yql ListZip(List, List)->List> ListZip(List?, List?)->List>? ListZipAll(List, List)->List> ListZipAll(List?, List?)->List>? ``` ## ListEnumerate {#listenumerate} Build a list of pairs (Tuple) containing the element number and the element itself (`List>`). #### Examples ```yql SELECT ListEnumerate(list_column) FROM my_table; ``` #### Signature ```yql ListEnumerate(List)->List> ListEnumerate(List?)->List>? ``` ## ListReverse {#listreverse} Reverse the list. #### Examples ```yql SELECT ListReverse(list_column) FROM my_table; ``` #### Signature ```yql ListReverse(List)->List ListReverse(List?)->List? ``` ## ListSkip {#listskip} Returns a copy of the list, skipping the specified number of its first elements. The first argument specifies the source list and the second argument specifies how many elements to skip. #### Examples ```yql SELECT ListSkip(list_column, 3) FROM my_table; ``` ```yql $l1 = AsList(1, 2, 3, 4, 5); SELECT ListSkip($l1, 2); -- [3,4,5] ``` #### Signature ```yql ListSkip(List, Uint64)->List ListSkip(List?, Uint64)->List? ``` ## ListTake {#listtake} Returns a copy of the list containing a limited number of elements from the second list. The first argument specifies the source list and the second argument specifies the maximum number of elements to be taken from the beginning of the list. #### Examples ```yql SELECT ListTake(list_column, 3) FROM my_table; ``` ```yql $l1 = AsList(1, 2, 3, 4, 5); SELECT ListTake($l1, 2); -- [1,2] ``` #### Signature ```yql ListTake(List, Uint64)->List ListTake(List?, Uint64)->List? ``` ## ListSample and ListSampleN {#listsample} Returns a sample of elements from the list without duplicates. - `ListSample` selects each element independently with a specified probability. - `ListSampleN` selects a fixed number of elements (if the list is smaller than the sample size, the original list is returned). If the probability or sample size is NULL, the original list is returned. An additional argument is used to control randomness. For more information, see the [documentation for `Random`](https://ytsaurus.tech/docs/en/basic.md#random). #### Examples ```yql $list = AsList(1, 2, 3, 4, 5); SELECT ListSample($list, 0.5); -- [1, 2, 5] SELECT ListSampleN($list, 2); -- [4, 2] ``` #### Signature ```yql ListSample(List, Double?[, U])->List ListSample(List?, Double?[, U])->List? ListSampleN(List, Uint64?[, U])->List ListSampleN(List?, Uint64?[, U])->List? ``` ## ListShuffle {#listshuffle} Returns a copy of the list with the elements rearranged in random order. An additional argument is used to control randomness. For more information, see the [documentation for `Random`](https://ytsaurus.tech/docs/en/basic.md#random). #### Examples ```yql $list = AsList(1, 2, 3, 4, 5); SELECT ListShuffle($list); -- [1, 3, 5, 2, 4] ``` #### Signature ```yql ListShuffle(List[, U])->List ListShuffle(List?[, U])->List? ``` ## ListIndexOf {#listindexof} Searches the list for an element with the specified value and returns its index at the first occurrence. Indexes count from 0. If such element is missing, it returns `NULL`. #### Examples ```yql SELECT ListIndexOf(list_column, 123) FROM my_table; ``` ```yql $l1 = AsList(1, 2, 3, 4, 5); SELECT ListIndexOf($l1, 2); -- 1 ``` #### Signature ```yql ListIndexOf(List, T)->Uint64? ListIndexOf(List?, T)->Uint64? ``` ## ListMap, ListFlatMap and ListFilter {#listmap} Apply the function specified as the second argument to each list element. The functions differ in their returned result: * `ListMap` returns a list with results. * `ListFlatMap` returns a list with results, combining and expanding the first level of results (lists or optional values) for each item. * `ListFilter` leaves only those elements where the function returned `true`. {% note info %} In `ListFlatMap`, optional values in function results are deprecated, use the combination of [`ListNotNull`](https://ytsaurus.tech/docs/en/#listnotnull) and `ListMap` instead. {% endnote %} Arguments: 1. Source list. 2. Functions for processing list elements, such as: * [Lambda function](https://ytsaurus.tech/docs/syntax/expressions.md#lambda). * `Module::Function` - С++ UDF; If the source list is optional, then the output list is also optional. #### Examples ```yql SELECT ListMap(list_column, ($x) -> { RETURN $x > 2; }), ListFlatMap(list_column, My::Udf) FROM my_table; ``` ```yql $list = AsList("a", "b", "c"); $filter = ($x) -> { RETURN $x == "b"; }; SELECT ListFilter($list, $filter); -- ["b"] ``` ```yql $list = AsList(1,2,3,4); $callable = Python::test(Callable<(Int64)->Bool>, "def test(i): return i % 2"); SELECT ListFilter($list, $callable); -- [1,3] ``` #### Signature ```yql ListMap(List, (T)->U)->List ListMap(List?, (T)->U)->List? ListFlatMap(List, (T)->List)->List ListFlatMap(List?, (T)->List)->List? ListFlatMap(List, (T)->U?)->List ListFlatMap(List?, (T)->U?)->List? ListFilter(List, (T)->Bool)->List ListFilter(List?, (T)->Bool)->List? ``` ## ListNotNull {#listnotnull} Applies transformation to the source list, skipping empty optional items and strengthening the item type to non-optional. For a list with non-optional items, it returns the unchanged source list. If the source list is optional, then the output list is also optional. #### Examples ```yql SELECT ListNotNull([1,2]), -- [1,2] ListNotNull([3,null,4]); -- [3,4] ``` #### Signature ```yql ListNotNull(List)->List ListNotNull(List?)->List? ``` ## ListFlatten {#listflatten} Expands the list of lists into a flat list, preserving the order of items. As the top-level list item, you can use an optional list that is interpreted as an empty list in the case of `NULL`. If the source list is optional, then the output list is also optional. #### Examples ```yql SELECT ListFlatten([[1,2],[3,4]]), -- [1,2,3,4] ListFlatten([null,[3,4],[5,6]]); -- [3,4,5,6] ``` #### Signature ```yql ListFlatten(List?>)->List ListFlatten(List?>?)->List? ``` ## ListUniq and ListUniqStable {#listuniq} Returns a copy of the list containing only distinct elements. For ListUniq, the order of elements in the resulting list is undefined. For ListUniqStable, elements appear in the same order as in the source list. #### Examples ```yql SELECT ListUniq([1, 2, 3, 2, 4, 5, 1]) -- [5, 4, 2, 1, 3] SELECT ListUniqStable([1, 2, 3, 2, 4, 5, 1]) -- [1, 2, 3, 4, 5] SELECT ListUniqStable([1, 2, null, 7, 2, 8, null]) -- [1, 2, null, 7, 8] ``` #### Signature ```yql ListUniq(List)->List ListUniq(List?)->List? ListUniqStable(List)->List ListUniqStable(List?)->List? ``` ## ListAny and ListAll {#listany} Returns `true` for a list of Boolean values if: * `ListAny`: At least one element is `true`. * `ListAll`: All elements are `true`. Otherwise, it returns `false`. Behavior with empty lists: * `ListAny` returns `false`. * `ListAll` returns `true`. #### Examples ```yql SELECT ListAll(bool_column), ListAny(bool_column) FROM my_table; ``` #### Signature ```yql ListAny(List)->Bool ListAny(List?)->Bool? ListAll(List)->Bool ListAll(List?)->Bool? ``` ## ListHas {#listhas} Show whether the list contains the specified element. In this case, `NULL` values are considered equal to each other. If the input list is `NULL`, the result is always `false`. #### Examples ```yql SELECT ListHas(list_column, "my_needle") FROM my_table; ``` ```yql $l1 = AsList(1, 2, 3, 4, 5); SELECT ListHas($l1, 2); -- true SELECT ListHas($l1, 6); -- false ``` #### Signature ```yql ListHas(List, U)->Bool ListHas(List?, U)->Bool ``` ## ListHead, ListLast {#listheadlast} Returns the first and last item of the list. #### Examples ```yql SELECT ListHead(numeric_list_column) AS head, ListLast(numeric_list_column) AS last FROM my_table; ``` #### Signature ```yql ListHead(List)->T? ListHead(List?)->T? ListLast(List)->T? ListLast(List?)->T? ``` ## ListMin, ListMax, ListSum and ListAvg {#listminy} Apply the appropriate aggregate function to all elements of the numeric list. #### Examples ```yql SELECT ListMax(numeric_list_column) AS max, ListMin(numeric_list_column) AS min, ListSum(numeric_list_column) AS sum, ListAvg(numeric_list_column) AS avg FROM my_table; ``` #### Signature ```yql ListMin(List)->T? ListMin(List?)->T? ``` ## ListFold, ListFold1 {#listfold} List folding. Arguments: 1. List 2. Initial value U for ListFold, initLambda(item:T)->U for ListFold1 3. updateLambda(item:T, state:U)->U Return type: U for ListFold, optional U for ListFold1. `ListFold` supports [linear types](https://ytsaurus.tech/docs/types/linear.md), `Linear`, in state type `U`. If the state contains linear values, `updateLambda` must consume each of them exactly once on every iteration and return state of the same type. In Query Tracker, select language version 2026.02 or later to use a linear state in `ListFold`. #### Examples ```yql $l = [1, 4, 7, 2]; $y = ($x, $y) -> { RETURN $x + $y; }; $z = ($x) -> { RETURN 4 * $x; }; SELECT ListFold($l, 6, $y) AS fold, -- 20 ListFold([], 3, $y) AS fold_empty, -- 3 ListFold1($l, $z, $y) AS fold1, -- 17 ListFold1([], $z, $y) AS fold1_empty; -- Null ``` A linear state makes it possible to modify a mutable dictionary without copying it on every iteration: ```yql SELECT Block(($arg) -> { $dict = ListFold([1, 2, 3, 2, 1], ToMutDict({0: 0}, $arg), ($item, $state) -> { RETURN MutDictInsert($state, $item, 1); }); RETURN ListSort(DictKeys(FromMutDict($dict))); }); -- [0, 1, 2, 3] ``` #### Signature ```yql ListFold(List, U, (T, U)->U)->U ListFold(List?, U, (T, U)->U)->U? ListFold1(List, (T)->U, (T, U)->U)->U? ListFold1(List?, (T)->U, (T, U)->U)->U? ``` ## ListFoldMap, ListFold1Map {#listfoldmap} Converts each element i in the list by calling handler(i, state). Arguments: 1. List 2. Initial state S for ListFoldMap, initLambda(item:T)->tuple (U S) for ListFold1Map 3. handler(item:T, state:S)->tuple (U S) Return type: List of elements U. #### Examples ```yql $l = [1, 4, 7, 2]; $x = ($i, $s) -> { RETURN ($i * $s, $i + $s); }; $t = ($i) -> { RETURN ($i + 1, $i + 2); }; SELECT ListFoldMap([], 1, $x), -- [] ListFoldMap($l, 1, $x), -- [1, 8, 42, 26] ListFold1Map([], $t, $x), -- [] ListFold1Map($l, $t, $x); -- [2, 12, 49, 28] ``` #### Signature ```yql ListFoldMap(List, S, (T, S)->Tuple)->List ListFoldMap(List?, S, (T, S)->Tuple)->List? ListFold1Map(List, (T)->Tuple, (T, S)->Tuple)->List ListFold1Map(List?, (T)->Tuple, (T, S)->Tuple)->List? ``` ## ListFromRange {#listfromrange} Generate a sequence of numbers or dates with the specified step. It's similar to `xrange` in Python 2, but additionally supports dates, `Decimal`, and floating-point numbers. Arguments: 1. Start 2. End 3. Step. Optional, 1 by default for numeric sequences, 1 day for `Date`/`TzDate`, 1 second for `Datetime`/`TzDatetime`, and 1 microsecond`Timestamp`/`TzTimestamp`/`Interval` Features: * *The end is not included, i.e. `ListFromRange(1,3) == AsList(1,2)`. * The type for the resulting elements is selected as the broadest from the argument types. For example, `ListFromRange(1, 2, 0.5)` results in a `Double` list. * `Decimal` ranges are supported starting with language version [2026.02](https://ytsaurus.tech/docs/changelog/2026.02.md). In Query Tracker, select language version 2026.02 or later. The start, end, and explicit step must have exactly the same `Decimal(N, M)` type. * If start and end have one of the date types, the step must have the `Interval` type. * The list is "lazy", but if it's used incorrectly, it can still consume a lot of RAM. * If the step is positive and the end is less than or equal to the start, the result list is empty. * If the step is negative and the end is greater than or equal to the start, the result list is empty. * If the step is neither positive nor negative (0 or NaN), the result list is empty. * If one of the parameters is optional, the result will be an optional list. * If one of the parameters is `NULL`, the result is `NULL`. * If a `Float`, `Double`, or `Decimal` parameter is `NaN` or infinite, the list is empty. #### Examples ```yql SELECT ListFromRange(-2, 2), -- [-2, -1, 0, 1] ListFromRange(2, 1, -0.5), -- [2.0, 1.5] ListFromRange(Decimal("1.00", 5, 2), Decimal("2.00", 5, 2), Decimal("0.25", 5, 2)); -- [1.00, 1.25, 1.50, 1.75] ``` ```yql SELECT ListFromRange(Datetime("2022-05-23T15:30:00Z"), Datetime("2022-05-30T15:30:00Z"), DateTime::IntervalFromDays(1)); ``` #### Signature ```yql ListFromRange(T{Flags:AutoMap}, T{Flags:AutoMap}, T?)->LazyList -- T is a numeric type ListFromRange(T{Flags:AutoMap}, T{Flags:AutoMap}, I?)->LazyList -- T is a date/time type; I is an interval ``` ## ListReplicate {#listreplicate} Creates a list containing multiple copies of the specified value. Mandatory arguments: 1. Value. 2. Number of copies. #### Examples ```yql SELECT ListReplicate(true, 3); -- [true, true, true] ``` #### Signature ```yql ListReplicate(T, Uint64)->List ``` ## ListConcat {#listconcat} Concatenates a list of strings into a single string. You can set a separator as the second parameter. #### Examples ```yql SELECT ListConcat(string_list_column), ListConcat(string_list_column, "; ") FROM my_table; ``` ```yql $l1 = AsList("h", "e", "l", "l", "o"); SELECT ListConcat($l1); -- "hello" SELECT ListConcat($l1, " "); -- "h e l l o" ``` #### Signature ```yql ListConcat(List)->String? ListConcat(List?)->String? ListConcat(List, String)->String? ListConcat(List?, String)->String? ``` ## ListExtract {#listextract} For a list of structures, it returns a list of contained fields having the specified name. #### Examples ```yql SELECT ListExtract(struct_list_column, "MyMember") FROM my_table; ``` ```yql $l = AsList( <|key:"a", value:1|>, <|key:"b", value:2|> ); SELECT ListExtract($l, "key"); -- ["a", "b"] ``` #### Signature ```yql ListExtract(List>, String)->List ListExtract(List>?, String)->List? ``` ## ListTakeWhile, ListSkipWhile {#listtakewhile} `ListTakeWhile` returns a list from the beginning while the predicate is true, then the list ends. `ListSkipWhile` skips the list segment from the beginning while the predicate is true, then returns the rest of the list disregarding the predicate. `ListTakeWhileInclusive` returns a list from the beginning while the predicate is true. Then the list ends, but it also includes the item on which the stopping predicate triggered. `ListSkipWhileInclusive` skips a list segment from the beginning while the predicate is true, then returns the rest of the list disregarding the predicate, but excluding the element that matched the predicate and starting with the next element after it. Mandatory arguments: 1. List. 2. Predicate. If the input list is optional, then the result is also optional. #### Examples ```yql $data = AsList(1, 2, 5, 1, 2, 7); SELECT ListTakeWhile($data, ($x) -> {return $x <= 3}), -- [1, 2] ListSkipWhile($data, ($x) -> {return $x <= 3}), -- [5, 1, 2, 7] ListTakeWhileInclusive($data, ($x) -> {return $x <= 3}), -- [1, 2, 5] ListSkipWhileInclusive($data, ($x) -> {return $x <= 3}); -- [1, 2, 7] ``` #### Signature ```yql ListTakeWhile(List, (T)->Bool)->List ListTakeWhile(List?, (T)->Bool)->List? ``` ## ListAggregate {#listaggregate} Apply the [aggregation factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) to the passed list. If the passed list is empty, the aggregation result is the same as for an empty table: 0 for the `COUNT` function and `NULL` for other functions. If the passed list is optional and `NULL`, the result is also `NULL`. Arguments: 1. List. 2. [Aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory). #### Examples ```yql SELECT ListAggregate(AsList(1, 2, 3), AggregationFactory("Sum")); -- 6 ``` #### Signature ```yql ListAggregate(List, AggregationFactory)->T ListAggregate(List?, AggregationFactory)->T? ``` ## ToDict and ToMultiDict {#todict} Convert a list of tuples containing key-value pairs to a dictionary. If there are conflicting keys in the input list, `ToDict` leaves the first value and `ToMultiDict` builds a list of all the values. It means that: * `ToDict` converts `List>` to `Dict` * `ToMultiDict` converts `List>` to `Dict>` Optional lists are also supported, resulting in an optional dictionary. #### Examples ```yql SELECT ToDict(tuple_list_column) FROM my_table; ``` ```yql $l = AsList(("a",1), ("b", 2), ("a", 3)); SELECT ToDict($l); -- {"a": 1,"b": 2} ``` #### Signature ```yql ToDict(List>)->Dict ToDict(List>?)->Dict? ``` ## ToSet {#toset} Converts a list to a dictionary where the keys are unique elements of this list, and values are omitted and have the type `Void`. For the `List` list, the result type is `Dict`. An optional list is also supported, resulting in an optional dictionary. Inverse function: get a list of keys for the [DictKeys](https://ytsaurus.tech/docs/en/dict.md#dictkeys) dictionary. #### Examples ```yql SELECT ToSet(list_column) FROM my_table; ``` ```yql $l = AsList(1,1,2,2,3); SELECT ToSet($l); -- {1,2,3} ``` #### Signature ```yql ToSet(List)->Set ToSet(List?)->Set? ``` ## ListFromTuple Creates a list from a tuple with compatible element types. For an optional tuple, returns an optional list. For a NULL argument, returns NULL. For an empty tuple, returns EmptyList. #### Examples ```yql $t = (1,2,3); SELECT ListFromTuple($t); -- [1,2,3] ``` #### Signature ```yql ListFromTuple(Null)->Null ListFromTuple(Tuple<>)->EmptyList ListFromTuple(Tuple)->List ListFromTuple(Tuple?)->List? ``` ## ListToTuple Creates a tuple from a list and an explicitly specified tuple width. All elements in the resulting tuple will have the same type as the list elements. If the list length doesn't match the specified tuple width, returns an error. For an optional list, returns an optional tuple. For a NULL argument, returns NULL. #### Examples ```yql $l = [1,2,3]; SELECT ListToTuple($l, 3); -- (1,2,3) ``` #### Signature ```yql ListToTuple(Null,N)->Null ListToTuple(EmptyList,N)->()) -- N must be 0 ListToTuple(List, N)->Tuple -- tuple width is N ListToTuple(List?, N)->Tuple? -- tuple width is N ``` ## ListTop, ListTopAsc, ListTopDesc, ListTopSort, ListTopSortAsc, and ListTopSortDesc {#listtop} Select the top values from the list. `ListTopSort*` additionally sorts the list of the returned values. By default, the lowest values are selected. Functions without a suffix are aliases for `*Asc` functions. `*Desc` functions select the highest values. `ListTopSort` is more efficient than calling `ListTop` followed by `ListSort`, because `ListTop` may partially pre-sort the list when looking for the desired values. However, `ListTop` is more efficient than `ListTopSort` if you don't need to sort the output. Arguments: 1. List. 2. Sample size. 3. An optional expression to get the sort key from a list element (it's the element itself by default). #### Signature ```yql ListTop(List{Flags:AutoMap}, N)->List ListTop(List{Flags:AutoMap}, N, (T)->U)->List ``` The signatures of other functions match those of `ListTop`. # Functions for working with dicts ## DictCreate {#dictcreate} #### Signature ```yql DictCreate(K,V)->Dict ``` Construct an empty dict. Two arguments are passed — for the key and the value. In each of them, a string describing the data type or the type itself obtained using the [functions intended for it](https://ytsaurus.tech/docs/en/types.md) are specified. There are no dicts with an unknown key or value type in YQL. A key type can be: * A [primitive data type](https://ytsaurus.tech/docs/types/primitive.md) (except for `Yson` and `Json`). * A primitive data type (except for `Yson` and `Json`) with an optionality sign. * A tuple with the length of at least two of the types listed above. [Type description format documentation](https://ytsaurus.tech/docs/types/type_string.md). #### Examples ```yql SELECT DictCreate(String, Tuple); ``` ```yql SELECT DictCreate(Tuple, OptionalType(DataType("String"))); ``` ```yql SELECT DictCreate(ParseType("Tuple"), ParseType("Tuple")); ``` ## SetCreate {#setcreate} #### Signature ```yql SetCreate(T)->Set ``` Construct an empty set. The argument is passed: a key type probably obtained using the [functions intended for it](https://ytsaurus.tech/docs/en/types.md). There are no sets with an unknown key type in YQL. Restrictions on the key type are the same as on the key type for the dict. Note that a set is a dict with the `Void` value type. A set can also be created using the `DictCreate` function. This also means that all functions that take `Dict` as input can also take `Set` as input. [Type description format documentation](https://ytsaurus.tech/docs/types/type_string.md). #### Examples ```yql SELECT SetCreate(String); ``` ```yql SELECT SetCreate(Tuple); ``` ## DictLength {#dictlength} #### Signature ```yql DictLength(Dict)->Uint64 DictLength(Dict?)->Uint64? ``` Number of items in the dict. #### Examples ```yql SELECT DictLength(AsDict(AsTuple(1, AsList("foo", "bar")))); ``` ```yql SELECT DictLength(dict_column) FROM my_table; ``` ## DictHasItems {#dicthasitems} #### Signature ```yql DictHasItems(Dict)->Bool DictHasItems(Dict?)->Bool? ``` Checking that the dict contains at least one item. #### Examples ```yql SELECT DictHasItems(AsDict(AsTuple(1, AsList("foo", "bar")))) FROM my_table; ``` ```yql SELECT DictHasItems(dict_column) FROM my_table; ``` ## DictItems {#dictitems} #### Signature ```yql DictItems(Dict)->List> DictItems(Dict?)->List>? ``` Getting the contents of the dict as a list of tuples with key-value pairs (`List>`). #### Examples ```yql SELECT DictItems(AsDict(AsTuple(1, AsList("foo", "bar")))); -- [ ( 1, [ "foo", "bar" ] ) ] ``` ```yql SELECT DictItems(dict_column) FROM my_table; ``` ## DictKeys {#dictkeys} #### Signature ```yql DictKeys(Dict)->List DictKeys(Dict?)->List? ``` Getting a list of dict keys. #### Examples ```yql SELECT DictKeys(AsDict(AsTuple(1, AsList("foo", "bar")))); -- [ 1 ] ``` ```yql SELECT DictKeys(dict_column) FROM my_table; ``` ## DictPayloads {#dictpayloads} #### Signature ```yql DictPayloads(Dict)->List DictPayloads(Dict?)->List? ``` Getting a list of dict values. #### Examples ```yql SELECT DictPayloads(AsDict(AsTuple(1, AsList("foo", "bar")))); -- [ [ "foo", "bar" ] ] ``` ```yql SELECT DictPayloads(dict_column) FROM my_table; ``` ## DictLookup {#dictlookup} #### Signature ```yql DictLookup(Dict, K)->V? DictLookup(Dict?, K)->V? DictLookup(Dict, K?)->V? DictLookup(Dict?, K?)->V? ``` Getting a dict item by key. #### Examples ```yql SELECT DictLookup(AsDict( AsTuple(1, AsList("foo", "bar")), AsTuple(2, AsList("bar", "baz")) ), 1); -- [ "foo", "bar" ] ``` ```yql SELECT DictLookup(dict_column, "foo") FROM my_table; ``` ## DictContains {#dictcontains} #### Signature ```yql DictContains(Dict, K)->Bool DictContains(Dict?, K)->Bool DictContains(Dict, K?)->Bool DictContains(Dict?, K?)->Bool ``` Checking presence of an item in the dict by key. Returns true or false. #### Examples ```yql SELECT DictContains(AsDict( AsTuple(1, AsList("foo", "bar")), AsTuple(2, AsList("bar", "baz")) ), 42); -- false ``` ```yql SELECT DictContains(dict_column, "foo") FROM my_table; ``` ## DictAggregate {#dictaggregate} #### Signature ```yql DictAggregate(Dict>, List->T)->Dict DictAggregate(Dict>?, List->T)->Dict? ``` Apply the [aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory) for the passed dict where each value is a list. The factory is applied individually within each key. If the list is empty, the aggregation result will be the same as for an empty table: 0 for the `COUNT` function and `NULL` for other functions. If the list in the passed dict is empty by a key, then such a key is removed from the result. If the passed dict is optional and contains the `NULL` value, the result will also be `NULL`. Arguments: 1. Dict. 2. [Aggregate function factory](https://ytsaurus.tech/docs/en/basic.md#aggregationfactory). #### Examples ```yql SELECT DictAggregate(AsDict( AsTuple(1, AsList("foo", "bar")), AsTuple(2, AsList("baz", "qwe"))), AggregationFactory("Max")); -- {1 : "foo", 2 : "qwe" } ``` ## SetIsDisjoint {#setisjoint} #### Signature ```yql SetIsDisjoint(Dict, Dict)->Bool SetIsDisjoint(Dict?, Dict)->Bool? SetIsDisjoint(Dict, Dict?)->Bool? SetIsDisjoint(Dict?, Dict?)->Bool? SetIsDisjoint(Dict, List)->Bool SetIsDisjoint(Dict?, List)->Bool? SetIsDisjoint(Dict, List?)->Bool? SetIsDisjoint(Dict?, List?)->Bool? ``` Checking that the dict and the list or other dict do not intersect by keys. Thus, there are two invocation variants: * With the `Dict` and `List` arguments. * With the `Dict` and `Dict` arguments. #### Examples ```yql SELECT SetIsDisjoint(ToSet(AsList(1, 2, 3)), AsList(7, 4)); -- true SELECT SetIsDisjoint(ToSet(AsList(1, 2, 3)), ToSet(AsList(3, 4))); -- false ``` ## SetIntersection {#setintersection} #### Signature ```yql SetIntersection(Dict, Dict)->Set SetIntersection(Dict?, Dict)->Set? SetIntersection(Dict, Dict?)->Set? SetIntersection(Dict?, Dict?)->Set? SetIntersection(Dict, Dict, (K,V1,V2)->U)->Dict SetIntersection(Dict?, Dict, (K,V1,V2)->U)->Dict? SetIntersection(Dict, Dict?, (K,V1,V2)->U)->Dict? SetIntersection(Dict?, Dict?, (K,V1,V2)->U)->Dict? ``` Builds an intersection of two dicts by keys. Arguments: * Two dicts: `Dict` and `Dict`. * An optional function that combines values from the source dicts to build values of the output dict. If the type of this function is `(K,V1,V2) -> U`, the result type is `Dict`. If the function is not set, the result type is `Dict` and values from the source dicts are ignored. #### Examples ```yql SELECT SetIntersection(ToSet(AsList(1, 2, 3)), ToSet(AsList(3, 4))); -- { 3 } SELECT SetIntersection( AsDict(AsTuple(1, "foo"), AsTuple(3, "bar")), AsDict(AsTuple(1, "baz"), AsTuple(2, "qwe")), ($k, $a, $b) -> { RETURN AsTuple($a, $b) }); -- { 1 : ("foo", "baz") } ``` {% note info %} The [lambda function](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) was used in the example. {% endnote %} ## SetIncludes {#setincludes} #### Signature ```yql SetIncludes(Dict, List)->Bool SetIncludes(Dict?, List)->Bool? SetIncludes(Dict, List?)->Bool? SetIncludes(Dict?, List?)->Bool? SetIncludes(Dict, Dict)->Bool SetIncludes(Dict?, Dict)->Bool? SetIncludes(Dict, Dict?)->Bool? SetIncludes(Dict?, Dict?)->Bool? ``` Checking that the keys of the given dict include all the list items or the keys of the second dict. Thus, there are two invocation variants: * With the `Dict` and `List` arguments. * With the `Dict` and `Dict` arguments. #### Examples ```yql SELECT SetIncludes(ToSet(AsList(1, 2, 3)), AsList(3, 4)); -- false SELECT SetIncludes(ToSet(AsList(1, 2, 3)), ToSet(AsList(2, 3))); -- true ``` ## SetUnion {#setunion} #### Signature ```yql SetUnion(Dict, Dict)->Set SetUnion(Dict?, Dict)->Set? SetUnion(Dict, Dict?)->Set? SetUnion(Dict?, Dict?)->Set? SetUnion(Dict, Dict,(K,V1?,V2?)->U)->Dict SetUnion(Dict?, Dict,(K,V1?,V2?)->U)->Dict? SetUnion(Dict, Dict?,(K,V1?,V2?)->U)->Dict? SetUnion(Dict?, Dict?,(K,V1?,V2?)->U)->Dict? ``` Builds a union of two dicts by keys. Arguments: * Two dicts: `Dict` and `Dict`. * An optional function that combines values from the source dicts to build values of the output dict. If the type of this function is `(K,V1?,V2?) -> U`, the result type is `Dict`. If the function is not set, the result type is `Dict` and values from the source dicts are ignored. #### Examples ```yql SELECT SetUnion(ToSet(AsList(1, 2, 3)), ToSet(AsList(3, 4))); -- { 1, 2, 3, 4 } SELECT SetUnion( AsDict(AsTuple(1, "foo"), AsTuple(3, "bar")), AsDict(AsTuple(1, "baz"), AsTuple(2, "qwe")), ($k, $a, $b) -> { RETURN AsTuple($a, $b) }); -- { 1 : ("foo", "baz"), 2 : (null, "qwe"), 3 : ("bar", null) } ``` ## SetDifference {#setdifference} #### Signature ```yql SetDifference(Dict, Dict)->Dict SetDifference(Dict?, Dict)->Dict? SetDifference(Dict, Dict?)->Dict? SetDifference(Dict?, Dict?)->Dict? ``` Builds a dict which has all keys with corresponding values of the first dict for which there is no key in the second dict. #### Examples ```yql SELECT SetDifference(ToSet(AsList(1, 2, 3)), ToSet(AsList(3, 4))); -- { 1, 2 } SELECT SetDifference( AsDict(AsTuple(1, "foo"), AsTuple(2, "bar")), ToSet(AsList(2, 3))); -- { 1 : "foo" } ``` ## SetSymmetricDifference {#setsymmetricdifference} #### Signature ```yql SetSymmetricDifference(Dict, Dict)->Set SetSymmetricDifference(Dict?, Dict)->Set? SetSymmetricDifference(Dict, Dict?)->Set? SetSymmetricDifference(Dict?, Dict?)->Set? SetSymmetricDifference(Dict, Dict,(K,V1?,V2?)->U)->Dict SetSymmetricDifference(Dict?, Dict,(K,V1?,V2?)->U)->Dict? SetSymmetricDifference(Dict, Dict?,(K,V1?,V2?)->U)->Dict? SetSymmetricDifference(Dict?, Dict?,(K,V1?,V2?)->U)->Dict? ``` Builds a symmetric difference of two dicts by keys. Arguments: * Two dicts: `Dict` and `Dict`. * An optional function that combines values from the source dicts to build values of the output dict. If the type of this function is `(K,V1?,V2?) -> U`, the result type is `Dict`. If the function is not set, the result type is `Dict` and values from the source dicts are ignored. #### Examples ```yql SELECT SetSymmetricDifference(ToSet(AsList(1, 2, 3)), ToSet(AsList(3, 4))); -- { 1, 2, 4 } SELECT SetSymmetricDifference( AsDict(AsTuple(1, "foo"), AsTuple(3, "bar")), AsDict(AsTuple(1, "baz"), AsTuple(2, "qwe")), ($k, $a, $b) -> { RETURN AsTuple($a, $b) }); -- { 2 : (null, "qwe"), 3 : ("bar", null) } ``` ## DictInsert {#dictinsert} #### Signature ```yql DictInsert(Dict,K,V)->Dict ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Returns a new dict with the specified key and value added to it. If this key already existed, the dict remains unchanged. When working with a `Set`, pass the `Void()` function as the value. #### Examples ```yql SELECT DictInsert({'foo':1}, 'bar', 2); -- {'foo':1,'bar':2} SELECT DictInsert({'foo':1}, 'foo', 2); -- {'foo':1} SELECT DictInsert({'foo'}, 'bar', Void()); -- {'foo','bar'} ``` ## DictUpsert {#dictupsert} #### Signature ```yql DictUpsert(Dict,K,V)->Dict ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Returns a new dict with the specified key and value either replaced or added to it. If this key already existed, the value is updated. #### Examples ```yql SELECT DictUpsert({'foo':1}, 'bar', 2); -- {'foo':1,'bar':2} SELECT DictUpsert({'foo':1}, 'foo', 2); -- {'foo':2} ``` ## DictUpdate {#dictupdate} #### Signature ```yql DictUpdate(Dict,K,V)->Dict ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Returns a new dict with the value for the specified key replaced. If this key did not exist, the dict remains unchanged. #### Examples ```yql SELECT DictUpdate({'foo':1}, 'bar', 2); -- {'foo':1} SELECT DictUpdate({'foo':1}, 'foo', 2); -- {'foo':2} ``` ## DictRemove {#dictremove} #### Signature ```yql DictRemove(Dict,K)->Dict ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Returns a new dict without the specified key. If this key did not exist, the dict remains unchanged. #### Examples ```yql SELECT DictRemove({'foo':1}, 'bar'); -- {'foo':1} SELECT DictRemove({'foo':1}, 'foo'); -- {} ``` ## ToMutDict {#tomutdict} #### Signature ```yql ToMutDict(Dict,dependArg1...)->Linear> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Converts a dict into its mutable version. You also need to pass one or more dependent expressions, for example, using the `lambda` argument in the [`Block`](https://ytsaurus.tech/docs/en/basic.md#block) function. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); return FromMutDict($dict); }); -- {'foo':1} ``` ## MutDictCreate {#mutdictcreate} #### Signature ```yql MutDictCreate(KeyType,ValueType,dependArg1...)->Linear> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Constructs an empty mutable dict with the specified key and value types. You also need to pass one or more dependent expressions, for example, using the `lambda` argument in the [`Block`](https://ytsaurus.tech/docs/en/basic.md#block) function. #### Examples ```yql SELECT Block( ($arg)->{ $dict = MutDictCreate(String, Int32, $arg); return FromMutDict($dict); }); -- {} ``` ## FromMutDict {#frommutdict} #### Signature ```yql FromMutDict(Linear>)->Dict ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Absorbs a mutable dict and converts it into an immutable one. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); return FromMutDict($dict); }); -- {'foo':1} ``` ## MutDictInsert {#mutdictinsert} #### Signature ```yql MutDictInsert(Linear>,K,V)->Linear> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Adds the specified key and value to a mutable dict and returns this mutable dict. If this key already existed in the dict, the dict remains unchanged. When working with a `Set`, pass the `Void()` function as the value. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictInsert($dict,'foo',2); return FromMutDict($dict); }); -- {'foo':1} SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictInsert($dict,'bar',2); return FromMutDict($dict); }); -- {'foo':1,'bar':2} SELECT Block( ($arg)->{ $dict = ToMutDict({'foo'}, $arg); $dict = MutDictInsert($dict,'bar', Void()); return FromMutDict($dict); }); -- {'foo','bar'} ``` ## MutDictUpsert {#mutdictupsert} #### Signature ```yql MutDictUpsert(Linear>,K,V)->Linear> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Adds the specified key and value to a mutable dict or replaces them and returns this mutable dict. If this key already existed in the dict, the value is updated. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictUpsert($dict,'foo',2); return FromMutDict($dict); }); -- {'foo':2} SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictUpsert($dict,'bar',2); return FromMutDict($dict); }); -- {'foo':1,'bar':2} ``` ## MutDictUpdate {#mutdictupdate} #### Signature ```yql MutDictUpdate(Linear>,K,V)->Linear> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Replaces a value for the specified key in a mutable dict and returns this mutable dict. If this key did not exist in the dict, the dict remains unchanged. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictUpdate($dict,'foo',2); return FromMutDict($dict); }); -- {'foo':2} SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictUpdate($dict,'bar',2); return FromMutDict($dict); }); -- {'foo':1} ``` ## MutDictRemove {#mutdictremove} #### Signature ```yql MutDictRemove(Linear>,K)->Linear> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Removes a value for the specified key from a mutable dict and returns this mutable dict. If this key did not exist in the dict, the dict remains unchanged. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictRemove($dict,'foo'); return FromMutDict($dict); }); -- {} SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict = MutDictRemove($dict,'bar'); return FromMutDict($dict); }); -- {'foo':1} ``` ## MutDictPop {#mutdictpop} #### Signature ```yql MutDictPop(Linear>,K)->Tuple>,V?> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Removes a value for the specified key from a mutable dict and returns this mutable dict and the value for the removed key. If this key did not exist in the dict, the dict remains unchanged and an empty Optional is returned. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictPop($dict,'foo'); return (FromMutDict($dict), $val); }); -- ({},1) SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictPop($dict,'bar'); return (FromMutDict($dict), $val); }); -- ({'foo':1},null) ``` ## MutDictContains {#mutdictcontains} #### Signature ```yql MutDictContains(Linear>,K)->Tuple>,Bool> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Checks if the specified key exists in a mutable dict and returns this mutable dict along with the result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictContains($dict,'foo'); return (FromMutDict($dict), $val); }); -- ({'foo':1},True) SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictContains($dict,'bar'); return (FromMutDict($dict), $val); }); -- ({'foo':1},False) ``` ## MutDictLookup {#mutdictlookup} #### Signature ```yql MutDictLookup(Linear>,K)->Tuple>,V?> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Gets a value for the specified key in a mutable dict and returns this mutable dict and the optional result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictLookup($dict,'foo'); return (FromMutDict($dict), $val); }); -- ({'foo':1},1) SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictLookup($dict,'bar'); return (FromMutDict($dict), $val); }); -- ({'foo':1},null) ``` ## MutDictHasItems {#mutdicthasitems} #### Signature ```yql MutDictHasItems(Linear>)->Tuple>,Bool> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Checks if a mutable dict is empty and returns this mutable dict and the result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictHasItems($dict); return (FromMutDict($dict), $val); }); -- ({'foo':1},True) SELECT Block( ($arg)->{ $dict = MutDictCreate(String, Int32, $arg); $dict, $val = MutDictHasItems($dict); return (FromMutDict($dict), $val); }); -- ({},False) ``` ## MutDictLength {#mutdictlength} #### Signature ```yql MutDictLength(Linear>)->Tuple>,Uint64> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Gets the number of items in a mutable dict and returns this mutable dict and the result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictLength($dict); return (FromMutDict($dict), $val); }); -- ({'foo':1},1) SELECT Block( ($arg)->{ $dict = MutDictCreate(String, Int32, $arg); $dict, $val = MutDictLength($dict); return (FromMutDict($dict), $val); }); -- ({},0) ``` ## MutDictKeys {#mutdictkeys} #### Signature ```yql MutDictKeys(Linear>)->Tuple>,List> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Gets a list of keys in a mutable dict and returns this mutable dict and the result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictKeys($dict); return (FromMutDict($dict), $val); }); -- ({'foo':1},['foo']) SELECT Block( ($arg)->{ $dict = MutDictCreate(String, Int32, $arg); $dict, $val = MutDictKeys($dict); return (FromMutDict($dict), $val); }); -- ({},[]) ``` ## MutDictPayloads {#mutdictpayloads} #### Signature ```yql MutDictPayloads(Linear>)->Tuple>,List> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Gets a list of values in a mutable dict and returns this mutable dict and the result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictPayloads($dict); return (FromMutDict($dict), $val); }); -- ({'foo':1},['1']) SELECT Block( ($arg)->{ $dict = MutDictCreate(String, Int32, $arg); $dict, $val = MutDictPayloads($dict); return (FromMutDict($dict), $val); }); -- ({},[]) ``` ## MutDictItems {#mutdictitems} #### Signature ```yql MutDictItems(Linear>)->Tuple>,List>> ``` Introduced in version [2025.04](https://ytsaurus.tech/docs/changelog/2025.04.md). Gets a list of tuples with key-value pairs in a mutable dict and returns this mutable dict and the result. #### Examples ```yql SELECT Block( ($arg)->{ $dict = ToMutDict({'foo':1}, $arg); $dict, $val = MutDictItems($dict); return (FromMutDict($dict), $val); }); -- ({'foo':1},[('foo',1)]) SELECT Block( ($arg)->{ $dict = MutDictCreate(String, Int32, $arg); $dict, $val = MutDictItems($dict); return (FromMutDict($dict), $val); }); -- ({},[]) ``` # Functions for working with structures ## TryMember {#trymember} An attempt to get the value of a field from the structure and if it is not among the fields or null in the structure value, use the default value. The `default_value` type must match the `key` field type from the structure. **Signature** ``` TryMember(struct:Struct<...>, key:String, default_value:T) -> T TryMember(struct:Struct<...>?, key:String, default_value:T) -> T? ``` Arguments: 1. struct: Original structure. 2. key: Field name. 3. default_value: Default value if the field is missing {% note info "Limitation" %} The field name (key) cannot depend on lambda data or arguments. In this case, the TryMember function cannot be prototyped. {% endnote %} **Examples** ```yql $struct = <|a:1|>; SELECT TryMember( $struct, "a", 123 ) AS a, -- 1 TryMember( $struct, "b", 123 ) AS b; -- 123 ``` ## ExpandStruct {#expandstruct} Adding one or more new fields to the structure. A new extended structure returns. If there are duplicates in the set of fields, an error will be returned. **Signature** ``` ExpandStruct(struct:Struct<...>, value_1:T1 AS key_1:K, value_2:T2 AS key_2:K, ....) -> Struct<...> ``` Arguments: * The source structure for the extension is passed to the first argument. * All other arguments must be named, each argument adds a new field, and the argument name is used as the field name (similar to [AsStruct](https://ytsaurus.tech/docs/en/basic.md#asstruct)). **Examples** ```yql $struct = <|a:1|>; SELECT ExpandStruct( $struct, 2 AS b, "3" AS c ) AS abc; -- ("a": 1, "b": 2, "c": "3") ``` ## AddMember {#addmember} Adding one new field to the structure. If you need to add multiple fields, better use [ExpandStruct](https://ytsaurus.tech/docs/en/#expandstruct). If there are duplicates in the set of fields, an error will be returned. **Signature** ``` AddMember(struct:Struct<...>, new_key:String, new_value:T) -> Struct<...> ``` Arguments: 1. struct: Original structure. 2. new_key: Name of the new field. 3. new_value: Value of the new field. **Examples** ```yql $struct = <|a:1|>; SELECT AddMember( $struct, "b", 2 ) AS ab; -- ("a": 1, "b": 2) ``` ## RemoveMember {#removemember} Removing a field from the structure. If the specified field did not exist, an error will be returned. **Signature** ``` RemoveMember(struct:Struct<...>, key_to_delete:String) -> Struct<...> ``` Arguments: 1. Original structure. 2. Name of the field to be removed **Examples** ```yql $struct = <|a:1, b:2|>; SELECT RemoveMember( $struct, "b" ) AS a; -- ("a": 1) ``` ## ForceRemoveMember {#forceremovemember} Removing a field from the structure. If the specified field did not exist, an error will not be returned unlike [RemoveMember](https://ytsaurus.tech/docs/en/#removemember). **Signature** ``` ForceRemoveMember(struct:Struct<...>, key_to_delete:String) -> Struct<...> ``` Arguments: 1. Original structure. 2. Name of the field to be removed. **Examples** ```yql $struct = <|a:1, b:2|>; SELECT ForceRemoveMember( $struct, "c" ) AS ab; -- ("a": 1, "b": 2) ``` ## ChooseMembers {#choosemembers} Selecting fields with given names from the structure. The new structure is returned only from the specified fields. If any of the fields did not exist, an error will be returned. **Signature** ``` ChooseMembers(struct:Struct<...>, list_of_keys:List) -> Struct<...> ``` Arguments: 1. Original structure. 2. List of field names. **Examples** ```yql $struct = <|a:1, b:2, c:3|>; SELECT ChooseMembers( $struct, ["a", "b"] ) AS ab; -- ("a": 1, "b": 2) ``` ## RemoveMembers {#removemembers} Removing fields with given names from the structure. If any of the fields did not exist, an error will be returned. **Signature** ``` RemoveMembers(struct:Struct<...>, list_of_delete_keys:List) -> Struct<...> ``` Arguments: 1. Original structure. 2. List of field names. **Examples** ```yql $struct = <|a:1, b:2, c:3|>; SELECT RemoveMembers( $struct, ["a", "b"] ) AS c; -- ("c": 3) ``` ## ForceRemoveMembers {#forceremovemembers} Removing fields with given names from the structure. If any of the fields did not exist, it is ignored. **Signature** ``` ForceRemoveMembers(struct:Struct<...>, list_of_delete_keys:List) -> Struct<...> ``` Arguments: 1. Original structure. 2. List of field names. **Examples** ```yql $struct = <|a:1, b:2, c:3|>; SELECT ForceRemoveMembers( $struct, ["a", "b", "z"] ) AS c; -- ("c": 3) ``` ## CombineMembers {#combinemembers} Combining fields of several structures into a new structure. If there are duplicates in the resulting set of fields, an error will be returned. **Signature** ``` CombineMembers(struct1:Struct<...>, struct2:Struct<...>, .....) -> Struct<...> CombineMembers(struct1:Struct<...>?, struct2:Struct<...>?, .....) -> Struct<...> ``` Arguments: two or more structures. **Examples** ```yql $struct1 = <|a:1, b:2|>; $struct2 = <|c:3|>; SELECT CombineMembers( $struct1, $struct2 ) AS abc; -- ("a": 1, "b": 2, "c": 3) ``` ## FlattenMembers {#flattenmembers} Combining the fields of several new structures into a new structure with prefix support. If there are duplicates in the resulting set of fields, an error will be returned. **Signature** ``` FlattenMembers(prefix_struct1:Tuple>, prefix_struct2:Tuple>, ...) -> Struct<...> ``` Arguments: two or more tuples from two elements: prefix and structure. **Examples** ```yql $struct1 = <|a:1, b:2|>; $struct2 = <|c:3|>; SELECT FlattenMembers( AsTuple("foo", $struct1), -- fooa, foob AsTuple("bar", $struct2) -- barc ) AS abc; -- ("barc": 3, "fooa": 1, "foob": 2) ``` ## StructMembers {#structmembers} Returns an unordered list of field names (possibly removing one optionality level) for a single argument — the structure. An empty list of strings is returned for a `NULL` argument. **Signature** ``` StructMembers(struct:Struct<...>) -> List StructMembers(struct:Struct<...>?) -> List StructMembers(NULL) -> [] ``` Argument: structure **Examples** ```yql $struct = <|a:1, b:2|>; SELECT StructMembers($struct) AS a, -- ['a', 'b'] StructMembers(NULL) AS b; -- [] ``` ## RenameMembers {#renamemembers} Renames fields in the passed structure. The original field can be renamed into several new ones. All fields not mentioned in the process of renaming as source fields are transferred to the resulting structure. If an original field is not in the list for renaming, an error is displayed. For an optional structure or `NULL`, so is the result. **Signature** ``` RenameMembers(struct:Struct<...>, rename_rules:List>) -> Struct<...> ``` Arguments: 1. Original structure. 2. A list of field names in the form of a tuple list: original name, new name. **Examples** ```yql $struct = <|a:1, b:2|>; SELECT RenameMembers($struct, [('a', 'c'), ('a', 'e')]); -- (b:2, c:1, e:1) ``` ## ForceRenameMembers {#forecerenamemembers} Renames fields in the passed structure. The original field can be renamed into several new ones. All fields not mentioned in the process of renaming as source fields are transferred to the resulting structure. If an original field is not in the list for renaming, it is ignored. For an optional structure or `NULL`, so is the result. **Signature** ``` ForceRenameMembers(struct:Struct<...>, rename_rules:List>) -> Struct<...> ``` Arguments: 1. Original structure. 2. A list of field names in the form of a tuple list: original name, new name. **Examples** ```yql $struct = <|a:1, b:2|>; SELECT ForceRenameMembers($struct, [('a', 'c'), ('d', 'e')]); -- (b:2, c:1) ``` ## GatherMembers {#gathermembers} Returns an unordered tuple list from the field name and value. For the `NULL` argument, `EmptyList` is returned. Can be used only when the item types in the structure are identical or compatible. For an optional structure, an optional list is returned. **Signature** ``` GatherMembers(struct:Struct<...>) -> List> GatherMembers(struct:Struct<...>?) -> List>? GatherMembers(NULL) -> [] ``` Argument: structure **Examples** ```yql $struct = <|a:1, b:2|>; SELECT GatherMembers($struct), -- [('a', 1), ('b', 2)] GatherMembers(null); -- [] ``` ## SpreadMembers {#spreadmembers} Creates a structure with a given list of fields and applies a given list of corrections in the (field name, value) format to it. All field types of the resulting structure are the same and equal to the type of values in the list of corrections with the added optionality (if they weren't already). If the field was not mentioned among the list of editable fields, it is returned as `NULL`. Among all the corrections for one field, the last one is saved. If there is a field in the list of corrections that is not in the list of expected fields, an error is displayed. **Signature** ``` SpreadMembers(list_of_tuples:List>, result_keys:List) -> Struct<...> ``` Arguments: 1. Tuple list: field name, field value. 2. A list of all possible field names in the structure. **Examples** ```yql SELECT SpreadMembers([('a',1),('a',2)],['a','b']); -- (a: 2, b: null) ``` ## ForceSpreadMembers {#forcespreadmembers} Creates a structure with a given list of fields and applies a given list of corrections in the (field name, value) format to it. All field types of the resulting structure are the same and equal to the type of values in the list of corrections with the added optionality (if they weren't already). If the field was not mentioned among the list of editable fields, it is returned as `NULL`. Among all the corrections for one field, the last one is saved. If there is a field in the list of corrections that is not in the list of expected fields, this correction is ignored. **Signature** ``` ForceSpreadMembers(list_of_tuples:List>, result_keys:List) -> Struct<...> ``` Arguments: 1. Tuple list: field name, field value. 2. A list of all possible field names in the structure. **Examples** ```yql SELECT ForceSpreadMembers([('a',1),('a',2),('c',100)],['a','b']); -- (a: 2, b: null) ``` # Functions for working with data types In addition to the ordinary functions that work with particular values (FIND, COALESCE types), YQL supports functions for working with [types](https://ytsaurus.tech/docs/types/index.md). The functions enable you to find out the type of an arbitrary expression, analyze a container type, and create a new container type based on the existing one. ## Examples ```yql $itemType = TypeOf($item); SELECT CAST($foo AS ListType($itemType)); -- cast $foo to type List<$itemType> ``` ## FormatType {#formattype} #### Signature ```yql FormatType(Type)->String FormatType(TypeHandle)->String ``` Serialization of a type or a type handle into a human-readable string. This is useful for debugging and will also be used in the following examples in this section. [Format documentation](https://ytsaurus.tech/docs/types/type_string.md). ## FormatTypeDiff and FormatTypeDiffPretty {#formattypediff} #### Signature ```yql FormatTypeDiff(Type, Type)->String FormatTypeDiff(TypeHandle, TypeHandle)->String FormatTypeDiffPretty(Type, Type)->String FormatTypeDiffPretty(TypeHandle, TypeHandle)->String ``` Getting a string representation of the difference between two types or two type handles. The pretty version makes the resulting string more human-readable by adding line breaks and spaces. ## ParseType {#parsetype} #### Signature ```yql ParseType(String)->Type ``` Building a type by a string with its description. [Its format documentation](https://ytsaurus.tech/docs/types/type_string.md). #### Examples ```yql SELECT FormatType(ParseType("List")); -- List ``` ## TypeOf {#typeof} #### Signature ```yql TypeOf()->Type ``` Getting the value type passed to the argument. #### Examples ```yql SELECT FormatType(TypeOf("foo")); -- String ``` ```yql SELECT FormatType(TypeOf(AsTuple(1, 1u))); -- Tuple ``` ## InstanceOf {#instanceof} #### Signature ```yql InstanceOf(Type)->Type object ``` Returns an instance of the object of the specified type. The resulting object does not have any particular meaning. InstanceOf can only be used if the result of the expression in which InstanceOf is used depends on the InstanceOf type, but not on the value. Otherwise, the operation will be completed with an error. #### Examples ```yql SELECT InstanceOf(ParseType("Int32")) + 1.0; -- error (Can't execute InstanceOf): the result depends on the (undefined) value of InstanceOf SELECT FormatType(TypeOf( InstanceOf(ParseType("Int32")) + InstanceOf(ParseType("Double")) )); -- will return Double, because adding Int32 and Double returns Double (InstanceOf is used in the context where only its type is important, but not its value) ``` ## DataType {#datatype} #### Signature ```yql DataType(String, [String, ...])->Type ``` Returns the type for [primitive data types](https://ytsaurus.tech/docs/types/primitive.md) by its name. For some types (for example, Decimal), you need to pass type parameters as additional arguments. #### Examples ```yql SELECT FormatType(DataType("Bool")); -- Bool SELECT FormatType(DataType("Decimal","5","1")); -- Decimal(5,1) ``` ## OptionalType {#optionaltype} #### Signature ```yql OptionalType(Type)->optional Type ``` Adds the ability to contain `NULL` to the passed type. #### Examples ```yql SELECT FormatType(OptionalType(DataType("Bool"))); -- Bool? SELECT FormatType(OptionalType(ParseType("List"))); -- List? ``` ## ListType and StreamType {#listtype} #### Signature ```yql ListType(Type)->list type with items of the Type type StreamType(Type)->stream type with items of the Type type ``` Builds a list or stream type based on the passed item type. #### Examples ```yql SELECT FormatType(ListType(DataType("Bool"))); -- List ``` ## DictType {#dicttype} #### Signature ```yql DictType(Type, Type)->dict type ``` Builds a dict type based on the passed key (first argument) and value (second argument) types. #### Examples ```yql SELECT FormatType(DictType( DataType("String"), DataType("Double") )); -- Dict ``` ## TupleType {#tupletype} #### Signature ```yql TupleType(Type, ...)->tuple type ``` Builds a tuple type based on the passed item types. #### Examples ```yql SELECT FormatType(TupleType( DataType("String"), DataType("Double"), OptionalType(DataType("Bool")) )); -- Tuple ``` ## StructType {#structtype} #### Signature ```yql StructType(Type AS ElementName1, Type AS ElementName2, ...)->structure type ``` Builds a structure type based on the passed item types. The standard syntax for named arguments is used to specify item names. #### Examples ```yql SELECT FormatType(StructType( DataType("Bool") AS MyBool, ListType(DataType("String")) AS StringList )); -- Struct<'MyBool':Bool,'StringList':List> ``` ## VariantType {#varianttype} #### Signature ```yql VariantType(StructType)->variant type on top of structure VariantType(TupleType)->variant type on top of tuple ``` Returns the variant type by the underlying type (structure or tuple). #### Examples ```yql SELECT FormatType(VariantType( ParseType("Struct") )); -- Variant<'bar':Double,'foo':Int32> ``` ## ResourceType {#resourcetype} #### Signature ```yql ResourceType(String)->resource type ``` Returns the [resource](https://ytsaurus.tech/docs/types/special.md) type by the passed string label. #### Examples ```yql SELECT FormatType(ResourceType("Foo")); -- Resource<'Foo'> ``` ## CallableType {#callabletype} #### Signature ```yql CallableType(Uint32, Type, [Type, ...])->callable value type ``` Builds the callable value type based on the following arguments: 1. Number of optional arguments (if all are mandatory, then 0). 2. Result type. 3. All subsequent CallableType arguments are interpreted as the argument types of the callable value with a shift of two mandatory ones (for example, the third CallableType argument describes the type of the first argument of the callable value). #### Examples ```yql SELECT FormatType(CallableType( 1, -- optional args count DataType("Double"), -- result type DataType("String"), -- arg #1 type OptionalType(DataType("Int64")) -- arg #2 type )); -- Callable<(String,[Int64?])->Double> ``` ## GenericType, UnitType and VoidType {#generictype} #### Signature ```yql GenericType()->type UnitType()->type VoidType()->type ``` Return [special data types](https://ytsaurus.tech/docs/types/special.md) with the same name. There are no arguments, because they are not parameterized. #### Examples ```yql SELECT FormatType(VoidType()); -- Void ``` ## OptionalItemType, ListItemType and StreamItemType {#optionalitemtype} #### Signature ```yql OptionalItemType(OptionalType)->optional item type ListItemType(ListType)->list item type StreamItemType(StreamType)->stream item type ``` If a type is passed to these functions, they perform the action opposite to [OptionalType](https://ytsaurus.tech/docs/en/#optionaltype), [ListType](https://ytsaurus.tech/docs/en/#listtype), and [StreamType](https://ytsaurus.tech/docs/en/#listtype) — they return the item type by the type of the corresponding container. If a type handle is passed to these functions, they perform the action opposite to [OptionalTypeHandle](https://ytsaurus.tech/docs/en/#optionaltypehandle), [ListTypeHandle](https://ytsaurus.tech/docs/en/#list-stream-typehandle), and [StreamTypeHandle](https://ytsaurus.tech/docs/en/#list-stream-typehandle) — they return the item type handle by the type handle of the corresponding container. #### Examples ```yql SELECT FormatType(ListItemType( ParseType("List") )); -- Int32 ``` ```yql SELECT FormatType(ListItemType( ParseTypeHandle("List") )); -- Int32 ``` ## DictKeyType and DictPayloadType {#dictkeytype} #### Signature ```yql DictKetType(DictType)->dict key type DictPayloadType(DictType)->dict meaning type ``` Return the key or meaning type by the dict type. #### Examples ```yql SELECT FormatType(DictKeyType( ParseType("Dict") )); -- Int32 ``` ## TupleElementType {#tupleelementtype} #### Signature ```yql TupleElementType(TupleType, String)->tuple item type ``` Returns the tuple item type by the tuple type and item index (index from zero). #### Examples ```yql SELECT FormatType(TupleElementType( ParseType("Tuple"), "1" )); -- Double ``` ## StructMemberType {#structmembertype} #### Signature ```yql StructMemberType(StructType, String)->structure item type ``` Returns the structure item type by the structure type and item name. #### Examples ```yql SELECT FormatType(StructMemberType( ParseType("Struct"), "foo" )); -- Int32 ``` ## CallableResultType and CallableArgumentType {#callableresulttype} #### Signature ```yql CallableResultType(CallableType)->callable value result type CallableArgumentType(CallableType, Uint32)->callable value argument type ``` `CallableResultType` returns the result type by the callable value type and `CallableArgumentType` returns the argument type by the callable value type and its index (index from zero). #### Examples ```yql $callable_type = ParseType("(String,Bool)->Double"); SELECT FormatType(CallableResultType( $callable_type )), -- Double FormatType(CallableArgumentType( $callable_type, 1 )); -- Bool ``` ## VariantUnderlyingType {#variantunderlyingtype} #### Signature ```yql VariantUnderlyingType(VariantType)->underlying variant type ``` If a type is passed to this function, it performs the action opposite to [VariantType](https://ytsaurus.tech/docs/en/#varianttype) — it returns the underlying type by the variant type. If a type handle is passed to this function, it performs the action opposite to [VariantTypeHandle](https://ytsaurus.tech/docs/en/#varianttypehandle) — it returns the underlying type handle by the variant type handle. #### Examples ```yql SELECT FormatType(VariantUnderlyingType( ParseType("Variant") )), -- Struct<'bar':Double,'foo':Int32> FormatType(VariantUnderlyingType( ParseType("Variant") )); -- Tuple ``` ```yql SELECT FormatType(VariantUnderlyingType( ParseTypeHandle("Variant") )), -- Struct<'bar':Double,'foo':Int32> FormatType(VariantUnderlyingType( ParseTypeHandle("Variant") )); -- Tuple ``` ## Functions for working with data types during computations To work with data types during computations, the type handle mechanism is used — the [resource](https://ytsaurus.tech/docs/types/special.md) containing an opaque type description. After constructing a type handle, you can return to a regular type using the [EvaluateType](https://ytsaurus.tech/docs/en/#evaluatetype) function. For debugging, convert the type handle into a string using the [FormatType](https://ytsaurus.tech/docs/en/#formattype) function. ### TypeHandle Getting a type handle from the type passed to the argument. #### Signature ```yql TypeHandle(Type)->хэндл типа ``` #### Examples ```yql SELECT FormatType(TypeHandle(TypeOf("foo"))); -- String ``` ### EvaluateType #### Signature ```yql EvaluateType(TypeHandle)->type ``` Getting a type from the type handle passed to the argument. The function is computed before the main computation process starts, just like [EvaluateExpr](https://ytsaurus.tech/docs/en/basic.md#evaluate_expr_atom). #### Examples ```yql SELECT FormatType(EvaluateType(TypeHandle(TypeOf("foo")))); -- String ``` ### ParseTypeHandle #### Signature ```yql ParseTypeHandle(String)->type handle ``` Building a type handle by a string with its description. [Its format documentation](https://ytsaurus.tech/docs/types/type_string.md). #### Examples ```yql SELECT FormatType(ParseTypeHandle("List")); -- List ``` ### TypeKind #### Signature ```yql TypeKind(TypeHandle)->String ``` Getting the name of the top-level type from the type handle passed to the argument. #### Examples ```yql SELECT TypeKind(TypeHandle(TypeOf("foo"))); -- Data SELECT TypeKind(ParseTypeHandle("List")); -- List ``` ### DataTypeComponents #### Signature ```yql DataTypeComponents(DataTypeHandle)->List ``` Getting the name and parameters of the [primitive data type](https://ytsaurus.tech/docs/types/primitive.md) from the handle of the primitive type passed to the argument. The reverse function is [DataTypeHandle](https://ytsaurus.tech/docs/en/#datatypehandle). #### Examples ```yql SELECT DataTypeComponents(TypeHandle(TypeOf("foo"))); -- ["String"] SELECT DataTypeComponents(ParseTypeHandle("Decimal(4,1)")); -- ["Decimal", "4", "1"] ``` ### DataTypeHandle #### Signature ```yql DataTypeHandle(List)->handle of the primitive data type ``` Building a handle of the [primitive data type](https://ytsaurus.tech/docs/types/primitive.md) from its name and parameters passed to the argument as a list. The reverse function is [DataTypeComponents](https://ytsaurus.tech/docs/en/#datatypecomponents). #### Examples ```yql SELECT FormatType(DataTypeHandle( AsList("String") )); -- String SELECT FormatType(DataTypeHandle( AsList("Decimal", "4", "1") )); -- Decimal(4,1) ``` ### OptionalTypeHandle #### Signature ```yql OptionalTypeHandle(TypeHandle)->optional type handle ``` Adds the ability to contain `NULL` to the passed type handle. #### Examples ```yql SELECT FormatType(OptionalTypeHandle( TypeHandle(DataType("Bool")) )); -- Bool? ``` ### PgTypeName #### Signature ```yql PgTypeName(PgTypeHandle)->String ``` Getting a PostgreSQL type name from the type handle passed to the argument. The reverse function is [PgTypeHandle](https://ytsaurus.tech/docs/en/#pgtypehandle). #### Examples ```yql SELECT PgTypeName(ParseTypeHandle("pgint4")); -- int4 ``` ### PgTypeHandle #### Signature ```yql PgTypeHandle(String)->type handle ``` Building a type handle by the name of the PostgreSQL type passed to the argument. The reverse function is [PgTypeName](https://ytsaurus.tech/docs/en/#pgtypename). #### Examples ```yql SELECT FormatType(PgTypeHandle("int4")); -- pgint4 ``` ### ListTypeHandle and StreamTypeHandle {#list-stream-typehandle} #### Signature ```yql ListTypeHandle(TypeHandle)->list type handle StreamTypeHandle(TypeHandle)->stream type handle ``` Builds a list or stream type handle based on the passed item type handle. #### Examples ```yql SELECT FormatType(ListTypeHandle( TypeHandle(DataType("Bool")) )); -- List ``` ### EmptyListTypeHandle and EmptyDictTypeHandle #### Signature ```yql EmptyListTypeHandle()->empty list type handle EmptyDictTypeHandle()->empty dict type handle ``` Builds an empty list or dict type handle. #### Examples ```yql SELECT FormatType(EmptyListTypeHandle()); -- EmptyList ``` ### TupleTypeComponents #### Signature ```yql TupleTypeComponents(TupleTypeHandle)->List ``` Getting the item type handle list from the tuple type handle passed to the argument. The reverse function is [TupleTypeHandle](https://ytsaurus.tech/docs/en/#tupletypehandle). #### Examples ```yql SELECT ListMap( TupleTypeComponents( ParseTypeHandle("Tuple") ), ($x)->{ return FormatType($x) } ); -- ["Int32", "String"] ``` ### TupleTypeHandle #### Signature ```yql TupleTypeHandle(List)->tuple type handle ``` Building a tuple type handle from the item type handles passed to the argument as a list. The reverse function is [TupleTypeComponents](https://ytsaurus.tech/docs/en/#tupletypecomponents). #### Examples ```yql SELECT FormatType( TupleTypeHandle( AsList( ParseTypeHandle("Int32"), ParseTypeHandle("String") ) ) ); -- Tuple ``` ### StructTypeComponents #### Signature ```yql StructTypeComponents(StructTypeHandle)->List> ``` Getting the item type handle list and their names from the structure type handle passed to the argument. The reverse function is [StructTypeHandle](https://ytsaurus.tech/docs/en/#structtypehandle). #### Examples ```yql SELECT ListMap( StructTypeComponents( ParseTypeHandle("Struct") ), ($x) -> { return AsTuple( FormatType($x.Type), $x.Name ) } ); -- [("Int32","a"), ("String","b")] ``` ### StructTypeHandle #### Signature ```yql StructTypeHandle(List>)->structure type handle ``` Building a structure type handle from the item type handles and names passed to the argument as a list. The reverse function is [StructTypeComponents](https://ytsaurus.tech/docs/en/#structtypecomponents). #### Examples ```yql SELECT FormatType( StructTypeHandle( AsList( AsStruct(ParseTypeHandle("Int32") as Type,"a" as Name), AsStruct(ParseTypeHandle("String") as Type, "b" as Name) ) ) ); -- Struct<'a':Int32,'b':String> ``` ### DictTypeComponents #### Signature ```yql DictTypeComponents(DictTypeHandle)->Struct ``` Getting the type-key handle and the type-value handle from the dict type handle passed to the argument. The reverse function is [DictTypeHandle](https://ytsaurus.tech/docs/en/#dicttypehandle). #### Examples ```yql $d = DictTypeComponents(ParseTypeHandle("Dict")); SELECT FormatType($d.Key), -- Int32 FormatType($d.Payload); -- String ``` ### DictTypeHandle #### Signature ```yql DictTypeHandle(TypeHandle, TypeHandle)->dict type handle ``` Building a dict type handle from the type-key and type-value handle passed to the arguments. The reverse function is [DictTypeComponents](https://ytsaurus.tech/docs/en/#dicttypecomponents). #### Examples ```yql SELECT FormatType( DictTypeHandle( ParseTypeHandle("Int32"), ParseTypeHandle("String") ) ); -- Dict ``` ### ResourceTypeTag #### Signature ```yql ResourceTypeTag(ResourceTypeHandle)->String ``` Getting the tag from the resource type handle passed to the argument. The reverse function is [ResourceTypeHandle](https://ytsaurus.tech/docs/en/#resourcetypehandle). #### Examples ```yql SELECT ResourceTypeTag(ParseTypeHandle("Resource")); -- foo ``` ### ResourceTypeHandle #### Signature ```yql ResourceTypeHandle(String)->resource type handle ``` Building a resource type handle by the value of the tag passed to the argument. The reverse function is [ResourceTypeTag](https://ytsaurus.tech/docs/en/#resourcetypetag). #### Examples ```yql SELECT FormatType(ResourceTypeHandle("foo")); -- Resource<'foo'> ``` ### TaggedTypeComponents #### Signature ```yql TaggedTypeComponents(TaggedTypeHandle)->Struct ``` Getting the tag and base type from the tagged type handle passed to the argument. The reverse function is [TaggedTypeHandle](https://ytsaurus.tech/docs/en/#taggedtypehandle). #### Examples ```yql $t = TaggedTypeComponents(ParseTypeHandle("Tagged")); SELECT FormatType($t.Base), $t.Tag; -- Int32, foo ``` ### TaggedTypeHandle #### Signature ```yql TaggedTypeHandle(TypeHandle, String)->tagged type handle ``` Building a tagged type handle by the base type handle and the tag name passed in the arguments. The reverse function is [TaggedTypeComponents](https://ytsaurus.tech/docs/en/#taggedtypecomponents). #### Examples ```yql SELECT FormatType(TaggedTypeHandle( ParseTypeHandle("Int32"), "foo" )); -- Tagged ``` ### VariantTypeHandle #### Signature ```yql VariantTypeHandle(StructTypeHandle)->variant type handle on top of structure VariantTypeHandle(TupleTypeHandle)->variant type handle on top of tuple ``` Building a variant type handle by the underlying type handle passed to the argument. The reverse function is [VariantUnderlyingType](https://ytsaurus.tech/docs/en/#variantunderlyingtype). #### Examples ```yql SELECT FormatType(VariantTypeHandle( ParseTypeHandle("Tuple") )); -- Variant ``` ### VoidTypeHandle and NullTypeHandle #### Signature ```yql VoidTypeHandle()->Void type handle NullTypeHandle()->Null type handle ``` Building a Void and Null type handle, respectively. #### Examples ```yql SELECT FormatType(VoidTypeHandle()); -- Void SELECT FormatType(NullTypeHandle()); -- Null ``` ### CallableTypeComponents #### Signature ```yql CallableTypeComponents(CallableTypeHandle)-> Struct< Arguments:List, Name:String, Type:TypeHandle>>, OptionalArgumentsCount:Uint32, Payload:String, Result:TypeHandle > ``` Getting a description of the callable value type handle passed to the argument. The reverse function is [CallableTypeHandle](https://ytsaurus.tech/docs/en/#callabletypehandle). #### Examples ```yql $formatArgument = ($x) -> { return AsStruct( FormatType($x.Type) as Type, $x.Name as Name, $x.Flags as Flags ) }; $formatCallable = ($x) -> { return AsStruct( $x.OptionalArgumentsCount as OptionalArgumentsCount, $x.Payload as Payload, FormatType($x.Result) as Result, ListMap($x.Arguments, $formatArgument) as Arguments ) }; SELECT $formatCallable( CallableTypeComponents( ParseTypeHandle("(Int32,[bar:Double?{Flags:AutoMap}])->String") ) ); -- (OptionalArgumentsCount: 1, Payload: "", Result: "String", Arguments: [ -- (Type: "Int32", Name: "", Flags: []), -- (Type: "Double?", Name: "bar", Flags: ["AutoMap"]), -- ]) ``` ### CallableArgument #### Signature ```yql CallableArgument(TypeHandle, [String, [List]])->Struct,Name:String,Type:TypeHandle> ``` Packing the callable value argument description into the structure to pass to the [CallableTypeHandle](https://ytsaurus.tech/docs/en/#callabletypehandle) function based on the following arguments: 1. Argument type handle. 2. Optional argument name. The default value is an empty string. 3. Optional argument flags as a list of strings. The default value is an empty list. Supported flags: "AutoMap". ### CallableTypeHandle #### Signature ```yql CallableTypeHandle(TypeHandle, List,Name:String,Type:TypeHandle>>, [Uint32, [String]])->callable value type handle ``` Building a callable value type handle based on the following arguments: 1. Returned value type handle. 2. A list of argument descriptions obtained via the [CallableArgument](https://ytsaurus.tech/docs/en/#callableargument) function. 3. Optional number of optional arguments in the callable value. The default value is 0. 4. Optional tag for the callable value type. The default value is an empty string. The reverse function is [CallableTypeComponents](https://ytsaurus.tech/docs/en/#callabletypecomponents). #### Examples ```yql SELECT FormatType( CallableTypeHandle( ParseTypeHandle("String"), AsList( CallableArgument(ParseTypeHandle("Int32")), CallableArgument(ParseTypeHandle("Double?"), "bar", AsList("AutoMap")) ), 1 ) ); -- Callable<(Int32,['bar':Double?{Flags:AutoMap}])->String> ``` ### LambdaArgumentsCount #### Signature ```yql LambdaArgumentsCount(LambdaFunction)->Uint32 ``` Getting the number of arguments in a lambda function. #### Examples ```yql SELECT LambdaArgumentsCount(($x, $y)->($x+$y)) ; -- 2 ``` ### LambdaOptionalArgumentsCount #### Signature ```yql LambdaOptionalArgumentsCount(LambdaFunction)->Uint32 ``` Getting the number of optional arguments in a lambda function. #### Examples ```yql SELECT LambdaOptionalArgumentsCount(($x, $y, $z?)->(if($x,$y,$z))) ; -- 1 ``` # Functions for working with code generation During computations, you can generate code consisting of S-expression nodes. To do this, use a mechanism for representing code packed in a [resource](https://ytsaurus.tech/docs/types/special.md). After constructing the code, you can insert it into the main program using the [EvaluateCode](https://ytsaurus.tech/docs/en/#evaluatecode) function. For debugging, you can convert the code into a string using the [FormatCode](https://ytsaurus.tech/docs/en/#formatcode) function. Possible node types in S-expressions that can be used to generate code: * Atom: An untyped string of zero or more characters. * List: A sequence of zero or more nodes. Corresponds to the `tuple` type in SQL. * Invokation of a built-in function: Consists of a name expressed by an atom and a sequence of zero or more nodes that are arguments of that function. * Declaration of a lambda function: Consists of the declaration of argument names and the node that is the root of the body of that lambda function. * A lambda function argument: A node that can only be used inside the body of the lambda function. * World: A special node that marks I/O operations. S-expressions nodes form an oriented graph. Atoms are always leaf nodes, since they cannot contain child nodes. S-expressions are written as follows in text form: * Atom: `'"foo"`. The apostrophe symbol (') is a sign of quoting a subsequent string usually enclosed in quotation marks. * List: `'("foo" "bar")`. The apostrophe symbol (') is a sign that there will be no function invocation in brackets. * Invocation of a built-in function: `(foo "bar")`. The first item inside the brackets is a mandatory function name, which is then followed by arguments. * Declaration of a lambda function: `(lambda '(x y) (+ x y))`. The `lambda` keyword is followed by a list of argument names and the body of the lambda function. * A lambda function argument: `x`. Unlike an atom, a string without an apostrophe symbol (') is a reference to the name in the current scope. When you declare a lambda function, the argument names are added to the scope of the body, hiding the name from the enclosing scope if necessary. * World: `world`. ## FormatCode Code serialization as S-expressions. The code must not contain free function arguments, i.e. to serialize the lambda function code, you must pass it in its entirety, not expressions that potentially contain lambda function arguments. #### Examples ```yql SELECT FormatCode(AtomCode("foo")); -- ( -- (return '"foo") -- ) ``` ## WorldCode Build a code node with the `world` type. #### Examples ```yql SELECT FormatCode(WorldCode()); -- ( -- (return world) -- ) ``` ## AtomCode Build a code node with the `atom` type from a string passed to the argument. #### Examples ```yql SELECT FormatCode(AtomCode("foo")); -- ( -- (return '"foo") -- ) ``` ## ListCode Build a code node with the `list` type from a set of nodes or lists of code nodes passed to the arguments. The lists from the arguments are embedded as separately listed code nodes. #### Examples ```yql SELECT FormatCode(ListCode( AtomCode("foo"), AtomCode("bar"))); -- ( -- (return '('"foo" '"bar")) -- ); SELECT FormatCode(ListCode(AsList( AtomCode("foo"), AtomCode("bar")))); -- ( -- (return '('"foo" '"bar")) -- ) ``` ## FuncCode Build a code node with the `built-in function invocation` type from a string with the function name and a set of nodes or lists of code nodes passed to the arguments. The lists from the arguments are embedded as separately listed code nodes. #### Examples ```yql SELECT FormatCode(FuncCode( "Baz", AtomCode("foo"), AtomCode("bar"))); -- ( -- (return (Baz '"foo" '"bar")) -- ) SELECT FormatCode(FuncCode( "Baz", AsList( AtomCode("foo"), AtomCode("bar")))); -- ( -- (return (Baz '"foo" '"bar")) -- ) ``` ## LambdaCode You can build a code node with the `lambda function declaration` type from the: * [Lambda function](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) if the number of arguments is known in advance. In this case, nodes of the `argument` type will be passed as arguments of this lambda function. * Number of arguments and [lambda function](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) with one argument. In this case, a list of nodes of the `argument` type will be passed as an argument of this lambda function. #### Examples ```yql SELECT FormatCode(LambdaCode(($x, $y) -> { RETURN FuncCode("+", $x, $y); })); -- ( -- (return (lambda '($1 $2) (+ $1 $2))) -- ) SELECT FormatCode(LambdaCode(2, ($args) -> { RETURN FuncCode("*", Unwrap($args[0]), Unwrap($args[1])); })); -- ( -- (return (lambda '($1 $2) (* $1 $2))) -- ) ``` ## EvaluateCode Placing the code node passed to the argument into the main program. #### Examples ```yql SELECT EvaluateCode(FuncCode("Int32", AtomCode("1"))); -- 1 $lambda = EvaluateCode(LambdaCode(($x, $y) -> { RETURN FuncCode("+", $x, $y); })); SELECT $lambda(1, 2); -- 3 ``` ## ReprCode Placing the code node that is a representation of the result of computing the expression passed to the argument into the main program. #### Examples ```yql $add3 = EvaluateCode(LambdaCode(($x) -> { RETURN FuncCode("+", $x, ReprCode(1 + 2)); })); SELECT $add3(1); -- 4 ``` ## QuoteCode Placing the code node that is a representation of the expression or [lambda function](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) passed to the argument into the main program. If free lambda function arguments are found during placing, they are computed and placed into the code as in the [ReprCode](https://ytsaurus.tech/docs/en/#reprcode) function. #### Examples ```yql $lambda = ($x, $y) -> { RETURN $x + $y }; $makeClosure = ($y) -> { RETURN EvaluateCode(LambdaCode(($x) -> { RETURN FuncCode("Apply", QuoteCode($lambda), $x, ReprCode($y)) })) }; $closure = $makeClosure(2); SELECT $closure(1); -- 3 ``` ## PositionOf {#positionof} #### Signature ```yql PositionOf(x)->Struct ``` This function is available starting from version [2026.01](https://ytsaurus.tech/docs/changelog/2026.01.md). It returns the position of a node in the source text. For named expressions, it returns the position of the original node. #### Example ```yql SELECT PositionOf(1); -- ("File": "
", "Row": 1, "Column": 19) ``` ## WithIssue {#with_issue} #### Signature ```yql WithIssue(x, File:String, Row: Uint32, Column: Uint32, Message: String)->TypeOf(x) ``` This function is available starting from version [2026.01](https://ytsaurus.tech/docs/changelog/2026.01.md). It adds a custom frame with a specified position and message to the validation stack. It has no effect if the request passes validation successfully. Typically used for linking to a DSL file. #### Examples ```yql -- Expression decoration SELECT WithIssue( 1 + "foo", -- to get an error "my_file", 100, 200, "my_message" ); -- Subquery decoration DEFINE SUBQUERY $sub() AS SELECT 1 + "foo"; -- to get an error END DEFINE; $sub = ($world)->{ return WithIssue( $sub($world), "my_file", 100, 200, "my_message" ); }; PROCESS $sub(); # Functions for JSON **JSON** is a lightweight [data-interchange format](https://www.json.org). It's represented by the `Json` type in YQL. Unlike relational tables, JSON can store data with no schema defined. Here is an example of a valid JSON object: ```json [ { "name": "Jim Holden", "age": 30 }, { "name": "Naomi Nagata", "age": "twenty years old" } ] ``` Despite the fact that the `age` field in the first object is of the `Number` type (`"age": 21`) and in the second object its type is `String` (`"age": "twenty years old"`), this is a fully valid JSON object. To work with JSON, YQL implements a subset of the [SQL support for JavaScript Object Notation (JSON)](https://www.iso.org/standard/67367.html) standard, which is a part of the generally adopted ANSI SQL standard. ## Cookbook ```yql $json = CAST(@@{ "friends": [ { "name": "James Holden", "age": 35 }, { "name": "Naomi Nagata", "age": 30 } ] }@@ AS Json); SELECT JSON_EXISTS($json, "$.friends[*].name"), -- True, JSON_VALUE($json, "$.friends[0].age"), -- "35" (Utf8? type) JSON_QUERY($json, "$.friends[0]"); -- {"name": "James Holden", "age": 35} ``` ### Accessing a database field Table data can be stored in JSON or as a string. `JSON_*` functions expect JSON as input. For the `String->JSON` conversion, use the `CAST` function, for example, `CAST (my_string AS JSON)`. ## JsonPath Values inside JSON objects are accessed using a query language called JsonPath. All functions for JSON accept a JsonPath query as an argument. Let's look at an example. Suppose we have a JSON object like: ```json { "comments": [ { "id": 123, "text": "A whisper will do, if it's all that you can manage." }, { "id": 456, "text": "My life has become a single, ongoing revelation that I haven’t been cynical enough." } ] } ``` Then, to get the text of the second comment, we can write the following JsonPath query: ```yql $.comments[1].text ``` In this query: 1. `$` is a way to access the entire JSON object. 2. `$.comments` accesses the `comments` key of the JSON object. 3. `$.comments[1]` accesses the second element of the JSON array (element numbering starts from 0). 4. `$.comments[1].text` accesses the `text` key of the JSON object. 5. Query execution result: `"My life has become a single, ongoing revelation that I haven’t been cynical enough."` ### Quick reference | Operation | Example | |-----------------------------------------|-------------------------------------------------| | Retrieving a JSON object key | `$.key` | | Retrieving all JSON object keys | `$.*` | | Accessing an array element | `$[25]` | | Retrieving an array subsegment | `$[2 to 5]` | | Accessing the last array element | `$[last]` | | Accessing all array elements | `$[*]` | | Unary operations | `- 1` | | Binary operations | `(12 * 3) % 4 + 8` | | Accessing a variable | `$variable` | | Logical operations | `(1 > 2) || (3 <= 4) && ("string" == "another")| | | Matching a regular expression | `$.name like_regex "^[A-Za-z]+$"` | | Checking the string prefix | `$.name starts with "Bobbie"` | | Checking if a path exists | `exists ($.profile.name)` | | Checking a Boolean expression for null | `($.age > 20) is unknown` | | Filtering values | `$.friends ? (@.age >= 18 && @.gender == "male")` | | Getting the value type | `$.name.type()` | | Getting the array size | `$.friends.size()` | | Converting a string to a number | `$.number.double()` | | Rounding up a number | `$.number.ceiling()` | | Rounding down a number | `$.number.floor()` | | Returning the absolute value | `$.number.abs()` | | Getting key-value pairs from an object | `$.profile.keyvalue()` | ### Data model The result of executing all JsonPath expressions is a sequence of JSON values. For example: - The result of executing the `"Bobbie"` expression is a sequence with the only element `"Bobbie"`. Its length is 1. - The result of executing the `$` expression (that takes the entire JSON object) in JSON `[1, 2, 3]` is `[1, 2, 3]`. A sequence of 1 element of the array `[1, 2, 3]` - The result of executing the `$[*]` expression (retrieving all array elements) in JSON `[1, 2, 3]` is `1, 2, 3`. A sequence of three items:`1`, `2`, and `3` If the input sequence consists of multiple values, some operations are performed for each element (for example, accessing a JSON object key). However, other operations require a sequence of one element as input (for example, binary arithmetic operations). The behavior of a specific operation is described in the corresponding section of the documentation. ### Execution mode JsonPath supports two execution modes, `lax` and `strict`. Setting the mode is optional. By default, `lax`. The mode is specified at the beginning of a query. For example, `strict $.key`. The behavior for each mode is described in the corresponding sections with JsonPath operations. #### Auto unpacking of arrays When accessing a JSON object key in `lax` mode, arrays are automatically unpacked. ##### Example ```json [ { "key": 123 }, { "key": 456 } ] ``` The `lax $.key` query is successful and returns `123, 456`. As `$` is an array, it's automatically unpacked and accessing the key of the `$.key` JSON object is executed for each element in the array. The `strict $.key` query returns an error. The `strict` mode doesn't support automatic unpacking of arrays. `$` is an array rather than an object. That's why you can't access the object key `$.key`. You can fix this by writing `strict $[*].key`. Unpacking is only 1 level deep. In the event of nested arrays, only the outermost one is unpacked. #### Wrapping values in arrays When accessing an array element in `lax` mode, JSON values are automatically wrapped in an array. ##### Example ```json { "name": "Avasarala" } ``` The `lax $[0].name` query is successful and returns `"Avasarala"`. As `$` isn't an array, it's automatically wrapped in an array of length 1. Accessing the first element `$[0]` returns the source JSON object where the `name` key is taken. The `strict $[0].name` query returns an error. The `strict` mode does not support automated wrapping into an array. `$` is an object rather than an array. That's why you can't access the item `$[0]`. You can fix this by writing `strict $.name`. #### Handling errors Some errors are converted to an empty result when a query is executed in `lax` mode. ### Literals Values of some types can be specified in a JsonPath query using literals: | Type | Example | | ------------------ | ---------------- | | Numbers | `42`, `-1.23e-5` | | Boolean values | `false`, `true` | | Null | `null` | | Stings | `"Belt"` | ### Accessing JSON object keys JsonPath supports accessing JSON object keys, such as `$.session.user.name`. {% note info %} Accessing keys without quotes is only supported for keys that start with an English letter or underscore and only contain English letters, underscores, numbers, and a dollar sign. Use quotes for all other keys. For example, `$.profile."this string has spaces"` or `$.user."42 is the answer"` {% endnote %} For each value from the input sequence: 1. If the value is an array, it's automatically unpacked in `lax` mode. 2. If the value isn't a JSON object or if it is and the specified key is missing from this JSON object, a query executed in `strict` mode fails. In `lax` mode, an empty result is returned for this value. The expression execution result is the concatenation of the results for each value from the input sequence. #### Example ```json { "name": "Amos", "friends": [ { "name": "Jim" }, { "name": "Alex" } ] } ``` | | `lax` | `strict` | |------------------|------------------|----------| | `$.name` | `"Amos"` | `"Amos"` | | `$.surname` | Empty result | Error | | `$.friends.name` | `"Jim", "Alex"` | Error | ### Accessing all JSON object keys JsonPath supports accessing all JSON object keys at once: `$.*`. For each value from the input sequence: 1. If the value is an array, it's automatically unpacked in `lax` mode. 2. If the value isn't a JSON object, a query executed in `strict` mode fails. In `lax` mode, an empty result is returned for this value. The expression execution result is the concatenation of the results for each value from the input sequence. #### Example ```json { "profile": { "id": 123, "name": "Amos" }, "friends": [ { "name": "Jim" }, { "name": "Alex" } ] } ``` | | `lax` | `strict` | | ------------- | --------------- | ------------- | | `$.profile.*` | `123, "Amos"` | `123, "Amos"` | | `$.friends.*` | `"Jim", "Alex"` | Error | ### Accessing an array element JsonPath supports accessing array elements: `$.friends[1, 3 to last - 1]`. For each value from the input sequence: 1. If the value isn't an array, a query executed in `strict` mode fails. In `lax` mode, values are automatically wrapped in an array. 2. The `last` keyword is replaced with the array's last index. Using `last` outside of accessing the array is an error in both modes. 3. The specified indexes are calculated. Each of them must be a single number, otherwise the query fails in both modes. 4. If the index is a fractional number, it's rounded down. 5. If the index goes beyond the array boundaries, the query executed in `strict` mode fails. In `lax` mode, this index is ignored. 6. If a segment is specified and its start index is greater than the end index (for example, `$[20 to 1]`), the query fails in `strict` mode. In `lax` mode, this segment is ignored. 7. All elements by the specified indexes are added to the result. Segments include **both ends**. #### Examples ```json [ { "name": "Camina", "surname": "Drummer" }, { "name": "Josephus", "surname": "Miller" }, { "name": "Bobbie", "surname": "Draper" }, { "name": "Julie", "surname": "Mao" } ] ``` | | `lax` | `strict` | | ----------------------------- | ------------------------------- | ------------------------------- | | `$[0].name` | `"Camina"` | `"Camina"` | | `$[1, 2 to 3].name` | `"Josephus", "Bobbie", "Julie"` | `"Josephus", "Bobbie", "Julie"` | | `$[last - 2].name` | `"Josephus"` | `"Josephus"` | | `$[2, last + 200 to 50].name` | `"Bobbie"` | Error | | `$[50].name` | Empty result | Error | ### Accessing all array elements JsonPath supports accessing all array elements at once: `$[*]`. For each value from the input sequence: 1. If the value isn't an array, a query executed in `strict` mode fails. In `lax` mode, values are automatically wrapped in an array. 2. All elements of the current array are added to the result. #### Examples ```json [ { "class": "Station", "title": "Medina" }, { "class": "Corvette", "title": "Rocinante" } ] ``` | | `lax` | `strict` | | ------------------- | ------------------------- | ----------------------- | | `$[*].title` | `"Medina", "Rocinante"` | `"Medina", "Rocinante"` | | `lax $[0][*].class` | `"Station"` | Error | Let's analyze the last example step by step: 1. `$[0]` returns the first element of the array, that is `{"class": "Station", "title": "Medina"}` 2. `$[0][*]` expects an array for input, but an object was input instead. It's automatically wrapped in an array as `[ {"class": "Station", "title": "Medina"} ]` 3. Now, `$[0][*]` can be executed and returns all elements of the array, that is `{"class": "Station", "title": "Medina"}` 4. `$[0][*]`.class returns the `class` field value: `"Station"`. ### Arithmetic operations {% note info %} All arithmetic operations work with numbers as with Double. You might [lose precision](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html) during calculations. {% endnote %} #### Unary operations JsonPath supports unary `+` and `-`. A unary operation applies to all values from the input sequence. If a unary operation's input is a value that isn't a number, a query fails in both modes. ##### Example ```json [1, 2, 3, 4] ``` The `strict -$[*]` query is successful and returns `-1, -2, -3, -4`. The `lax -$` query fails as `$` is an array and not a number. #### Binary operations JsonPath supports binary arithmetic operations (in descending order of priority): 1. Multiplication `*`, dividing floating-point numbers `/`, and taking the remainder `%` (works as the `MOD` function in `SQL`). 2. Addition `+`, subtraction `-`. You can change the order of operations using parentheses. If each argument of a binary operation is not a single number or a number is divided by 0, the query fails in both modes. ##### Examples - `(1 + 2) * 3` returns `9` - `1 / 2` returns `0.5` - `5 % 2` returns `1` - `1 / 0` fails - If JSON is `[-32.4, 5.2]`, the `$[0] % $[1]` query returns `-1.2` - If JSON is `[1, 2, 3, 4]`, the `lax $[*] + $[*]` query fails as the `$[*]` expression execution result is `1, 2, 3, 4`, that is multiple numbers. A binary operation only requires one number for each of its arguments. ### Boolean values Unlike some other programming languages, Boolean values in JsonPath are not only `true` and `false`, but also `null` (uncertainty). JsonPath considers any values received from a JSON document to be non-Boolean. For example, a query like `! $.is_valid_user` (a logical negation applied to the `is_valid_user`) field is syntactically invalid because the `is_valid_user` field value is not Boolean (even when it actually stores `true` or `false`). The correct way to write this kind of query is to explicitly use a comparison with a Boolean value, such as `$.is_valid_user == false`. ### Logical operations JsonPath supports some logical operations for Boolean values. The arguments of any logical operation must be a single Boolean value. All logical operations return a Boolean value. #### Logical negation, ! Truth table: | `x` | `!x` | | ------- | ------- | | `true` | `false` | | `false` | `true` | | `null` | `null` | #### Logical AND, && In the truth table, the first column is the left argument, the first row is the right argument, and each cell is the result of using the Logical AND both with the left and right arguments: | `&&` | `true` | `false` | `null` | | ------- | ------- | ------- | ------- | | `true` | `true` | `false` | `null` | | `false` | `false` | `false` | `false` | | `null` | `null` | `false` | `null` | #### Logical OR, || In the truth table, the first column is the left argument, the first row is the right argument, and each cell is the result of using the logical OR with both the left and right arguments: | `\|\|` | `true` | `false` | `null` | | ------- | ------- | ------- | ------- | | `true` | `true` | `true` | `true` | | `false` | `true` | `false` | `null` | | `null` | `true` | `null` | `null` | ##### Examples - `! (true == true)`, the result is `false` - `(true == true) && (true == false)`, the result is `false` - `(true == true) || (true == false)`, the result is `true` ### Comparison operators JsonPath implements comparison operators for values: - Equality, `==` - Not equal to, `!=` and `<>` - Less than and less than or equal to, `<` and `<=` - Greater than and greater than or equal to, `>` and `>=` All comparison operators return a Boolean value. Both operator arguments support multiple values. If an error occurs when calculating the operator arguments, it returns `null`. In this case, the JsonPath query execution continues. The arrays of each of the arguments are automatically unpacked. After that, for each pair where the first element is taken from the sequence of the left argument and the second one from the sequence of the right argument: 1. The elements of the pair are compared 2. If an error occurs during the comparison, the flag set is `ERROR` 3. If the comparison result is true, the flag set is `FOUND` 4. If either the `ERROR` or `FOUND` flag is set and the query is executed in `lax` mode, no more pairs are analyzed. If the pair analysis results in: 1. The `ERROR` flag is set, the operator returns `null` 2. The `FOUND` flag is set, the operator returns `true` 3. Otherwise, it returns `false` We can say that this algorithm considers all pairs from the Cartesian product of the left and right arguments, trying to find the pair whose comparison returns true. Elements in a pair are compared according to the following rules: 1. If the left or right argument is an array or object, the comparison fails. 2. `null == null` returns true 3. In all other cases, if one of the arguments is `null`, false is returned. 4. If the left and right arguments are of different types, the comparison fails. 5. Strings are compared byte by byte. 6. `true` is considered greater than `false` 7. Numbers are compared with the accuracy of `1e-20` #### Example Let's take a JSON document as an example ```json { "left": [1, 2], "right": [4, "Inaros"] } ``` and analyze the steps for executing the `lax $.left < $.right` query: 1. Auto unpacking of arrays in the left and right arguments. As a result, the left argument is the sequence `1, 2` and the right argument is `4, "Iranos"` 2. Let's take the pair `(1, 4)`. The comparison is successful as `1 < 4` is true. Set the flag `FOUND` 3. Since the query is executed in `lax` mode and the `FOUND` flag is set, we aren't analyzing any more pairs. 4. Since we have the `FOUND` flag set, the operator returns true. Let's take the same query in a different execution mode: `strict $.left < $.right`: 1. Auto unpacking of arrays in the left and right arguments. As a result, the left argument is the sequence `1, 2` and the right argument is `4, "Iranos"` 2. Let's take the pair `(1, 4)`. The comparison is successful as `1 < 4` is true. Set the flag `FOUND` 3. Let's take the pair `(2, 4)`. The comparison is successful as `2 < 4` is true. Set the flag `FOUND` 4. Let's take the pair `(1, "Iranos")`. The comparison fails as a number can't be compared with a string. Set the flag `ERROR` 5. Let's take the pair `(2, "Iranos")`. The comparison fails as a number can't be compared with a string. Set the flag `ERROR` 6. Since we have the `ERROR` flag set, the operator returns `null` ### Predicates JsonPath supports predicates which are expressions that return a Boolean value and check a certain condition. You can use them, for example, in filters. #### `like_regex` The `like_regex` predicate lets you check if a string matches a regular expression. The syntax of regular expressions is the same as in [Hyperscan UDF](https://ytsaurus.tech/docs/udf/list/hyperscan.md) and [REGEXP](https://ytsaurus.tech/docs/syntax/expressions.md#regexp). ###### Syntax ```yql like_regex [flag ] ``` Where: 1. `` is a JsonPath expression with strings to be checked for matching the regular expression. 2. `` is a string with the regular expression 3. `flag ` is an optional section where `` is a string with regular expression execution flags. Supported flags: - `i`: Disable the case sensitivity. ##### Execution Before the check, the input sequence arrays are automatically unpacked. After that, for each element of the input sequence: 1. A check is made to find out if a string matches a regular expression. 2. If the element isn't a string, the flag set is `ERROR` 3. If the check result is true, the flag set is `FOUND` 4. If either the `ERROR` or `FOUND` flag is set and the query is executed in `lax` mode, no more pairs are analyzed. If the pair analysis results in: 1. Setting the `ERROR` flag, the predicate returns `null` 2. Setting the `FOUND` flag, the predicate returns `true` 3. Otherwise, the predicate returns `false` #### Examples 1. `"123456" like_regex "^[0-9]+$"` returns `true` 2. `"123abcd456" like_regex "^[0-9]+$"` returns `false` 3. `"Naomi Nagata" like_regex "nag"` returns `false` 4. `"Naomi Nagata" like_regex "nag" flag "i"` returns `true` #### `starts with` The `starts with` predicate lets you check if one string is a prefix of another. ###### Syntax ```yql starts with ``` Where: 1. `` is a JsonPath expression with the string to check. 2. `` is a JsonPath expression with a prefix string. This means that the predicate will check that the `` starts with the `` string. ##### Execution The first argument of the predicate must be a single string. The second argument of the predicate must be a sequence of (possibly, multiple) strings. For each element in a sequence of prefix strings: 1. A check is made for whether "an element is a prefix of an input string" 2. If the element isn't a string, the flag set is `ERROR` 3. If the check result is true, the flag set is `FOUND` 4. If either the `ERROR` or `FOUND` flag is set and the query is executed in `lax` mode, no more pairs are analyzed. If the pair analysis results in: 1. Setting the `ERROR` flag, the predicate returns `null` 2. Setting the `FOUND` flag, the predicate returns `true` 3. Otherwise, the predicate returns `false` ##### Examples 1. `"James Holden" starts with "James"` returns `true` 2. `"James Holden" starts with "Amos"` returns `false` #### `exists` The `exists` predicate lets you check whether a JsonPath expression returns at least one element. ###### Syntax ```yql exists () ``` Where `` is the JsonPath expression to be checked. Parentheses around the expression are required. ##### Execution 1. The passed JsonPath expression is executed. 2. If an error occurs, the predicate returns `null` 3. If an empty sequence is obtained as a result of the execution, the predicate returns `false` 4. Otherwise, the predicate returns `true` ##### Examples Let's take a JSON document: ```json { "profile": { "name": "Josephus", "surname": "Miller" } } ``` 1. `exists ($.profile.name)` returns `true` 2. `exists ($.friends.profile.name)` returns `false` 3. `strict exists ($.friends.profile.name)` returns `null`, because accessing non-existent object keys in `strict` mode is an error. #### `is unknown` The `is unknown` predicate lets you check if a Boolean value is `null`. ###### Syntax ```yql () is unknown ``` Where `` is the JsonPath expression to be checked. Only expressions that return a Boolean value are allowed. Parentheses around the expression are required. ##### Execution 1. If the passed expression returns `null`, the predicate returns `true` 2. Otherwise, the predicate returns `false` ##### Examples 1. `(1 == 2) is unknown` returns `false`. The `1 == 2` expression returned `false`, which is not `null` 2. `(1 == "string") is unknown` returns `true`. The `1 == "string"` expression returned `null`, because strings and numbers can't be compared in JsonPath. ### Filters JsonPath lets you filter values obtained during query execution. An expression in a filter must return a Boolean value. Before filtering, the input sequence arrays are automatically unpacked. For each element of the input sequence: 1. The value of the current filtered `@` object becomes equal to the current element of the input sequence. 2. Executing the expression in the filter 3. If an error occurs during the expression execution, the current element of the input sequence is skipped. 4. If the expression execution result is the only `true` value, the current element is added to the filter result. #### Example Suppose we have a JSON document describing the user's friends ```json { "friends": [ { "name": "James Holden", "age": 35, "money": 500 }, { "name": "Naomi Nagata", "age": 30, "money": 345 } ] } ``` and we want to get a list of friends who are over 32 years old using a JsonPath query. To do this, you can write the following query: ```yql $.friends ? (@.age > 32) ``` Let's analyze the query in parts: - `$.friends` accesses the `friends` array in the JSON document. - `? ( ... )` is the filter syntax. An expression inside the parentheses is called a predicate. - `@` accesses the currently filtered object. In our example, it's the object describing a friend of the user. - `@.age` accesses the `age` field of the currently filtered object. - `@.age > 32` compares the `age` field with the value 32. As a result of the query, only the values for which this predicate returned true remain. The query only returns the first friend from the array of user's friends. Like many other JsonPath operators, filters can be arranged in chains. Let's take a more complex query that selects the names of friends who are older than 20 and have less than 400 currency units: ```yql $.friends ? (@.age > 20) ? (@.money < 400) . name ``` Let's analyze the query in parts: - `$.friends` accesses the `friends` array in the JSON document. - `? (@.age > 20)` is the first filter. Since all friends are over 20, it just returns all the elements of the `friends` array. - `? (@.money < 400)` is the second filter. It only returns the second element of the `friends` array, since only its `money` field value is less than 400. - `.name` accesses the `name` field of filtered objects. The query returns a sequence of a single element: `"Naomi Nagata"`. In practice, it's recommended to combine multiple filters into one if possible. The above query is equivalent to `$.friends ? (@.age > 20 && @.money < 400) . name`. ### Methods JsonPath supports methods that are functions converting one sequence of values to another. The syntax for calling a method is similar to accessing the object key: ```yql $.friends.size() ``` Just like in the case of accessing object keys, method calls can be arranged in chains: ```yql $.numbers.double().floor() ``` #### `type` The `type` method returns a string with the type of the passed value. For each element of the input sequence, the method adds this string to the output sequence according to the table below: | Value type | String with type | | ------------------ | ------------------------ | | Null | `"null"` | | Boolean value | `"boolean"` | | Number | `"number"` | | String | `"string"` | | Array | `"array"` | | Objects | `"object"` | ##### Examples 1. `"Naomi".type()` returns `"string"` 2. `false.type()` returns `"boolean"` #### `size` The `size` method returns the size of the array. For each element of the input sequence, the method adds the following to the output sequence: 1. The size of the array if the element type is an array. 2. For all other types (including objects), it adds `1` #### Examples Let's take a JSON document: ```json { "array": [1, 2, 3], "object": { "a": 1, "b": 2 }, "scalar": "string" } ``` And queries to it: 1. `$.array.size()` returns `3` 2. `$.object.size()` returns `1` 3. `$.scalar.size()` returns `1` #### `double` The `double` method converts strings to numbers. Before its execution, the input sequence arrays are automatically unpacked. All elements in the input sequence must be strings that contain decimal numbers. It's allowed to specify the fractional part and exponent. ##### Examples 1. `"125".double()` returns `125` 2. `"125.456".double()` returns `125.456` 3. `"125.456e-3".double()` returns `0.125456` #### `ceiling` The `ceiling` method rounds up a number. Before its execution, the input sequence arrays are automatically unpacked. All elements in the input sequence must be numbers. ##### Examples 1. `(1.3).ceiling()` returns `2` 2. `(1.8).ceiling()` returns `2` 3. `(1.5).ceiling()` returns `2` 4. `(1.0).ceiling()` returns `1` #### `floor` The `floor` method rounds down a number. Before its execution, the input sequence arrays are automatically unpacked. All elements in the input sequence must be numbers. ##### Examples 1. `(1.3).floor()` returns `1` 2. `(1.8).floor()` returns `1` 3. `(1.5).floor()` returns `1` 4. `(1.0).floor()` returns `1` #### `abs` The `abs` method calculates the absolute value of a number (removes the sign). Before its execution, the input sequence arrays are automatically unpacked. All elements in the input sequence must be numbers. ##### Examples 1. `(0.0).abs()` returns `0` 2. `(1.0).abs()` returns `1` 3. `(-1.0).abs()` returns `1` #### `keyvalue` The `keyvalue` method converts an object to a sequence of key-value pairs. Before its execution, the input sequence arrays are automatically unpacked. All elements in the input sequence must be objects. For each element of the input sequence: 1. Each key-value pair in the element is analyzed. 2. For each key-value pair, an object is generated with the keys `name` and `value` 3. `name` stores a string with the name of the key from the pair. 4. `value` stores the value from the pair. 5. All objects for this element are added to the output sequence. ##### Examples Let's take a JSON document: ```json { "name": "Chrisjen", "surname": "Avasarala", "age": 70 } ``` The `$.keyvalue()` query returns the following sequence for it: ```json { "name": "age", "value": 70 }, { "name": "name", "value": "Chrisjen" }, { "name": "surname", "value": "Avasarala" } ``` ### Variables Functions using JsonPath can pass values into a query. They are called variables. To access a variable, write the `$` character and the variable name: `$variable`. #### Example Let the `planet` variable be equal to ```json { "name": "Mars", "gravity": 0.376 } ``` Then the strict `$planet.name` query returns `"Mars"`. Unlike many programming languages, JsonPath doesn't support creating new variables or modifying existing ones. ## Common arguments All functions for JSON accept: 1. A JSON value (can be an arbitrary `Json` or `Json?` expression) 2. A JsonPath query (must be explicitly specified with a string literal) 3. **(Optional)** `PASSING` section ### PASSING section Lets you pass values to a JsonPath query as variables. ##### Syntax ```yql PASSING AS , AS , ... ``` `` can be of the following types: - Numbers, `Date`, `DateTime`, and `Timestamp` (a `CAST` into `Double` will be made before passing a value to JsonPath) - `Utf8`, `Bool` and `Json` You can set a `` in several ways: - As an SQL name like `variable` - In quotes, for example, `"variable"` #### Example ```yql JSON_VALUE( $json, "$.timestamp - $Now + $Hour" PASSING 24 * 60 as Hour, CurrentUtcTimestamp() as "Now" ) ``` ## JSON_EXISTS {#json_exists} The `JSON_EXISTS` function checks if a JSON value meets the specified JsonPath. #### Syntax ```yql JSON_EXISTS( , , [] [{TRUE | FALSE | UNKNOWN | ERROR} ON ERROR] ) ``` Returned value: `Bool?` Default value: If the `ON ERROR` clause is not specified, the default error-handling behavior is `FALSE ON ERROR`. Behavior: 1. If `` is `NULL` or an empty `Json?`, it returns an empty `Bool?` 2. If an error occurs during JsonPath execution, the returned value depends on the `ON ERROR` section: - `TRUE`: Return `True` - `FALSE`: Return `False` - `UNKNOWN`: Return an empty `Bool?` - `ERROR`: Abort the entire query 3. If the result of JsonPath execution is one or more values, the return value is `True`. 4. Otherwise, `False` is returned. #### Examples ```yql $json = CAST(@@{ "title": "Rocinante", "crew": [ "James Holden", "Naomi Nagata", "Alex Kamai", "Amos Burton" ] }@@ as Json); SELECT JSON_EXISTS($json, "$.title"), -- True JSON_EXISTS($json, "$.crew[*]"), -- True JSON_EXISTS($json, "$.nonexistent"); -- False because JsonPath will return an empty result SELECT -- JsonPath error will return False because FALSE ON ERROR is used by default JSON_EXISTS($json, "strict $.nonexistent"); SELECT -- JsonPath error, the entire YQL query fails with an error JSON_EXISTS($json, "strict $.nonexistent" ERROR ON ERROR); ``` ## JSON_VALUE {#json_value} The `JSON_VALUE` function retrieves a scalar value from JSON (anything that isn't an array or object). #### Syntax ```yql JSON_VALUE( , , [] [RETURNING ] [{ERROR | NULL | DEFAULT } ON EMPTY] [{ERROR | NULL | DEFAULT } ON ERROR] ) ``` Returned value: `?` Default values: 1. If the `ON EMPTY` section isn't specified, the section used is `NULL ON EMPTY` 2. If the `ON ERROR` section isn't specified, the section used is `NULL ON ERROR` 3. If the `RETURNING` section is omitted, it uses `Utf8` as `` Behavior: 1. If `` is `NULL` or an empty `Json?`, it returns an empty `?` 2. If an error occurs, the returned value depends on the `ON ERROR` section: - `NULL`: Return an empty `?` - `ERROR`: Abort the entire query - `DEFAULT `: Return `` with a prior `CAST` to the type `?`. If the `CAST` fails, the entire query fails, too. 3. If the JsonPath execution result is empty, the returned value depends on the `ON EMPTY` section: - `NULL`: Return an empty `?` - `ERROR`: Abort the entire query - `DEFAULT `: Return `` with a prior `CAST` to the type `?`. If the `CAST` fails, the behavior matches the `ON ERROR` section. 4. If the result of JsonPath execution is a single value, then: - If the `RETURNING` section isn't specified, the value is converted to `Utf8`. - Otherwise, the `CAST` function is run to convert the value to ``. If the `CAST` fails, the behavior matches the `ON ERROR` section. In this case, the value from JSON must match the `` type. 5. Return the result Correlation between JSON and YQL types: - JSON Number: Numeric types, `Date`, `DateTime`, and `Timestamp` - JSON Bool - `Bool` - JSON String: `Utf8` and `String` Errors executing `JSON_VALUE` are as follows: - Errors evaluating JsonPath - The result of JsonPath execution is a number of values or a non-scalar value. - The type of value returned by JSON doesn't match the expected one. The `RETURNING` section supports such types as numbers, `Date`, `DateTime`, `Timestamp`, `Utf8`, `String`, and `Bool`. #### Examples ```yql $json = CAST(@@{ "friends": [ { "name": "James Holden", "age": 35 }, { "name": "Naomi Nagata", "age": 30 } ] }@@ as Json); SELECT JSON_VALUE($json, "$.friends[0].age"), -- "35" (Utf8? type) JSON_VALUE($json, "$.friends[0].age" RETURNING Uint64), -- 35 (Uint64? type) JSON_VALUE($json, "$.friends[0].age" RETURNING Utf8); -- an empty Utf8? because an error has occurred. JSON type of the Number value mismatches the Utf8 string type SELECT -- "empty" (String? type) JSON_VALUE( $json, "$.friends[50].name" RETURNING String DEFAULT "empty" ON EMPTY ); SELECT -- 20 (Uint64? type). The result of JsonPath execution is empty, but the default value -- from the ON EMPTY section cannot be cast to Uint64. -- That's why the value from ON ERROR is used JSON_VALUE( $json, "$.friends[50].age" RETURNING Uint64 DEFAULT -1 ON EMPTY DEFAULT 20 ON ERROR ); ``` ## JSON_QUERY {#json_query} The `JSON_QUERY` function lets you retrieve arrays and objects from JSON. #### Syntax ```yql JSON_QUERY( , , [] [WITHOUT [ARRAY] | WITH [CONDITIONAL | UNCONDITIONAL] [ARRAY] WRAPPER] [{ERROR | NULL | EMPTY ARRAY | EMPTY OBJECT} ON EMPTY] [{ERROR | NULL | EMPTY ARRAY | EMPTY OBJECT} ON ERROR] ) ``` Returned value: `Json?` Default values: 1. If the `ON EMPTY` section isn't specified, the section used is `NULL ON EMPTY` 2. If the `ON ERROR` section isn't specified, the section used is `NULL ON ERROR` 3. If the `WRAPPER` section isn't specified, the section used is `WITHOUT WRAPPER` 4. If the `WITH WRAPPER` section is specified but `CONDITIONAL` or `UNCONDITIONAL` is omitted, then the section used is `UNCONDITIONAL` Behavior: {% note info %} You can't specify the `WITH ... WRAPPER` and `ON EMPTY` sections at the same time. {% endnote %} 1. If `` is `NULL` or an empty `Json?`, it returns an empty `Json?` 2. If the `WRAPPER` section is specified, then: - `WITHOUT WRAPPER` or `WITHOUT ARRAY WRAPPER`: Don't convert the result of JsonPath execution in any way. - `WITH UNCONDITIONAL WRAPPER` or `WITH UNCONDITIONAL ARRAY WRAPPER`: Wrap the result of JsonPath execution in an array. - `WITH CONDITIONAL WRAPPER` or `WITH CONDITIONAL ARRAY WRAPPER`: Wrap the result of JsonPath execution in an array if it isn't the only array or object. 3. If the JsonPath execution result is empty, the returned value depends on the `ON EMPTY` section: - `NULL`: Return an empty `Json?` - `ERROR`: Abort the entire query - `EMPTY ARRAY`: Return an empty JSON array, `[]` - `EMPTY OBJECT`: Return an empty JSON object, `{}` 4. If an error occurs, the returned value depends on the `ON ERROR` section: - `NULL`: Return an empty `Json?` - `ERROR`: Abort the entire query - `EMPTY ARRAY`: Return an empty JSON array, `[]` - `EMPTY OBJECT`: Return an empty JSON object, `{}` 5. Return the result Errors running a `JSON_QUERY`: - Errors evaluating JsonPath - The result of JsonPath execution is a number of values (even after using the `WRAPPER` section) or a scalar value. #### Examples ```yql $json = CAST(@@{ "friends": [ { "name": "James Holden", "age": 35 }, { "name": "Naomi Nagata", "age": 30 } ] }@@ as Json); SELECT JSON_QUERY($json, "$.friends[0]"); -- {"name": "James Holden", "age": 35} SELECT JSON_QUERY($json, "$.friends.name" WITH UNCONDITIONAL WRAPPER); -- ["James Holden", "Naomi Nagata"] SELECT JSON_QUERY($json, "$.friends[0]" WITH CONDITIONAL WRAPPER), -- {"name": "James Holden", "age": 35} JSON_QUERY($json, "$.friends.name" WITH CONDITIONAL WRAPPER); -- ["James Holden", "Naomi Nagata"] ``` # Functions of built-in C++ libraries Many application functions that on the one hand are too specific to become part of the YQL core, and on the other hand might be useful to a wide range of users, are available through built-in C++ libraries. Embedded C++ libraries are linked using UDFs in C++. * [Clickhouse](https://ytsaurus.tech/docs/en/clickhouse.md) * [DateTime](https://ytsaurus.tech/docs/en/datetime.md) * [Digest](https://ytsaurus.tech/docs/en/digest.md) * [Histogram](https://ytsaurus.tech/docs/en/histogram.md) * [Hyperscan](https://ytsaurus.tech/docs/en/hyperscan.md) * [Ip](https://ytsaurus.tech/docs/en/ip.md) * [Math](https://ytsaurus.tech/docs/en/math.md) * [Pcre](https://ytsaurus.tech/docs/en/pcre.md) * [Pire](https://ytsaurus.tech/docs/en/pire.md) * [Protobuf](https://ytsaurus.tech/docs/en/protobuf.md) * [Re2](https://ytsaurus.tech/docs/en/re2.md) * [Streaming](https://ytsaurus.tech/docs/en/streaming.md) * [String](https://ytsaurus.tech/docs/en/string.md) * [Unicode](https://ytsaurus.tech/docs/en/unicode.md) * [Url](https://ytsaurus.tech/docs/en/url.md) * [Yson](https://ytsaurus.tech/docs/en/yson.md) # ClickHouse UDF {% note warning %} This function is not yet available in Query Tracker. We will add it soon. {% endnote %} # Compress UDF и Decompress UDF A set of common data compression and decompression algorithms is listed below: ``` yql Compress::Gzip(String{Flags:AutoMap}, Uint8) -> String Compress::Zlib(String{Flags:AutoMap}, Uint8) -> String Compress::Brotli(String{Flags:AutoMap}, Uint8) -> String Compress::Lzma(String{Flags:AutoMap}, Uint8) -> String Compress::BZip2(String{Flags:AutoMap}, Uint8) -> String Compress::Snappy(String{Flags:AutoMap}) -> String Compress::Zstd(String{Flags:AutoMap}, Uint8) -> String Compress::Xz(String{Flags:AutoMap}) -> String ``` ``` yql Decompress::Gzip(String{Flags:AutoMap}) -> String Decompress::Zlib(String{Flags:AutoMap}) -> String Decompress::Brotli(String{Flags:AutoMap}) -> String Decompress::Lzma(String{Flags:AutoMap}) -> String Decompress::BZip2(String{Flags:AutoMap}) -> String Decompress::Snappy(String{Flags:AutoMap}) -> String Decompress::Zstd(String{Flags:AutoMap}) -> String Decompress::Xz(String{Flags:AutoMap}) -> String ``` ``` yql TryDecompress::Gzip(String{Flags:AutoMap}) -> String? TryDecompress::Zlib(String{Flags:AutoMap}) -> String? TryDecompress::Brotli(String{Flags:AutoMap}) -> String? TryDecompress::Lzma(String{Flags:AutoMap}) -> String? TryDecompress::BZip2(String{Flags:AutoMap}) -> String? TryDecompress::Snappy(String{Flags:AutoMap}) -> String? TryDecompress::Zstd(String{Flags:AutoMap}) -> String? TryDecompress::Xz(String{Flags:AutoMap}) -> String? ``` #### Examples ``` yql SELECT Compress::Brotli("YQL", 8); -- "\x0b\x01\x80\x59\x51\x4c\x03" SELECT Decompress::Brotli("\x0b\x01\x80\x59\x51\x4c\x03"); -- "YQL" SELECT TryDecompress::Brotli("YQL"); -- NULL ``` # DateTime In the DateTime module, there are two internal representation formats: `Resource` for basic types and `Resource` for extended ones. `Resource` and `Resource` store the following date components: * Year (12 bits, unsigned for `Resource`; 19 bits, signed for `Resource`). * Month (4 bits). * Day (5 bits). * Hour (5 bits). * Minute (6 bits). * Second (6 bits). * Microsecond (20 bits). * TimezoneId (16 bits). * DayOfYear (9 bits): Day since the beginning of the year. * WeekOfYear (6 bits): Week since the beginning of the year, January 1 is always in week 1. * WeekOfYearIso8601 (6 bits): Week of the year according to ISO 8601 (the first week is the one that includes January 4). * DayOfWeek (3 bits): Day of the week. If the timezone is not GMT, the components store the local time for the relevant timezone. ## Split {#split} Conversion from a primitive type to an internal representation. It's always successful on a non-empty input. #### List of functions * ```DateTime::Split(Date/TzDate/Datetime/TzDatetime/Timestamp/TzTimestamp{Flags:AutoMap}) -> Resource``` * ```DateTime::Split(Date32/TzDate32/Datetime64/TzDatetime64/Timestamp64/TzTimestamp64{Flags:AutoMap}) -> Resource``` Functions that accept `Resource` or `Resource` as input, can be called directly from the primitive date/time type. An implicit conversion will be made in this case by calling a relevant `Split` function. ## Make... {#make} Making a primitive type from an internal representation. It's always successful on a non-empty input. #### List of functions * `DateTime::MakeDate(Resource{Flags:AutoMap}) -> Date` * `DateTime::MakeTzDate(Resource{Flags:AutoMap}) -> TzDate` * `DateTime::MakeDate32(Resource{Flags:AutoMap}) -> Date32` * `DateTime::MakeTzDate32(Resource{Flags:AutoMap}) -> TzDate32` * `DateTime::MakeDatetime(Resource{Flags:AutoMap}) -> Datetime` * `DateTime::MakeTzDatetime(Resource{Flags:AutoMap}) -> TzDatetime` * `DateTime::MakeDatetime64(Resource{Flags:AutoMap}) -> Datetime64` * `DateTime::MakeTzDatetime64(Resource{Flags:AutoMap}) -> TzDatetime64` * `DateTime::MakeTimestamp(Resource{Flags:AutoMap}) -> Timestamp` * `DateTime::MakeTzTimestamp(Resource{Flags:AutoMap}) -> TzTimestamp` * `DateTime::MakeTimestamp64(Resource{Flags:AutoMap}) -> Timestamp64` * `DateTime::MakeTzTimestamp64(Resource{Flags:AutoMap}) -> TzTimestamp64` #### Examples ```yql SELECT DateTime::MakeTimestamp(DateTime::Split(Datetime("2019-01-01T15:30:00Z"))), -- 2019-01-01T15:30:00.000000Z DateTime::MakeDate(Datetime("2019-01-01T15:30:00Z")), -- 2019-01-01 DateTime::MakeTimestamp(DateTime::Split(TzDatetime("2019-01-01T00:00:00,Europe/Moscow"))), -- 2018-12-31T21:00:00Z (conversion to UTC) DateTime::MakeDate(TzDatetime("2019-01-01T12:00:00,GMT")) -- 2019-01-01 (Datetime -> Date with implicit Split)> ``` ## Get... {#get} Extracting a component from an internal representation. #### List of functions * `DateTime::GetYear(Resource{Flags:AutoMap}) -> Uint16` * `DateTime::GetYear(Resource{Flags:AutoMap}) -> Int32` * `DateTime::GetDayOfYear(Resource/Resource{Flags:AutoMap}) -> Uint16` * `DateTime::GetMonth(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetMonthName(Resource/Resource{Flags:AutoMap}) -> String` * `DateTime::GetWeekOfYear(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetWeekOfYearIso8601(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetDayOfMonth(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetDayOfWeek(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetDayOfWeekName(Resource/Resource{Flags:AutoMap}) -> String` * `DateTime::GetHour(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetMinute(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetSecond(Resource/Resource{Flags:AutoMap}) -> Uint8` * `DateTime::GetMillisecondOfSecond(Resource/Resource{Flags:AutoMap}) -> Uint32` * `DateTime::GetMicrosecondOfSecond(Resource/Resource{Flags:AutoMap}) -> Uint32` * `DateTime::GetTimezoneId(Resource/Resource{Flags:AutoMap}) -> Uint16` * `DateTime::GetTimezoneName(Resource/Resource{Flags:AutoMap}) -> String` #### Examples ```yql $tm = DateTime::Split(TzDatetime("2019-01-09T00:00:00,Europe/Moscow")); SELECT DateTime::GetDayOfMonth($tm) as Day, -- 9 DateTime::GetMonthName($tm) as Month, -- "January" DateTime::GetYear($tm) as Year, -- 2019 DateTime::GetTimezoneName($tm) as TzName, -- "Europe/Moscow" DateTime::GetDayOfWeekName($tm) as WeekDay; -- "Wednesday" ``` ## Update {#update} Updating one or more components in the internal representation. Returns either an updated copy or NULL, if an update produces an invalid date or other inconsistencies. #### List of functions ```yql DateTime::Update(Resource{Flags:AutoMap}, [ Year:Uint16?, Month:Uint8?, Day:Uint8?, Hour:Uint8?, Minute:Uint8?, Second:Uint8?, Microsecond:Uint32?, Timezone:String? ]) -> Resource? DateTime::Update(Resource{Flags:AutoMap}, [ Year:Int32?, Month:Uint8?, Day:Uint8?, Hour:Uint8?, Minute:Uint8?, Second:Uint8?, Microsecond:Uint32?, Timezone:String? ]) -> Resource? ``` #### Examples ```yql $tm = DateTime::Split(Timestamp("2019-01-01T01:02:03.456789Z")); SELECT DateTime::MakeDate(DateTime::Update($tm, 2012)), -- 2012-01-01 DateTime::MakeDate(DateTime::Update($tm, 2000, 6, 6)), -- 2000-06-06 DateTime::MakeDate(DateTime::Update($tm, NULL, 2, 30)), -- NULL (February 30) DateTime::MakeDatetime(DateTime::Update($tm, NULL, NULL, 31)), -- 2019-01-31T01:02:03Z DateTime::MakeDatetime(DateTime::Update($tm, 15 as Hour, 30 as Minute)), -- 2019-01-01T15:30:03Z DateTime::MakeTimestamp(DateTime::Update($tm, 999999 as Microsecond)), -- 2019-01-01T01:02:03.999999Z DateTime::MakeTimestamp(DateTime::Update($tm, "Europe/Moscow" as Timezone)), -- 2018-12-31T22:02:03.456789Z (conversion to UTC) DateTime::MakeTzTimestamp(DateTime::Update($tm, "Europe/Moscow" as Timezone)); -- 2019-01-01T01:02:03.456789,Europe/Moscow ``` ## From... {#from} Getting a Timestamp from the number of seconds/milliseconds/microseconds since the UTC epoch. When the Timestamp limits are exceeded, NULL is returned. #### List of functions * `DateTime::FromSeconds(Uint32{Flags:AutoMap}) -> Timestamp` * `DateTime::FromSeconds64(Int64{Flags:AutoMap}) -> Timestamp64` * `DateTime::FromMilliseconds(Uint64{Flags:AutoMap}) -> Timestamp` * `DateTime::FromMilliseconds64(Int64{Flags:AutoMap}) -> Timestamp64` * `DateTime::FromMicroseconds(Uint64{Flags:AutoMap}) -> Timestamp` * `DateTime::FromMicroseconds64(Int64{Flags:AutoMap}) -> Timestamp64` ## To... {#to} Getting a number of seconds/milliseconds/microseconds since the UTC Epoch from a primitive type. #### List of functions * ```DateTime::ToSeconds(Date/Datetime/Timestamp/TzDate/TzDatetime/TzTimestamp{Flags:AutoMap}) -> Uint32``` * ```DateTime::ToSeconds(Date32/Datetime64/Timestamp64/TzDate32/TzDatetime64/TzTimestamp64{Flags:AutoMap}) -> Int64``` * ```DateTime::ToMilliseconds(Date/Datetime/Timestamp/TzDate/TzDatetime/TzTimestamp{Flags:AutoMap}) -> Uint64``` * ```DateTime::ToMilliseconds(Date32/Datetime64/Timestamp64/TzDate32/TzDatetime64/TzTimestamp64{Flags:AutoMap}) -> Int64``` * ```DateTime::ToMicroseconds(Date/Datetime/Timestamp/TzDate/TzDatetime/TzTimestamp{Flags:AutoMap}) -> Uint64``` * ```DateTime::ToMicroseconds(Date32/Datetime64/Timestamp64/TzDate32/TzDatetime64/TzTimestamp64{Flags:AutoMap}) -> Int64``` #### Examples ```yql SELECT DateTime::FromSeconds(1546304523), -- 2019-01-01T01:02:03.000000Z DateTime::ToMicroseconds(Timestamp("2019-01-01T01:02:03.456789Z")); -- 1546304523456789 ``` ## Interval... {#interval} Conversions between `Interval` and various time units. #### List of functions * `DateTime::ToDays(Interval{Flags:AutoMap}) -> Int32` * `DateTime::ToDays(Interval64{Flags:AutoMap}) -> Int32` * `DateTime::ToHours(Interval{Flags:AutoMap}) -> Int32` * `DateTime::ToHours(Interval64{Flags:AutoMap}) -> Int64` * `DateTime::ToMinutes(Interval{Flags:AutoMap}) -> Int32` * `DateTime::ToMinutes(Interval64{Flags:AutoMap}) -> Int64` * `DateTime::ToSeconds(Interval{Flags:AutoMap}) -> Int32` Until [2025.03](https://ytsaurus.tech/changelog/2025.03.md) * `DateTime::ToSeconds(Interval{Flags:AutoMap}) -> Int64` Since [2025.03](https://ytsaurus.tech/changelog/2025.03.md) * `DateTime::ToSeconds(Interval64{Flags:AutoMap}) -> Int64` * `DateTime::ToMilliseconds(Interval{Flags:AutoMap}) -> Int64` * `DateTime::ToMilliseconds(Interval64{Flags:AutoMap}) -> Int64` * `DateTime::ToMicroseconds(Interval{Flags:AutoMap}) -> Int64` * `DateTime::ToMicroseconds(Interval64{Flags:AutoMap}) -> Int64` * `DateTime::IntervalFromDays(Int32{Flags:AutoMap}) -> Interval` * `DateTime::Interval64FromDays(Int32{Flags:AutoMap}) -> Interval64` * `DateTime::IntervalFromHours(Int32{Flags:AutoMap}) -> Interval` * `DateTime::Interval64FromHours(Int64{Flags:AutoMap}) -> Interval64` * `DateTime::IntervalFromMinutes(Int32{Flags:AutoMap}) -> Interval` * `DateTime::Interval64FromMinutes(Int64{Flags:AutoMap}) -> Interval64` * `DateTime::IntervalFromSeconds(Int32{Flags:AutoMap}) -> Interval` — before version [2025.03](https://ytsaurus.tech/changelog/2025.03.md) * `DateTime::IntervalFromSeconds(Int64{Flags:AutoMap}) -> Interval` — starting from version [2025.03](https://ytsaurus.tech/changelog/2025.03.md) * `DateTime::Interval64FromSeconds(Int64{Flags:AutoMap}) -> Interval64` * `DateTime::IntervalFromMilliseconds(Int64{Flags:AutoMap}) -> Interval` * `DateTime::Interval64FromMilliseconds(Int64{Flags:AutoMap}) -> Interval64` * `DateTime::IntervalFromMicroseconds(Int64{Flags:AutoMap}) -> Interval` * `DateTime::Interval64FromMicroseconds(Int64{Flags:AutoMap}) -> Interval64` AddTimezone doesn't affect the output of ToSeconds() in any way, because ToSeconds() always returns GMT time. You can also create an Interval from a string literal in the format [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601%23Durations). {% note warning %} At the moment, only literals for deterministic intervals are supported. All components greater than week -- M (month) and Y (year) -- are not fixed, since the interval can be defined only considering the particular context (month, year). {% endnote %} #### Examples ```yql SELECT DateTime::ToDays(Interval("PT3000M")), -- 2 DateTime::IntervalFromSeconds(1000000), -- 11 days 13 hours 46 minutes 40 seconds DateTime::ToDays(cast('2018-01-01' as date) - cast('2017-12-31' as date)); --1 ``` ## StartOf... / EndOf... / TimeOfDay {#startof} Get the start (end) of the period including the date/time. If the result is invalid, NULL is returned. If the timezone is different from GMT, then the period start (end) is in the specified time zone. #### List of functions * `DateTime::StartOfYear(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfYear(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfYear(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfYear(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfQuarter(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfQuarter(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfQuarter(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfQuarter(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfMonth(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfMonth(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfMonth(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfMonth(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfWeek(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfWeek(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfWeek(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfWeek(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfDay(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOfDay(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfDay(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::EndOfDay(Resource{Flags:AutoMap}) -> Resource?` * `DateTime::StartOf(Resource{Flags:AutoMap}, Interval{Flags:AutoMap}) -> Resource?` * `DateTime::StartOf(Resource{Flags:AutoMap}, Interval64{Flags:AutoMap}) -> Resource?` * `DateTime::EndOf(Resource{Flags:AutoMap}, Interval{Flags:AutoMap}) -> Resource?` * `DateTime::EndOf(Resource{Flags:AutoMap}, Interval64{Flags:AutoMap}) -> Resource?` The `StartOf`/`EndOf` functions are intended for grouping by an arbitrary period within a day. The result differs from the input value only by time components. A period exceeding one day is treated as a day (an equivalent of `StartOfDay`/`EndOfDay`). If a day doesn't include an integer number of periods, the number is rounded to the nearest time from the beginning of the day that is a multiple of the specified period. When the interval is zero, the output is same as the input. A negative interval is treated as a positive one. The `EndOf...` functions are intended for obtaining the latest moment in the same period of time as the specified one. The functions treat periods longer than one day in a different manner than the same-name functions in the old library. The time components are always reset to zero (this makes sense, because these functions are mainly used for grouping by the period). You can also specify a time period within a day: * `DateTime::TimeOfDay(Resource{Flags:AutoMap}) -> Interval` * `DateTime::TimeOfDay(Resource{Flags:AutoMap}) -> Interval64` #### Examples ```yql SELECT DateTime::MakeDate(DateTime::StartOfYear(Date("2019-06-06"))), -- 2019-01-01 (implicit Split here and below) DateTime::MakeDatetime(DateTime::StartOfQuarter(Datetime("2019-06-06T01:02:03Z"))), -- 2019-04-01T00:00:00Z (time components are reset to zero) DateTime::MakeDate(DateTime::StartOfMonth(Timestamp("2019-06-06T01:02:03.456789Z"))), -- 2019-06-01 DateTime::MakeDate(DateTime::StartOfWeek(Date("1970-01-01"))), -- NULL (the beginning of the epoch is Thursday, the beginning of the week is 1969-12-29 that is beyond the limits) DateTime::MakeTimestamp(DateTime::StartOfWeek(Date("2019-01-01"))), -- 2018-12-31T00:00:00Z DateTime::MakeDatetime(DateTime::StartOfDay(Datetime("2019-06-06T01:02:03Z"))), -- 2019-06-06T00:00:00Z DateTime::MakeTzDatetime(DateTime::StartOfDay(TzDatetime("1970-01-01T05:00:00,Europe/Moscow"))), -- NULL (beyond the epoch in GMT) DateTime::MakeTzTimestamp(DateTime::StartOfDay(TzTimestamp("1970-01-02T05:00:00.000000,Europe/Moscow"))), -- 1970-01-02T00:00:00,Europe/Moscow (the beginning of the day in Moscow) DateTime::MakeDatetime(DateTime::StartOf(Datetime("2019-06-06T23:45:00Z"), Interval("PT7H"))), -- 2019-06-06T21:00:00Z DateTime::MakeDatetime(DateTime::StartOf(Datetime("2019-06-06T23:45:00Z"), Interval("PT20M"))), -- 2019-06-06T23:40:00Z DateTime::TimeOfDay(Timestamp("2019-02-14T01:02:03.456789Z")); -- 1 hour 2 minutes 3 seconds 456789 microseconds ``` ## Shift... {#shift} Add/subtract the specified number of units to/from the component in the internal representation and update the other fields. Returns either an updated copy or NULL, if an update produces an invalid date or other inconsistencies. #### List of functions * `DateTime::ShiftYears(Resource{Flags:AutoMap}, Int32) -> Resource?` * `DateTime::ShiftYears(Resource{Flags:AutoMap}, Int32) -> Resource?` * `DateTime::ShiftQuarters(Resource{Flags:AutoMap}, Int32) -> Resource?` * `DateTime::ShiftQuarters(Resource{Flags:AutoMap}, Int32) -> Resource?` * `DateTime::ShiftMonths(Resource{Flags:AutoMap}, Int32) -> Resource?` * `DateTime::ShiftMonths(Resource{Flags:AutoMap}, Int32) -> Resource?` If the resulting number of the day in the month exceeds the maximum allowed, then the `Day` field will accept the last day of the month without changing the time (see examples). #### Examples ```yql $tm1 = DateTime::Split(Datetime("2019-01-31T01:01:01Z")); $tm2 = DateTime::Split(TzDatetime("2049-05-20T12:34:50,Europe/Moscow")); SELECT DateTime::MakeDate(DateTime::ShiftYears($tm1, 10)), -- 2029-01-31T01:01:01 DateTime::MakeDate(DateTime::ShiftYears($tm2, -10000)), -- NULL (beyond the limits) DateTime::MakeDate(DateTime::ShiftQuarters($tm2, 0)), -- 2049-05-20T12:34:50,Europe/Moscow DateTime::MakeDate(DateTime::ShiftQuarters($tm1, -3)), -- 2018-04-30T01:01:01 DateTime::MakeDate(DateTime::ShiftMonths($tm1, 1)), -- 2019-02-28T01:01:01 DateTime::MakeDate(DateTime::ShiftMonths($tm1, -35)), -- 2016-02-29T01:01:01 ``` ## Format {#format} Get a string representation of a time using an arbitrary formatting string. #### List of functions * ```DateTime::Format(String, [alwaysWriteFractionalSeconds:Bool?, writeOffsetWithColon:Bool?]) -> (Resource{Flags:AutoMap}) -> String``` A set of specifiers is implemented for the formatting string: * `%%`: % character; * `%Y`: Variable length (1-6 digits) year with minus for the dates BC; * `%m`: 2-digit month; * `%d`: 2-digit day; * `%H`: 2-digit hour; * `%M`: 2-digit minutes; * `%S`: 2-digit seconds or `XX.XXXXXX` in the case of non-empty microseconds (only if `alwaysWriteFractionalSeconds` is not set to `True`); * `%z`: `+hhmm` or `-hhmm` by default; `+hh:mm` or `-hh:mm` if `writeOffsetWithColon` (added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#datetime-module)) is set to `True`; * `%Z`: IANA name of the timezone (GMT); * `%b`: A short three-letter English name of the month (Jan); * `%B`: A full English name of the month (January). All other characters in the format string are passed on without changes. #### Examples ```yql $format = DateTime::Format("%Y-%m-%d %H:%M:%S %Z"); SELECT $format(DateTime::Split(TzDatetime("2019-01-01T01:02:03,Europe/Moscow"))); -- "2019-01-01 01:02:03 Europe/Moscow" ``` ## Parse/Parse64 {#parse} Parse a string into an internal representation using an arbitrary formatting string. Default values are used for empty fields. If errors are raised, NULL is returned. #### List of functions * `DateTime::Parse(String) -> (String{Flags:AutoMap}) -> Resource?` * `DateTime::Parse64(String) -> (String{Flags:AutoMap}) -> Resource?` Implemented specifiers: * `%%`: the % character; * `%Y`: 4-digit year when using `Parse`, and variable length (1-6 digits) year with minus for the dates BC when using `Parse64`; * `%m`: 2-digit month; * `%d`: 2-digit day; * `%H`: 2-digit hour; * `%M`: 2-digit minutes; * `%S`: Seconds, can also accept microseconds in the formats from `XX` up to `XX.XXXXXX`; * `%z`: Timezone offset in one of the following formats: `±hh:mm`, `±hhmm`, `±hh` — added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#datetime-module); * `%Z`: The IANA name of the timezone (GMT); * `%b`: A short three-letter case-insensitive English name of the month (Jan); * `%B`: A full case-insensitive English name of the month (January). #### Examples ```yql $parse1 = DateTime::Parse("%H:%M:%S"); $parse2 = DateTime::Parse("%S"); $parse3 = DateTime::Parse("%m/%d/%Y"); $parse4 = DateTime::Parse("%Z"); $parse5 = DateTime::Parse64("%m/%d/%Y"); SELECT DateTime::MakeDatetime($parse1("01:02:03")), -- 1970-01-01T01:02:03Z DateTime::MakeTimestamp($parse2("12.3456")), -- 1970-01-01T00:00:12.345600Z DateTime::MakeTimestamp($parse3("02/30/2000")), -- NULL (Feb 30) DateTime::MakeTimestamp($parse4("Canada/Central")), -- 1970-01-01T06:00:00Z (conversion to UTC) DateTime::MakeTimestamp64($parse5("02/10/1931")), -- 1931-02-10T00:00:00Z (conversion to UTC) ``` For the common formats, wrappers around the corresponding util methods are supported. You can only get TM with components in the UTC timezone. ## Parse specific formats #### List of functions * `DateTime::ParseRfc822(String{Flags:AutoMap}) -> Resource?` * `DateTime::ParseIso8601(String{Flags:AutoMap}) -> Resource?` * `DateTime::ParseHttp(String{Flags:AutoMap}) -> Resource?` * `DateTime::ParseX509(String{Flags:AutoMap}) -> Resource?` #### Examples ```yql SELECT DateTime::MakeTimestamp(DateTime::ParseRfc822("Fri, 4 Mar 2005 19:34:45 EST")), -- 2005-03-05T00:34:45Z DateTime::MakeTimestamp(DateTime::ParseIso8601("2009-02-14T02:31:30+0300")), -- 2009-02-13T23:31:30Z DateTime::MakeTimestamp(DateTime::ParseHttp("Sunday, 06-Nov-94 08:49:37 GMT")), -- 1994-11-06T08:49:37Z DateTime::MakeTimestamp(DateTime::ParseX509("20091014165533Z")) -- 2009-10-14T16:55:33Z ``` ## Standard scenarios ### Conversions between strings and seconds Converting a string date (in the Moscow timezone) to seconds (in GMT timezone): ```yql $datetime_parse = DateTime::Parse("%Y-%m-%d %H:%M:%S"); $datetime_parse_tz = DateTime::Parse("%Y-%m-%d %H:%M:%S %Z"); SELECT DateTime::ToSeconds(TzDatetime("2019-09-16T00:00:00,Europe/Moscow")) AS md_us1, -- 1568581200 DateTime::ToSeconds(DateTime::MakeDatetime($datetime_parse_tz("2019-09-16 00:00:00" || " Europe/Moscow"))), -- 1568581200 DateTime::ToSeconds(DateTime::MakeDatetime(DateTime::Update($datetime_parse("2019-09-16 00:00:00"), "Europe/Moscow" as Timezone))), -- 1568581200 -- INCORRECT (Date imports time as GMT, but AddTimezone has no effect on ToSeconds that always returns GMT time) DateTime::ToSeconds(AddTimezone(Date("2019-09-16"), 'Europe/Moscow')) AS md_us2, -- 1568592000 ``` Converting a string date (in the Moscow timezone) to seconds (in the Moscow timezone). DateTime::ToSeconds() exports only to GMT. That's why we should put timezones aside for a while and use only GMT (as if we assumed for a while that Moscow is in GMT): ```yql $date_parse = DateTime::Parse("%Y-%m-%d"); $datetime_parse = DateTime::Parse("%Y-%m-%d %H:%M:%S"); $datetime_parse_tz = DateTime::Parse("%Y-%m-%d %H:%M:%S %Z"); SELECT DateTime::ToSeconds(Datetime("2019-09-16T00:00:00Z")) AS md_ms1, -- 1568592000 DateTime::ToSeconds(Date("2019-09-16")) AS md_ms2, -- 1568592000 DateTime::ToSeconds(DateTime::MakeDatetime($date_parse("2019-09-16"))) AS md_ms3, -- 1568592000 DateTime::ToSeconds(DateTime::MakeDatetime($datetime_parse("2019-09-16 00:00:00"))) AS md_ms4, -- 1568592000 DateTime::ToSeconds(DateTime::MakeDatetime($datetime_parse_tz("2019-09-16 00:00:00 GMT"))) AS md_ms5, -- 1568592000 -- INCORRECT (imports the time in the Moscow timezone, but RemoveTimezone doesn't affect ToSeconds in any way) DateTime::ToSeconds(RemoveTimezone(TzDatetime("2019-09-16T00:00:00,Europe/Moscow"))) AS md_ms6, -- 1568581200 DateTime::ToSeconds(DateTime::MakeDatetime($datetime_parse_tz("2019-09-16 00:00:00 Europe/Moscow"))) AS md_ms7 -- 1568581200 ``` Converting seconds (in the GMT timezone) to a string date (in the Moscow timezone): ```yql $date_format = DateTime::Format("%Y-%m-%d %H:%M:%S %Z"); SELECT $date_format(AddTimezone(DateTime::FromSeconds(1568592000), 'Europe/Moscow')) -- "2019-09-16 03:00:00 Europe/Moscow" ``` Converting seconds (in the Moscow timezone) to a string date (in the Moscow timezone). In this case, the %Z timezone is output for reference: usually, it's not needed because it's "GMT" and might mislead you. ```yql $date_format = DateTime::Format("%Y-%m-%d %H:%M:%S %Z"); SELECT $date_format(DateTime::FromSeconds(1568592000)) -- "2019-09-16 00:00:00 GMT" ``` Converting seconds (in the GMT timezone) to three-letter days of the week (in the Moscow timezone): ```yql SELECT SUBSTRING(DateTime::GetDayOfWeekName(AddTimezone(DateTime::FromSeconds(1568581200), "Europe/Moscow")), 0, 3) -- "Mon" ``` ### Date and time formatting Usually a separate named expression is used to format time, but you can do without it: ```yql $date_format = DateTime::Format("%Y-%m-%d %H:%M:%S %Z"); SELECT -- A variant with a named expression $date_format(AddTimezone(DateTime::FromSeconds(1568592000), 'Europe/Moscow')), -- A variant without a named expression DateTime::Format("%Y-%m-%d %H:%M:%S %Z") (AddTimezone(DateTime::FromSeconds(1568592000), 'Europe/Moscow')) ; ``` ### Converting types This way, you can convert only constants: ```yql SELECT TzDatetime("2019-09-16T00:00:00,Europe/Moscow"), -- 2019-09-16T00:00:00,Europe/Moscow Date("2019-09-16") -- 2019-09-16 ``` But this way, you can convert a constant, a named expression, or a table field: ```yql SELECT CAST("2019-09-16T00:00:00,Europe/Moscow" AS TzDatetime), -- 2019-09-16T00:00:00,Europe/Moscow CAST("2019-09-16" AS Date) -- 2019-09-16 ``` ### Converting time to date A CAST to Date or TzDate outputs a GMT date for a midnight, local time (for example, for Moscow time 2019-10-22 00:00:00, the date 2019-10-21 is returned). To get a date in the local timezone, you can use DateTime::Format. ```yql $x = Datetime("2019-10-21T21:00:00Z"); SELECT AddTimezone($x, "Europe/Moscow"), -- 2019-10-22T00:00:00,Europe/Moscow cast($x as TzDate), -- 2019-10-21,GMT cast(AddTimezone($x, "Europe/Moscow") as TzDate), -- 2019-10-21,Europe/Moscow cast(AddTimezone($x, "Europe/Moscow") as Date), -- 2019-10-21 DateTime::Format("%Y-%m-%d %Z")(AddTimezone($x, "Europe/Moscow")), -- 2019-10-22 Europe/Moscow ``` It's worth mentioning that several `TzDatetime` or `TzTimestamp` values with a positive timezone offset cannot be cast to `TzDate`. Consider the example below: ```yql SELECT CAST(TzDatetime("1970-01-01T23:59:59,Europe/Moscow") as TzDate); /* Fatal: Timestamp 1970-01-01T23:59:59.000000,Europe/Moscow cannot be casted to TzDate */ ``` Starting from the Unix epoch, there is no valid value representing midnight on 01/01/1970 for the Europe/Moscow timezone. As a result, such a cast is impossible and fails at runtime. At the same time, values with a negative timezone offset are converted correctly: ```yql SELECT CAST(TzDatetime("1970-01-01T23:59:59,America/Los_Angeles") as TzDate); /* 1970-01-01,America/Los_Angeles */ ``` ### Daylight saving time Please note that daylight saving time depends on the year: ```yql SELECT RemoveTimezone(TzDatetime("2019-09-16T10:00:00,Europe/Moscow")) as DST1, -- 2019-09-16T07:00:00Z RemoveTimezone(TzDatetime("2008-12-03T10:00:00,Europe/Moscow")) as DST2, -- 2008-12-03T07:00:00Z RemoveTimezone(TzDatetime("2008-07-03T10:00:00,Europe/Moscow")) as DST3, -- 2008-07-03T06:00:00Z (DST) ``` # Digest Digest functions calculate hashes (aka fingerprints) of original messages. A hash is usually a short or fixed-length string. Hash length depends on the algorithm. The result is a bit sequence. The module includes both cryptographic and non-cryptographic functions. An important feature of algorithms is that changing the original message also changes the hash output. Also, digest functions are one-way functions, meaning that it's hard to find a message that will produce a given digest. ## List of functions * `Digest::Crc32c(String{Flags::AutoMap}) -> Uint32` * `Digest::Crc64(String{Flags::AutoMap}) -> Uint64` * `Digest::Fnv32(String{Flags::AutoMap}) -> Uint32` * `Digest::Fnv64(String{Flags::AutoMap}) -> Uint64` * `Digest::MurMurHash(String{Flags:AutoMap}) -> Uint64` * `Digest::MurMurHash32(String{Flags:AutoMap}) -> Uint32` * `Digest::MurMurHash2A(String{Flags:AutoMap}) -> Uint64` * `Digest::MurMurHash2A32(String{Flags:AutoMap}) -> Uint32` * `Digest::CityHash(String{Flags:AutoMap}, [Uint64?]) -> Uint64` (the second optional argument defines the seed) * `Digest::CityHash128(String{Flags:AutoMap}) -> Tuple` CityHash is a byte string function that returns a uint128 value. The result is represented by a pair of two uint64 numbers: ``. ```yql SELECT Digest::CityHash128("Who set this ancient quarrel new abroach?"); -- (11765163929838407746,2460323961016211789) ``` * `Digest::NumericHash(Uint64{Flags:AutoMap}) -> Uint64` ```yql SELECT Digest::NumericHash(123456789); -- 1734215268924325803 ``` * `Digest::Md5Hex(String{Flags:AutoMap}) -> String` * `Digest::Md5Raw(String{Flags:AutoMap}) -> String` * `Digest::Md5HalfMix(String{Flags:AutoMap}) -> Uint64` MD5 is a widely used hashing algorithm that results in a 128-bit hash value. Md5Hex returns an ASCII string hash with hex encoding. Md5Raw returns a hash as a byte string. Md5HalfMix is a lightweight MD5 that returns a Uint64 value. ```yql SELECT Digest::Md5Hex("Who set this ancient quarrel new abroach?"), -- "644e98bae764871650f2d93e14c6488d" Digest::Md5Raw("Who set this ancient quarrel new abroach?"), -- Binary String: 64 4e 98 ba e7 64 87 16 50 f2 d9 3e 14 c6 48 8d Digest::Md5HalfMix("Who set this ancient quarrel new abroach?"); -- 17555822562955248004 ``` * `Digest::Argon2(string:String{Flags:AutoMap}, salt:String{Flags:AutoMap}) -> String` Argon2 is a key derivation function aimed at a high memory-filling rate and good utilization of multiple computing units. Parameters: - string — the source string - salt — the string to be used as the salt for the hash function ```yql SELECT Digest::Argon2("Who set this ancient quarrel new abroach?", "zcIvVcuHEIL8"); -- Binary String: fa 50 34 d3 c3 23 a4 de 22 c7 7c e1 9c 65 64 88 25 b3 59 75 c5 b8 8c 73 da 88 eb 79 31 70 e8 f1 SELECT Digest::Argon2("Who set this ancient quarrel new abroach?", "M78P42R8HA=="); -- Binary String: d2 0e f1 3e 72 5a e9 32 65 ed 28 4b 12 1f 39 70 e5 10 aa 1a 15 67 6d 96 5d e8 19 b3 bd d5 04 e9 ``` * `Digest::Blake2B(string:String{Flags:AutoMap},[key:String?]) -> String` BLAKE2 is a cryptographic hash function designed as an alternative to MD5 and SHA-1 algorithms, which have been shown to be vulnerable. Parameters: - string — the source string - key is the key used to encrypt the source string (it can be used as a shared secret for the sender and receiver) ```yql SELECT Digest::Blake2B("Who set this ancient quarrel new abroach?"); -- Binary String: 62 21 91 d8 11 5a da ad 5e 7c 86 47 41 02 7f 8f a8 a6 82 07 47 d8 f8 30 ab b4 c3 00 db 9c 24 2f ``` * `Digest::SipHash(low:Uint64,high:Uint64,string:String{Flags:AutoMap}) -> Uint64` Hashes the source message (`string`) using a 128-bit key. The key is represented by a pair of uint64 numbers: low, high ```yql SELECT Digest::SipHash(0,0,"Who set this ancient quarrel new abroach?"); -- 14605466535756698285 ``` * ```Digest::HighwayHash(key0:Uint64,key1:Uint64,key2:Uint64,key3:Uint64,string:String{Flags:AutoMap}) -> Uint64``` Hashes the source message (`string`) using a 256-bit key. The key has the following structure: - key0 — first 8 bytes of the key - key1 — next 8 bytes of the key - key2 — subsequent 8 bytes of the key - key3 — last 8 bytes of the key * `Digest::FarmHashFingerprint(source:Uint64{Flags:AutoMap}) -> Uint64` * `Digest::FarmHashFingerprint2(low:Uint64{Flags:AutoMap}, high:Uint64{Flags:AutoMap}) -> Uint64` FarmHash is a function for a 128-bit number. This 128-bit number is made by combining bits of two uit64 numbers. * `Digest::FarmHashFingerprint32(string:String{Flags:AutoMap}) -> Uint32` * `Digest::FarmHashFingerprint64(string:String{Flags:AutoMap}) -> Uint64` * `Digest::FarmHashFingerprint128(string:String{Flags:AutoMap}) -> Tuple` FarmHash is a byte string function that returns a uint128 value. The result is represented by a pair of two uint64 numbers: ``. ```yql SELECT Digest::FarmHashFingerprint2(31880,6990065); -- 237693065644851126 SELECT Digest::FarmHashFingerprint128("Who set this ancient quarrel new abroach?"); -- (17165761740714960035, 5559728965407786337) ``` * `Digest::SuperFastHash(String{Flags:AutoMap}) -> Uint32` * `Digest::Sha1(String{Flags:AutoMap}) -> String` * `Digest::Sha256(String{Flags:AutoMap}) -> String` * `Digest::IntHash64(Uint64{Flags:AutoMap}) -> Uint64` * `Digest::XXH3(String{Flags:AutoMap}) -> Uint64` * `Digest::XXH3_128(String{Flags:AutoMap}) -> Tuple` XXH3 is a non-cryptographic hash function that belongs to the xxxHash family. XXH3_128 generates a 128-bit hash represented by a pair of two uint64 numbers: ``. ```yql SELECT Digest::XXH3_128("Who set this ancient quarrel new abroach?"); -- (17117571879768798812, 14282600258804776266) ``` # Dsv UDF Functions that convert strings formatted as `"key1=value1\tkey2=value2"` to dictionaries. ```yql Dsv::ReadRecord(Struct) -> Struct> Dsv::Parse(String,[String]) -> Dict -- the second argument defines the delimiter (tab by default) ``` #### Example ```yql SELECT Dsv::Parse("a=b@@c=d@@e=f", "@@")["c"]; -- "d" ``` # Histogram Set of auxiliary functions for the [`HISTOGRAM` aggregate function](https://ytsaurus.tech/builtins/aggregation.md). In the signature description below, HistogramStruct refers to the result of the aggregate function `HISTOGRAM`, `LinearHistogram` or `LogarithmicHistogram` being a structure of a certain type. ## List of functions {#function-list} * `Histogram::Print(HistogramStruct{Flags:AutoMap}, Byte?) -> String` * `Histogram::Normalize(HistogramStruct{Flags:AutoMap}, [Double?]) -> HistogramStruct`. In the second argument, the desired histogram area is 100 by default. * `Histogram::ToCumulativeDistributionFunction(HistogramStruct{Flags:AutoMap}) -> HistogramStruct` * `Histogram::GetSumAboveBound(HistogramStruct{Flags:AutoMap}, Double) -> Double` * `Histogram::GetSumBelowBound(HistogramStruct{Flags:AutoMap}, Double) -> Double` * `Histogram::GetSumInRange(HistogramStruct{Flags:AutoMap}, Double, Double) -> Double` * `Histogram::CalcUpperBound(HistogramStruct{Flags:AutoMap}, Double) -> Double` * `Histogram::CalcLowerBound(HistogramStruct{Flags:AutoMap}, Double) -> Double` * `Histogram::CalcUpperBoundSafe(HistogramStruct{Flags:AutoMap}, Double) -> Double` * `Histogram::CalcLowerBoundSafe(HistogramStruct{Flags:AutoMap}, Double) -> Double` `Histogram::Print` has an optional numeric argument that sets the maximum length of the histogram columns (the length is in characters, since the histogram is rendered in ASCII art). The default value is 25. First and foremost, this function serves to view histograms on the console. Web interface performs their interactive visualization automatically. # Hyperscan [Hyperscan](https://www.hyperscan.io) is an open source library developed by Intel for searching with regular expressions. The library features 4 implementations with different processor instruction sets (SSE3, SSE4.2, AVX2, and AVX512) that are chosen automatically depending on the current processor. By default, all functions work in the single-byte mode. However, if the regular expression is a valid UTF-8 string but is not a valid ASCII string, the UTF-8 mode is enabled automatically. ## List of functions {#function-list} * `Hyperscan::Grep(pattern:String) -> (string:String?) -> Bool` * `Hyperscan::Match(pattern:String) -> (string:String?) -> Bool` * `Hyperscan::BacktrackingGrep(pattern:String) -> (string:String?) -> Bool` * `Hyperscan::BacktrackingMatch(pattern:String) -> (string:String?) -> Bool` * `Hyperscan::MultiGrep(pattern:String) -> (string:String?) -> Tuple` * `Hyperscan::MultiMatch(pattern:String) -> (string:String?) -> Tuple` * `Hyperscan::Capture(pattern:String) -> (string:String?) -> String?` * `Hyperscan::Replace(pattern:String) -> (string:String?, replacement:String) -> String?` ## Call syntax {#syntax} To avoid compiling a regular expression at each table row at direct call, wrap the function call by a [named expression](https://ytsaurus.tech/syntax/expressions.md#named-nodes). ```yql $re = Hyperscan::Grep("\\d+"); -- we create a called value to test the specific regular expression SELECT * FROM table WHERE $re(key); -- we use the value to filter the table ``` **Please note** escaping of special characters in regular expressions. Be sure to use the second slash, since all the standard string literals in SQL can accept C-escaped strings, and the `\d` sequence is not a valid sequence (even if it were, it wouldn't search for numbers as intended). You can enable the case-insensitive mode by specifying, at the beginning of the regular expression, the flag `(?i)`. ## Grep {#grep} Matches the regular expression with a **part of the string** (arbitrary substring). ## Match {#match} Matches the **whole string** against the regular expression. To get a result similar to that of `Grep` (that matches against a substring), enclose the regular expression in `.*` on both sides (`.*foo.*` instead of `foo`). However, in terms of code readability, it's usually better to change the function. ## BacktrackingGrep / BacktrackingMatch {#backtrackinggrep} The functions are identical to the same-name functions without the `Backtracking` prefix. However, they support a broader range of regular expressions. This is due to the fact that if a specific regular expression is not fully supported by Hyperscan, the library switches to the prefilter mode. In this case, it responds not by "Yes" or "No", but by "Definitely not" or "Maybe yes". The "maybe yes" answers are then automatically rechecked using the slower but more powerful [libpcre](https://www.pcre.org) library. ## MultiGrep / MultiMatch {#multigrep} Hyperscan lets you match against multiple regular expressions in a single pass through the text, and get a separate response for each match. However, if you want to match a string against any of the listed expressions (the results would be joined with "or"), it would be more efficient to combine the query parts in a single regular expression with `|` and match it with regular `Grep` or `Match`. When you call `MultiGrep`/`MultiMatch`, regular expressions are passed one per line using [multiline string literals](https://ytsaurus.tech/syntax/lexer.md#multiline-string-literals): #### Example ```yql $multi_match = Hyperscan::MultiMatch(@@a.* .*x.* .*axa.*@@); SELECT $multi_match("a") AS a, -- (true, false, false) $multi_match("axa") AS axa; -- (true, true, true) ``` ## Capture and Replace {#capture} If a string matches the given regexp, `Hyperscan::Capture` returns the last substring matched by the regexp. `Hyperscan::Replace` replaces all substrings matched by the specified regexp with the given string. Hyperscan offers limited capabilities for such operations. This means that although `Hyperscan::Capture` and `Hyperscan::Replace` are implemented for uniformity, we recommend using same-name functions from Re2 for non-obvious search and replacement operations: * [Re2::Capture](https://ytsaurus.tech/docs/en/re2.md#capture); * [Re2::Replace](https://ytsaurus.tech/docs/en/re2.md#replace). ## Usage example ```yql $value = "xaaxaaXaa"; $match = Hyperscan::Match("a.*"); $grep = Hyperscan::Grep("axa"); $insensitive_grep = Hyperscan::Grep("(?i)axaa$"); $multi_match = Hyperscan::MultiMatch(@@a.* .*a.* .*a .*axa.*@@); $capture = Hyperscan::Capture(".*a{2}.*"); $capture_many = Hyperscan::Capture(".*x(a+).*"); $replace = Hyperscan::Replace("xa"); SELECT $match($value) AS match, -- false $grep($value) AS grep, -- true $insensitive_grep($value) AS insensitive_grep, -- true $multi_match($value) AS multi_match, -- (false, true, true, true) $multi_match($value).0 AS some_multi_match, -- false $capture($value) AS capture, -- "xaa" $capture_many($value) AS capture_many, -- "xa" $replace($value, "b") AS replace -- "babaXaa" ; ``` # Ip The `Ip` module supports both the IPv4 and IPv6 addresses. By default, they are represented as binary strings of 4 and 16 bytes, respectively. ## List of functions * `Ip::FromString(String{Flags:AutoMap}) -> String?` — From a human-readable representation to a binary representation. * `Ip::SubnetFromString(String{Flags:AutoMap}) -> String?` — From a human-readable representation of a subnet to a binary representation. * `Ip::ToString(String{Flags:AutoMap}) -> String?` — From a binary representation to a human-readable representation. * `Ip::SubnetToString(String{Flags:AutoMap}) -> String?` — From a binary representation of a subnet to a human-readable representation. * `Ip::Ipv4FromUint32(Uint32{Flags:AutoMap}) -> String` — From a Uint32 to a binary representation; the integer `A << 24 | B << 16 | C << 8 | D` corresponds to address `A.B.C.D`. * `Ip::Ipv4ToUint32(String{Flags:AutoMap}) -> Uint32?` — From a binary IPv4 representation to Uint32; address `A.B.C.D` corresponds to the integer `A << 24 | B << 16 | C << 8 | D`; IPv6 is not supported. * `Ip::IsIPv4(String?) -> Bool` * `Ip::IsIPv6(String?) -> Bool` * `Ip::IsEmbeddedIPv4(String?) -> Bool` * `Ip::ConvertToIPv6(String{Flags:AutoMap}) -> String` — IPv6 remains unchanged, and IPv4 becomes embedded in IPv6. * `Ip::GetSubnet(String{Flags:AutoMap}, [Uint8?]) -> String` — The second argument is the subnet size; by default, it's 24 for IPv4 and 64 for IPv6. * `Ip::GetSubnetByMask(String{Flags:AutoMap}, String{Flags:AutoMap}) -> String` — The second argument is the subnet bitmask. * `Ip::SubnetMatch(String{Flags:AutoMap}, String{Flags:AutoMap}) -> Bool` — The first argument is the subnet, the second argument is the subnet or address. ## Examples ```yql SELECT Ip::IsEmbeddedIPv4( Ip::FromString("::ffff:77.75.155.3") ); -- true SELECT Ip::ToString( Ip::GetSubnet( Ip::FromString("213.180.193.3") ) ); -- "213.180.193.0" SELECT Ip::SubnetMatch( Ip::SubnetFromString("192.168.0.1/16"), Ip::FromString("192.168.1.14"), ); -- true SELECT Ip::ToString( Ip::GetSubnetByMask( Ip::FromString("192.168.0.1"), Ip::FromString("255.255.0.0") ) ); -- "192.168.0.0" SELECT Ip::ToString( Ip::Ipv4FromUint32(1) ); -- "0.0.0.1" SELECT Ip::Ipv4ToUint32( Ip::FromString("0.0.0.1") ); -- 1 ``` # Math A set of wrappers around the functions from the libm library and the Yandex utilities. ## Constants {#constants} #### List of functions * `Math::Pi() -> Double` * `Math::E() -> Double` * `Math::Eps() -> Double` #### Examples ```yql SELECT Math::Pi(); -- 3.141592654 SELECT Math::E(); -- 2.718281828 SELECT Math::Eps(); -- 2.220446049250313e-16 ``` ## (Double) -> Bool {#double-bool} #### List of functions * `Math::IsInf(Double{Flags:AutoMap}) -> Bool` * `Math::IsNaN(Double{Flags:AutoMap}) -> Bool` * `Math::IsFinite(Double{Flags:AutoMap}) -> Bool` #### Examples ```yql SELECT Math::IsNaN(0.0/0.0); -- true SELECT Math::IsFinite(1.0/0.0); -- false ``` ## (Double) -> Double {#double-double} #### List of functions * `Math::Abs(Double{Flags:AutoMap}) -> Double` * `Math::Acos(Double{Flags:AutoMap}) -> Double` * `Math::Asin(Double{Flags:AutoMap}) -> Double` * `Math::Asinh(Double{Flags:AutoMap}) -> Double` * `Math::Atan(Double{Flags:AutoMap}) -> Double` * `Math::Cbrt(Double{Flags:AutoMap}) -> Double` * `Math::Ceil(Double{Flags:AutoMap}) -> Double` * `Math::Cos(Double{Flags:AutoMap}) -> Double` * `Math::Cosh(Double{Flags:AutoMap}) -> Double` * `Math::Erf(Double{Flags:AutoMap}) -> Double` * `Math::ErfInv(Double{Flags:AutoMap}) -> Double` * `Math::ErfcInv(Double{Flags:AutoMap}) -> Double` * `Math::Exp(Double{Flags:AutoMap}) -> Double` * `Math::Exp2(Double{Flags:AutoMap}) -> Double` * `Math::Fabs(Double{Flags:AutoMap}) -> Double` * `Math::Floor(Double{Flags:AutoMap}) -> Double` * `Math::Lgamma(Double{Flags:AutoMap}) -> Double` * `Math::Rint(Double{Flags:AutoMap}) -> Double` * `Math::Sigmoid(Double{Flags:AutoMap}) -> Double` * `Math::Sin(Double{Flags:AutoMap}) -> Double` * `Math::Sinh(Double{Flags:AutoMap}) -> Double` * `Math::Sqrt(Double{Flags:AutoMap}) -> Double` * `Math::Tan(Double{Flags:AutoMap}) -> Double` * `Math::Tanh(Double{Flags:AutoMap}) -> Double` * `Math::Tgamma(Double{Flags:AutoMap}) -> Double` * `Math::Trunc(Double{Flags:AutoMap}) -> Double` * `Math::Log(Double{Flags:AutoMap}) -> Double` * `Math::Log2(Double{Flags:AutoMap}) -> Double` * `Math::Log10(Double{Flags:AutoMap}) -> Double` #### Examples ```yql SELECT Math::Sqrt(256); -- 16 SELECT Math::Trunc(1.2345); -- 1 ``` ## (Double, Double) -> Double {#doubledouble-double} #### List of functions * `Math::Atan2(Double{Flags:AutoMap}, Double{Flags:AutoMap}) -> Double` * `Math::Fmod(Double{Flags:AutoMap}, Double{Flags:AutoMap}) -> Double` * `Math::Hypot(Double{Flags:AutoMap}, Double{Flags:AutoMap}) -> Double` * `Math::Pow(Double{Flags:AutoMap}, Double{Flags:AutoMap}) -> Double` * `Math::Remainder(Double{Flags:AutoMap}, Double{Flags:AutoMap}) -> Double` #### Examples ```yql SELECT Math::Atan2(1, 0); -- 1.570796327 SELECT Math::Remainder(2.1, 2); -- 0.1 ``` ## (Double, Int32) -> Double {#doubleint32-double} #### List of functions * `Math::Ldexp(Double{Flags:AutoMap}, Int32{Flags:AutoMap}) -> Double` * `Math::Round(Double{Flags:AutoMap}, [Int32?]) -> Double` — the second argument specifies the power of 10 that the number is rounded to (a negative value for decimal places, and a positive for tens, thousands, millions, etc.). 0 by default #### Examples ```yql SELECT Math::Pow(2, 10); -- 1024 SELECT Math::Round(1.2345, -2); -- 1.23 ``` ## (Double, Double, \[Double?\]) -> Bool {#doubledouble-bool} #### List of functions * `Math::FuzzyEquals(Double{Flags:AutoMap}, Double{Flags:AutoMap}, [Double?]) -> Bool` — compares two Doubles with a tolerance specified by the third argument. 1.0e-13 by default. The tolerance is measured in relative units based on the argument whose absolute value is less. {% note alert %} If any compared value equals `0.0`, the function returns `false`. {% endnote %} #### Examples ```yql SELECT Math::FuzzyEquals(1.01, 1.0, 0.05); -- true ``` ## Functions for computing remainders #### List of functions * `Math::Mod(Int64{Flags:AutoMap}, Int64) -> Int64?` * `Math::Rem(Int64{Flags:AutoMap}, Int64) -> Int64?` These functions behave similarly to the built-in % operator in the case of non-negative arguments. The differences are noticeable in the case of negative arguments: * Math::Mod preserves the sign of the second argument (the denominator). * Math::Rem preserves the sign of the first argument (the numerator). Functions return null if the divisor is zero. #### Examples ```yql SELECT Math::Mod(-1, 7); -- 6 SELECT Math::Rem(-1, 7); -- -1 ``` ## Functions for rounding to integer in a given mode #### List of functions * `Math::RoundDownward() -> Tagged` — Rounding toward negative infinity. * `Math::RoundToNearest() -> Tagged` — Rounding toward the nearest representable value. * `Math::RoundTowardZero() -> Tagged` — Rounding toward zero. * `Math::RoundUpward() -> Tagged` — Rounding toward positive infinity. * `Math::NearbyInt(AutoMap, Tagged) -> Optional` — Rounding to the nearest integer. The `Math::NearbyInt` function rounds the first argument to an integer according to the mode specified by the second argument. If the result is beyond a 64-bit integer, it returns NULL. #### Examples ```yql SELECT Math::NearbyInt(1.5, Math::RoundDownward()); -- 1 SELECT Math::NearbyInt(1.5, Math::RoundToNearest()); -- 2 SELECT Math::NearbyInt(2.5, Math::RoundToNearest()); -- 2 SELECT Math::NearbyInt(1.5, Math::RoundTowardZero()); -- 1 SELECT Math::NearbyInt(1.5, Math::RoundUpward()); -- 2 SELECT Math::NearbyInt(-1.5, Math::RoundDownward()); -- -2 SELECT Math::NearbyInt(-1.5, Math::RoundToNearest()); -- -2 SELECT Math::NearbyInt(-2.5, Math::RoundToNearest()); -- -2 SELECT Math::NearbyInt(-1.5, Math::RoundTowardZero()); -- -1 SELECT Math::NearbyInt(-1.5, Math::RoundUpward()); -- -1 ``` # Pcre The Pcre library is currently an alias to [Hyperscan](https://ytsaurus.tech/docs/en/hyperscan.md). If you rely on specific functionality of a certain regular expression engine, you should use a UDF with a particular library inside and treat Pcre as the current recommended option for simple matches, which might change in the future. Currently available engines: * [Hyperscan](https://ytsaurus.tech/docs/en/hyperscan.md) (Intel) * [Pire](https://ytsaurus.tech/docs/en/pire.md) (Yandex) * [Re2](https://ytsaurus.tech/docs/en/re2.md) (Google) All three modules provide nearly the same functionality with an identical interface, which lets you switch between them while minimizing changes to your query. Inside Hyperscan, there are several implementations that use different sets of processor instructions, with the relevant instruction automatically selected based on the current processor. In HyperScan, some functions support backtracking (referencing the previously found part of the string). Those functions are implemented through hybrid use of the two libraries: Hyperscan and libpcre. [Pire](https://github.com/yandex/pire) (Perl Incompatible Regular Expressions) is a very fast regular expression library developed by Yandex. At the lower level, it parses the input string once without backtracking and uses 5 instructions per character (both for x86 and x86_64). However, its development was almost frozen in 2011–2013, so, as the name suggests ("i" stands for incompatible), you might have to adapt the expressions themselves. Hyperscan and Pire are best-suited for Grep and Match. Re2 uses [google::RE2](https://github.com/google/re2), which offers a wide range of options ([see official documentation](https://github.com/google/re2/wiki/Syntax)). The key advantage of Re2 is a broad Capture and Replace functionality. Use this library if you need these capabilities. # Pire ## List of functions * `Pire::Grep(pattern:String) -> (string:String?) -> Bool` * `Pire::Match(pattern:String) -> (string:String?) -> Bool` * `Pire::MultiGrep(pattern:String) -> (string:String?) -> Tuple` * `Pire::MultiMatch(pattern:String) -> (string:String?) -> Tuple` * `Pire::Capture(pattern:String) -> (string:String?) -> String?` * `Pire::Replace(pattern:String) -> (string:String?, replacement:String) -> String?` One of the options to match regular expressions in YQL is to use [Pire](https://github.com/yandex/pire) (Perl Incompatible Regular Expressions). This is a very fast regular expression library developed by Yandex. At the lower level, it parses the input string once without backtracking and uses 5 instructions per character (both for x86 and x86_64). The speed is achieved by using the reasonable restrictions: * Pire is primarily focused at checking whether a string matches a regular expression. * The matching substring can also be returned (by Capture), but with restrictions (a match with only one group is returned). By default, all functions work in the single-byte mode. However, if the regular expression is a valid UTF-8 string but is not a valid ASCII string, the UTF-8 mode is enabled automatically. To enable Unicode mode, you can include one non-ASCII character in the regular expression using the `?` operator, e.g. `\\w+я?`. ## Call syntax {#call-syntax} To avoid compiling a regular expression at each table row, wrap the function call by a [named expression](https://ytsaurus.tech/syntax/expressions.md#named-nodes): ```yql $re = Pire::Grep("\\d+"); -- create a callable value to test the specific regular expression SELECT * FROM table WHERE $re(key); -- use the value to filter a table ``` {% note alert %} When escaping special characters in a regular expression, be sure to use the second slash, since all the standard string literals in SQL can accept C-escaped strings, and the `\d` sequence is not a valid sequence (even if it were, it wouldn't search for numbers as intended). {% endnote %} You can enable the case-insensitive mode by specifying, at the beginning of the regular expression, the flag `(?i)`. ### Examples ```yql $value = "xaaxaaxaa"; $match = Pire::Match("a.*"); $grep = Pire::Grep("axa"); $insensitive_grep = Pire::Grep("(?i)axa"); $multi_match = Pire::MultiMatch(@@a.* .*a.* .*a .*axa.*@@); $capture = Pire::Capture(".*x(a).*"); $capture_many = Pire::Capture(".*x(a+).*"); $replace = Pire::Replace(".*x(a).*"); SELECT $match($value) AS match, -- false $grep($value) AS grep, -- true $insensitive_grep($value) AS insensitive_grep, -- true $multi_match($value) AS multi_match, -- (false, true, true, true) $multi_match($value).0 AS some_multi_match, -- false $capture($value) AS capture, -- "a" $capture_many($value) AS capture_many, -- "aa" $replace($value, "b") AS replace; -- "xaaxaaxba" ``` ## Grep {#grep} Matches the regular expression with a **part of the string** (arbitrary substring). ## Match {#match} Matches the **whole string** against the regular expression. To get a result similar to `Grep` (where substring matching is included), enclose the regular expression in `.*`. For example, use `.*foo.*` instead of `foo`. ## MultiGrep / MultiMatch {#multigrep} Pire lets you match against multiple regular expressions in a single pass through the text and get a separate response for each match. Use the MultiGrep/MultiMatch functions to optimize the query execution speed. Be sure to do it carefully, since the size of the state machine used for matching grows exponentially with the number of regular expressions: * If you want to match a string against any of the listed expressions (the results are joined with "or"), it would be much more efficient to combine the query parts in a single regular expression with `|` and match it using regular Grep or Match. * Pire has a limit on the size of the state machine (YQL uses the default value set in the library). If you exceed the limit, the error is raised at the start of the query: `Failed to glue up regexes, probably the finite state machine appeared to be too large`. When you call MultiGrep/MultiMatch, regular expressions are passed one per line using [multiline string literals](https://ytsaurus.tech/syntax/lexer.md#multiline-string-literals): ### Examples ```yql $multi_match = Pire::MultiMatch(@@a.* .*x.* .*axa.*@@); SELECT $multi_match("a") AS a, -- (true, false, false) $multi_match("axa") AS axa; -- (true, true, true) ``` ## Capture {#capture} If a string matches the specified regular expression, it returns a substring that matches the group enclosed in parentheses in the regular expression. Capture is non-greedy: the shortest possible substring is returned. {% note alert %} The expression must contain only **one** group in parentheses. `NULL` (empty Optional) is returned in case of no match. {% endnote %} If the restrictions and characteristics described above are unacceptable, you should consider using [Re2::Capture](https://ytsaurus.tech/docs/en/re2.md#capture). ## Replace {#replace} Pire doesn't support replace based on a regular expression. `Pire::Replace` implemented in YQL is a simplified emulation using `Capture`. It may run correctly, if the substring occurs more than once in the source string. As a rule, it's better to use [Re2::Replace](https://ytsaurus.tech/docs/en/re2.md#replace) instead. # PostgreSQL module YQL supports PostgreSQL-compatible types, expressions, built-in functions, and syntax when processing data in YTsaurus. PostgreSQL values use the `p` suffix, for example `'text'p` and `1p`. ## Calling PostgreSQL functions from YQL {#callpgfunction} Use `PgCall(, )` to call a PostgreSQL function explicitly: ```yql SELECT PgCall('lower', 'Test'p); -- 'test'p ``` For collation-sensitive functions, pass a named `Collation` argument: ```yql SELECT PgCall('upper', 'straße'p, 'de-DE-x-icu' AS Collation); -- 'STRASSE'p ``` The value must be a collation name from `pg_catalog.pg_collation`: one of `default`, `C`, `POSIX`, `ucs_basic`, or `unicode`, or an ICU locale in the `-x-icu` form. Use `PgRangeCall` for a function returning a set of `pgrecord` values: ```yql SELECT * FROM AS_TABLE(PgRangeCall("json_each", pgjson('{"a":"foo", "b":"bar"}'))); ``` ## PostgreSQL syntax and warnings {#postgresql-syntax} PostgreSQL syntax supports YT cluster-qualified table paths. Replace the placeholders with your cluster and table path: ```sql SELECT * FROM ."//path/to/table"; ``` Starting with version [2026.01](https://ytsaurus.tech/changelog/2026.01.md), `SET` can configure warning handling similarly to [PRAGMA Warning](https://ytsaurus.tech/syntax/pragma/global.md#warning): ```sql SET Warning = "error", "4503"; ``` # Protobuf UDF Converting protobuf messages to YQL structs. Function list: `Protobuf::Parse(String?{Flags:AutoMap}) -> ?` — Unpackages a message into a YQL struct. `Protobuf::TryParse(String?{Flags:AutoMap}) -> ?` — Safely unpackages a message. Returns Null on error. `Protobuf::Serialize(?{Flags:AutoMap}) -> String` — Serializes a struct into a protobuf string. Before using, create metainformation similar to what is written to the `_yql_proto_field` meta attribute. ## Supported descriptor fields {#yql_proto_field_desc} * `name` — The name of the protobuf message. It is comprised of the package name and message name separated by the dot ("package.message"), where: * package stores the proto file. * message is used to collect data. * `meta` — The descriptor of the protobuf message (compressed zlib, base64). * `format` — The protobuf serialization format. Possible values: `protobin` (default) — binary, `prototext` — text, `json`. * `skip` — The number of bytes in each value to be ignored before parsing (0 by default). * `view` — The dictionary defining the view settings for protobuf messages. The following settings are supported: * `enum` — Protobuf enumeration view in YQL. Possible values: `number` (default), `name`, `full_name`. * `recursion` — A method to process recursive messages. Possible values: `fail` (default) — display an error, `ignore`— skip recursive insertions, `bytes` (deprecated) — convert recursive insertions to byte strings, `bytesV2` — same as bytes, but with the output type changed to optional byte strings for optional fields. * `yt_mode` — The `YT Mode` (`false` by default) used to process YT-specific protobuf options. If the message lacks `NYT.default_field_flags` or its value is not `SERIALIZATION_YT`, the message is converted to a byte string. If its value is `SERIALIZATION_YT`, the message is processed as usual (recursive message processing is defined by the `recursion` option). * `lists` — The dictionary that defines view settings for repeated fields in protobuf messages. The following settings are supported: * `optional` — Displays repeated fields as optional lists instead of regular ones (`true` by default). {% note warning "Attention" %} If `lists` `optional=false`, we don't recommend writing the `Protobuf::Parse` result into tables because the table schema becomes incompatible if new repeated fields are introduced. {% endnote %} ## Examples ```yql $config = @@{ "name": "my.service.proto.BufferWrapper", "meta": "H4sIAAAAAAAAA51YT3PbVBCv/9vr2FGUAkE9AG6BtAWnSdrp39AmbUo6JZBpy2SGYaajSM+xJraeR5KdpB+B6ZkjnDlx4RPADF+EM2cOXNi3+yzZabDV3KSn3d/b3bf7232C13m47QTHvche2g/sXnvJtSP7pdftySBaOhR7A++VDJZ6gYzkkhgIP3oZHfdE2KQV80LQbx7bviuOml0RBd6B3TwcrPDHK7/koLKpVF6ghlmDSlf2Q9GVA2GcM6tQotd+z8jE31x56BtZswIFp+M5B0bOBCiGTiA7HSNv1gEOPR9l9jr9wCiYs1Dl95Z0+qFRVHr8WDLLkCepslmCnJAto6I2CUVHOJEnfQMUstO2/X1hVJWi5/f6kTGjpCLZd9pkZk3tSa9hZAeRUTdnoEzvwneNWWUBvTm274iOYZjnwaCFlgwcoeHnTANmUGJgh9siakvXME0T6ryyE8ieCKJjY16Z/ErKrnFemRaI0HsljHfUFl3herZWfdecgxotxJrvKZsPxHEYBfJAGAsK3BUDzxHPZGSTt+8rP1p4sJ3NIJCBYan3th22tYkX9op0ZKvwownX3yod0mSCdfYMa/xUhgIlkXkd8urDQubDzGJ95ZPmhA2bSdrVoYhHty+ihSzqzWE8S63A7oon7kKOFtYAKPdIZSGPa9WVTyeCb8fiW+fM+1DlBGX9AukvTtR/nsgzAGcxAxRTAOwm8giwCfU4rxmjRBhXJxsxpsJ2cDYwRjmFHQ8TeQRY06XC+pUUgXwRi/P+Xcpx1ocU+28n8gjwEGo9XRMMUSWIKxMhdkY1EOQuVFQRMsAMAUzOtO+G0uwC1y2r11K48CyRR4AvYTYu5JBB6gTy2USQp+M6CPQNzI9TAIPNEti1iWCP3tRjyxL+YDAjhWWPx3UYKCEeBppLAbQ1rrN1bqMIeRSzGw2ApCCRyTNHxBAF9XhMRV9oLEN1pOhOF0Jqz/fsfUG0UG7UoDpSZo0dqI9XDJJugZqCBqpim/FdDTWHrUZ9+1q6jKeaVQm/00KebFqD6kj9KLiB3ekzvVWUODYZ50AwZFnxWNtzXeFr+37IACT1Yz6AEvelEAFyGM6llJXXfN7fwzqyrTtQ0o/Yf7Keqw2hWCkTshyrHD3WVKfFFke+ZBt/ZKASF4K5AWVVQ48D2SWQaRkXa9LTjvTIn6LCeCFp7zMgWNfYJIZDezvYWzpkTzZxqpA4VWg8h+pIOSqdQ8+N2vqA1QEIb78dJWes0mWXRPiMMXBqaYvF+JivQnWEphQKs5wOLyYddqcQMXOLlcY1qI0REk4O5SGnaYU4T5QVlcavGZg9Uf5Y/JCwiE6Hu29DIMk7vVpPoT6+MpIhlPm4Hduj/PPUmGNzmioHutL1Wp4IKCCVxhOYP4VipkV7Hqoy8FCQVPSBrcPsCYJR8XRUjXGw8IzEkSN6pJMdxg9L0zkgiErjA5g9QS0KQjEUQ8SD0d9luJl2iun22cw0s1HjdRnK21phZFrhMLC13Z6Owi3mPLJ9WmcbYjappEfGHnUMc9YtKD5CQovE2Fnicy/Qm+Gzf6QzW63bnNEWFntu3XVP6vldHWGWjfX8kA9eYxdofQ2ytpp1VGoup3MDt2yuR5t+FBwrKIfHnIo2s0ymXYbSUERn5WlFcyd7K2NdhOqGQA4T661IBIoGZMJ4OlmsP7G61iM0aa8fCWbrMbefkBtZcmMtpRvjcEOXrO//x/QHo6anDtaIa+TtLjYLcRSd4sIahfKs4NY/GSgxamjehIyt6WZyM4+RdAZeh8yejuLl1MmAkck4mGZK68aZYo/DXkaVQ25qf4kRkiBa30KeKqum7o6opuP5hWrcFA/t0efpkHUQY775uQSraflGNZ1UXPM7Dk07KKzuUkQkmRQTrlJgEsERwZF+pGa2NKdFig9Z4Y17l3UJchvyaArvI02Vv5IOk6MiZhlGujC4NUbSkR1NPDTA6WZcsRahyDPfG6RaH/5TIC8K1l9ZfZRooyud+JJJvBF5UUfzhvpuuy6O+CHvoQqpb2t6Q3sC0RJBIJjkyJ49OxR0pSNlbCsbaqFErfEGmSFwouO71sfTg6kCdhPKA08cqjzQl6yUiveg3NGh1LerKVcjpRgH/z7UPN+LPLvDUcXbVW7q7YYgWF5xaWmYDKMEhFHyMS90iLcB7GGVhrq6b6fOsqTCQ6ZSPOqeHQwv9jSFDzOYzshaHiH4MFXniCv03zzcS1uhgXBk4IrgJduc4pfJWYrfOuuEYp3pn0/jEdQ2+i3M+V1UwyHVXIXiHi3oNnBx4smxbqMPRX5Kph3OjBXIK0t0c7o0+ZeItmD0Ekfjk9Pu+wd0+jPDO1qBLlC/YT4OtZa1FtPhR1PzDe+xt3Gw1UHUBk4uwyHRo+oqFCiMeoxrTP+TxdddFYxh9v0H5WlC0LgVAAA=" }@@; $try_parse = Udf(Protobuf::TryParse, $config As TypeConfig); SELECT $try_parse(data) AS proto, $try_parse('BAD ROW') AS bad_row FROM `//home/yql/protobuf_example`; ``` # Re2 ## List of functions ```yql Re2::Grep(pattern:String, options:Struct<...>?) -> (string:String?) -> Bool Re2::Match(pattern:String, options:Struct<...>?) -> (string:String?) -> Bool Re2::Capture(pattern:String, options:Struct<...>?) -> (string:String?) -> Struct<_1:String?,foo:String?,...> Re2::FindAndConsume(pattern:String, options:Struct<...>?) -> (string:String?) -> List Re2::Replace(pattern:String, options:Struct<...>?) -> (string:String?, replacement:String) -> String? Re2::Count(pattern:String, options:Struct<...>?) -> (string:String?) -> Uint32 Re2::Options([CaseSensitive:Bool?,DotNl:Bool?,Literal:Bool?,LogErrors:Bool?,LongestMatch:Bool?,MaxMem:Uint64?,NeverCapture:Bool?,NeverNl:Bool?,OneLine:Bool?,PerlClasses:Bool?,PosixSyntax:Bool?,Utf8:Bool?,WordBoundary:Bool?]) -> Struct ``` The Re2 module supports regular expressions based on [google::RE2](https://github.com/google/re2) with a wide range of features provided ([see the official documentation](https://github.com/google/re2/wiki/Syntax)). By default, the UTF-8 mode is enabled automatically if the regular expression is a valid UTF-8-encoded string, but is not a valid ASCII string. You can manually control the settings of the re2 library, if you pass the result of the `Re2::Options` function as the second argument to other module functions, next to the regular expression. {% note info "Note" %} All regular expressions passed to functions must be valid. Otherwise, your query may fail. Starting from version [2025.03](https://ytsaurus.tech/changelog/2025.03.md#re2-module), such a query will definitely fail. {% endnote %} {% note warning %} Make sure to double all the backslashes in your regular expressions (if they are within a quoted string): standard string literals are treated as C-escaped strings in SQL. You can also format regular expressions as raw strings `@@regexp@@`: double slashes are not needed in this case. {% endnote %} ## Examples ```yql $value = "xaaxaaxaa"; $options = Re2::Options(false AS CaseSensitive); $match = Re2::Match("[ax]+\\d"); $grep = Re2::Grep("a.*"); $capture = Re2::Capture(".*(?Pxa?)(a{2,}).*"); $replace = Re2::Replace("x(a+)x"); $count = Re2::Count("a", $options); SELECT $match($value) AS match, -- false $grep($value) AS grep, -- true $capture($value) AS capture, -- (_0: 'xaaxaaxaa', _1: 'aa', foo: 'x') $capture($value)._1 AS capture_member, -- "aa" $replace($value, "b\\1z") AS replace, -- "baazaaxaa" $count($value) AS count; -- 6 ``` ## Re2::Grep / Re2::Match {#match} If you leave out the details of implementation and syntax of regular expressions, those functions are totally similar [to the same-name functions](https://ytsaurus.tech/docs/en/pire.md#match) from the Pire module. With other things equal and no specific preferences, we recommend that you use `Pire::Grep or Pire::Match`. You can call the `Re2::Grep` function by using a `REGEXP` expression (see the [basic expression syntax](https://ytsaurus.tech/syntax/expressions.md#regexp)). For example, the following two queries are equivalent (also in terms of computing efficiency): * `$grep = Re2::Grep("b+"); SELECT $grep("aaabccc");` * `SELECT "aaabccc" REGEXP "b+";` ## Re2::Capture {#capture} Unlike [Pire::Capture](https://ytsaurus.tech/docs/en/pire.md#capture), `Re2::Capture` supports multiple and named capturing groups. Result type: a structure with the fields of the type `String?`. * Each field corresponds to a capturing group with the applicable name. * For unnamed groups, the following names are generated: `_1`, `_2`, etc. * The result always includes the `_0` field containing the entire substring matching the regular expression. For more information about working with structures in YQL, see the [section on containers](https://ytsaurus.tech/types/containers.md). ## Re2::FindAndConsume {#findandconsume} Searches for all occurrences of the regular expression in the passed text and returns a list of values corresponding to the parenthesized part of the regular expression for each occurrence. ## Re2::Replace {#replace} Works as follows: * In the input string (first argument), all the non-overlapping substrings matching the regular expression are replaced by the specified string (second argument). * In the replacement string, you can use the contents of capturing groups from the regular expression using back-references in the format: `\\1`, `\\2` etc. The `\\0` back-reference stands for the whole substring that matches the regular expression. ## Re2::Count {#count} Returns the number of non-overlapping substrings of the input string that have matched the regular expression. ## Re2::Options {#options} Notes on Re2::Options from the official [repository](https://github.com/google/re2/blob/main/re2/re2.h#L595-L617) | Parameter | Default | Comments | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------- | | CaseSensitive:Bool? | true | match is case-sensitive (regexp can override with (?i) unless in posix_syntax mode) | | DotNl:Bool? | false | let `.` match `\n` (default ) | | Literal:Bool? | false | interpret string as literal, not regexp | | LogErrors:Bool? | true | this option is ignored | | LongestMatch:Bool? | false | search for longest match, not first match | | MaxMem:Uint64? | - | (see below) approx. max memory footprint of RE2 | | NeverCapture:Bool? | false | parse all parents as non-capturing | | NeverNl:Bool? | false | never match \n, even if it is in regexp | | PosixSyntax:Bool? | false | restrict regexps to POSIX egrep syntax | | Utf8:Bool? | true | text and pattern are UTF-8; otherwise Latin-1 | | The following options are only consulted when PosixSyntax == true. When PosixSyntax == false, these features are always enabled and cannot be turned off; to perform multi-line matching in that case, begin the regexp with (?m). | | PerlClasses:Bool? | false | allow Perl's \d \s \w \D \S \W | | WordBoundary:Bool? | false | allow Perl's \b \B (word boundary and not) | | OneLine:Bool? | false | ^ and $ only match beginning and end of text | It is not recommended to use Re2::Options in the code. Most parameters can be replaced with regular expression flags. ### Flag usage examples ```yql $value = "Foo bar FOO"u; -- enable case-insensitive mode $capture = Re2::Capture(@@(?i)(foo)@@); SELECT $capture($value) AS capture; -- ("_0": "Foo", "_1": "Foo") $capture = Re2::Capture(@@(?i)(?PFOO).*(?Pbar)@@); SELECT $capture($value) AS capture; -- ("_0": "Foo bar", "bar": "bar", "foo": "Foo") ``` In both cases, the word FOO will be found. Using the raw string @@regexp@@ lets you avoid double slashes. # SimplePg The SimplePg module provides access to some functions from the PostgreSQL codebase, at the same time the input and output arguments are adapted to [primitive types](https://ytsaurus.tech/types/primitive.md). By default you need to specify the module name as `SimplePg::foo` to call these functions. But if you add the PRAGMA `SimplePg`, you can use them in the global visibility scope as `foo` (it is guaranteed that they will override the built-in YQL function, if any exists). Many functions in this module have analogues among other YQL functions with better performance. ## now ### Signature ```yql SimplePg::now() -> Timestamp? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Takes no arguments. Returns the current time. Original [documentation](https://www.postgresql.org/docs/16/functions-datetime.html). Analogue — [CurrentUtcTimestamp](https://ytsaurus.tech/builtins/basic.md#current-utc). ### Examples ```yql PRAGMA SimplePg; SELECT now(); ``` ## to_date ### Signature ```yql SimplePg::to_date(Utf8?, Utf8?) -> Date32? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * a string with the date value; * format string. Parses the date according to the format string. Original [documentation](https://www.postgresql.org/docs/16/functions-formatting.html). Analogue — [DateTime::Parse64](https://ytsaurus.tech/docs/en/datetime.md#parse). ### Examples ```yql PRAGMA SimplePg; SELECT to_date('05 Dec 2000', 'DD Mon YYYY'); -- 2000-12-05 ``` ## to_char ### Signature ```yql SimplePg::to_char(AnyPrimitiveType?, Utf8?) -> Utf8? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * primitive type value; * format string. Formats the value according to the format string. Original [documentation](https://www.postgresql.org/docs/16/functions-formatting.html). Analogue — [DateTime::Format](https://ytsaurus.tech/docs/en/datetime.md#format). ### Examples ```yql PRAGMA SimplePg; SELECT to_char(125.8, '999D9'); -- " 125.8" SELECT to_char(Timestamp('2002-04-20T17:31:12.66Z'), 'HH12:MI:SS'); -- 05:31:12 ``` ## date_part ### Signature ```yql SimplePg::date_part(Utf8?, Timestamp64?) -> Double? SimplePg::date_part(Utf8?, Interval64?) -> Double? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * component, possible [values](https://www.postgresql.org/docs/16/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT); * timestamp. Extracts the specified component from the timestamp or interval. Original [documentation](https://www.postgresql.org/docs/16/functions-datetime.html). Analogue — [DateTime::Get](https://ytsaurus.tech/docs/en/datetime.md#get). ### Examples ```yql PRAGMA SimplePg; SELECT date_part('hour', Timestamp('2001-02-16T20:38:40Z')); -- 20 SELECT date_part('minute', Interval('PT01H02M03S')); -- 2 ``` ## date_trunc ### Signature ```yql SimplePg::date_trunc(Utf8?, Timestamp64?) -> Timestamp64? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * scale, possible [values](https://www.postgresql.org/docs/16/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC); * timestamp. Rounds the timestamp to the beginning of the specified scale. Original [documentation](https://www.postgresql.org/docs/16/functions-datetime.html). Analogue — [DateTime::StartOf](https://ytsaurus.tech/docs/en/datetime.md#startof), etc. ### Examples ```yql PRAGMA SimplePg; SELECT date_trunc('hour', Timestamp('2001-02-16T20:38:40Z')); -- 2001-02-16 20:00:00 SELECT date_trunc('year', Timestamp('2001-02-16T20:38:40Z')); -- 2001-01-01 00:00:00 ``` ## floor ### Signature ```yql SimplePg::floor(Double?) -> Double? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * floating-point value. Rounds the value down to the nearest integer. Original [documentation](https://www.postgresql.org/docs/16/functions-math.html). Analogue — [Math::Floor](https://ytsaurus.tech/docs/en/math.md). ### Examples ```yql PRAGMA SimplePg; SELECT floor(42.8); -- 42 SELECT floor(-42.8); -- -43 ``` ## ceil ### Signature ```yql SimplePg::ceil(Double?) -> Double? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * floating-point value. Rounds the value up to the nearest integer. Original [documentation](https://www.postgresql.org/docs/16/functions-math.html). Analogue — [Math::Ceil](https://ytsaurus.tech/docs/en/math.md). ### Examples ```yql PRAGMA SimplePg; SELECT ceil(42.2); -- 43 SELECT ceil(-42.8); -- -42 ``` ## round ### Signature ```yql SimplePg::round(Double?, [Int32?]) -> Double? ``` Available since version [2025.04](https://ytsaurus.tech/changelog/2025.04.md#simple-pg-module). Arguments: * floating-point value; * optional number of decimal places (default is 0). Rounds the value to the specified number of decimal places. Rounds away from zero. Original [documentation](https://www.postgresql.org/docs/16/functions-math.html). Analogue — [Math::Round](https://ytsaurus.tech/docs/en/math.md). ### Examples ```yql PRAGMA SimplePg; SELECT round(42.4382, 2); -- 42.44 SELECT round(1234.56, -1); -- 1230 ``` # Streaming UDF Functions designed to launch an arbitrary script or executable in the streaming mode: ```yql Streaming::Process(List>, String, List) -> List> Streaming::ProcessInline(List>, String, List) -> List> ``` Argument description: 1. Input data (`List>`): * A child process is launched once in the beginning of list processing (or a part of it, see the next section), and each element is fed to it via stdin in separate lines. * If the input list is a large table, many identical processes are launched to process its parts in parallel. Processing of all table data in one process and the order of line processing are not guaranteed. * `Process` and `ProcessInline` expect each line in the input data list to be wrapped in a structure. It simplifies using `SELECT` because this is the type of `SELECT ... AS Data FROM ...`. Also, it helps discourage the user from choosing a very inefficient scenario in which a separate process is launched for each line of the source MapReduce table. * The `Data` name for the list item structure is fixed at the time of writing. 2. The second argument (`String`): * `Streaming::Process` is the program name from `$PATH` (you must ensure that it's installed on the target cluster) or the executable path that can be attached to the query and then receive the path via the built-in `FilePath("file alias")` function. * `Streaming::ProcessInline` is the line with the script that will be launched using the interpreter specified in [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) (i.e. the first line of the script must read like `#!/usr/bin/env bash`, for example). Basic interpreters, such as sh, bash, awk, perl, or python, are usually already installed. If you need something unconventional, you can attach it to your query. The script can be defined inline using a multiline string literal (`@@`) or in a separate file (a tab in the web interface). In the second case, you can get a file's content in a line using the built-in `FileContent("file alias")` function. 3. The list of arguments to be passed to the script or executable when launching (`List`, optional argument): * An easy way to create a list of arguments using literals is to use the built-in `AsList` function (accepts 1 or more arguments and creates a list of them). * If you don't need to pass arguments, skip them. If you're interested in a specific interaction with a child process that's different from the line-by-line approach described above, tell us about your use scenario in the [community chat](https://t.me/ytsaurus). Notes on implementation: * `Streaming::ProcessInline` usually uses the interpreter installed on the target cluster, which might lead to unexpected results in case of version conflict. To avoid this, you can attach your interpreter to the query (which isn't always straightforward) or ask the cluster administrator to install the required interpreter or its version (it's better if you contact them directly, but you can also ask us for help). * stderr can be used for troubleshooting issues: some part of it is saved and returned to the client in case of a runtime error when running the query. The size of the saved part of stderr is configured on the cluster side and usually is several kilobytes. #### Examples ```yql $input = [ <| "Data":"a" |>, <| "Data":"b" |>, <| "Data":"c" |>, ]; PROCESS AS_TABLE($input) USING Streaming::Process(TableRows(), "cat", ["-n"]); /* [(Data: ' 1 a'), (Data: ' 2 b'), (Data: ' 3 c')] */ ``` ```yql $ips = (SELECT COALESCE(ip, "") AS Data FROM .`home/yql/tutorial/users`); $script = @@#!/usr/bin/awk -f BEGIN { FS="."; } $2 ~ /170/ { print $0" has 170 in second octet!"; } @@; PROCESS $ips USING Streaming::ProcessInline( TableRows(), $script ); /* "93.170.111.29 has 170 in second octet!" "93.170.111.28 has 170 in second octet!" */ ``` # String Functions for ASCII strings: ## List of functions * `String::Base64Encode(String{Flags:AutoMap}) -> String` * `String::Base64Decode(String) -> String?` * `String::Base64StrictDecode(String) -> String?` * `String::EscapeC(String{Flags:AutoMap}) -> String` * `String::UnescapeC(String{Flags:AutoMap}) -> String` * `String::HexEncode(String{Flags:AutoMap}) -> String` * `String::HexDecode(String) -> String?` * `String::EncodeHtml(String{Flags:AutoMap}) -> String` * `String::DecodeHtml(String{Flags:AutoMap}) -> String` * `String::CgiEscape(String{Flags:AutoMap}) -> String` * `String::CgiUnescape(String{Flags:AutoMap}) -> String` * `String::Strip(String{Flags:AutoMap}) -> String` * `String::Collapse(String{Flags:AutoMap}) -> String` * `String::CollapseText(String{Flags:AutoMap}, Uint64) -> String` * `String::AsciiEqualsIgnoreCase(String?, String) -> Bool` Added in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::Contains(String?, String) -> Bool` * `String::AsciiContainsIgnoreCase(String?, String) -> Bool` Added in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::Find(String{Flags:AutoMap}, String, [Uint64?]) -> Int64`: Returns the first position found or -1. The optional argument is the offset from the beginning of the string. * `String::ReverseFind(String{Flags:AutoMap}, String, [Uint64?]) -> Int64`: Returns the last position found or -1. The optional argument is the offset from the beginning of the string. * `String::AsciiStartsWithIgnoreCase(String?, String) -> Bool` Added in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::HasPrefix(String?, String) -> Bool` * `String::HasPrefixIgnoreCase(String?, String) -> Bool` Removed in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::StartsWith(String?, String) -> Bool` * `String::StartsWithIgnoreCase(String?, String) -> Bool` Removed in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::AsciiEndsWithIgnoreCase(String?, String) -> Bool` Added in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::HasSuffix(String?, String) -> Bool` * `String::HasSuffixIgnoreCase(String?, String) -> Bool` Removed in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::EndsWith(String?, String) -> Bool` * `String::EndsWithIgnoreCase(String?, String) -> Bool` Removed in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) * `String::Substring(String{Flags:AutoMap}, [Uint64?, Uint64?]) -> String` * `String::AsciiToLower(String{Flags:AutoMap}) -> String`: Changes only Latin characters. For working with other alphabets, see Unicode::ToLower * `String::AsciiToUpper(String{Flags:AutoMap}) -> String`: Changes only Latin characters. For working with other alphabets, see Unicode::ToUpper * `String::AsciiToTitle(String{Flags:AutoMap}) -> String`: Changes only Latin characters. For working with other alphabets, see Unicode::ToTitle * `String::SplitToList( String?, String, [ DelimeterString:Bool?, SkipEmpty:Bool?, Limit:Uint64? ]) -> List` The first argument is the source string The second argument is a delimiter The third argument includes the following parameters: - DelimeterString:Bool? — treating a delimiter as a string (true, by default) or a set of characters "any of" (false) - SkipEmpty:Bool? — whether to skip empty strings in the result, is false by default - Limit:Uint64? — Limits the number of fetched components (unlimited by default); if the limit is exceeded, the raw suffix of the source string is returned in the last item * `String::JoinFromList(List{Flags:AutoMap}, String) -> String` * `String::ToByteList(List{Flags:AutoMap}) -> List` * `String::FromByteList(List) -> String` * `String::ReplaceAll(String{Flags:AutoMap}, String, String) -> String`: Arguments: input, find, replacement * `String::ReplaceFirst(String{Flags:AutoMap}, String, String) -> String`: Arguments: input, find, replacement * `String::ReplaceLast(String{Flags:AutoMap}, String, String) -> String`: Arguments: input, find, replacement * `String::RemoveAll(String{Flags:AutoMap}, String) -> String`: The second argument is interpreted as an unordered set of characters to delete * `String::RemoveFirst(String{Flags:AutoMap}, String) -> String`: An unordered set of characters in the second argument, only the first encountered character from set is deleted * `String::RemoveLast(String{Flags:AutoMap}, String) -> String`: An unordered set of characters in the second argument, only the last encountered character from the set is deleted * `String::ReverseBytes(String{Flags:AutoMap}) -> String` - Added in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) Reverses a string, treating it as a byte sequence. * `String::ReverseBits(String{Flags:AutoMap}) -> String` - Added in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) Reverses a string, treating it as a bit sequence. * `String::IsAscii(String{Flags:AutoMap}) -> Bool` * `String::IsAsciiSpace(String{Flags:AutoMap}) -> Bool` * `String::IsAsciiUpper(String{Flags:AutoMap}) -> Bool` * `String::IsAsciiLower(String{Flags:AutoMap}) -> Bool` * `String::IsAsciiAlpha(String{Flags:AutoMap}) -> Bool` * `String::IsAsciiAlnum(String{Flags:AutoMap}) -> Bool` * `String::IsAsciiHex(String{Flags:AutoMap}) -> Bool` * `String::LevensteinDistance(String{Flags:AutoMap}, String{Flags:AutoMap}) -> Uint64` * `String::LeftPad(String{Flags:AutoMap}, Uint64, [String?]) -> String` * `String::RightPad(String{Flags:AutoMap}, Uint64) -> String` * `String::Hex(Uint64{Flags:AutoMap}) -> String` * `String::SHex(Int64{Flags:AutoMap}) -> String` * `String::Bin(Uint64{Flags:AutoMap}) -> String` * `String::SBin(Int64{Flags:AutoMap}) -> String` * `String::HexText(String{Flags:AutoMap}) -> String` * `String::BinText(String{Flags:AutoMap}) -> String` * `String::HumanReadableDuration(Uint64{Flags:AutoMap}) -> String` * `String::HumanReadableQuantity(Uint64{Flags:AutoMap}) -> String` * `String::HumanReadableBytes(Uint64{Flags:AutoMap}) -> String` * `String::Prec(Double{Flags:AutoMap}, Uint64) -> String` * `String::Reverse(String?) -> String?` Removed in the version [2025.02](https://ytsaurus.tech/changelog/2025.02.md#string-module) {% note alert %} The functions from the String library don't support Cyrillic and can only work with ASCII characters. To work with UTF-8 encoded strings, use functions from [Unicode](https://ytsaurus.tech/docs/en/unicode.md). {% endnote %} ## Examples ```yql SELECT String::Base64Encode("YQL"); -- "WVFM" SELECT String::Strip("YQL "); -- "YQL" SELECT String::SplitToList("1,2,3,4,5,6,7", ",", 3 as Limit); -- ["1", "2", "3", "4,5,6,7"] ``` # Unicode Functions for Unicode strings. ## List of functions * `Unicode::IsUtf(String) -> Bool` Checks whether a string is a valid utf-8 sequence. For example, the string `"\xF0"` isn't a valid utf-8 sequence, while the string `"\xF0\x9F\x90\xB1"` correctly describes a utf-8 cat emoji. * `Unicode::GetLength(Utf8{Flags:AutoMap}) -> Uint64` Returns the utf-8 string's length in characters (unicode code points). Surrogate pairs are counted as one character. ```yql SELECT Unicode::GetLength("жніўня"); -- 6 ``` * `Unicode::Find(string:Utf8{Flags:AutoMap}, subString:Utf8, [pos:Uint64?]) -> Uint64?` * `Unicode::RFind(string:Utf8{Flags:AutoMap}, subString:Utf8, [pos:Uint64?]) -> Uint64?` Searches for the first (or the last in the case of `RFind`) substring occurrence in the string starting from the `pos` position. Returns the first character position from the found string. Null is returned in case of failure. ```yql SELECT Unicode::Find("aaa", "bb"); -- Null ``` * `Unicode::Substring(string:Utf8{Flags:AutoMap}, from:Uint64?, len:Uint64?) -> Utf8` Returns a substring from the `string`, starting from the `from` character, with length equaling `len` characters. If the `len` argument is omitted, the substring is taken to the end of the source string. If `from` is greater than the source string length, an empty `""` string is returned. ```yql SELECT Unicode::Substring("0123456789abcdefghij", 10); -- "abcdefghij" ``` * `Unicode::Normalize...` functions reduce the transmitted utf-8 string to a [normal form](https://unicode.org/reports/tr15/#Norm_Forms): * `Unicode::Normalize(Utf8{Flags:AutoMap}) -> Utf8` -- NFC * `Unicode::NormalizeNFD(Utf8{Flags:AutoMap}) -> Utf8` * `Unicode::NormalizeNFC(Utf8{Flags:AutoMap}) -> Utf8` * `Unicode::NormalizeNFKD(Utf8{Flags:AutoMap}) -> Utf8` * `Unicode::NormalizeNFKC(Utf8{Flags:AutoMap}) -> Utf8` * `Unicode::Translit(string:Utf8{Flags:AutoMap}, [lang:String?]) -> Utf8` Transliterates with Latin letters the words from the passed string, consisting entirely of characters of the alphabet of the language passed by the second argument. If no language is specified, the words are transliterated from Russian. Available languages: "kaz", "rus", "tur", and "ukr". ```yql SELECT Unicode::Translit("Тот уголок земли, где я провел"); -- "Tot ugolok zemli, gde ya provel" ``` * `Unicode::LevensteinDistance(stringA:Utf8{Flags:AutoMap}, stringB:Utf8{Flags:AutoMap}) -> Uint64` Calculates the Levenshtein distance for the passed strings. * `Unicode::Fold(Utf8{Flags:AutoMap}, [ Language:String?, DoLowerCase:Bool?, DoRenyxa:Bool?, DoSimpleCyr:Bool?, FillOffset:Bool? ]) -> Utf8` Performs [case folding](https://www.w3.org/TR/charmod-norm/#definitionCaseFolding) for the transmitted string. Parameters: - `Language` is set according to the same rules as in `Unicode::Translit()`. - `DoLowerCase` converts a string to lowercase letters, `true` by default. - `DoRenyxa` converts diacritical characters to similar Latin characters, `true` by default. - `DoSimpleCyr` converts diacritical Cyrillic characters to similar Latin characters, `true` by default. - `FillOffset` parameter is not used. ```yql SELECT Unicode::Fold("Kongreßstraße", false AS DoSimpleCyr, false AS DoRenyxa); -- "kongressstrasse" SELECT Unicode::Fold("ҫурт"); -- "сурт" SELECT Unicode::Fold("Eylül", "Turkish" AS Language); -- "eylul" ``` * `Unicode::ReplaceAll(input:Utf8{Flags:AutoMap}, find:Utf8, replacement:Utf8) -> Utf8` * `Unicode::ReplaceFirst(input:Utf8{Flags:AutoMap}, find:Utf8, replacement:Utf8) -> Utf8` * `Unicode::ReplaceLast(input:Utf8{Flags:AutoMap}, find:Utf8, replacement:Utf8) -> Utf8` Replaces all/first/last occurrence(s) of the `find` string in `input` through `replacement`. * `Unicode::RemoveAll(input:Utf8{Flags:AutoMap}, symbols:Utf8) -> Utf8` * `Unicode::RemoveFirst(input:Utf8{Flags:AutoMap}, symbols:Utf8) -> Utf8` * `Unicode::RemoveLast(input:Utf8{Flags:AutoMap}, symbols:Utf8) -> Utf8` Removes all/first/last occurrence(s) of characters defined in a `symbols` set from `input`. The second argument is interpreted as an unordered set of characters to be deleted. ```yql SELECT Unicode::ReplaceLast("absence", "enc", ""); -- "abse" SELECT Unicode::RemoveAll("abandon", "an"); -- "bdo" ``` * `Unicode::ToCodePointList(Utf8{Flags:AutoMap}) -> List` Splits the string into a unicode sequence of codepoints. * `Unicode::FromCodePointList(List{Flags:AutoMap}) -> Utf8` Forms a unicode string from codepoints. ```yql SELECT Unicode::ToCodePointList("Щавель"); -- [1065, 1072, 1074, 1077, 1083, 1100] SELECT Unicode::FromCodePointList(AsList(99,111,100,101,32,112,111,105,110,116,115,32,99,111,110,118,101,114,116,101,114)); -- "code points converter" ``` * `Unicode::Reverse(Utf8{Flags:AutoMap}) -> Utf8` Reverses the string. * `Unicode::ToLower(Utf8{Flags:AutoMap}) -> Utf8` * `Unicode::ToUpper(Utf8{Flags:AutoMap}) -> Utf8` * `Unicode::ToTitle(Utf8{Flags:AutoMap}) -> Utf8` Changes the character case in the string to upper, lower, or title case. * `Unicode::SplitToList( string:Utf8?, separator:Utf8, [ DelimeterString:Bool?, SkipEmpty:Bool?, Limit:Uint64? ]) -> List` Splits the string into substrings with a separator. `string` — Source string. `separator` — Separator. Parameters: - DelimeterString:Bool? — treating a separator as a string (`true` by default) or a set of characters "any of" (`false`). - SkipEmpty:Bool? — whether to skip empty strings in the result, `false` by default. - Limit:Uint64? — limits the number of fetched components (unlimited by default); if the limit is exceeded, the raw suffix of the source string is returned in the last item. * `Unicode::JoinFromList(List{Flags:AutoMap}, separator:Utf8) -> Utf8` Concatenates a list of strings into a single string using a `separator`. ```yql SELECT Unicode::SplitToList("One, two, three, four, five", ", ", 2 AS Limit); -- ["One", "two", "three, four, five"] SELECT Unicode::JoinFromList(["One", "two", "three", "four", "five"], ";"); -- "One;two;three;four;five" ``` * `Unicode::ToUint64(string:Utf8{Flags:AutoMap}, [prefix:Uint16?]) -> Uint64` Converts from the string into a number. The second optional argument sets the numeral system. By default, 0 (automatic detection by prefix). Supported prefixes: `0x(0X)` (base-16), `0` (base-8). Default system: base-10. The `-` sign before a number is interpreted as in the C's unsigned arithmetic. For example, `-0x1` -> UI64_MAX. If a string contains invalid characters, or a number is beyond ui64, the function finishes with an error. * `Unicode::TryToUint64(string:Utf8{Flags:AutoMap}, [prefix:Uint16?]) -> Uint64?` Similar to the `Unicode::ToUint64()` function, but returns `NULL` instead of an error. ```yql SELECT Unicode::ToUint64("77741"); -- 77741 SELECT Unicode::ToUint64("-77741"); -- 18446744073709473875 SELECT Unicode::TryToUint64("asdh831"); -- Null ``` * `Unicode::Strip(string:Utf8{Flags:AutoMap}) -> Utf8` Cuts off the string's outermost characters of Space Unicode category. ```yql SELECT Unicode::Strip("\u2002ыкль\u2002"u); -- "ыкль" ``` * `Unicode::IsAscii(string:Utf8{Flags:AutoMap}) -> Bool` Checks whether the utf-8 string consists exclusively of ASCII characters. * `Unicode::IsSpace(string:Utf8{Flags:AutoMap}) -> Bool` * `Unicode::IsUpper(string:Utf8{Flags:AutoMap}) -> Bool` * `Unicode::IsLower(string:Utf8{Flags:AutoMap}) -> Bool` * `Unicode::IsAlpha(string:Utf8{Flags:AutoMap}) -> Bool` * `Unicode::IsAlnum(string:Utf8{Flags:AutoMap}) -> Bool` * `Unicode::IsHex(string:Utf8{Flags:AutoMap}) -> Bool` Checks whether the utf-8 string satisfies the specified condition. * `Unicode::IsUnicodeSet(string:Utf8{Flags:AutoMap}, unicode_set:Utf8) -> Bool` Checks whether the utf-8 `string` consists exclusively of characters indicated in `unicode_set`. Characters in `unicode_set` must be indicated in square brackets. ```yql SELECT Unicode::IsUnicodeSet("ваоао"u, "[вао]"u); -- true SELECT Unicode::IsUnicodeSet("ваоао"u, "[ваб]"u); -- false ``` # Url ## Normalize {#normalize} * `Url::Normalize(String) -> String?` Normalizes the URL in a robot-friendly way: converts the hostname into lowercase, strips out certain fragments, and so on. The normalization result only depends on the URL itself. The normalization **DOES NOT** include operations depending on the external data: transformation based on duplicates, mirrors, etc. Returned value: * Normalized URL. * `NULL`, if the passed string argument can't be parsed as a URL. #### Examples ```yql SELECT Url::Normalize("hTTp://wWw.yDb.TECH/"); -- "http://www.ydb.tech/" SELECT Url::Normalize("http://ydb.tech#foo"); -- "http://ydb.tech/" ``` ## NormalizeWithDefaultHttpScheme {#normalizewithdefaulthttpscheme} * `Url::NormalizeWithDefaultHttpScheme(String?) -> String?` Normalizes similarly to `Url::Normalize`, but inserts the `http://` schema in case there is no schema. Returned value: * Normalized URL. * Source URL, if the normalization has failed. #### Examples ```yql SELECT Url::NormalizeWithDefaultHttpScheme("wWw.yDb.TECH"); -- "http://www.ydb.tech/" SELECT Url::NormalizeWithDefaultHttpScheme("http://ydb.tech#foo"); -- "http://ydb.tech/" ``` ## Encode / Decode {#encode} Encode a UTF-8 string to the urlencoded format (`Url::Encode`) and back (`Url::Decode`). #### List of functions * `Url::Encode(String?) -> String?` * `Url::Decode(String?) -> String?` #### Examples ```yql SELECT Url::Decode("http://ydb.tech/%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0"); -- "http://ydb.tech/page" SELECT Url::Encode("http://ydb.tech/page"); -- "http://ydb.tech/%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0" ``` ## Parse {#parse} Parses the URL into parts. ```yql Url::Parse(Parse{Flags:AutoMap}) -> Struct< Frag: String?, Host: String?, ParseError: String?, Pass: String?, Path: String?, Port: String?, Query: String?, Scheme: String?, User: String? > ``` #### Examples ```yql SELECT Url::Parse( "https://en.wikipedia.org/wiki/Isambard_Kingdom_Brunel?s=24&g=h-24#Great_Western_Railway"); /* ( "Frag": "Great_Western_Railway", "Host": "en.wikipedia.org", "ParseError": null, "Pass": null, "Path": "/wiki/Isambard_Kingdom_Brunel", "Port": null, "Query": "s=24&g=h-24", "Scheme": "https", "User": null ) */ ``` ## Get... {#get} Get a component of the URL. #### List of functions * `Url::GetScheme(String{Flags:AutoMap}) -> String` * `Url::GetHost(String?) -> String?` * `Url::GetHostPort(String?) -> String?` * `Url::GetSchemeHost(String?) -> String?` * `Url::GetSchemeHostPort(String?) -> String?` * `Url::GetPort(String?) -> String?` * `Url::GetTail(String?) -> String?` -- everything following the host: path + query + fragment * `Url::GetPath(String?) -> String?` * `Url::GetFragment(String?) -> String?` * `Url::GetCGIParam(String?, String) -> String?` -- The second parameter is the name of the intended CGI parameter. * `Url::GetDomain(String?, Uint8) -> String?` -- The second parameter is the required domain level. * `Url::GetTLD(String{Flags:AutoMap}) -> String` * `Url::IsKnownTLD(String{Flags:AutoMap}) -> Bool` -- Registered on [iana.org](http://www.iana.org/). * `Url::IsWellKnownTLD(String{Flags:AutoMap}) -> Bool` -- Belongs to a small whitelist of com, net, org, ru, and so on. * `Url::GetDomainLevel(String{Flags:AutoMap}) -> Uint64` * `Url::GetSignificantDomain(String{Flags:AutoMap}, [List?]) -> String` Returns a second-level domain in most cases and a third-level domain for the hostnames like: `***.XXX.YY`, where `XXX` is com, net, org, co, gov, or edu. You can redefine this list using an optional second argument * `Url::GetOwner(String{Flags:AutoMap}) -> String` Returns the domain that's most likely owned by an individual or organization. Unlike `Url::GetSignificantDomain`, it uses a special whitelist. Besides the `***.co.uk` domains, it can return a third-level domain used by free hosting sites and blogs (for example: something.livejournal.com) #### Examples ```yql SELECT Url::GetScheme("https://ydb.tech"); -- "https://" SELECT Url::GetDomain("http://www.ydb.tech", 2); -- "ydb.tech" ``` ## Cut... {#cut} * `Url::CutScheme(String?) -> String?` Returns the passed URL without the schema (`http://`, `https://`, etc.). * `Url::CutWWW(String?) -> String?` Returns the passed domain without the "www." prefix (if any). * `Url::CutWWW2(String?) -> String?` Returns the passed domain without the prefixes like "www.", "www2.", "wwww777." (if any). * `Url::CutQueryStringA­ndFragment(String{Flags:AutoMap}) -> String` Returns a copy of the passed URL, stripping out all the CGI parameters and fragments ("?foo=bar" and/or "#baz"). #### Examples ```yql SELECT Url::CutScheme("http://www.ydb.tech"); -- "www.ydb.tech" SELECT Url::CutWWW("www.ydb.tech"); -- "ydb.tech" ``` ## ...Punycode... {#punycode} [Punycode](https://en.wikipedia.org/wiki/Punycode) transformations. #### List of functions * `Url::HostNameToPunycode(String{Flag:AutoMap}) -> String?` * `Url::ForceHostNameToPunycode(String{Flag:AutoMap}) -> String` * `Url::PunycodeToHostName(String{Flag:AutoMap}) -> String?` * `Url::ForcePunycodeToHostName(String{Flag:AutoMap}) -> String` * `Url::CanBePunycodeHostName(String{Flag:AutoMap}) -> Bool` #### Examples ```yql SELECT Url::PunycodeToHostName("xn--80aniges7g.xn--j1aef"); -- "example.com" ``` ## ...Query... {#query} [Query](https://docs.python.org/3/library/urllib.parse.html) transformations. #### List of functions ```yql Url::QueryStringToList(String{Flag:AutoMap}, [ KeepBlankValues:Bool?, -- Empty values in percent-encoded queries are interpreted as empty strings, defaults to false. Strict:Bool?, -- If false, parsing errors are ignored and incorrect fields are skipped, defaults to true. MaxFields:Uint32?, -- The maximum number of fields. If exceeded, an exception is thrown. Defaults to Max. Separator:String? -- A key-value pair separator, defaults to '&'. ]) -> List> Url::QueryStringToDict(String{Flag:AutoMap}, [ KeepBlankValues:Bool?, -- Empty values in percent-encoded queries are interpreted as empty strings, defaults to false. Strict:Bool?, -- If false, parsing errors are ignored and incorrect fields are skipped, defaults to true. MaxFields:Uint32?, -- The maximum number of fields. If exceeded, an exception is thrown. Defaults to Max. Separator:String? -- A key-value pair separator, defaults to '&'. ]) -> Dict> Url::BuildQueryString(Dict>{Flag:AutoMap}, [ Separator:String? -- A key-value pair separator, defaults to '&'. ]) -> String Url::BuildQueryString(Dict{Flag:AutoMap}, [ Separator:String? -- A key-value pair separator, defaults to '&'. ]) -> String Url::BuildQueryString(List>{Flag:AutoMap}, [ Separator:String? -- A key-value pair separator, defaults to '&'. ]) -> String ``` #### Examples ```yql SELECT Url::QueryStringToList("a=1&b=2&a=3"); -- [("a", "1"), ("b", "2"), ("a", "3")] SELECT Url::QueryStringToDict("a=1&b=2&a=3"); -- {"b" : ["2"], "a" : ["1", "3"]} SELECT Url::BuildQueryString([("a", "1"), ("a", "3"), ("b", "2")]); -- "a=1&a=3&b=2" SELECT Url::BuildQueryString({"a" : "1", "b" : "2"}); -- "b=2&a=1" SELECT Url::BuildQueryString({"a" : ["1", "3"], "b" : ["2", "4"]}); -- "b=2&b=4&a=1&a=3" ``` # Yson [YSON](https://ytsaurus.tech/docs/en/user-guide/storage/yson) is a data format similar to JSON developed by Yandex. * Similarities with JSON: * Does not have a strict scheme. * Besides simple data types, it supports dictionaries and lists in arbitrary combinations. * Some differences from JSON: * It also has a binary representation in addition to the text representation. * The text representation uses semicolons instead of commas and equal signs instead of colons. * The concept of "attributes" is supported, that is, named properties that can be assigned to a node in the tree. Implementation specifics and functionality of the module: * Along with YSON, this module also supports standard JSON to expand the application scope in a way. * It works with a DOM representation of YSON in memory that in YQL terms is passed between functions as a "resource" (see the [description of special data types](https://ytsaurus.tech/types/special.md)). Most of the module's functions have the semantics of a query to perform a specified operation with a resource and return an empty [optional](https://ytsaurus.tech/types/optional.md) type if the operation failed because the actual data type mismatched the expected one. * Provides several main classes of functions (find below a complete list and detailed description of functions): * `Yson::Parse***`: Getting a resource with a DOM object from serialized data, with all further operations performed on the obtained resource. * `Yson::From`: Getting a resource with a DOM object from simple YQL data types or containers (lists or dictionaries). * `Yson::ConvertTo***`: Converting a resource to [primitive data types](https://ytsaurus.tech/types/primitive.md) or [containers](https://ytsaurus.tech/types/containers.md). * `Yson::Lookup***`: Getting a single list item or a dictionary with optional conversion to the relevant data type. * `Yson::YPath***`: Getting one element from the document tree based on the relative path specified, optionally converting it to the relevant data type. * `Yson::Serialize***`: Getting a copy of data from the resource and serializing the data in one of the formats. * For convenience, when serialized Yson and Json are passed to functions expecting a resource with a DOM object, implicit conversion using `Yson::Parse` or `Yson::ParseJson` is done automatically. In SQL syntax, the dot or square brackets operator automatically adds a `Yson::Lookup` call. To serialize a resource, you still need to call `Yson::ConvertTo***` or `Yson::Serialize***`. It means that, for example, to get the "foo" element as a string from the Yson column named mycolumn and serialized as a dictionary, you can write: `SELECT Yson::ConvertToString(mycolumn["foo"]) FROM mytable;` or `SELECT Yson::ConvertToString(mycolumn.foo) FROM mytable;`. In the variant with a dot, special characters can be escaped by [general rules for IDs](https://ytsaurus.tech/syntax/expressions.md#escape). The module's functions must be considered as "building blocks" from which you can assemble different structures, for example: * `Yson::Parse*** -> Yson::Serialize***`: Converting from one format to other. * `Yson::Parse*** -> Yson::Lookup -> Yson::Serialize***`: Extracting the value of the specified subtree in the source YSON tree. * `Yson::Parse*** -> Yson::ConvertToList -> ListMap -> Yson::Lookup***`: Extracting items by a key from the YSON list. ## Examples ```yql $node = Json(@@ {"abc": {"def": 123, "ghi": "hello"}} @@); SELECT Yson::SerializeText($node.abc) AS `yson`; -- {"def"=123;"ghi"="\xD0\xBF\xD1\x80\xD0\xB8\xD0\xB2\xD0\xB5\xD1\x82"} ``` ```yql $node = Yson(@@ [ {abc=123; def=456}; {abc=234; xyz=789}; ] @@); $attrs = Yson::YPath($node, "/@"); SELECT ListMap(Yson::ConvertToList($node), ($x) -> { return Yson::LookupInt64($x, "abc") }) AS abcs, Yson::ConvertToStringDict($attrs) AS attrs, Yson::SerializePretty(Yson::Lookup($node, "7", Yson::Options(false AS Strict))) AS miss; /* - abcs: `[123; 234]` - attrs: `{"a"="z";"x"="y"}` - miss: `NULL` */ ``` ## Yson::Parse... {#ysonparse} ```yql Yson::Parse(Yson{Flags:AutoMap}) -> Resource<'Yson2.Node'> Yson::ParseJson(Json{Flags:AutoMap}) -> Resource<'Yson2.Node'> Yson::ParseJsonDecodeUtf8(Json{Flags:AutoMap}) -> Resource<'Yson2.Node'> Yson::Parse(String{Flags:AutoMap}) -> Resource<'Yson2.Node'>? -- accepts YSON in any format Yson::ParseJson(String{Flags:AutoMap}) -> Resource<'Yson2.Node'>? Yson::ParseJsonDecodeUtf8(String{Flags:AutoMap}) -> Resource<'Yson2.Node'>? ``` The result of all three functions is non-serializable: it can only be passed as the input to other function from the Yson library. However, you can't save it to a table or return to the client as a result of the operation: such an attempt results in a typing error. You also can't return it outside [subqueries](https://ytsaurus.tech/syntax/select/index.md): if you need to do this, call [Yson::Serialize](https://ytsaurus.tech/docs/en/#ysonserialize), and the optimizer will remove unnecessary serialization and deserialization if materialization isn't needed in the end. {% note info %} The `Yson::ParseJsonDecodeUtf8` expects that characters outside the ASCII range must be additionally escaped. {% endnote %} ## Yson::From {#ysonfrom} ```yql Yson::From(T) -> Resource<'Yson2.Node'> ``` `Yson::From` is a polymorphic function that converts most primitive data types and containers (lists, dictionaries, tuples, structures, and so on) into a Yson resource. The source object type must be Yson-compatible. For example, in dictionary keys, you can only use the `String` or `Utf8` data types, but not `String?` or `Utf8?` . #### Example ```yql SELECT Yson::Serialize(Yson::From(TableRow())) FROM table1; ``` ## Yson::WithAttributes ```yql Yson::WithAttributes(Resource<'Yson2.Node'>{Flags:AutoMap}, Resource<'Yson2.Node'>{Flags:AutoMap}) -> Resource<'Yson2.Node'>? ``` Adds attributes (the second argument) to the Yson node (the first argument). The attributes must constitute a map node. ## Yson::Equals ```yql Yson::Equals(Resource<'Yson2.Node'>{Flags:AutoMap}, Resource<'Yson2.Node'>{Flags:AutoMap}) -> Bool ``` Checking trees in memory for equality. The operation is tolerant to the source serialization format and the order of keys in dictionaries. ## Yson::GetHash ```yql Yson::GetHash(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Uint64 ``` Calculating a 64-bit hash from an object tree. ## Yson::Is... {#ysonis} ```yql Yson::IsEntity(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsString(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsDouble(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsUint64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsInt64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsBool(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsList(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsDict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool Yson::IsUtf8(Resource<'Yson2.Node'>{Flags:AutoMap}) -> bool ``` Checking that the current node has the appropriate type. The Entity is `#`. The `Yson::IsUtf8` function is available starting from version [2026.01](https://ytsaurus.tech/changelog/2026.01.md#yson-module). It returns success only for nodes obtained from `Yson::ParseJson`/`Yson::ParseJsonDecodeUtf8`, or if the string node was built via `Yson::From` from the `Utf8` type. ## Yson::GetLength ```yql Yson::GetLength(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Uint64? ``` Getting the number of elements in a list or dictionary. ## Yson::ConvertTo... {#ysonconvertto} ```yql Yson::ConvertTo(Resource<'Yson2.Node'>{Flags:AutoMap}, Type) -> T Yson::ConvertToBool(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Bool? Yson::ConvertToInt64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Int64? Yson::ConvertToUint64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Uint64? Yson::ConvertToDouble(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Double? Yson::ConvertToString(Resource<'Yson2.Node'>{Flags:AutoMap}) -> String? Yson::ConvertToList(Resource<'Yson2.Node'>?) -> List> Yson::ConvertToBoolList(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List Yson::ConvertToInt64List(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List Yson::ConvertToUint64List(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List Yson::ConvertToDoubleList(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List Yson::ConvertToStringList(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List Yson::ConvertToDict(Resource<'Yson2.Node'>?) -> Dict> Yson::ConvertToBoolDict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict Yson::ConvertToInt64Dict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict Yson::ConvertToUint64Dict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict Yson::ConvertToDoubleDict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict Yson::ConvertToStringDict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict ``` {% note warning %} These functions do not do implicit type casting by default, that is, the value in the argument must exactly match the function called. {% endnote %} `Yson::ConvertTo` is a polymorphic function that converts the data type that is specified in the second argument and supports containers (lists, dictionaries, tuples, structures, and so on) into a Yson resource. Starting from version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module), it's recommended to use [As*/TryAs*](https://ytsaurus.tech/docs/en/#ysonas) functions for casting a Yson node to a specific primitive type, list, or dictionary, and `Yson::ConvertTo` in all other cases. #### Example ```yql $data = Yson(@@{ "name" = "Anya"; "age" = 15u; "params" = { "ip" = "95.106.17.32"; "last_time_on_site" = 0.5; "region" = 213; "user_agent" = "Mozilla/5.0" } }@@); SELECT Yson::ConvertTo($data, Struct< name: String, age: Uint32, params: Dict > ); ``` ## Yson::Contains {#ysoncontains} ```yql Yson::Contains(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Bool? ``` Checks for a key in the dictionary. If the object type is a map, then it searches among the keys. If the object type is a list, then the key must be a decimal number, i.e., an index in the list. ## Yson::Lookup... {#ysonlookup} ```yql Yson::Lookup(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Resource<'Yson2.Node'>? Yson::LookupBool(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Bool? Yson::LookupInt64(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Int64? Yson::LookupUint64(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Uint64? Yson::LookupDouble(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Double? Yson::LookupString(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> String? Yson::LookupDict(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Dict>? Yson::LookupList(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> List>? ``` The above functions are short notations for a typical use case: `Yson::YPath`: go to a level in the dictionary and then extract the value — `Yson::ConvertTo***`. For all the listed functions, the second argument is a key name from the dictionary (unlike YPath, it has no `/`prefix) or an index from the list (for example, `7`). They simplify the query and produce a small gain in speed. {% note warning %} Starting from version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module), it's recommended to use `Yson::Lookup` to retrieve a node by key and [As*/TryAs*](https://ytsaurus.tech/docs/en/#ysonas) functions for further type narrowing. In most cases, instead of multiple `Yson::Lookup` calls, consider converting data using `Yson::ConvertTo` into a set of dictionaries, lists, and primitive types. {% endnote %} ## Yson::YPath {#ysonypath} ```yql Yson::YPath(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Resource<'Yson2.Node'>? Yson::YPathBool(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Bool? Yson::YPathInt64(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Int64? Yson::YPathUint64(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Uint64? Yson::YPathDouble(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Double? Yson::YPathString(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> String? Yson::YPathDict(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> Dict>? Yson::YPathList(Resource<'Yson2.Node'>{Flags:AutoMap}, String) -> List>? ``` Lets you get a part of the resource based on the source resource and the part's path in YPath format. {% note warning %} Starting from version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module), it's recommended to use `Yson::YPath` to retrieve a node by key and [As*/TryAs*](https://ytsaurus.tech/docs/en/#ysonas) functions for further type narrowing. In most cases, instead of multiple `Yson::YPath` calls, consider converting data using `Yson::ConvertTo` into a set of dictionaries, lists, and primitive types. {% endnote %} ## Yson::Attributes {#ysonattributes} ```yql Yson::Attributes(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict> ``` Getting all node attributes as a dictionary. ## Yson::Serialize... {#ysonserialize} ```yql Yson::Serialize(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Yson -- A binary representation Yson::SerializeText(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Yson Yson::SerializePretty(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Yson -- To get a text result, wrap it in ToBytes(...) ``` ## Yson::SerializeJson {#ysonserializejson} ```yql Yson::SerializeJson(Resource<'Yson2.Node'>{Flags:AutoMap}, [Resource<'Yson2.Options'>?, SkipMapEntity:Bool?, EncodeUtf8:Bool?, WriteNanAsString:Bool?]) -> Json? ``` * `SkipMapEntity` serializes `#` values in dictionaries. The value of attributes is not affected by the flag. By default, `false`. * `EncodeUtf8` responsible for escaping non-ASCII characters. By default, `false`. * `WriteNanAsString` allows serializing `NaN` and `Inf` values as strings. By default, `false`. The `Yson` and `Json` data types returned by serialization functions are special cases of a string that is known to contain data in the given format (Yson/Json). ## Yson::Options {#ysonoptions} ```yql Yson::Options([AutoConvert:Bool?, Strict:Bool?]) -> Resource<'Yson2.Options'> ``` It's passed in the last optional argument (omitted for brevity) to the methods `Parse...`, `ConvertTo...`, `Contains`, `Lookup...`, and `YPath...` that accept the result of the `Yson::Options` call. By default, all the `Yson::Options` fields are false and when enabled (true), they modify the behavior as follows: * **AutoConvert**: If the value passed to Yson doesn't match the result data type exactly, the value is converted where possible. For example, `Yson::ConvertToInt64` in this mode will convert even Double numbers to Int64. * **Strict**: By default, all functions from the Yson library return an error in case of issues during query execution (for example, an attempt to parse a string that is not Yson/Json, or an attempt to search by a key in a scalar type, or when a conversion to an incompatible data type has been requested, and so on). If you disable the strict mode, `NULL` is returned instead of an error in most cases. When converting to a dictionary or list (`ConvertToDict` or `ConvertToList`), improper items are excluded from the resulting collection. #### Example ```yql $yson = @@{y = true; x = 5.5}@@y; SELECT Yson::LookupBool($yson, "z"); --- null SELECT Yson::LookupBool($yson, "y"); --- true -- SELECT Yson::LookupInt64($yson, "x"); --- Error occurs SELECT Yson::LookupInt64($yson, "x", Yson::Options(false as Strict)); --- null SELECT Yson::LookupInt64($yson, "x", Yson::Options(true as AutoConvert)); --- 5 -- SELECT Yson::ConvertToBoolDict($yson); --- Error occurs SELECT Yson::ConvertToBoolDict($yson, Yson::Options(false as Strict)); --- { "y": true } SELECT Yson::ConvertToDoubleDict($yson, Yson::Options(false as Strict)); --- { "x": 5.5 } ``` If you need to use the same Yson library settings throughout the query, it's more convenient to use [PRAGMA yson.AutoConvert;](https://ytsaurus.tech/syntax/pragma/yson.md#autoconvert) and/or [PRAGMA yson.Strict;](https://ytsaurus.tech/syntax/pragma/yson.md#strict). Only with these `PRAGMA` you can affect implicit calls to the Yson library occurring when you work with Yson/Json data types. {% note warning %} If you explicitly pass a `Yson::Options` object, the default values of its fields may differ from the settings used when no options are passed at all. For example, `Yson::Parse` works in strict mode by default, but if you create and pass an options object without specifying the `Strict` field, it will be set to `false` and the method will work in non-strict mode. {% endnote %} ## Yson::Iterate {#ysoniterate} ```yql Yson::Iterate(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List, 'PreValue':Resource<'Yson2.Node'>, 'Value':Resource<'Yson2.Node'>>> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Retrieves a full list of events when traversing a Yson tree. Leaf nodes (`Entity`, `Bool`, `Int64`, `Uint64`, `Double`, `String`) are returned as `Value` events. For a `List` node, the output contains the following sequence: * `PreValue` containing the node itself * `BeginList` * `Item` (precedes each `List` element) * Events for the `List` element * `EndList` * `PostValue` containing the node itself For a `Map` node, the output contains the following sequence: * `PreValue` containing the node itself * `BeginMap` * `Key` (precedes each `Map` element) * Events for the `Map` element * `EndMap` * `PostValue` containing the node itself The order of returned keys is arbitrary. For a node with non-empty attributes, the output contains the following sequence: * `PreValue` containing the node itself * `BeginAttributes` * `Key` (precedes each attribute name) * Events for the attribute * `EndAttributes` * Events for the node, without attributes * `PostValue` containing the node itself The order of returned attributes is arbitrary. #### Examples ```yql -- view the all function output Yson::Iterate $dump = ($x) -> ( ( Way($x), $x.Key, Yson::Serialize($x.PreValue), Yson::Serialize($x.Value), Yson::Serialize($x.PostValue) ) ); SELECT ListMap(Yson::Iterate('{a=1;b=[2u;%true;#;-3.2]}'y), $dump); /* Events: PreValue [1] BeginMap Key a Value 1 Key b PreValue [2] BeginAttributes Key c Value foo EndAttributes PreValue [3] BeginList Item Value 2 Item Value %true Item Value # Item Value -3.2 EndList PostValue [3] PostValue [2] EndMap PostValue [1] */ ``` ```yql -- Getting a the list values - opening all the lists $yson = '[[1;2];[3;4]]'y; SELECT ListFlatMap(Yson::Iterate($yson), ($x)->(IF($x.Value IS NOT NULL, $x.Value))); -- [1;2;3;4] ``` ```yql -- Searching a key with a given name of any level $yson = '{a={b={c=1}};e={f=2}}'y; SELECT ListHasItems(ListFilter(Yson::Iterate($yson), ($x)->($x.Key == 'b'))); -- true ``` ```yql -- Searching a string in a values of any level $yson = '{a={b={c="x"}};e={f="y"}}'y; SELECT ListHasItems(ListFilter(Yson::Iterate($yson), ($x)->(Yson::ConvertToString($x.Value) == 'y'))); -- true ``` ```yql -- Getting attributes `name` for all Map nodes without an attribute `children` $yson = @@{ name=foo; children=[ { name=bar } ] }@@y; SELECT ListFlatMap(Yson::Iterate($yson), ($x)->( IF(Yson::IsDict($x.PreValue) and not Yson::Contains($x.PreValue,'children'), Yson::LookupString($x.PreValue, 'name')))); -- [bar] ``` ## Yson::As... and Yson::TryAs... {#ysonas} ```yql Yson::AsString(Resource<'Yson2.Node'>{Flags:AutoMap}) -> String Yson::AsDouble(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Double Yson::AsUint64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Uint64 Yson::AsInt64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Int64 Yson::AsBool(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Bool Yson::AsList(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List> Yson::AsDict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict> Yson::AsUtf8(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Utf8 Yson::TryAsString(Resource<'Yson2.Node'>{Flags:AutoMap}) -> String? Yson::TryAsDouble(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Double? Yson::TryAsUint64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Uint64? Yson::TryAsInt64(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Int64? Yson::TryAsBool(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Bool? Yson::TryAsList(Resource<'Yson2.Node'>{Flags:AutoMap}) -> List>? Yson::TryAsDict(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Dict>? Yson::TryAsUtf8(Resource<'Yson2.Node'>{Flags:AutoMap}) -> Utf8? ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Casts a Yson node to the specified type. If the specified type isn't a valid Yson node type, `TryAs*` functions return `NULL`, while `As*` functions return a query error. To process a node with the `Entity` ('#') type, use the [`IsEntity`](https://ytsaurus.tech/docs/en/#ysonis) function. The `Yson::AsUtf8` and `Yson::TryAsUtf8` functions are available starting from version [2026.01](https://ytsaurus.tech/changelog/2026.01.md#yson-module). See the [`Yson::IsUtf8`](https://ytsaurus.tech/docs/en/#ysonis) function description for information about when a node can be cast to this type. ## In-place Yson node editing {#yson-modify} Starting with version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module), you can work with a mutable Yson tree based on [linear](https://ytsaurus.tech/types/linear.md) types. Use the [`MutCreate`](https://ytsaurus.tech/docs/en/#mutcreate) function to create a new tree and the [`Mutate`](https://ytsaurus.tech/docs/en/#mutate) function to make an existing immutable tree mutable. Trees should be created/transformed inside the [`Block`](https://ytsaurus.tech/builtins/basic.md#block) function. The resulting mutable Yson tree must ultimately be converted into an immutable one using the [`MutFreeze`](https://ytsaurus.tech/docs/en/#mutfreeze) function. Each instance of a mutable Yson tree has a single current node that you can move while simultaneously creating new nodes as needed. ## Yson::MutCreate {#mutcreate} ```yql Yson::MutCreate() -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Creates a new empty tree with a single node in the `Invalid` state, which corresponds to an undefined type. You can change the value of this node, for example, using the [`MutUpsert`](https://ytsaurus.tech/docs/en/#mutupsert) function. In this case, the root of the resulting tree is set as the current node. Since this function creates an instance of a linear type, the dependent nodes should be specified via a [`Udf`](https://ytsaurus.tech/builtins/basic.md#udf) when using it. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); return Yson::MutFreeze($m); -- Error: Invalid or deleted node is not allowed }) ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); $m = Yson::MutUpsert($m, '1'y); return Yson::MutFreeze($m); -- 1 }); ``` ## Yson::Mutate {#mutate} ```yql Yson::Mutate(Resource) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Creates a mutable Yson tree from an immutable one. In this case, the root of the resulting tree is set as the current node. Since this function creates an instance of a linear type, the dependent nodes should be specified via a [`Udf`](https://ytsaurus.tech/builtins/basic.md#udf) when using it. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); return Yson::MutFreeze($m); -- 1 }); ``` ## Yson::MutFreeze {#mutfreeze} ```yql Yson::MutFreeze(Linear>) -> Resource ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Converts a mutable Yson tree to an immutable one. All nodes in the `Deleted` state are removed from maps and lists. The root node of the mutable tree must not be in the `Invalid` or `Deleted` state, and its child nodes must not be in the `Invalid` state. Otherwise, the conversion will fail. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); return Yson::MutFreeze($m); -- 1 }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); -- Root node has the state Invalid return Yson::MutFreeze($m); -- Error: Invalid or deleted node is not allowed }) ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); $m = Yson::MutRemove($m); -- Chanching the node state to Deleted return Yson::MutFreeze($m); -- Error: Invalid or deleted node is not allowed }) ``` ## Yson::MutUpsert {#mutupsert} ```yql Yson::MutUpsert(Linear>, Resource) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Replaces the current node in a mutable Yson tree with the specified Yson subtree. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); $m = Yson::MutUpsert($m, '2'y); return Yson::MutFreeze($m); -- 2 }); ``` ## Yson::MutInsert {#mutinsert} ```yql Yson::MutInsert(Linear>, Resource) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Replaces the current node in a mutable Yson tree with the specified Yson subtree if the current node is in the `Invalid` or `Deleted` state. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); $m = Yson::MutInsert($m, '1'y); $m = Yson::MutInsert($m, '2'y); -- has no effect return Yson::MutFreeze($m); -- 1 }); ``` ## Yson::MutUpdate {#mutupdate} ```yql Yson::MutUpdate(Linear>, Resource) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Replaces the current node in a mutable Yson tree with the specified Yson subtree if the current node isn't in the `Invalid` or `Deleted` state. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); $m = Yson::MutUpdate($m, '1'y); -- has no effect $m = Yson::MutInsert($m, '2'y); $m = Yson::MutUpdate($m, '3'y); return Yson::MutFreeze($m); -- 3 }); ``` ## Yson::MutRemove {#mutremove} ```yql Yson::MutRemove(Linear>) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Changes the current node to the `Deleted` state. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); $m = Yson::MutRemove($m); $m = Yson::MutInsert($m, '2'y); return Yson::MutFreeze($m); -- 2 }); ``` ## Yson::MutRewind {#mutrewind} ```yql Yson::MutRewind(Linear>) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Moves the current node to the root of the tree. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('{a=1;b=2}'y); $m = Yson::MutDown($m, 'a'); -- Move to key 'a' in the map $m = Yson::MutRewind($m); -- Move to the root $m = Yson::MutUpsert($m, '1'y); -- Replace the tree starting at the root return Yson::MutFreeze($m); -- 1 }); ``` ## Yson::MutUp {#mutup} ```yql Yson::MutUp(Linear>) -> Linear> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Moves the current node one level closer to the root of the tree. If the current node is already at the root of the tree, generates an error. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('{a=1;b=2}'y); $m = Yson::MutDown($m, 'a'); -- Move to key 'a' in the map $m = Yson::MutUp($m); -- Move to the root $m = Yson::MutUpsert($m, '1'y); -- Replace the tree starting at the root return Yson::MutFreeze($m); -- 1 }); ``` ## Yson::MutDown, Yson::MutDownOrCreate, Yson::MutTryDown {#mutdown} ```yql Yson::MutDown(Linear>,location:String) -> Linear> Yson::MutDownOrCreate(Linear>,location:String) -> Linear> Yson::MutTryDown(Linear>,location:String) -> Tuple>, Bool> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Moves the current node to an element within a map or list. This operation can create a new node in the `Invalid` state. The location string is escaped using a backslash (`\`). If the location string begins with the operator `<`, `=`, or `>`, it must be followed by a specific zero-based list index in decimal format, `first` (index 0), or `last` (last element in the list). Otherwise, location is interpreted as a map key. For an empty list, `first` and `last` correspond to positions outside the list. The `MutDown` function can't create new keys within maps. It can only apply the `=` modifier to lists and generates an error if the current node can't be moved. Unlike `MutDown`, the `MutTryDown` function returns a tuple where the second element is `false` if the current node can't be moved. If the current node is in the `Invalid` or `Deleted` state, the `MutDownOrCreate` function first creates it as an empty list or map, depending on the type of location. If the type of the current node (list or map) doesn't match location, both `MutDown` and `MutDownOrCreate` return an error. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('{a=1;b=2}'y); $m = Yson::MutDown($m, 'a'); -- moved to the key 'a' in a dict $m = Yson::MutUpsert($m, '3'y); -- replaced the current node return Yson::MutFreeze($m); -- {a=3;b=2} }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('{a=1;b=2}'y); $m = Yson::MutDownOrCreate($m, 'c'); -- moved to the key 'c' in a dict - it was created in the state `Invalid` $m = Yson::MutUpsert($m, '3'y); -- replaced the current node return Yson::MutFreeze($m); -- {a=1;b=2;c=3} }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('{a=1;b=2}'y); $m = Yson::MutDown($m, 'c'); -- removed to the key 'c' in a dict, error, it doesn`t exists return Yson::MutFreeze($m); }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('{a=1;b=2}'y); $m, $ok = Yson::MutTryDown($m, 'c'); -- tried to move to the key 'c' in a dict, returned `false` return (Yson::MutFreeze($m), $ok); -- ({a=1;b=2}, false) }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('[1;2]'y); $m = Yson::MutDownOrCreate($m, '>last'); -- added node to the end of a list in a state `Invalid` $m = Yson::MutUpsert($m, '3'y); -- replaced the current node return Yson::MutFreeze($m); -- [1;2;3] }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('[1;2]'y); $m = Yson::MutDownOrCreate($m, '<1'); -- added node before the node with the index 1 (value '2') $m = Yson::MutUpsert($m, '3'y); -- replaced the current node return Yson::MutFreeze($m); -- [1;3;2] }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('[1;2]'y); $m = Yson::MutDownOrCreate($m, '{ $m = Udf(Yson::Mutate, $arg as Depends)('[1;2]'y); $m = Yson::MutDown($m, '=last'); -- moving to the last node of the list $m = Yson::MutUpsert($m, '3'y); -- replaced the current node return Yson::MutFreeze($m); -- [1;3] }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('[1;2]'y); $m = Yson::MutDownOrCreate($m, 'a'); -- error, current node is not a dict return Yson::MutFreeze($m); }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('[a=1;b=2]'y); $m = Yson::MutDownOrCreate($m, '=0'); -- error, current node is not a list return Yson::MutFreeze($m); }); ``` ## Yson::MutExists {#mutexists} ```yql Yson::MutExists(Linear>) -> Tuple>,Bool> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Checks whether the current node is in the `Invalid` or `Deleted` state. This function doesn't modify the tree itself or the position of its current node. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); $m, $exists = Yson::MutExists($m); -- current node is Invalid return LinearDestroy($exists, $m); -- retorning the value $exists=false, absorbing linear type $m }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); $m, $exists = Yson::MutExists($m); -- current node is not Invalid return LinearDestroy($exists, $m); -- returning the value $exists=true, absorbing linear type $m }); ``` ## Yson::MutView {#mutview} ```yql Yson::MutView(Linear>) -> Tuple>, Optional>> ``` Added in version [2025.05](https://ytsaurus.tech/changelog/2025.05.md#yson-module). Returns an immutable Yson tree starting from the current node unless it's in the `Invalid` or `Deleted` state. Otherwise, returns `NULL`. Similar to the `MutFreeze` function, all child nodes of the current node must not be in the `Invalid` state, and `Deleted` nodes are skipped when creating the resulting Yson tree. This function doesn't modify the tree itself or the position of its current node. #### Examples ```yql SELECT Block(($arg)->{ $m = Udf(Yson::MutCreate, $arg as Depends)(); $m, $view = Yson::MutView($m); -- Current node is in the Invalid state return LinearDestroy($view, $m); -- Return the $view=NULL value and consume the linear type $m }); ``` ```yql SELECT Block(($arg)->{ $m = Udf(Yson::Mutate, $arg as Depends)('1'y); $m, $view = Yson::MutView($m); -- Current node isn't Invalid return LinearDestroy($view, $m); -- Return the $view='1'y value and consume the linear type $m }); ``` ## See also * [Accessing values inside JSON with YQL](https://ytsaurus.tech/recipes/accessing-json.md) * [Modifying JSON with YQL](https://ytsaurus.tech/recipes/modifying-json.md) # YQL user-defined functions This section describes YQL user-defined functions for supported languages and how to develop them. - [C++ UDF overview](https://ytsaurus.tech/docs/en/cpp_about_udf.md) - [Developing C++ UDFs](https://ytsaurus.tech/docs/en/cpp.md) - [Python UDFs](https://ytsaurus.tech/docs/en/python.md) - [Preinstalled UDFs](https://ytsaurus.tech/docs/en/preinstalled.md) # Built-in UDFs The list of built-in UDFs is available at [Built-in functions / C++ libraries](https://ytsaurus.tech/docs/en/list/index.md). # Instructions for writing Python user-defined functions (UDFs) for YQL Using a purely declarative, SQL-like approach to describe task results can sometimes be impractical. You may need to specify which data to `JOIN` and how to group it declaratively, while handling the data processing logic imperatively. For these use cases, YQL provides a mechanism for writing and calling user-defined functions (UDFs) in Python. To learn more about UDFs, see [Python user-defined functions (UDFs) in YQL](https://ytsaurus.tech/user-guide/query-tracker/system-python-udf.md). # Developing UDFs in C++ ## Introduction YQL currently supports user-defined functions (UDFs) in C++ and Python. The information below covers only C++. [Python UDFs](https://ytsaurus.tech/docs/en/python.md) are covered on a separate page. YQL offers a set of basic functions, and we recommend viewing the [list](https://ytsaurus.tech/docs/en/list/index.md) of them. ## Terminology * **A callable value** is an entity with a fixed, strictly typed signature that, when called, can return a result based on its arguments. * **A user-defined function** (UDF) is a generator of callable values. * **A module** is a set of closely related functions with a common prefix. All module functions are distributed together in a single shared library (`.so`). If a module is project-specific, it's advisable to reflect this in its name by adding a prefix. * **A registrator** is an interface for adding modules and functions to the general registry. * **A module group** is a group of modules belonging to a structural unit or a large project. It serves only for logical separation of source code (see the next section) and is not visible from YQL/SQL. * **Configuration parameters** are an optional tool for setting up a user-defined function (generator of callable values). You can use configuration parameters to change the behavior of callable values without recompilation. * `Type Config`, `User Type`: They are used by the function to define and declare the signature of the callable value; used when building a computation graph. They must be known at compile time, so `Type Config` must be an atom, and `User Type` must be a certain type (for example, obtained using the TypeOf operation). * `Run Config`: Function uses it to return the callable value with the previously declared signature. The Run Config value type is specified in the UDF code when declaring the signature of the callable value. It differs from a regular argument in that it is passed at initialization, not at each call. ## Examples Examples of ready-to-use modules are available at [https://github.com/ytsaurus/ytsaurus/tree/main/yql/essentials/udfs](https://github.com/ytsaurus/ytsaurus/tree/main/yql/essentials/udfs). See the [examples](https://github.com/ytsaurus/ytsaurus/tree/main/yql/essentials/udfs/examples) subdirectory for very basic examples and other subdirectories (such as [common](https://github.com/ytsaurus/ytsaurus/tree/main/yql/essentials/udfs/common)) for more realistic ones. Before you move on to the next section, we recommend opening one or multiple examples to follow along: the section provides only an overview of the code, without complete listings. ## Interfaces (С++) ### TUnboxedValue (udf\_value.h) Wrapper class for typed data. It provides a set of constructors for creating composite data types from primitive types along with a set of helpers for working with the former. Numbers, intervals, dates (including dates with a timezone), and short strings up to 14 bytes are stored by value; the `IsEmbedded()` method returns `true` for them. Strings larger than 14 bytes are stored by pointer. ### TUnboxedValuePod(udf\_value.h) It is a base class for TUnboxedValue. It does not automatically count references to its value, unlike its subclass, which does it in the constructor and destructor. The key point here is that the TUnboxedValuePod class does not have constructors and destructors. It implements the entire logic of working with typed data. ### IValueBuilder (udf\_value\_builder.h) The Run method must return TUnboxedValue. For numeric types, you can use the type constructor, for example, `return TUnboxedValue(123);`. To create TUnboxedValue with more complex content, you can pass IValueBuilder to the Run method. It can be used to call one of the `New***` methods to get the `I***ValueBuilder` interface, where *** is a String, Struct, Dict, or another type. Use this to add type-specific elements and then call `Build` to get the filled TUnboxedValue. You can also create a Struct or Tuple with preallocation to store the contents. To do this, call the corresponding method `New***(const TType* type, TUnboxedValue*& items)` and fill the items. See [yql/essentials/udfs/examples/structs](https://github.com/ytsaurus/ytsaurus/tree/main/yql/essentials/udfs/examples/structs) for an example with structs. ### TBoxedValue (udf\_value.h) A typical UDF class inherits from TBoxedValue and, in addition to the constructor, overrides the Run method where the main logic for handling one particular call should be placed. The IValueBuilder interface (more on it below below) and the passed arguments in the form of the TUnboxedValue array are passed to the Run arguments. In the constructor, you can store a state in private class variables. Thus, a specific object of our TBoxedValue subclass becomes a callable value with potentially unique behavior. For simple UDFs (without using YQL structures, C++ templates, inheritance, and so on), you can use the `SIMPLE_UDF(udfName, signature)` macro (you can find it in `udf_helpers.h`). `signature` is specified as `returnType(arg1, arg2, ..., argN)`. Example: ```cpp SIMPLE_UDF(TAbs, ui64(i64)) { i64 input = args[0].Get(); ui64 result = static_cast(input >= 0 ? input : -input); return TUnboxedValue(result); } ``` The function will be registered as `Abs`, the argument type is `i64`, and the result is `ui64`. An example where a successful result is not guaranteed: ```cpp SIMPLE_UDF(TParseDuration, TOptional(char*)) { TDuration result; auto input = args[0].AsStringRef(); bool success = TDuration::TryParse(input, result); return success ? TUnboxedValue::Optional(result.Seconds()) : TUnboxedValue(); } ``` `Optional (type)` ([nullable type](https://ytsaurus.tech/docs/types/optional.md)) means that a value of the specified type may be missing (for example, if there is an error). In SQL, you can use the `IS [NOT] NULL` expression to check for this type in the result. ### IUdfModule (udf\_registrator.h) For common modules, you can use the `SIMPLE_MODULE(moduleName, udfs...)` macro (you can find it in `udf_helpers.h`): * The first argument is the name of the module class; when registering, the `T` prefix and the `Module` suffix will be removed. * Next is a list of N constituent UDFs; when registering them, only the `T` prefix will be removed. In the example `SIMPLE_MODULE(TFooModule, TBar, TBaz)`, the `Foo` module with two UDFs (`Bar` and `Baz`) will be available from YQL after registration (more on registration below). #### Manual module implementation A typical module class inherits from IUdfModule and must implement two methods: * `GetAllFunctionNames`: In the passed `IFunctionNamesSink`, you need to call the `Add(name)` method for each UDF included in the module. * `BuildFunctionTypeInfo`: Using the passed UDF name and Type Config, you need to call on the passed `IFunctionTypeInfoBuilder`: * `Args()->Add()[->Add(foo)->Add...].Done()`: * To specify arguments and their types, for numbers and strings, you can specify the argument type using a template. * You can describe a composite type, such as a Struct or Dict, using the same-name `IFunctionTypeInfoBuilder` method, then passing it to the `Add` argument. For example: ```cpp ui32 a = 0; ui32 b = 0; auto foo = builder.Struct()->AddField("A", &a).AddField("B", &b).Build(); builder.Args()->Add(foo); ``` * When describing types like `builder.List()->...`, you should copy examples of others as carefully as possible: calling a chain of builder methods in a wrong sequence or with wrong arguments often compiles fine, but crashes at runtime with a non-trivial backtrace. Example of such a chain: ```cpp // udf returns list of lists of char* auto retType = buider.List()->Item(builder.List()->Item().Build()).Build(); // Correct auto retType = buider.List()->Item().Build(); // Wrong type, coredump with backtrace in casting to string deep in validating auto retType = buider.List()->Build(); // No type at all, coredump with attempt to dereference nullptr deep in type machinery builder.Returns(retType); ``` * The arguments also pass the `ui32 flags` bitmask, which at the time of writing can contain only `(cpp)TFlags::TypesOnly` that, if present, does not require calling `builder.Implementation(new TMyUdf);` for the UDF corresponding to the passed name. ### IRegistrator (udf\_registrator.h) For simplified module registration in the system, we recommend using a macro: ```cpp REGISTER_MODULES( TFooModule, TBarModule, TBarModule ) ``` In this case, the module name is taken from the `Name()` method that must return `TStringRef` by value; the `SIMPLE_MODULE` macro creates it automatically. #### Alternative method for advanced users You need to declare the `Register` function of the following form, then register the module(s) in the passed interface and export the ABI version: ```cpp extern "C" YQL_UDF_API void Register(NKikimr::NUdf::IRegistrator& registrator, ui32 flags) { registrator->AddModule("Foo", new NDetail::TFooModule(flags)); } extern "C" YQL_UDF_API ui32 AbiVersion() { return CurrentAbiVersion(); } ``` You can get the list of flags with `IUdfModule::TFlags`. At the time of writing, only `TypesOnly` is implemented. It is used as an indication that no real computations will be performed yet, because the computation graph is still being built, and only function signatures are required at this stage. ### Exception and error handling To return fatal exceptions to the user, use the following block: ```cpp try { ... } catch (const std::exception& e) { UdfTerminate(e.what()); } ``` Instead of `e.what()`, you can write a human-readable error description if possible. {% note warning "Attention" %} We strongly recommend against completely terminating program execution using `abort`, `exit`, `Y_ABORT_UNLESS`, `Y_ABORT`, and other similar mechanisms from UDF code, because this would limit its scope to environments where a separate process is created for each calculation. The `UdfTerminate` implementation is aware of the current environment and will invoke the error handling mechanism it expects. {% endnote %} ### Overview of UDFs in C++ In C++ code, a UDF is simply a `TBoxedValue` subclass with the mandatory `Run()` method. An easy way to implement a particular UDF inside a UDF module is to use a class with three overridden functions (`Name()`, `DeclareSignature(...)`, and `Run(...)`) to be called within the module as needed. Example of a UDF module class: ```cpp class TSomeYQLModule: public IUdfModule { public: TStringRef Name() const { return TStringRef::Of("SomeModule"); } void GetAllFunctions(IFunctionsSink& sink) const final { sink.Add(TSomeUdfFunction::Name()); sink.Add(TSomeTypeAwareUdfFunction::Name())->SetTypeAwareness(); } void BuildFunctionTypeInfo(const TStringRef& name, TType* userType, const TStringRef& typeConfig, ui32 flags, IFunctionTypeInfoBuilder& builder) const override { try { Y_UNUSED(typeConfig); bool typesOnly = (flags & TFlags::TypesOnly); if (TSomeUdfFunction::Name() == name) { TSomeUdfFunction::DeclareSignature(typesOnly, builder); } else if (TSomeTypeAwareUdfFunction::Name() == name) { TSomeTypeAwareUdfFunction::DeclareSignature( typesOnly, builder, userType); } } catch (const std::exception& e) { builder.SetError(CurrentExceptionMessage()); } } }; ``` Method description: * `static const TStringRef& Name()` A method that returns the function name to be used in YQL. In this implementation, the method is used to add a function to a UDF module. * `static bool DeclareSignature(bool typesOnly, IFunctionTypeInfoBuilder& builder [, TType* userType])` A method that declares the function signature. It is used in the ```BuildFunctionTypeInfo``` method of a UDF module to get information about the indexes of structure fields used inside the class and to create an instance of a particular UDF. * `TUnboxedValue Run(const IValueBuilder* valueBuilder, const TUnboxedValuePod* args) const override` A method that implements all the internal logic of a UDF. When the UDF is called from YQL, it is this particular method that is called. Example of a UDF class: ```cpp class TMyUdfFunction : public ::NKikimr::NUdf::TBoxedValue { public: // Each member struct is a C++ description of a YQL struct and has it's own // constructor that takes in a builder instance and initializes a proper // struct from it. If you want to build your own YQL struct and work with // it in C++, you will need to write the same kind of constructor. These // structs contain indices from the YQL runtime (in C++ you can't use field // names, only indices) and TType* ResultStructType (YQL description of the // struct), which is a description of the struct understandable for YQL and // will be used later when specifying the return type of the UDF. struct TMemberIndices { TMemberIndices(::NKikimr::NUdf::IFunctionTypeInfoBuilder& builder) { ... } }; // Name() function, needed to register UDF in YQL, the function in YQL will // have the name we specify here. In our UDF Module class we have a // GetAllFunctions method, where we add all function names into our module. static const ::NKikimr::NUdf::TStringRef& Name() { static auto name = ::NKikimr::NUdf::TStringRef::Of("MyUdfFunction"); return name; } // This function is also used in the UDF module class in the // BuildFunctionTypeInfo method. Here we use the passed in builder instance // 1) to take information about indices we need for our class to work // 2) to create an instance of this UDF function // This is the only function where we can grab info about indices for our // class constructor, thus this is the only place where constructor is // called static bool DeclareSignature( bool typesOnly, ::NKikimr::NUdf::IFunctionTypeInfoBuilder& builder, ::NKikimr::NUdf::TType* userType) { auto members = buildReturnSignature(builder, userType); // If typesOnly flag is specified, we don't need to register // implementation, just build the signature if (!typesOnly) { builder.Implementation(new TMyUdfFunction(members)); } return true; } // Run() function is derived from TBoxedValue (and we need to override it) // and contains our UDF logic. (When we call it from YQL, this function is // what is gonna happen). // It always has signature // Run(IValueBuilder*, TUnboxedValuePod*) -> TUnboxedValue ::NKikimr::NUdf::TUnboxedValue Run( const ::NKikimr::NUdf::IValueBuilder* valueBuilder, const ::NKikimr::NUdf::TUnboxedValuePod* args) const override { ... } private: TMemberIndices IndicesDescription; /// Constructor accepts YQL struct member descriptions and source code /// position, for logging. /// Instance of this class is created within DeclareSignature method, /// as that is the only method that knows YQL Struct member indices /// For the reasons above, it is made private TMyUdfFunction(const TMemberIndices& indicesDescription, const ::NKikimr::NUdf::TSourcePosition& pos) { ... } /// This function will initialize proper signature on given builder. /// It will also return a struct with indices of YQL Struct members (in C++ /// you have to access YQL Struct members by index, not by name) static TMemberIndices buildReturnSignature( ::NKikimr::NUdf::IFunctionTypeInfoBuilder& builder, ::NKikimr::NUdf::TType* userType) { ... } }; ``` ### Type-aware UDFs Type awareness is implemented using the `TType* userType` argument of the `DeclareSignature(...)` and `buildReturnSignature(...)` methods. You can use this argument to see with which arguments a UDF function was called from YQL and build the desired signature. A user-defined type can be parsed using the `ITypeInfoHelper::TPtr` object, which is returned by the `builder.TypeInfoHelper()` method and which can be used to create TypeInspectors, such as `TTupleTypeInspector`, `TCallableTypeInspector`, `TOptionalTypeInspector`, and others. {% note warning %} For the YQL engine to pass a non-empty userType to `DeclareSignature()` as input, the class of your UDF must contain a subtype definition: ``` typedef bool TTypeAwareMarker; ``` {% endnote %} Using such functions, you can: * Support optional or default arguments: `SomeTypeAwareUdfFunc(Double, Double) -> Bool` `SomeTypeAwareUdfFunc(Double, Double, Double) -> Bool` ```cpp TMemberIndices TSomeTypeAwareUdfFunc::buildReturnSignature(IFunctionTypeInfoBuilder& builder, TType* userType) { ... auto userTypeInspector = TTupleTypeInspector(*typeHelper, userType); auto argsTypeTuple = userTypeInspector.GetElementType(0); auto argsTypeInspector = TTupleTypeInspector(*typeHelper, argsTypeTuple); // Build right signature by number of arguments if (argsTypeInspector.GetElementsCount() == 2) { /* Build one signature */ ... } else if (argsTypeInspector.GetElementsCount() == 3) { /* Build other signature */ ... } else { ... } ... } TUnboxedValue TSomeTypeAwareUdfFunc::Run(IValueBuilder* valueBuilder, TUnboxedValuePod* args) { ... double value = 0.0; // default if (/* hasThirdElement */) { value = args[2].Get(); // non-default } ... } ``` * Support overloading: `SomeTypeAwareUdfFunc(List) -> String` `SomeTypeAwareUdfFunc(Stream) -> String` ```cpp TMemberIndices buildReturnSignature(IFunctionTypeInfoBuilder& builder, TType* userType) { ... auto userTypeInspector = TTupleTypeInspector(*typeHelper, userType); auto argsTypeTuple = userTypeInspector.GetElementType(0); auto argsTypeInspector = TTupleTypeInspector(*typeHelper, argsTypeTuple); auto typeKind = typeHelper->GetTypeKind(argsTypeInspector.GetElementType(0)); if (typeKind == ETypeKind::List) { ... } else if (typeKind == ETypeKind::Stream) { ... } else { ... } ... } ``` * Support changing the response type depending on the type returned by the custom callback: `SomeTypeAwareUdfFunc(Callable<...>->Int)->List` `SomeTypeAwareUdfFunc(Callable<...>->Double)->List` `SomeTypeAwareUdfFunc(Callable<...>->CallbackReturnType)->List` ```cpp TMemberIndices buildReturnSignature(IFunctionTypeInfoBuilder& builder, TType* userType) { ... auto userTypeInspector = TTupleTypeInspector(*typeHelper, userType); auto argsTypeTuple = userTypeInspector.GetElementType(0); auto argsTypeInspector = TTupleTypeInspector(*typeHelper, argsTypeTuple); auto callbackInspector = TCallableTypeInspector(*typeHelper, argsTypeInspector.GetElementType(0)); auto resultType = builder.List()->Item(callbackInspector.GetReturnType()).Build(); builder.Returns(resultType).Args()->Add(argsTypeInspector.GetElementType(0)).Done(); ... } ``` ### Working with YQL-Stream in a C++ UDF YQL-Stream is a single-pass iterator. In YQL, it can be obtained at the ```PROCESS``` or ```REDUCE``` output (see the YQL documentation). In C++, YQL-Stream is a ```TBoxedValue``` subclass with the overridden ```Fetch()``` method. ```cpp class TMyStreamProvider: public TBoxedValue { public: TMyStreamProvider(...) { ... } TMyStreamProvider() = delete; TMyStreamProvider(const TMyStreamProvider&) = delete; virtual ~TMyStreamProvider() = default; void operator=(const TMyStreamProvider&) = delete; TMyStreamProvider(TMyStreamProvider&& other) = default; TMyStreamProvider& operator=(TMyStreamProvider&& other) = default; EFetchStatus Fetch(TUnboxedValue& result) override; }; ``` ```Fetch()``` can return three statuses: * ```EFetchStatus::Ok``` Indicates successful writing of a value to the result. * ```EFetchStatus::Finish``` Indicates the end of the stream, the result remains unchanged. * ```EFetchStatus::Yield``` Special status. If a UDF has no YQL-Stream at the input, your stream will never generate this status. If the ```EFetchStatus::Yield``` status appears when reading the input stream, you can write 0 or more values to the output stream but the ```EFetchStatus::Yield``` status should still be generated and returned. Remember that after the UDF returns a stream in C++ code, whatever was in the ```Run()``` body is destroyed by the destructor. So you need to pass everything through the constructor to the Stream class to make it the owner of all resources. ### Data processing in UDFs To properly use composite data types, you need to process and store information about their structure. This is done at the UDF signature generation stage. Example of a class that contains this information ```cpp struct TInputIndices { // These two fields will contain indexes of fields in the Struct type. Using these indexes, we will be able to obtain the values of the structure fields of a specific object of that structure. ui32 oneInputFieldIndex = 0; ui32 anotherInputFieldIndex = 0; static constexpr const int FieldsCount = 2; NYql::NUdf::TType* ResultStructType = nullptr; NYql::NUdf::TType* StructType() const { return ResultStructType; } TInputIndices(NYql::NUdf::IFunctionTypeInfoBuilder& builder) { auto optionalAnotherInputFieldType = builder.Optional()->Item(TDataType::Id).Build(); // After calling Build, oneInputFieldIndex and anotherInputFieldIndex will contain the index of the corresponding structure fields. ResultStructType = builder.Struct(FieldsCount) -> AddField("oneInputFieldName", &oneInputFieldIndex) . AddField("anotherInputFieldName", optionalAnotherInputFieldType, &anotherInputFieldIndex) . Build(); } TSomeStruct UnpackUnboxedValue(const NYql::NUdf::TUnboxedValue& item) const { TSomeStruct someStruct; // We can get the value of a particular field using the index. auto oneInputFieldIValue = item.GetElement(oneInputFieldIndex); auto anotherInputFieldIValue = item.GetElement(anotherInputFieldIndex); ... return someStruct; } }; ``` You will need `IFunctionTypeInfoBuilder` to generate the input data type. This interface supports generation of composite types, such as structures, lists, or maps. Suppose that we pass a structure containing two fields (`"oneInputField"` of the `Double` type and `"anotherInputField"` of the `TDatetime?` type) as input. Using `IFunctionTypeInfoBuilder`, we create an optional type for the second field and a structure type filled with the required fields. The structure type is declared using the builder by calling the `Struct(num_of_fields)` method. Internally, the structure in a YQL UDF is a regular array of field values. You can add fields using the `AddField` method of the returned interface for working with structures. After executing the Build() method, the `oneInputFieldIndex` and `anotherInputFieldIndex` variables will contain the indexes by which the data of a particular field will be stored in the input data array. You can use them to retrieve the value of a particular field from the input data array as it was done in the `UnpackUnboxedValue` method in the example above. Then you can use this class in the `Run` method of the UDF to unpack input data using the `UnpackUnboxedValue` method. To pack the results of your UDF, you can use classes for storing output data details. For example: ```cpp struct TOutputIndices { ui32 firstOutputFieldIndex = 0; ui32 secondOutputFieldIndex = 0; ui32 thirdOutputFieldIndex = 0; static constexpr const int FieldsCount = 3; NYql::NUdf::TType* ResultStructType = nullptr; NYql::NUdf::TType* StructType() const { return ResultStructType; } TOutputIndices(NYql::NUdf::IFunctionTypeInfoBuilder& builder) { auto thirdOutputFieldType = builder.List()->Item(TDataType::Id).Build() ResultStructType = builder.Struct(FieldsCount) -> AddField("firstOutputFieldName", &firstOutputFieldIndex) . AddField("secondOutputFieldName", &secondOutputFieldIndex) . AddField("thirdOutputFieldName", thirdOutputFieldType, &thirdOutputFieldIndex); . Build(); } NYql::NUdf::TUnboxedValue PackUnboxedValue(const NYql::NUdf::IValueBuilder* valueBuilder, ui32 valueForFirstField, const TString& valueForSecondField, std::vector valuesForThirdField) { // Pay attention to what happens here. There are TWO output parameters: result is what you need to return, while elemItems contains a pointer to the array where you need to enter the field values. TUnboxedValue* elemItems = nullptr; auto result = valueBuilder->NewArray(FieldsCount, elemItems); // Entering field values elemItems[firstOutputFieldIndex] = TUnboxedValuePod(valueForFirstField); elemItems[secondOutputFieldIndex] = valueBuilder->NewString(valueForSecondField); std::vector thirdFieldItems; thirdFieldItems.reserve(valuesForThirdField.size()); for (const auto& value : valuesForThirdField) { TUnboxedValue yqlValue = TUnboxedValuePod(value); if (yqlValue) { thirdFieldItems.push_back(std::move(yqlValue)); } } auto thirdFieldValueHandler = valueBuilder->NewList(thirdFieldItems.data(), thirdFieldItems.size()); elemItems[thirdOutputFieldIndex] = thirdFieldValueHandler; return result; } }; ``` Generating the output data structure type is no different from generating the input data structure type: you need to declare the structure fields and save their indexes to then fill the output data array. In the example above, the structure consists of three fields: the first field is a primitive numeric type, the second one is a string, and the third one is a list. To create a list type, use the `List()` method of the `NYql::NUdf::IFunctionTypeInfoBuilder` interface. This method returns a pointer to the interface that you can use to create a list type. Specify the type of list items using the `Item(type)` method. Thus, in addition to lists, you can create maps, structures, tuples, and more. The output data array is generated using `NYql::NUdf::IValueBuilder` and its `NewArray` method, which accepts a set of output elements and a reference to the pointer. The method returns TUnboxedValue describing the entire object and adds a value to the passed pointer, which in turn helps fill the array. This method returns `NYql::NUdf::TUnboxedValue` that should be returned after filling the output array with the necessary values. Keep in mind that, in order to fill the output array, you should use the pointer that the method fills, not the one it returns. When filling the array with a specific value for a specific field, use the indexes associated with that field at the stage of generating the output data type. If the returned value is a primitive numeric type, you can simply wrap it in `TUnboxedValuePod` and place it according to the appropriate index. If the returned value is a string, use the `NewString` method of the `NYql::NUdf::IValueBuilder` interface where you need to pass the string to. If the returned value is, for example, a list, create a `TUnboxedValue` value array, fill it, and then pass its size and pointer to the `NewList` method of the `NYql::NUdf::IValueBuilder` interface. This method returns `TUnboxedValue`, which you can then place in the output array according to the index that corresponds to that field. # YQL recipes This section contains practical examples of data transformation and metaprogramming in YQL. - [Accessing JSON](https://ytsaurus.tech/docs/en/accessing-json.md) - [Modifying JSON](https://ytsaurus.tech/docs/en/modifying-json.md) - [Metaprogramming](https://ytsaurus.tech/docs/en/metaprogramming.md) - [Code generation](https://ytsaurus.tech/docs/en/codegen.md) - [User-defined aggregate functions](https://ytsaurus.tech/docs/en/udaf.md) # Custom aggregate functions in YQL ## Introduction If the [built-in aggregate functions](https://ytsaurus.tech/docs/builtins/aggregation.md) do not meet your needs for whatever reason, you can define your own function in YQL. This mechanism is referred to as `UDAF` (User Defined Aggregation Functions) and works the same as standard aggregate functions. Here are the peculiarities/benefits of this approach compared to Map/Reduce ([PROCESS](https://ytsaurus.tech/docs/syntax/process.md) + [REDUCE](https://ytsaurus.tech/docs/syntax/reduce.md) or working with YTsaurus directly without YQL): 1. You can easily apply the same logic to several fields or expressions within a single computing run (that is, we calculate different non-related aggregates, reading the data only once). Moreover, you can automatically skip Null values. 2. Support for `DISTINCT`, that is, calling a function only by unique values. 3. Automatic creation of a map-side combiner. Often this allows you to substantially save on the query execution time without having to implement a map-side combiner yourself. {% note info %} If you have built or want to build an aggregate function which, in your opinion, could be useful to many users, write to us in the [Telegram chat](https://t.me/ytsaurus). We'll consider adding it to standard functions. {% endnote %} ## How it works UDAFs operate with the following abstractions: * `Item`: Aggregated value. * `State`: Aggregation state. * `Result`: Aggregation result. * `Serialized` (optional): A serialized representation of State. * `Parent` (optional): A node that should be used as a parent node if the handler is written in lambda. This is needed when the state includes mutable resources. In each specific case, there should be an arbitrary [data type](https://ytsaurus.tech/docs/types/primitive.md) behind each of these abstractions. However, the type corresponding to each abstraction should be the same for different functions. If one function uses `String` as `Item` and another function uses `Int64` as Item, you will get a type mismatch error. UDAF calling procedure in high-level terms: 1. To perform each aggregation step, the user provides from three to seven simple functions (Callable values, to be precise) that operate with the above abstractions. The main methods to get them is by using [lambda](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) or [Python UDF](https://ytsaurus.tech/docs/udf/python.md). Main options: * 3: Minimal. * 4: If State isn't a result. * 6: If State is non-serializable. * 7: If you need to override the default `NULL` value. 2. The user calls a special aggregate `UDAF` function. To this function, as the first argument, the user passes a column or an aggregated expression. Other arguments include callable values. The order of callable values is described in the table below. 3. Benefits. List of callable values. Required values are highlighted in bold. | No. | Name | Description | Signature | | --- | --- | --- | --- | | 1 | **Create** | Initializing State by the first non-Null Item. | `(Item)->State`. Input arguments for lambda (Item, Parent) | | 2 | **Add** | Adding the next Item to State. | `(State,Item)->State`. Input arguments for lambda (State, Item, Parent) | | 3 | **Merge** | Merging of two States. | `(State,State)->State` | | 4 | Get Result | Getting the aggregation result from the final State.
If omitted, the final State is used as a result. | `(State)->Result` | | 5 | Serialize | Serialization of State, if it isn't serializable by itself.
Any combination of basic types and containers is serializable. That's why this case applies only when you use a Resource as a State. | `(State)->Serialized` | | 6 | Deserialize | Recovering a State after serialization. | `(Serialized)->State` | | 7 | Default Value | The result in the situation when the aggregate function hasn't been called (zero rows were input) The default value is `NULL`. | `()->Result` or `(TypeOf(Result))->Result` | They are passed one-by-one as individual arguments after the aggregated value. The aggregate `UDAF` function can have between 4 and 8 arguments. If several values in the query are aggregated in the same manner using `UDAF`, then, to avoid enumerating the callable values every time, use the [aggregate function factory](https://ytsaurus.tech/docs/builtins/basic.md#aggregationfactory). In this case, "UDAF" is passed instead of a data column, and the call is executed using [AGGREGATE_BY](https://ytsaurus.tech/docs/builtins/aggregation.md#aggregateby). ### Examples ```yql -- Emulate the aggregate COUNT function $create = ($item, $parent) -> { return 1ul }; $add = ($state, $item, $parent) -> { return 1ul + $state }; $merge = ($state1, $state2) -> { return $state1 + $state2 }; $get_result = ($state) -> { return $state }; $serialize = ($state) -> { return $state }; $deserialize = ($state) -> { return $state }; $default = 0ul; $udaf_factory = AggregationFactory( "UDAF", $create, $add, $merge, $get_result, $serialize, $deserialize, $default ); SELECT AGGREGATE_BY(value1, $udaf_factory) AS cnt1, AGGREGATE_BY(value2, $udaf_factory) AS cnt2 -- Count non-NULL values -- in the value1 and value2 columns. FROM my_table; ``` # Recipes for code generation ## Introduction When working with tables of many columns, you might spend too much effort listing them repeatedly in different parts of your SQL query. This happens when you can't select all your columns by `*` and, optionally, by `WITHOUT`. Sometimes, column selection may depend both on the column name and its data type. To avoid writing lengthy SQL queries manually, you can use [type introspection](https://ytsaurus.tech/docs/builtins/types.md) and [code generation](https://ytsaurus.tech/docs/builtins/codegen.md). Code will be generated before the main query is executed, and the resulting programs are fully equivalent to those written manually. When you use code generation, keep in mind that as it is executed before the main process starts, so you can only use column types when generating code, but not column values. You can also use constants and expressions that do not depend on table columns, without any limitation. Now let's discuss typical use cases and how to implement them. ## Use cases ### Declaring a UDF type by computations ```yql $tuple = AsTuple(1, "foo", AsList(-1.2, 3.0)); $tupleType = TypeOf($tuple); $reverseTupleType = EvaluateType(TupleTypeHandle( ListReverse(TupleTypeComponents(TypeHandle($tupleType))))); $script = @@ def reverse_any_tuple(t): return tuple(reversed(list(t))) @@; $udfType = CallableType(0, $reverseTupleType, $tupleType); $udf = SystemPython3_8::reverse_any_tuple($udfType, $script); SELECT $udf($tuple); ``` Links to documentation: * [EvaluateType](https://ytsaurus.tech/docs/builtins/types.md#evaluatetype) ### Filtering columns by a list of names ```yql USE ; $makeFieldsSelector = ($fields) -> { RETURN EvaluateCode(LambdaCode(($row) -> { $items = ListMap($fields, ($f) -> { $atom = AtomCode($f); RETURN ListCode($atom, FuncCode("Member", $row, $atom)); }); RETURN FuncCode("AsStruct", $items); })) }; $selectFields1 = $makeFieldsSelector(AsList("age", "name", "region")); $selectFields2 = $makeFieldsSelector(AsList("age", "name")); INSERT INTO @tmp1 SELECT * FROM ( SELECT $selectFields1(TableRow()) FROM `home/yql/tutorial/users` WHERE region != 225 ) FLATTEN COLUMNS; INSERT INTO @tmp2 SELECT * FROM ( SELECT $selectFields2(TableRow()) FROM `home/yql/tutorial/users` WHERE region = 225 ) FLATTEN COLUMNS; COMMIT; SELECT * FROM @tmp1; SELECT * FROM @tmp2; ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [TableRow](https://ytsaurus.tech/docs/builtins/basic.md#tablerow) ### Searching in all string columns ```yql USE ; $containsAnyColumns = ($strValue, $needle) -> { -- Lambda function, a predicate that will be applied to the value of each column $func = ($where) -> { RETURN String::Contains($where, $needle); }; $code = EvaluateCode(LambdaCode(($strCode) -> { -- Select a list of struct fields $members = StructTypeComponents(TypeHandle(TypeOf($strValue))); $filteredMembers = ListFilter($members, ($x) -> { $type = $x.Type; -- Remove the optional top-level type $cleanType = If (TypeKind($type) == "Optional", OptionalItemType($type), $type); -- Leave only String and String? columns RETURN TypeKind($cleanType) == "Data" AND DataTypeComponents($cleanType)[0] == "String"; }); -- Generate an expression in the format $func(column1) OR $func(column2)... RETURN Yql::Fold($filteredMembers, ReprCode(false), ($item, $state) -> { $member = FuncCode("Member", $strCode, AtomCode($item.Name)); $apply = FuncCode("Apply", QuoteCode($func), $member); RETURN FuncCode("Or", $state, $apply); }); })); RETURN $code($strValue); }; SELECT * FROM `home/yql/tutorial/users` WHERE $containsAnyColumns(TableRow(), "comment"); ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListFilter](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [TableRow](https://ytsaurus.tech/docs/builtins/basic.md#tablerow) ### Merging structures left-to-right ```yql -- Remove the high-level Optional from the data type $removeOptional = ($type) -> { RETURN IF(TypeKind($type) == "Optional", OptionalItemType($type), $type); }; $combineMembersLeft = ($str1, $str2) -> { $code = EvaluateCode(LambdaCode(($str1code, $str2code) -> { -- Get lists of fields from both structures $leftMembers = StructTypeComponents($removeOptional(TypeHandle(TypeOf($str1)))); $rightMembers = StructTypeComponents($removeOptional(TypeHandle(TypeOf($str2)))); -- Generate field retrieval from the left structure $members1 = ListMap($leftMembers, ($x) -> { $atom = AtomCode($x.Name); RETURN ListCode($atom, FuncCode("Member", $str1code, $atom)); }); -- Collect the names of fields in the left structure into a set to facilitate search $leftNames = ToSet(ListExtract($leftMembers, "Name")); -- Generate retrieval of fields from the right structure unless they are already in the left structure $members2 = ListFlatMap($rightMembers, ($x) -> { $atom = AtomCode($x.Name); $member = ListCode($atom, FuncCode("Member", $str2code, $atom)); RETURN IF(DictContains($leftNames, $x.Name), NULL, $member); }); -- Generate a final structure RETURN FuncCode("AsStruct", ListExtend($members1, $members2)); })); RETURN $code($str1, $str2); }; SELECT $combineMembersLeft( Just(AsStruct(1 as a, 2 as b)), AsStruct(3 as c, 4 as b) ); -- (a:1, b:2, c:3) ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) ### Reading a column by the name written to another column ```yql $input = AsList( AsStruct(1 as key, "value1" as index, "foo" as value1), AsStruct(2 as key, "value2" as index, "bar" as value2), AsStruct(3 as key, "value3" as index) ); -- Build a Lambda function that returns, for the input table row, two columns - a key copied unchanged and a value -- retrieved from a column whose name is written to the index column. $makeDynamicAccessor = ($keyColumn, $indexColumn, $valueColumn) -> { RETURN ($row) -> { RETURN EvaluateCode(LambdaCode(($rowCode) -> { -- Build a list of all fields except the key field and index field $names = ListExtract(StructTypeComponents(TypeHandle(TypeOf($row))), "Name"); $names = ListFilter($names, ($x) -> { return $x != $keyColumn and $x != $indexColumn }); -- Build a dictionary from the column name contained in variant $dict = EvaluateCode(FuncCode("AsDict", ListMap($names, ($x) -> { RETURN ListCode(ReprCode($x), FuncCode("Variant", FuncCode("Void"), AtomCode($x), FuncCode("VariantType", FuncCode("StructType", ListMap($names, ($x) -> { RETURN ListCode(AtomCode($x), FuncCode("VoidType")) }))))) }))); -- Index column value $indexValue = FuncCode("Member", $rowCode, AtomCode($indexColumn)); -- Take the column value unchanged $keyMember = ListCode(AtomCode($keyColumn), FuncCode("Member", $rowCode, AtomCode($keyColumn))); -- Search in the dictionary $enumLookup = FuncCode("Lookup", ReprCode($dict), $indexValue); -- Process the value found in the dictionary $value = FuncCode("FlatMap", $enumLookup, LambdaCode(($x) -> { -- Build a list of handlers for each column name $handlers = ListFlatMap($names, ($name) -> { RETURN AsList(AtomCode($name), LambdaCode(($u) -> { RETURN FuncCode("Member", $rowCode, AtomCode($name)); })); }); -- Apply a visitor to the found value RETURN FuncCode("Visit", $x, $handlers); })); $valueMember = ListCode(AtomCode($valueColumn), $value); -- Build an output structure of the key and value RETURN FuncCode("AsStruct", AsList($keyMember, $valueMember)); }))($row); }; }; SELECT * FROM ( SELECT $makeDynamicAccessor("key","index","value")(TableRow()) FROM AS_TABLE($input) ) FLATTEN COLUMNS ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) ### Calculate a sum across tuple items ```yql $t = AsTuple(1,2,3); $sumTuple = ($tupleValue) -> { $code = EvaluateCode(LambdaCode(($tupleCode) -> { -- Count the tuple size $count = ListLength(TupleTypeComponents(TypeHandle(TypeOf($tupleValue)))); -- Generate the sum $pair = Yql::Fold(ListFromRange(0ul, $count), AsTuple(ReprCode(0), 0), ($item, $state) -> { -- $state is a pair of the sum and current index taken from the code RETURN AsTuple( FuncCode("+", $state.0, FuncCode("Nth", $tupleCode, AtomCode( CAST($state.1 as string)))), $state.1 + 1 ) }); RETURN $pair.0; })); RETURN $code($tupleValue); }; SELECT $sumTuple($t); ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) ### Aggregating values of all numeric columns Select all the numeric columns in a table, apply several aggregate functions to them, and write the values to output columns, adding the name of the aggregate function to the output column names. ```yql USE ; -- Remove the top-level Optional from the data type $removeOptional = ($type) -> { RETURN IF(TypeKind($type) == "Optional", OptionalItemType($type), $type); }; -- Filter only numeric columns and put them in the nums struct $filterOnlyNumericColumns = ($strValue) -> { $code = EvaluateCode(LambdaCode(($str) -> { $members = StructTypeComponents(TypeHandle(TypeOf($strValue))); $filteredMembers = ListFilter($members, ($x) -> { $type = $x.Type; $cleanType = $removeOptional($type); RETURN TypeKind($cleanType) == "Data" AND DataTypeComponents($cleanType)[0] REGEXP "Int[0-9]+|Uint[0-9]+|Float|Double" }); $list = ListMap($filteredMembers, ($x) -> { RETURN ListCode(AtomCode($x.Name), FuncCode("Member", $str, AtomCode($x.Name))); }); RETURN FuncCode("AsStruct",$list); })); RETURN $code($strValue); }; $nums = (SELECT $filterOnlyNumericColumns(TableRow()) as nums FROM `home/yql/tutorial/users`); SELECT * FROM $nums; -- Apply the set aggregate function to the entire nums struct at once. -- Aggregation results are nested structures for the columns count/min/... $agg = (SELECT MULTI_AGGREGATE_BY(nums, AggregationFactory("count")) as count, MULTI_AGGREGATE_BY(nums, AggregationFactory("min")) as min, MULTI_AGGREGATE_BY(nums, AggregationFactory("max")) as max, MULTI_AGGREGATE_BY(nums, AggregationFactory("avg")) as avg, MULTI_AGGREGATE_BY(nums, AggregationFactory("percentile", 0.9)) as p90 FROM $nums); SELECT * FROM $agg; -- Expand the structures so that the columns have the format originalcolumn_aggrfunc. -- You can't manage with FLATTEN COLUMNS here only, because there are conflicts: in count/min, etc. -- the nested fields are identical. $rotateStruct = ($value) -> { $code = EvaluateCode(LambdaCode(($strValue) -> { -- Retrieve the top-level structure columns $topMembers = StructTypeComponents(TypeHandle(TypeOf($value))); $list = ListFlatMap($topMembers, ($x) -> { $topMember = FuncCode("Member", $strValue, AtomCode($x.Name)); -- Traverse fields in the structures $nestedMembers = StructTypeComponents($removeOptional($x.Type)); RETURN ListMap($nestedMembers, ($y) -> { RETURN ListCode(AtomCode($y.Name || "_" || $x.Name), FuncCode("Member", $topMember, AtomCode($y.Name))); }) }); RETURN FuncCode("AsStruct", $list); })); RETURN $code($value); }; $rotate = (SELECT $rotateStruct(TableRow()) FROM $agg); SELECT * FROM $rotate; ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListFilter](https://ytsaurus.tech/docs/builtins/list.md#listfilter) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [MULTI_AGGREGATE_BY](https://ytsaurus.tech/docs/builtins/aggregation.md#multiaggregateby) * [AggregationFactory](https://ytsaurus.tech/docs/builtins/basic.md#aggregationfactory) ### Transposing a table (PIVOT) ```yql -- Sample data $input = AsList( AsStruct("2012" as year, 1 as key, 3.0 as value), AsStruct("2013" as year, 1 as key, 4.0 as value), AsStruct("2013" as year, 2 as key, 5.0 as value), AsStruct("2014" as year, 1 as key, 6.0 as value), AsStruct("2014" as year, 1 as key, 7.0 as value) ); $makePivot = ($nameColumn, $valueColumn, $nameList) -> { RETURN EvaluateCode(LambdaCode(($row) -> { $name = FuncCode("Member", $row, AtomCode($nameColumn)); $value = FuncCode("Member", $row, AtomCode($valueColumn)); $row = FuncCode("RemoveMember", $row, AtomCode($nameColumn)); $row = FuncCode("RemoveMember", $row, AtomCode($valueColumn)); $ensureOptional = ($x) -> { RETURN YQL::MatchType($x, AsAtom("Optional"), () -> { RETURN $x }, () -> { RETURN Just($x) }); }; $structItems = ListMap($nameList, ($item) -> { $adjustedValue = FuncCode("Apply", QuoteCode($ensureOptional), $value); $ifValue = FuncCode("FlatOptionalIf", FuncCode("Coalesce", FuncCode("==", $name, ReprCode($item)), ReprCode(false)), $adjustedValue); RETURN ListCode(AtomCode($item), $ifValue); }); $struct = FuncCode("AsStruct", $structItems); RETURN FuncCode("AddMember", $row, AtomCode($valueColumn), $struct); })); }; -- Build a PIVOT operation: -- 1. The `value` field is populated with the structure where you created the columns from the last list. -- 2. Depending on `year` field value, a certain field in the output `value` structure is populated. -- 3. After that, the `year` field is deleted $pivot = $makePivot("year", "value", AsList("2012", "2013", "2014")); -- Source data SELECT * FROM AS_TABLE($input); $x = (SELECT * FROM ( SELECT $pivot(TableRow()) FROM AS_TABLE($input) ) FLATTEN COLUMNS); -- Result SELECT * FROM ( SELECT AsStruct(key as key), MULTI_AGGREGATE_BY(value, AggregationFactory("sum")) FROM $x GROUP BY key ) FLATTEN COLUMNS; ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [MULTI_AGGREGATE_BY](https://ytsaurus.tech/docs/builtins/aggregation.md#multiaggregateby) * [AggregationFactory](https://ytsaurus.tech/docs/builtins/basic.md#aggregationfactory) ### Merging subquery templates ```yql $combineQueries = ($query, $list) -> { RETURN EvaluateCode(LambdaCode(($world) -> { -- An implicit parameter in the subquery template is the world argument used to pass -- dependencies, for example, visible PRAGMA or COMMIT operations at the subquery template point of use. $queries = ListMap($list, ($arg) -> { -- Pass world further, as the first argument of the subquery template RETURN FuncCode("Apply", QuoteCode($query), $world, ReprCode($arg)) }); -- Merge all the results into a common list (this requires that types are the same). -- For weaker merging, you can use the UnionAll function. RETURN FuncCode("Extend", $queries); })); }; DEFINE SUBQUERY $sub($n) AS SELECT $n; END DEFINE; -- Build a query template on merging -- the results of applying the $sub subquery template for each value between 0 and 9. $fullQuery = $combineQueries($sub, ListFromRange(0, 10)); SELECT * FROM $fullQuery(); ``` Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [Subquery templates](https://ytsaurus.tech/docs/syntax/subquery.md) ### Building a subquery template with sorting by a list of column ```yql USE ; $sorted = ($world, $input, $orderByColumns, $asc) -> { $n = ListLength($orderByColumns); $keySelector = LambdaCode(($row) -> { $items = ListMap($orderByColumns, ($x) -> { RETURN FuncCode("Member", $row, AtomCode($x)); }); RETURN ListCode($items); }); $sort = EvaluateCode(LambdaCode(($x) -> { return FuncCode("Sort", $x, ListCode(ListReplicate(ReprCode($asc), $n)), $keySelector) })); RETURN $sort($input($world)); }; DEFINE SUBQUERY $source() AS PROCESS `home/yql/tutorial/users`; END DEFINE; PROCESS $sorted($source, AsList("name","age"), true); PROCESS $sorted($source, AsList("name"), true); PROCESS $sorted($source, ListCreate(TypeOf("")), true); ``` The same functionality can be achieved using [SubqueryOrderBy](https://ytsaurus.tech/docs/syntax/subquery.md#subquery-order-by). Links to documentation: * [EvaluateCode](https://ytsaurus.tech/docs/builtins/codegen.md#evaluatecode) * [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) * [SubqueryOrderBy](https://ytsaurus.tech/docs/syntax/subquery.md#subquery-order-by) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [Subquery templates](https://ytsaurus.tech/docs/syntax/subquery.md) # Metaprogramming practices in YQL ## Introduction YTsaurus doesn't support partitioned tables natively. That's why typical is the situation where a logically single table is stored as a set of physical tables split by date, hour, or the division remainder of some field hash. As a result, the queries run in YQL over YTsaurus often access a huge number of tables (dozens, hundreds, or thousands). Measured by the SQL standard, this is an exclusive situation and there are no generally adopted mechanisms to describe such queries in SQL terms. Rachel offers many extensions for the standard designed for such specific tasks. In the majority of regularly or periodically ran queries, they enable you to totally skip generation of query texts on the client. Each mechanism [is detailed in the main YQL syntax tutorial](https://ytsaurus.tech/docs/syntax/index.md), and the purpose of this article is to demonstrate how you can combine these mechanisms to achieve practically useful results on near-to-real examples. ## Use cases ### Selecting tables by dates Most logs are archived to a flat directory in YTsaurus where an independent table is created for each date. If you want to make a calculation for certain dates, but don't want to add them manually to the query text, use the `FILTER` table function and select the dates with a Lambda function. ```yql USE ; $log_type = "yql-log"; $base_folder = "logs/" || $log_type || "/1d"; $today = CurrentUtcDate(); $yesterday = $today - Interval("P1D"); $a_week_ago = $today - Interval("P1W"); $is_during_last_week = ($table_name) -> { $date = CAST($table_name AS Date); RETURN ($date BETWEEN $a_week_ago AND $yesterday) ?? false; }; SELECT COUNT(*) FROM FILTER($base_folder, $is_during_last_week); ``` Links to documentation: * [CurrentUtcDate](https://ytsaurus.tech/docs/builtins/basic.md#currentutcdate) * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [FILTER](https://ytsaurus.tech/docs/syntax/select/index.md) ### Processing tables with a cached sliding window Imagine that every day you need to calculate some aggregate of values for the previous week (like in the previous example). By their very nature, most log tables don't change beginning from the night of the next day. Hence, nearly 6/7 of computing resources every day are wasted to repeat calculations from the previous day. YQL offers a mechanism for [automatic caching of intermediate computing results](https://ytsaurus.tech/docs/syntax/pragma/yt#querycachemode). However, as it wholly runs at the MapReduce level, the previous query would have zero cache hit: every day will produce a different set of input tables for the first Map operation (and this set is used to calculate a key in the cache). To give the automatic caching mechanism a chance to succeed (or even guarantee success, if you override [PRAGMA yt.TmpFolder](https://ytsaurus.tech/docs/syntax/pragma/yt#tmpfolder) to your custom directory/quota instead of `//tmp`), you need to calculate an aggregate for each day separately and then combine the aggregates into a single resulting value (fortunately, most aggregate functions support this). ```yql USE ; $log_type = "visit-log"; $base_folder = "logs/" || $log_type || "/1d/"; $today = CurrentUtcDate(); $range = ListFromRange(1, 9); $shift_back_by_days = ($days_count) -> { RETURN Unwrap( $today - $days_count * Interval("P1D"), "Failed to shift today back by " || CAST($days_count AS String) || " days" ); }; $dates = ListMap($range, $shift_back_by_days); DEFINE ACTION $max_income($date) AS $table_path = $base_folder || CAST($date AS String); INSERT INTO @daily_results SELECT MAX(CAST(Income AS Double)) AS MaxIncome FROM $table_path; END DEFINE; EVALUATE FOR $date IN $dates DO $max_income($date); COMMIT; SELECT MAX(MaxIncome) AS MaxIncome FROM @daily_results; DISCARD SELECT Ensure( NULL, COUNT(*) == 7, "Unexpected number of aggregates to be merged: " || CAST(COUNT(*) AS String) || " instead of 7" ) FROM @daily_results; ``` In contrast to the previous example, here we don't select table paths based on the directory contents. Instead, we generate them "out of thin air" based on the table naming conventions configured for this log. We first define an action to process each table separately (`DEFINE ACTION`) and then use a loop to apply this action to each calculated date (`EVALUATE FOR`). This query also demonstrates several ways to terminate a query with a meaningful error if something strange occurs, instead of producing an invalid result: * Going beyond the range of supported values is almost impossible for this use case. However, only in these situations, the date arithmetic can return NULL. In this context, it's quite safe to use Unwrap to convert an Optional value to a non-Optional one (exactly not NULL), producing a runtime error if the Optional type, nevertheless, included NULL. * Using Ensure, you can check any conditions when running a query and return an error if the condition isn't met. You can add it at runtime, but if you combine it with `DISCARD` (calculate something and discard the result), you can make a separate side-check, for example, based on an aggregate of a temporary or summary table as here. In the case with `DISCARD`, the first argument is Ensure, which is usually a result of its call. It doesn't bear much sense, so you can write any constant to it, for example, NULL. Links to documentation: * [Lambda functions](https://ytsaurus.tech/docs/syntax/expressions.md#lambda) * [CurrentUtcDate](https://ytsaurus.tech/docs/builtins/basic.md#currentutcdate) * [ListFromRange](https://ytsaurus.tech/docs/builtins/list.md#listfromrange) / [ListMap](https://ytsaurus.tech/docs/builtins/list.md#listmap) / [ListLength](https://ytsaurus.tech/docs/builtins/list.md#listlength) * [Unwrap](https://ytsaurus.tech/docs/builtins/basic.md#unwrap) * [DEFINE ACTION](https://ytsaurus.tech/docs/syntax/action.md) * [EVALUATE FOR](https://ytsaurus.tech/docs/syntax/action.md#evaluate-for) * [@foo](https://ytsaurus.tech/docs/syntax/select/temporary_table.md) * [COMMIT](https://ytsaurus.tech/docs/syntax/commit.md) * [DISCARD](https://ytsaurus.tech/docs/syntax/discard.md) * [Ensure](https://ytsaurus.tech/docs/builtins/basic.md#ensure) ### Use case with job processing For example, you have an external process that writes tables to a directory. Basically, these tables are jobs to be processed. You need to run a computation on the contents of such table, write the result to another directory, and delete the processed tables: all this in the transactional mode. {% cut "How can you emulate such an external process on YQL for testing purposes?" %} ```yql USE ; $root_folder = "//tmp/tasks/"; $values = ListFromRange(1, 10); DEFINE ACTION $create_task($i) AS $path = $root_folder || CAST($i AS String); INSERT INTO $path SELECT $i AS Task; END DEFINE; EVALUATE FOR $value IN $values DO $create_task($value); ``` {% endcut %} First, start the processing ```yql USE ; $tasks_folder = "tmp/tasks"; $results_folder = "tmp/results"; DEFINE ACTION $process_task($input_path) AS $output_path = String::ReplaceAll($input_path, $tasks_folder, $results_folder); INSERT INTO $output_path WITH TRUNCATE SELECT Task * Task AS Result FROM $input_path; COMMIT; DROP TABLE $input_path; END DEFINE; $tasks = ( SELECT AGGREGATE_LIST(Path) FROM FOLDER($tasks_folder, "row_count") WHERE Type == "table" AND Yson::LookupInt64(Attributes, "row_count") == 1 ); EVALUATE FOR $task IN $tasks ?? ListCreate(TypeOf($tasks)) DO $process_task($task); ``` Here, you also use the mechanism of actions that loop through a list. However, the list of input tables isn't taken "out of thin air", but from the `FOLDER` table function that provides access to the contents of an arbitrary directory in YTsaurus. Since `FOLDER` outputs not only tables but other types of nodes as well, meaning you can't be sure that the directory hosts only tables, we recommend adding a filter by Type or by arbitrary meta attributes of the table (we use row_count here as an example). For this, you'll need to query them in the second `FOLDER` argument (if you have several of them, use a semicolon) and then access the Attributes Yson-type column. ListExtract takes a list of structures that include a single element and converts it to a list of strings with paths. However, it's also possible to retrieve Path from inside the action. Links to documentation: * [String UDF](https://ytsaurus.tech/docs/udf/list/string.md) / [Yson UDF](https://ytsaurus.tech/docs/udf/list/yson.md) * [ListFromRange](https://ytsaurus.tech/docs/builtins/list.md#listfromrange) / [ListExtract](https://ytsaurus.tech/docs/builtins/list.md#listextract) * [FOLDER](https://ytsaurus.tech/docs/syntax/select/folder.md) * [DROP TABLE](https://ytsaurus.tech/docs/syntax/drop_table.md) # Accessing values inside JSON with YQL YQL provides two main methods for extracting values from JSON: - Using [**JSON functions from the SQL standard**](https://ytsaurus.tech/docs/builtins/json.md). This approach is recommended for simple cases and for teams that are familiar with them from other DBMSs. - Using [**YSON UDFs**](https://ytsaurus.tech/docs/udf/list/yson.md), built-in functions for [lists](https://ytsaurus.tech/docs/builtins/list.md), [dicts](https://ytsaurus.tech/docs/builtins/dict.md), and [lambdas](https://ytsaurus.tech/docs/syntax/expressions.md#lambda). This approach is more flexible and is tightly integrated with YQL's data type system, making it a better choice for complex cases. Below are recipes that use the same input JSON to demonstrate how to use each option to check if a key exists, get a specific value, and extract a subtree. ## JSON functions ```yql $json = @@{ "friends": [ { "name": "James Holden", "age": 35 }, { "name": "Naomi Nagata", "age": 30 } ] }@@j; SELECT JSON_EXISTS($json, "$.friends[*].name"), -- True CAST(JSON_VALUE($json, "$.friends[0].age") AS Int32), -- 35 JSON_QUERY($json, "$.friends[0]"); -- {"name": "James Holden", "age": 35} ``` `JSON_*` functions expect the `Json` data type as input. In this example, the string literal has the `j` suffix, marking it as `Json`. Tables can store data either in JSON format or as a string representation. To convert data from `String` to `JSON`, use the `CAST` function: for example, `CAST(my_string AS JSON)`. ## Yson UDF This approach typically combines multiple functions and expressions, so one query can include different strategies. ### Converting the whole JSON into YQL containers ```yql $json = @@{ "friends": [ { "name": "James Holden", "age": 35 }, { "name": "Naomi Nagata", "age": 30 } ] }@@j; $containers = Yson::ConvertTo($json, Struct>>); $has_name = ListAny( ListMap($containers.friends, ($friend) -> { return $friend.name IS NOT NULL; }) ); $get_age = $containers.friends[0].age; $get_first_friend = Yson::SerializeJson(Yson::From($containers.friends[0])); SELECT $has_name, -- True $get_age, -- 35 $get_first_friend; -- {"name": "James Holden", "age": 35} ``` You **don't** have to convert the whole JSON object into a structured combination of containers. Some fields may be omitted if not used, and some subtrees may be left in an unstructured data type like `Json`. ### Working with in-memory representation ```yql $json = @@{ "friends": [ { "name": "James Holden", "age": 35 }, { "name": "Naomi Nagata", "age": 30 } ] }@@j; $has_name = ListAny( ListMap(Yson::ConvertToList($json.friends), ($friend) -> { return Yson::Contains($friend, "name"); }) ); $get_age = Yson::ConvertToInt64($json.friends[0].age); $get_first_friend = Yson::SerializeJson($json.friends[0]); SELECT $has_name, -- True $get_age, -- 35 $get_first_friend; -- {"name": "James Holden", "age": 35} ``` ## See also - [Modifying JSON with YQL](https://ytsaurus.tech/docs/en/modifying-json.md) # Modifying JSON with YQL In memory, YQL works with immutable values. When a query needs to change something inside a JSON value, think of it as constructing a new value from parts of the old one. This sample query takes an input JSON named `$fields`, parses it, substitutes the `a` key with 0, deletes the `d` key, and adds the `c` key with value 3: ```yql $fields = '{"a": 1, "b": 2, "d": 4}'j; $pairs = DictItems(Yson::ConvertToInt64Dict($fields)); $result_pairs = ListExtend(ListNotNull(ListMap($pairs, ($item) -> { $item = if ($item.0 == "a", ("a", 0), $item); return if ($item.0 == "d", null, $item); })), [("c", 3)]); $result_dict = ToDict($result_pairs); SELECT Yson::SerializeJson(Yson::From($result_dict)); ``` ## See also - [Yson](https://ytsaurus.tech/docs/udf/list/yson.md) - [Functions for lists](https://ytsaurus.tech/docs/builtins/list.md) - [Functions for working with dicts](https://ytsaurus.tech/docs/builtins/dict.md) - [Accessing values inside JSON with YQL](https://ytsaurus.tech/docs/en/accessing-json.md) # YQL differences This section describes how YQL differs from other query languages and systems. - [Differences from ANSI SQL](https://ytsaurus.tech/docs/en/ansi.md) - [Differences from ClickHouse](https://ytsaurus.tech/docs/en/clickhouse.md) # Distinctions of YQL SQL dialect from ANSI SQL This section describes distinctions of YQL SQL dialect and the ISO/IEC 9075-2 standard (Part 2: Foundation) in the version SQL-2011. To make the comparison, we took table 38 from the standard ("Feature taxonomy and definition for mandatory features"). ## Distinctions in behavior between YQL and ANSI SQL In this table, we've put together the scenarios where the same query produces different results in YQL and ANSI SQL. | ID | Name | Behavioral distinctions | | --- | --- | --- | | E011-04 | Arithmetic operators | YQL suppresses errors in arithmetic calculations. For example, the expression `1/0` has the type `Int32?` and the value `NULL`. ANSI SQL will throw an error in this case: | | E011-06 | Implicit casting among the numeric data types | YQL doesn't provide implicit casting between NUMERIC and INTEGER/FLOAT. | | E031-01 | Delimited identifiers | In YQL, the expression `SELECT "a" FROM foo` returns a string constant. In ANSI, `"a"` is treated as the ID (the `a` column) | | E071-02 | UNION ALL table operator | In ANSI SQL, columns are joined according to their positions; in YQL, they are joined by names. Adding the `PositionalUnionAll` pragma enables the standard columnar execution for UNION ALL. In this case, column ordering is enabled automatically. | ## ANSI SQL support status | ID | Name | Support | Comment | | --- | --- | --- | --- | | **E011** | **Numeric data types** | **Partial** | | | E011-01 | INTEGER and SMALLINT data types | Yes | Signed and unsigned integral types are additionally supported: `[U]INT{8,16,32,64}` and `TINYINT`. | | E011-02 | REAL, DOUBLE PRECISION and FLOAT data types data types | Partial | `FLOAT()`isn't supported | | E011-03 | DECIMAL and NUMERIC data types | Yes | `DECIMAL(p,s)` is supported | | E011-04 | Arithmetic operators | Yes | | | E011-05 | Numeric comparison | Yes | | | E011-06 | Implicit casting among the numeric data types | Partial | The standard allows for implicit mutual casting between all the numeric types. We only implicitly cast "related" types. | | [**E021**](https://ytsaurus.tech/docs/en/####E021%20Character%20string%20types) | **Character string types** | **Partial** | `CHAR(len)`, `VARCHAR(len)` is not supported | | E021-01 | CHARACTER data type | No | | | E021-02 | CHARACTER VARYING data type | Partial | The closest analog is `String` | | E021-03 | Character literals | Partial | Automatic `'aaa' 'bbb'` concatenation isn't supported | | E021-04 | CHARACTER_LENGTH function | No | | | E021-05 | OCTET_LENGTH function | Partial | The closest analog is `LEN()` | | E021-06 | SUBSTRING | Partial | Distinctions in syntax | | E021-07 | Character concatenation | Yes | Supported for the String type | | E021-08 | UPPER and LOWER functions | No | Available using String UDF | | E021-09 | TRIM function | No | Can be implemented using String UDF | | E021-10 | Implicit casting among the fixed-length and variable-length character string types | No | Relevant types are missing | | E021-11 | POSITION function | Partial | The counterpart is `FIND` | | E021-12 | Character comparison | Partial | String type is compared bitwise | | **E031** | **Identifiers** | **Partial** | | | E031-01 | Delimited identifiers | Partial | YQL uses backticks for IDs rather than double quotes prescribed by the standard | | E031-02 | Lower case identifiers | Yes | | | E031-03 | Trailing underscore | Yes | | | **E051** | **Basic query specification** | **Partial** | | | E051-01 | SELECT DISTINCT | Yes | | | E051-02 | GROUP BY clause | Yes | | | E051-04 | GROUP BY can contain columns not in \
::= DISTINCT | ALL [ { ::= | ::= | ::= [ { }... ] ::= ::= [ ] ::= [ AS ] ::= [ AS ] ::=
::= [ ] [ ] [ ] [ ] ::= FROM
::=
[ {
}... ] ``` YQL doesn't support multiple tables after `FROM` (`
` includes more than one item) ##### E051-01 SELECT DISTINCT SELECT DISTINCT is supported except for the variant with asterisk/qualified asterisk ##### E051-07 Qualified * in select list In YQL: * `` can only consist of one item * `` doesn't support aliases `` * `` can only include a link to a table ##### E061-01 Comparison predicate YQL supports operators <,>,=,<>,<=,>= for compatible types ##### E061-07 Quantified comparison predicate ``` ::= ::=
::= | ::= ALL ::= SOME | ANY ``` # Differences between ClickHouse and YQL dialects of SQL ## Terminology * A ClickHouse `ARRAY` is called a `List` in YQL, with the same naming difference applying to most functions used to handle them. * The ClickHouse `ARRAY JOIN` operation is called [FLATTEN BY](https://ytsaurus.tech/docs/syntax/flatten.md) in YQL. * From the YQL perspective, the data type of both physical and nested (Nested in ClickHouse) logical tables is `List>`. In YQL, [container data types](https://ytsaurus.tech/docs/types/containers.md) can be combined in any way. ## Syntax * In ClickHouse, you can assign an alias to almost any part of an expression using `AS` and then reference that alias in another part of the query. In contrast, YQL is very strict about the scope of columns in different parts of a [SELECT](https://ytsaurus.tech/docs/syntax/select/index.md) statement. # YQL FAQ This section answers common questions about YQL errors, performance, and queries. - [Errors](https://ytsaurus.tech/docs/en/errors.md) - [Performance](https://ytsaurus.tech/docs/en/performance.md) - [Queries](https://ytsaurus.tech/docs/en/queries.md) - [Temporary tables](https://ytsaurus.tech/docs/en/temp.md) # Errors ## Expected comparable type, but got: Yson This might happen if the table has been created in YTsaurus without a schema and then sorted. In this case, the system (YT) assigns the `any` type to the sort column in the table schema. In YQL, this is equivalent to the `Yson` column type (arbitrary YSON data). For example, for this query: ```yql SELECT * FROM my_table WHERE key = 'www.youtube.com/watch?v=Xn599R0ZBwg'; ``` ... you can make a workaround: ```yql SELECT * FROM my_table WHERE Yson::ConvertToString(Yson::Parse(key)) = 'www.youtube.com/watch?v=Xn599R0ZBwg'; ``` **The drawback of this solution:** The queries used in the examples output all the rows that match a certain key. If the table has been sorted by this key, the query execution time is usually independent of the table size. But for this workaround, such optimization won't work, and you'll have to scan the entire table. {% note info "Note" %} This issue doesn't occur with tables that have a valid schema. Configure the schema if you know how to do it. If not, ask the people responsible for the tables to do it for you. {% endnote %} More [about Yson UDF](https://ytsaurus.tech/docs/udf/list/yson.md). ## Cannot add type String (String?) and String (String?) This error arises when you try to concatenate strings using the '+' operator. Use the '||' operator instead: ```yql SELECT "Hello, " || "world" || "!"; ``` ## Expression has to be an aggregation function or key column, because aggregation is used elsewhere in this subquery This issue most often arises when you try to use aliases used in your `SELECT` clause, further in the `HAVING` clause. Aliases are names of fields that follow `AS` in a `SELECT` statement. They define a projection and are applied after the `HAVING` clause (that's why you can't use them in HAVING). An example of an invalid query (such a query **WON'T** work): ```yql USE ; SELECT region, COUNT(age) AS counter FROM `home/yql/tutorial/users` GROUP BY region HAVING counter > 3 ``` To avoid calling aggregate functions twice, add a subquery, moving the logic from the `HAVING` to the `WHERE` clause. ```yql USE ; SELECT * FROM (SELECT region, COUNT(age) AS counter FROM `home/yql/tutorial/users` GROUP BY region) WHERE counter > 3 ``` ## Value type "double" is not a number The reason for this error is, most often, division by zero: it results in an NaN value (not a number) not supported by YTsaurus in some cases. ## Row weight is too large This error occurs when the maximum memory allocated for a row is exhausted. Use the relevant [PRAGMA](https://ytsaurus.tech/docs/syntax/pragma#yt) to increase the maximum row length in your YTsaurus tables. The default value is "16M", the maximum value is "128M". If you exceed the maximum value, change your computing logic, for example, split your table into parts or apply compression algorithms ([Compress UDF](https://ytsaurus.tech/docs/udf/list/compress_decompress.md)) ```yql PRAGMA yt.MaxRowWeight = "32M"; ``` ## Key weight is too large This error arises when the value in a key field of the table is too large. One of the methods to resolve the issue is to use a [PRAGMA](https://ytsaurus.tech/docs/syntax/pragma#yt) that increases the maximum length of key table fields in YTsaurus, where fields are used for `ORDER BY`, `GROUP BY`, `PARTITION BY`, `DISTINCT`, `REDUCE`. The default value is "16K" and the maximum value is "256K". ```yql PRAGMA yt.MaxKeyWeight = "32K"; ``` ## The same table column includes values of different types {#badtabledata} When processing such tables, you might encounter different errors depending on query settings: * ``` ReadYsonValue(): requirement cmd == Int64Marker failed, message: Expected char: "\2", but read: "\1"``` * ``` { 'Table input pipe failed' { 'Unexpected type of "score" column, expected: "int64" found "uint64"' } }``` To read or process such a table, you need to patch the column type to `Yson?` using [WITH COLUMNS](https://ytsaurus.tech/docs/syntax/select/with.md) and then parse the data manually using [Yson UDF](https://ytsaurus.tech/docs/udf/list/yson.md): ```yql SELECT Yson::ConvertToDouble(bad_column, Yson::Options(false as Strict)) ?? CAST(Yson::ConvertToString(bad_column, Yson::Options(false as Strict)) as Double) as bad_column FROM `path/to/bad_table` WITH COLUMNS Struct; ``` If the types can be converted automatically, you can do like this: ```yql SELECT Yson::ConvertToDouble(bad_column, Yson::Options(true as AutoConvert)) as bad_column FROM `path/to/bad_table` WITH COLUMNS Struct; ``` ## Access denied for user <>: "read" permission for node <> is not allowed by any matching ACE {#symlinkaccess} ### Requesting rights in YTsaurus Check whether you can open the table in the YTsaurus interface. If you don't have access rights, request them in the web interface YTsaurus or in the CLI. ### Rights for table symlinks If you can open the table in the YTsaurus interface but YQL can't see it, the issue may be in symlinks to tables, when the user has access rights for the underlying table but doesn't have a read access to its symlink. To resolve the underlying table in YTsaurus, you don't need read access to its symlink: that's why in YTsaurus the contents of the table would display normally. However, besides resolving the table, YQL also reads its symlink attributes because the attributes can override, for example, the table schema. To resolve this issue, request the rights to read the symlink. ## Maximum allowed data weight per sorted job exceeds the limit {#key_monster} In most cases, the error arises when executing a `JOIN` if its inputs include a monster key (a huge number of records with the same key value). To check for a monster key, click the red circle in the query execution plan and drill down to the failed YTsaurus operation. After that, in the YTsaurus interface, open the `Partition sizes` tab of the failed operation. ![](https://ytsaurus.tech/_images/partition_sizes.png) If you see large partitions (more than 200 GB), this indicates that you have monster keys. To find the value of the problem key, expand the error message in the operation's `Details` tab. Most probably, you'll find it there. ![](https://ytsaurus.tech/_images/max_allowed_data_error.png) In this example, the problem key is an empty string. The main way to resolve this issue is to filter the problem key values at JOIN inputs. Monster keys typically include irrelevant values (NULL, empty string) that you can safely filter out. If the values of monster keys are critical for you, process them separately. Besides JOIN, such an error may also arise in the following cases: 1. Huge amounts of data in `GROUP BY`, `JOIN`. Solution: try to decrease the data amount. 2. The [window](https://ytsaurus.tech/docs/syntax/window) includes the entire table. Solution: review the partitioning conditions for the window function 3. Huge amounts of data for certain values of the `GROUP BY` key. The method used to find a problem key in this case is the same as for `JOIN`. Solution: filter out the problem key, add more columns to the group key. ## Reading multiple times from the same source is not supported DQ can't execute the query because the resulting query execution plan splits the input into multiple streams. YTsaurus does not currently support this behavior, and you can't correctly execute this query. ## Strict Yson type is not allowed to write, please use Optional This is a current YTsaurus restriction that doesn't allow writing non-optional Yson columns in `type_v3` mode. The error occurs when using the [yt.UseNativeYtTypes](https://ytsaurus.tech/docs/syntax/pragma#yt-usenativeyttypes) pragma in the query. Please note that this pragma is enabled by default. To fix the error: - Make sure that the query doesn't explicitly generate Yson-type data at any level (for example, the `List` type can't be written either). - Make sure that your query tables don't contain any columns with non-optional Yson at any level. - Some aggregate functions (for example, `AGG_LIST`) inherently remove optionality from their elements. Even if the original column was of the `Optional` type, `AGG_LIST` will result in the `List` type that can't be written. In this case, convert the column type into any other type before executing `AGG_LIST`. - You can also disable the `PRAGMA yt.UseNativeYtTypes` pragma. Please note that if you disable the pragma, you will lose the benefits of strong typing, since native types will be represented as strings. # Performance ## Why do queries with a JOIN clause take so long?{#perf_join} All the basic [JOIN](https://ytsaurus.tech/docs/syntax/join.md) types (except SEMI/ONLY) build a Cartesian product of rows by the matching key values. If one or more key values are singular, it might have many rows on each side and result in a huge Cartesian product. This is the most common reason behind slow JOINs, and since such singular values don't have much practical value, you can exclude them using the `WHERE` clause. To find such frequent anomalies, you can execute two queries against each table with a GROUP BY the same keys as in `JOIN`, using the `COUNT` aggregate function and descending sorting by its result. You can also add `LIMIT 100` to speed up the process. Sometimes, your query won't actually need a Cartesian product in `JOIN` because in the end you'll only be interested in distinct values. In this case, eliminate duplicates in your subquery using [GROUP BY](https://ytsaurus.tech/docs/syntax/group_by.md) or [SELECT DISTINCT](https://ytsaurus.tech/docs/syntax/select/distinct.md) even before `JOIN`. ## My query used to take N hours, but now it takes M hours. Why is that? {#perf_slowdown} When analyzing performance, consider the following factors: ### Has the input data volume changed? For example, more tables were added to the query or their volume changed significantly. To check if the input data volume changed, click the first YTsaurus operation in the query plan and view `Data flow`/`Data weight` in the YTsaurus interface: ![](https://ytsaurus.tech/_images/data_weight.png) ### Has the query execution plan changed? A plan can change for several reasons: * A different `JOIN` strategy was applied. To learn more about triggering criteria for a particular strategy, see the [article](https://ytsaurus.tech/docs/syntax/join.md#join_strategy). * The schema of some input tables in the folder changed. YQL aligns the schema of input tables, which creates additional operations. * YQL optimizers changed in the new version. ### Does the query use a pool with dedicated guarantees? The query must specify the [yt.StaticPool](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytstaticpool) pragma to apply the pool. If the pragma is missing, the query execution time isn't guaranteed in any way. Neighboring queries and seasonal cluster load fluctuations can affect execution. You should also make sure that the pool is set by the [yt.StaticPool](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytstaticpool) pragma, not [yt.Pool](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytpool). The latter is a dynamic pragma applied at runtime. Because of this, service operations launched at the optimization stage don't use its value and are launched in the pool by default, which may result in their significant slowdown. The [yt.StaticPool](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytstaticpool) pragma applies to all operations, including service ones. ### Checking summary statistics for a query The YQL query's `Statistics` tab contains the summary query statistics: ![](https://ytsaurus.tech/_images/stat.png) First of all, note and compare the following metrics: * `yt/total/Job_CalcTime` (Sum): Useful time spent across all jobs with user code of all YTsaurus operations of the query. A change in this value, with no variation in input data volume, indicates performance changes in the YQL runtime. * `yt/total/Job_InputDecodeTime` (Sum): Time spent by the YQL decoder to read input data across all jobs of all YTsaurus operations of the query. A change in this value, with no variation in input data volume, indicates performance changes in the YQL decoder. * `yt/total/Job_OutputEncodeTime` (Sum): Time spent by the YQL encoder to write input data across all jobs of all YTsaurus operations of the query. A change in this value, with no variation in input data volume, indicates performance changes in the YQL encoder. * `yt/total/CodeGen_FullTime` (Sum): Time spent to generate code across all jobs of all YTsaurus operations of the query. A change in this value, with the same number of YTsaurus jobs, indicates code generation performance changes in the YQL runtime. If this time represents a significant portion of `Job_CalcTime`, consider disabling LLVM in the query via `pragma config.flags("LLVM", "OFF")`. * `yt/total/Join_Spill_Count` (Sum): A non-zero value of this metric indicates `JOIN` performance issues in the query. If the keys in the first side of the JOIN have a large number of writes, they can't fit into memory and are stored on disk. This causes significant degradation in the execution time of YTsaurus jobs performing the `JOIN`. To resolve this issue, you can increase the [yt.CommonJoinCoreLimit](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytcommonjoincorelimit) pragma value or swap `JOIN` sides. * `yt/total/data/input/data_weight` (Sum): Total data volume processed by all jobs of all YTsaurus operations of the query. If there are no changes in the query plan, a change in this metric indicates a change in the input data volume of the query. ### Finding the slowest YTsaurus operation in a query In the YQL query's `Progress` tab, switch to `Timeline` mode: ![](https://ytsaurus.tech/_images/timeline.png) In this mode, you can clearly see which YTsaurus operation affected the YQL query execution time the most. You can also drill down to execution details at various stages of the operation: ![](https://ytsaurus.tech/_images/pending.png) Here you can see that the operation was `Pending` for a while due to insufficient limits on the number of running operations in the pool. For further analysis, you can open a slow operation in the YTsaurus interface by clicking its name in the timeline column on the left. In a YTsaurus operation, first of all, pay attention to `Total job wall time` and `Total CPU time spent`. If these values remain largely unchanged for two YQL query runs, but `Duration` has changed, it indicates potential query pool issues. Click the name of the pool ![](https://ytsaurus.tech/_images/pool.png) and open it in the `Monitoring` tab in the YTsaurus interface to analyze its load. # Temporary data ## Directory for temporary data Using the `yt.TmpFolder` pragma, the user can set a directory for hosting temporary tables and files. This might be needed in the following cases: * There is no access to the `//tmp` directory of the YTsaurus cluster. * The query is processing tables with sensitive data. By default, temporary tables and files are saved to //tmp where any cluster user can access them. To avoid disclosure of sensitive information, protect the temporary file directory used in your queries by an ACL. ## Control of TmpFolder directory growth When you use the `yt.TmpFolder` pragma in your query, the specified directory begins to accumulate data gradually. The following subdirectories are created in it: * query_cache: [Query cache](https://ytsaurus.tech/docs/syntax/pragma#querycache). * tmp: Temporary tables that exist only while the query is running. * new_cache: YTsaurus API file cache: * YQL executables. * Custom and system UDFs. * Files attached to the query. * Tables used in MapJoin (if transmitted not in a YTsaurus-native manner). The query_cache subdirectory and its contents' time-to-live are managed by the [yt.QueryCacheMode](https://ytsaurus.tech/docs/syntax/pragma#querycache) and [yt.QueryCacheTtl](https://ytsaurus.tech/docs/syntax/pragma#ytquerycachettl) pragmas. For `yt.QueryCacheMode="disable"`, the query cache is fully disabled and this subdirectory isn't created or used. The `yt.QueryCacheTtl` pragma enables you to limit the tables' TTL in the query cache. Once the query is completed, the query's temporary tables are cleared automatically from the tmp subdirectory. Exceptions are the queries with the `yt.ReleaseTempData="never"` and tables delivered in query results as "Full result". You can manage time-to-live for these tables using the [yt.TempTablesTtl](https://ytsaurus.tech/docs/syntax/pragma#yttemptablesttl) pragma. You can override the path to temporary tables using the [yt.TablesTmpFolder](https://ytsaurus.tech/docs/syntax/pragma#yttablestmpfolder) pragma. For objects inside new_cache, time-to-live is managed by the [yt.FileCacheTtl](https://ytsaurus.tech/docs/syntax/pragma#ytfilecachettl) pragma. To exclude executable YQL and UDF files from this directory, use the [yt.BinaryTmpFolder](https://ytsaurus.tech/docs/syntax/pragma#ytbinarytmpfolder) pragma to set up a separate directory and TTL for them (see [yt.BinaryExpirationInterval](https://ytsaurus.tech/docs/syntax/pragma#ytbinaryexpirationinterval)). You can also move your tables used in MapJoin to a separate directory by the [yt.TableContentTmpFolder](https://ytsaurus.tech/docs/syntax/pragma#yt.tablecontenttmpfolder) pragma. Keep in mind, however, that such tables can include sensitive data. # Queries, syntax, UDF ## How to run a query across logs for the past day/week/month or some other relative time period? If adding dates to your query text before execution isn't convenient, do as follows: 1. Write a user-defined function (UDF) in [Python](https://ytsaurus.tech/docs/udf/python.md) or [C++](https://ytsaurus.tech/docs/udf/cpp.md) with the signature `(String)->Bool`. By the table name, this function should return TRUE only for the dates from the relevant range, using the current time or date obtained by the tools of the selected programming language. 2. Pass this UDF to the [FILTER](https://ytsaurus.tech/docs/syntax/select/index.md#func_table) function, including the prefix to the log directory. 3. Use the FILTER result anywhere where you expect a path to the input data ([FROM](https://ytsaurus.tech/docs/syntax/select/from.md), [PROCESS](https://ytsaurus.tech/docs/syntax/process.md), [REDUCE](https://ytsaurus.tech/docs/syntax/reduce.md)) Table names can use arbitrary date and time formats, and the logic of selecting suitable tables may be indefinitely complex. That's why no ad-hoc solution is provided for this task. ## How do I filter by a large list of values? Of course, you can generate a giant `WHERE` clause, but better do like this: * Generate a text file with a list of values (one per row), and then add it to the query and use [IN](https://ytsaurus.tech/docs/syntax/expressions.md#in) together with [ParseFile](https://ytsaurus.tech/docs/builtins/basic#parsefile). * Generate a table of values you want to filter by and do `LEFT SEMI JOIN` with this table. * You can generate a regular expression to check rows with the logical OR and then use the [REGEXP](https://ytsaurus.tech/docs/syntax/expressions.md#regexp) keyword or a built-in regular expression function ([Hyperscan](https://ytsaurus.tech/docs/udf/list/hyperscan.md), [Pire](https://ytsaurus.tech/docs/udf/list/pire.md), or [Re2](https://ytsaurus.tech/docs/udf/list/re2.md)). ## What is the difference between MIN, MIN\_BY, and MIN\_OF? (or MAX, MAX\_BY, and MAX\_OF) * `MIN(column)` is an SQL standard aggregate function that returns the minimal value found in the selected column. * `MIN_BY(result, by)` is a non-standard aggregate function that, in contrast to the previous function, doesn't return the minimal value itself, but some other value from the row where it found the minimum. * `MIN_OF(a, b, c, d)` is an inbuilt function that returns the minimum value among N values of its arguments. ## How do I access a column whose name includes a minus or another special character? Wrap the column name into backticks similarly to table names: ```yql SELECT `field-with-minus` FROM `table`; ``` ## What is the \_other column? This is a virtual column. It is created only by the YQL engine (the YTsaurus system is unaware of it) and only for tables with a [non-strict schema](https://ytsaurus.tech/docs/en/*schema-not-strict). This column provides access to table columns that weren't initially defined in the schema. The column has the `Dict` type. There are multiple popular methods for retrieving a table containing this column: - For example, you can create a table without a schema and then sort it. The YTsaurus system will assign the sorted table a schema where only the names of columns used for sorting will be explicitly specified. If you read the sorted table in YQL, other columns will be available in the `_other` field. - Another method is to create a table with a non-strict schema and add columns that weren't initially specified in the schema to it. Below is an example of this scenario. Example: ```bash # Create a table with a non-strict schema. Two columns are explicitly specified in the schema: 'id' and 'text'. $ yt create table //tmp/table_not_strict --attributes '{schema = [{name = id; type = int64}; {name = text; type = string}]}' # Now add data to the table, specifying an additional 'newColumn' not provided in the schema. $ echo '{ "id": 0, "text": "Hello, World!", "newColumn": "This column is not in schema" } ' | yt write-table //tmp/table_not_strict --format json ``` {% note info %} Tables with a non-strict schema can be created only via the YTsaurus SDK. You can't do this in the web interface. {% endnote %} If you read this table in YQL, the `newColumn` field will be available in the `_other` column: ```yql SELECT * FROM `//tmp/table_not_strict` AS t; ``` ![](https://ytsaurus.tech/_images/yql_other_field.png) There are multiple methods for retrieving the value of new `newColumn`: ```yql SELECT WeakField(t.newColumn, "String") FROM `//tmp/table_not_strict` AS t; ``` or ```yql SELECT t._other["newColumn"] FROM `//tmp/table_not_strict` AS t; ``` The first method enables you to work with your table consistently if the `newColumn` ends up in a strict schema. With [WeakField](https://ytsaurus.tech/docs/builtins/basic#weakfield), you can immediately get a value of the correct type, but with `_other`, you'd have to process the resulting values as Yson. In case of a strict schema, the system checks that the type passed to WeakField is the same as in the schema. {% note warning %} If the `SELECT * FROM
` query result is written to another table, the `_other` column will become a real column in the new table, but the schema of the table will be strict. Attempting to read a non-strict table in YQL that already has a real `_other` column will result in an error: "Non-strict schema contains '_other' column, which conflicts with YQL virtual column". {% endnote %} ## How do I create an empty table with a schema from another table? ```yql -- Determine the record type for the source data $s = select * from `path/to/source_table`; $s = process $s; -- Create an empty table with the desired schema INSERT INTO `path/to/my_destination_table` WITH TRUNCATE SELECT * FROM as_table(ListCreate(ListItemType(TypeOf($s)))) ``` ## How to work with a string without unpacking protobuf? YQL can unpack protobuf messages. We recommend setting up automatic unpacking by using table metadata. In some cases, in case of an unpacked message, you need to work with a source row. For this purpose, it makes sense to use a special [raw](https://ytsaurus.tech/docs/syntax/select/index.md#view) view: ```yql SELECT * FROM `path/to/my_table` VIEW raw; ``` {% note info "Note" %} Keep in mind that the meta attribute that enables automatic unpacking of tables created by such a query won't be copied, and you'll need to re-enable it. {% endnote %} ## How do I convert the string "a=b,c=d" to a dictionary? For this, you only need to apply the pre-installed function from the UDF [Dsv](https://ytsaurus.tech/docs/udf/list/dsv.md) module: ```yql $str = "a=b,c=d"; SELECT Dsv::Parse($str, ","); /* Result: { "a": "b", "c": "d" } */ ``` ## How do I delete duplicate rows from my table without listing all the columns? ```yql SELECT * WITHOUT g FROM ( SELECT Some(TableRow()) AS s FROM `path/to/table` AS t GROUP BY Digest::Sha256(StablePickle(TableRow())) AS g ) FLATTEN COLUMNS; ``` ## How do I copy user attributes to the output table? ```yql $input = "some/input/path"; $output = "destination/path"; -- Filter user attributes that start with an underscore and exclude some YQL system attributes $user_attr = ($attr) -> { RETURN ListFilter( Yson::ConvertToStringList($attr.user_attribute_keys), ($name) -> { RETURN StartsWith($name, "_") AND $name NOT IN ["_yql_op_id", "_yql_row_spec", "_yql_runner"]; } ); }; -- Add the // prefix to the path $yt_path = ($path) -> { RETURN IF(StartsWith($path, "//"), $path, "//" || $path); }; -- Create a list of attributes for copying $attribute_keys = ( SELECT String::JoinFromList($user_attr(Attributes), ";") FROM Folder(Substring($input, NULL, RFind($input, "/")), "user_attribute_keys") WHERE $yt_path(Path) == $yt_path($input) ); -- Read the user attributes $attributes = ( SELECT CAST(Yson::SerializePretty(Attributes) AS String) as attrs FROM Folder(Substring($input, NULL, RFind($input, "/")), $attribute_keys) WHERE $yt_path(Path) == $yt_path($input) ); -- Force the scalar context $attributes = EvaluateExpr($attributes); INSERT INTO $output WITH (TRUNCATE, USER_ATTRS=$attributes) SELECT * FROM ( SELECT * FROM $input LIMIT 5 ) ``` ## JSON and YSON ### How to convert a table row to JSON? For this, you will need pre-installed serialization [functions](https://ytsaurus.tech/docs/udf/list/yson.md) and the standard [TableRow()](https://ytsaurus.tech/docs/builtins/basic.md#tablerow) function to access the entire row: ```yql USE ; SELECT Yson::SerializeJson(Yson::From(TableRow())) FROM `home/yql/tutorial/users`; ``` ### How do I get a value by a key from JSON/YSON? {#yson-json} When working with JSON/YSON, keep in mind: * You can't output a non-serialized JSON/YSON field in a browser or save it to a table because of the following error` Expected persistable data, but got: List>>`; * It doesn't make sense to convert these formats to a dictionary if you need to retrieve a value by a single key. ```yql -- my_column type — Yson SELECT Yson::Serialize(my_json_column.myKey) AS my_column FROM my_table; ``` If you need a specific data type, you can use the function [Yson::ConvertTo...](https://ytsaurus.tech/docs/udf/list/yson.md#ysonconvertto): ```yql -- my_column type — String SELECT Yson::ConvertToString(my_json_column.myKey) AS my_column FROM my_table; ``` If the original column type is string, use the function [Yson::Parse...](https://ytsaurus.tech/docs/udf/list/yson.md#ysonparse): ```yql SELECT Yson::Serialize(Yson::Parse(my_yson_column)) AS my_yson; -- As a result, you have Yson, when working with which Yson::Parse is added automatically SELECT Yson::Serialize(my_yson.myKey) AS my_key_value; ``` If you need to retrieve a list of full values by a key, use the [ListMap](https://ytsaurus.tech/docs/builtins/list#listmap) function: ```yql SELECT ListMap( Yson::ConvertToList(my_yson_column), ($x) -> { RETURN Yson::Serialize($x.foo) } ); -- The execution result is a list of rows ``` Example of retrieving a list item: ```yql SELECT Yson::ConvertToString(Yson::ConvertToList(my_yson_column)[0].foo); -- The execution result is the field of a given list item ``` ### How do I flatten a dictionary/list from a table? {#flatten} If the container that you need to flatten is a list, use `FLATTEN LIST BY`: ```yql SELECT parents, name, geo_parents_list FROM .`home/yql/tutorial/users` VIEW complex FLATTEN LIST BY geo_parents_list AS parents; ``` If the list is in a Yson column, convert it to List first. For example, like this: ```yql SELECT * FROM ( SELECT ListMap( Yson::ConvertToList(my_yson_column), ($x) -> { RETURN Yson::Serialize($x.foo) } ) AS my_list FROM my_table) FLATTEN LIST BY my_list; ``` Use `FLATTEN DICT BY` if you need to flatten a dictionary: ```yql SELECT dsv.dict.0 AS key, dsv.dict.1 AS value FROM .`home/yql/tutorial/users_dsv` AS dsv FLATTEN DICT BY dict; ``` For more information about the `FLATTEN BY` statement, see this [section](https://ytsaurus.tech/docs/syntax/flatten.md). ## How do I pass data from one table (reference) as a list for processing another table? {#list-table-attach} If the reference table size doesn't exceed the limit on the size of a single table row, you can use scalar context and the `AGGREGATE_LIST` function. ```yql $data = SELECT AGGREGATE_LIST(TableRow()) FROM small_table; -- scalar context when using $data in the expression, the Optional type SELECT MyModule::MyUdf($data ?? [])(x) FROM big_table; ``` If the reference table is larger but can still be loaded entirely into each job, you can use table context. ```yql $data = SELECT * FROM medium_table; $data = PROCESS $data; -- enable table context, $data will then have the List type SELECT MyModule::MyUdf($data)(x) FROM big_table; ``` [*schema-not-strict]: A schema with a set attribute ``. # Additional YQL materials This section contains additional information about YQL query execution. - [Query execution steps](https://ytsaurus.tech/docs/en/exec_steps.md) # Query execution stages YQL is a strongly typed programming language. This specifically means that before a YQL query is executed, it must be typed (the type for each node must be computed in the computation graph). Clicking the **Validate** button in the web interface triggers typing. For example, the query ```yql SELECT key + 1 AS new_key FROM T; ``` is typed as follows (with some simplifications): 1. The metadata of table `T` is loaded, revealing that the `key` column is of the Uint32 type. 2. The literal type is always known: type 1 is Int32. 3. The `+` operation type is computed based on the argument types. For Int32 and Uint32 arguments, the result is Int32. 4. As a result, the query type is a list of structs with one Int32 element, namely `List>`. However, YQL also supports more complex typing cases where the query type depends on the *values* of certain expressions, and these values may depend on the table *contents*. Typical example: ```yql $from = "2022-02-01"; $to = SELECT MAX(dt) FROM T1; INSERT INTO Result SELECT * FROM RANGE(Dir, $from, $to); ``` To infer the [RANGE](https://ytsaurus.tech/docs/syntax/select/concat.md) type (that is, to infer the common type for tables), compute the `$to` value. Another example: ```yql $cols = SELECT AGGREGATE_LIST(DISTINCT name) FROM T1 WHERE ; -- dynamically generate the list of columns -- select the columns from the list SELECT * FROM (SELECT ChooseMembers(TableRow(), $cols) FROM T2) FLATTEN COLUMNS; ``` To type [ChooseMembers](https://ytsaurus.tech/docs/builtins/struct.md#choosemembers), compute the *value* (not type) of `$cols`. We can draw the following conclusions: 1. Some YQL queries require computations for typing. 2. Sometimes these computations require table reads. YQL has a dedicated Evaluation stage for typing such queries, which is where all the necessary computations are performed. Please note that this stage is subject to certain limitations. The main one is that the total size of tables that the Evaluation stage depends on is limited. The default limit is 1 MB, but it can be expanded to 10 MB using the [yt.EvaluationTableSizeLimit](https://ytsaurus.tech/docs/syntax/pragma/yt.md#ytevaluationtablesizelimit) pragma. These limitations are designed to avoid a lengthy Evaluation stage during which the query plan cannot yet be displayed, and after which user typing errors can still occur. This prevents scenarios where you run a computation for several hours only to encounter a "No such column" error and a failed query. The only way to get around this limitation is to split the query into two. The first query computes the evaluation part and writes data to the table, while the second one depends on that (small-sized) table. ## Changes in YQL versions This section contains a list of changes in different versions of the YQL ## General description of YQL versions The goal of language versioning is a controlled and user-safe evolution of the language, within which it is possible to: * add new features; * change default values ​​for pragmas or perform other non-backward-compatible changes; * remove obsolete features. The starting version of the language is `2025.01`. Once a year, a new version of the language is released, called `YEAR.X`, for example `2025.01`. Other versions in the same year will increase the minor version number (`YEAR.02`, etc.). For each version of the type `YEAR.X` **support is guaranteed for three years with the preservation of semantics and bug fixes**. When version `YEAR+2.1` is released, a warning is issued for all versions of the type `YEAR.X` that they will soon cease to be supported. When version `YEAR+3.1` is released, a warning is issued for all versions of the type `YEAR.X` that they are not supported and query execution is not guaranteed. When specifying a version of `YEAR.X` greater than what is currently available in some service/library/tool, a query error is issued. When each version is released, the documentation in the changelog indicates the changes included in this version and recommendations for migration. This initial version contains all features as of early 2025. ## Changing the visibility of aliases entered in GROUP BY expr AS alias for WHERE {#group-by-expr-alias-where} Since this version, in a query like ```yql SELECT foo, ... WHERE foo = 1 GROUP BY expr AS foo ``` the value of foo in `WHERE` is taken from the original table, and not equal to `expr` as it was previously. To get an error from the query if it changes behavior in this version, you need to add ```yql PRAGMA FailOnGroupByExprOverride; ``` If you want to keep the previous behavior, you need to transform the query into this form: ```yql SELECT foo, ... FROM (SELECT expr AS foo ...) WHERE foo = 1 GROUP BY foo ``` If you need to prepare the query for the transition to a new version, it is enough to remove the shadowing of the column name by the alias: ```yql SELECT foo_new, ... WHERE foo = 1 GROUP BY expr AS foo_new ``` ## Changes in the String module {#string-module} * Removed `String::Reverse` function, use `Unicode::Reverse`. * Removed `String::HasPrefixIgnoreCase` and `String::StartsWithIgnoreCase` functions, use `String::AsciiStartsWithIgnoreCase`. * Removed `String::HasSuffixIgnoreCase` and `String::EndsWithIgnoreCase` functions, use `String::AsciiEndsWithIgnoreCase`. * Added `String::AsciiContainsIgnoreCase` and `String::AsciiEqualsIgnoreCase` functions. * Added `String::ReverseBytes` and `String::ReverseBits` functions. ## Changes to the RE2 module {#re2-module} * Queries are guaranteed to fail if invalid regular expressions are passed. ## Changes to the Digest module {#digest-module} * Added the `Digest::Sha512` function. ## Changes to the DateTime module {#datetime-module} * Changed the return type for `DateTime::ToSeconds(Interval)` from `Int32` to `Int64`. * Changed the argument type for `DateTime::IntervalFromSeconds` from `Int32` to `Int64`. ## Changes to the Math module {#math-module} * Added the `Math::SwapBytes` function. ## Changes to the IP module {#ip-module} * Added the `Ip::Ipv4FromUint32` and `Ip::Ipv4ToUint32` functions. ## Changes to the SELECT syntax * You can now explicitly specify `DISTINCT` for `UNION`. * Added `INTERSECT [DISTINCT|ALL]` implementation. * Added `EXCEPT [DISTINCT|ALL]` implementation. ## Changes to the Udf function * Was added the ability to specify a dependency on the calculation of another expression. ## Changes to the semantics of `FLATTEN OPTIONAL BY` * The `FLATTEN OPTIONAL BY` expression now accepts any non-Optional type, ensuring that the resulting column does not contain NULL. ## Changes to data types Linear types were added. ## Changes to the built-in functions * Added the `WithSideEffects`/`WithSideEffectsMode` functions for working with side effects. * Functions for creating new dictionaries based on existing ones with content modification were added: `DictInsert`/`DictUpsert`/`DictUpdate`/`DictRemove`. * Functions for working with mutable dictionaries using linear types were added. * The `Block` function was added in order to execute a code block using anonymous dependent nodes. * Functions `ToDynamicLinear`/`FromDynamicLinear` were added for convertation between linear types. * Functions `LinearType`/`DynamicLinearType` were added for constructing linear types. * Functions `LinearTypeHandle`/`DynamicLinearTypeHandle` were added for constructing linear types during code generation. * The `LinearItemType` function was added on order to get the parameter type from a linear type. * The `Concat` function was added for string concatenation. * The `NullIf` function was added. ## Changes to the NOT NULL expression * The `x NOT NULL` expression will generate an error instead of a warning. To fix this, replace it with `x IS NOT NULL`. ## Changes to PRAGMA * The PRAGMA `SimplePg` was added for all functions importing from the `SimplePg` module to the global scope of functions visibility. * The PRAGMA `Layers` were added for specifying the layers that can annotate individual UDF calls, rather than all operations performing in the query. ## Changes to the Udf function * A new named argument `Layers` was added for specifying a list of required layers for the UDF. ## The SimplePg module was added {#simple-pg-module} * It includes the `now`, `to_date`, `round`, `date_trunc`, `floor`, `ceil`, `to_char`, `date_part` functions. * For functions available through the `SimplePg` or `Pg` modules, an extended error message is provided when they are used in the global scope of visibility. ## Changes to aggregate functions * The `RANDOM_VALUE` and `RANDOM_SAMPLE` aggregate functions were added on order to randomly select the number of elements from the source. ## Changes to table functions * The `PARTITIONS` and `PARTITION_LIST` functions were added. ## Inserting into dynamic tables * Added support for the `REPLACE INTO` operator for inserting data into dynamic YTsaurus tables. ## Changes to the built-in functions Added the `LinearDestroy` function for consuming instances of linear types when their value isn't needed. ## Changes to the DateTime module {#datetime-module} Added a new flag for printing the timezone offset with a colon to the `Datetime::Format` function. Added a new specifier for reading the timezone offset to the `Datetime::Parse` and `Datetime::Parse64` functions. ## Changes to the Yson module {#yson-module} Added the `Yson::Iterate` function for traversing subtrees of Yson nodes. Added the `Yson::As*` and `Yson::TryAs*` functions for converting a Yson node to a specified type. Added functions for [in-place editing](https://ytsaurus.tech/docs/udf/list/yson.md#yson-modify) of Yson nodes. ## Window function changes {#window-functions} Added support for `RANGE` mode in [window](https://ytsaurus.tech/docs/syntax/window.md) frame definitions. ## Built-in function changes {#built-in-functions} Added the `PositionOf` function that lets you retrieve node positions. Added the `WithIssue` function that lets you include a custom diagnostic frame in the validation stack. Added the `AsOptional` function for idempotently wrapping a value in `Optional`. ## PostgreSQL module changes {#postgresql-module} Added the ability to handle warnings as errors using `SET`, similarly to `PRAGMA Warning` in YQL syntax. ## Top-level statement changes {#top-level-statements} Added support for [`MATERIALIZE`](https://ytsaurus.tech/docs/syntax/materialize.md). Added support for [`COMBINE`](https://ytsaurus.tech/docs/syntax/combine.md). ## Built-in function changes {#built-in-functions} Added `Decimal` support to [`ListFromRange`](https://ytsaurus.tech/docs/builtins/list.md#listfromrange). Added [`AsErased` and `PeekErased`](https://ytsaurus.tech/docs/builtins/basic.md#type_erasure) for value type erasure. ## Changes to yt pragma defaults {#yt-pragma-defaults} The `yt.ConvertDynamicTablesToStatic="join"` pragma is enabled by default. To roll back this change in your query, add `pragma yt.ConvertDynamicTablesToStatic="disable"`. # Distributed locks This section explains how to use [Cypress](https://ytsaurus.tech/docs/storage/cypress.md) as a distributed locking service, similar to [ZooKeeper](https://zookeeper.apache.org). ## General information To use YTsaurus as a distributed locking service, you need to deploy master servers of a cluster in multiple locations. There must be at least three locations so that the service can work if one of them is not available. If the master servers are located in five locations, the service can work if any two of them are not available. ### How it works YTsaurus supports master and tablet [transactions](https://ytsaurus.tech/docs/storage/transactions.md). Master transactions use a pessimistic locking model. This means that a lock on a Cypress node is acquired synchronously, and the system checks for potential conflicts. Using the pessimistic model, you can construct a simple emulation of a distributed lock: 1. Create a node in Cypress outside of transactions. This node will respond to the lock. The node can be of any type, for example, `map_node`. 2. In the program, create a transaction where you want to acquire a lock on the given node. An attempt to acquire an exclusive lock on the node is under this transaction. 3. If the lock is acquired, then you can execute the code that this lock protects. And it's important that you ping the transaction. If the ping call fails or the transaction is canceled, then the execution is no longer exclusive. In this case, you have to stop the process or take other actions. 4. If the lock **isn't** acquired, repeat the transaction creation and lock acquisition. As with any distributed lock system, you can't fully guarantee that a process under a lock is exclusive when it makes changes to a third-party system that is not integrated with YTsaurus transactions. For example, between consecutive ping calls, it might happen that a transaction is interrupted and a parallel process that acquires a lock on the same node starts. When you make changes on the cluster where the lock is acquired, you should use the `prerequisite_transactions` option in requests to the cluster. In this option, you should specify the transaction under which the lock was acquired. Thus, YTsaurus ensures that the request is executed only if the transaction is alive and the lock is acquired. ### How to use To run a process under a lock, you can use the [run-command-with-lock](https://ytsaurus.tech/api/cli/commands.md#run-command-with-lock) command. Alternatively, you can define how to acquire a lock in the application code. If the process makes changes on a cluster and needs to make them exclusively, we recommend using a lock directly on that cluster. ## Quotas The request quota is calculated for a user of the cluster regardless of the host from which they send these requests. The default quota on requests is 100 RPS. If this limit is reached, requests are queued. The default queue depth is 100 requests. If the queue is full, the user gets the `Request queue size limit exceeded` error when they send another request. You can ask your cluster administrator to increase the limits. # Proxy This section contains information about the different interfaces for working with the YTsaurus system: [HTTP](https://ytsaurus.tech/docs/en/#http_proxy) and [RPC](https://ytsaurus.tech/docs/en/#rpc_proxy) proxies. ## HTTP proxy { #http_proxy } **HTTP proxy** is a universal language-agnostic interface to work with YTsaurus. {% note warning "Warning" %} The low-level HTTP API is not a good choice for writing simple and reliable application code. Using it will require you to correctly log queries and label their correlation IDs, balance the load on the proxy, acquire additional explicit locks on tables, handle errors, repeat actions in some cases, and so on. We strongly recommend using our supported SDKs for your language (for example, for [Python](https://ytsaurus.tech/api/python/start.md) or [С++](https://ytsaurus.tech/api/cpp/description.md)) that already address these issues. To work in the console, you should use the [CLI](https://ytsaurus.tech/api/cli/cli.md) instead of sending direct HTTP requests. {% endnote %} Each HTTP proxy is assigned a certain role that specifies which kind of load the proxy will handle. - Cypress requests (e.g. `get`, `set`, `create`) are relatively light and processed by control HTTP proxies, the `control` proxy role. - Read or write requests (for example, `read-table`, `write-table`) can potentially transfer a significantly larger amount of data, resulting in a bigger load on YTsaurus cluster nodes. Such queries are therefore processed by so-called heavy proxies (data proxies), the `data` role. - Individual processes can use their own roles so that their load does not interfere with other processes. Client SDKs typically hide role-related operations. The only thing you may need to do is to specify the role in the client configuration if you need a non-standard role for heavy queries. However, when working directly with HTTP, light and heavy queries should be sent to different hosts. If you send a heavy query via a light HTTP proxy, the server will return an error. For more information, see [List of heavy proxies](https://ytsaurus.tech/docs/en/http-reference.md#hosts). To get examples of how to interract with YTsaurus via the HTTP interface, check the [HTTP proxy](https://ytsaurus.tech/docs/en/http.md) section. For reference information about the HTTP proxy, see [HTTP proxy reference](https://ytsaurus.tech/docs/en/http-reference.md). ## RPC proxy { #rpc_proxy } **RPC proxy** is a special interface for interacting with YTsaurus via the RPC protocol. The RPC proxy is accessed via TCP, port 9013. What basically distinguishes the RPC proxy from the HTTP proxy: - Higher query processing speed achieved due to the following features: - Work with the RPC proxy is performed using a special internal protocol that is better suited for working with a large number of concurrent queries. - In the RPC proxy, a query is sent in native YTsaurus format and does not require additional time and resources for conversion. - The ability to maintain compatibility at the user code level with changes in the protocol of interaction of components within a YTsaurus cluster. Like HTTP proxies, all RPC proxies are divided into roles. The role is specified in connection configuration and allows to balance the user load. The default rpc proxy role is `default`. The following programming languages are supported: - [C++](https://ytsaurus.tech/docs/en/rpc.md#c_plus_plus) - [Java](https://ytsaurus.tech/docs/en/rpc.md#java) - [Python](https://ytsaurus.tech/docs/en/rpc.md#python) Examples are available for each programming language. # HTTP proxy This section demonstrates how to work with YTsaurus via the HTTP proxy using the example of running the [Word Count](https://en.wikipedia.org/wiki/Word_count) task. You can find a full description of all YTsaurus commands in the [Commands](https://ytsaurus.tech/api/commands.md) section. {% note info "Note" %} To understand the material presented in this section, you need to know the [HTTP protocol](https://en.wikipedia.org/wiki/HTTP) structure, the principles of working via HTTP, and the basics of [data storage](https://ytsaurus.tech/docs/storage/cypress.md) in the YTsaurus system. {% endnote %} ## Setup { #prepare } The Word Count example is a typical task for any [MapReduce](https://ytsaurus.tech/docs/data-processing/operations/mapreduce.md) system. The general schema is as follows: 1. A [Map](https://ytsaurus.tech/docs/data-processing/operations/map.md) operation is performed on the source text, producing a pair `(word, 1)` for each word. 2. The result is sorted by the first coordinate. 3. A [Reduce](https://ytsaurus.tech/docs/data-processing/operations/reduce.md) operation that summarizes the second coordinate is performed on the first coordinate. The result is a set of pairs (`word, number of word mentions)`. Further, the [curl](https://en.wikipedia.org/wiki/CURL) utility is used to work over HTTP. The environment variables listed below were set for convenience. Setting the environment variables: ```bash $ export YT_PROXY=cluster-name $ export YT_TOKEN=`cat ~/.yt/token` $ export YT_HOME=//tmp/yt_examples ``` To make the examples clearer, irrelevant and duplicate information has been removed from them. ## Uploading data { #upload } Data in the YTsaurus system is stored in [tables](https://ytsaurus.tech/docs/storage/static-tables.md), so you need to create a table. The table contains two columns: row number and row contents. Use the [create](https://ytsaurus.tech/api/commands.md#create) command to create a table. Creating a directory and a table: ```bash # Creating a working directory $ curl -v -X POST "http://$YT_PROXY/api/v3/create?path=$YT_HOME&type=map_node" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > POST /api/v3/create?path=//tmp/yt_examples&type=map_node HTTP/1.1 < HTTP/1.1 200 OK "0-3c35f-12f-8c397340" # Creating a table for source data $ curl -v -X POST "http://$YT_PROXY/api/v3/create?path=$YT_HOME/input&type=table" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > POST /api/v3/create?path=//tmp/yt_examples/input&type=table HTTP/1.1 < HTTP/1.1 200 OK "0-6a7-191-8075d1f7" ``` As a result of the command execution, IDs (GUIDs) of created objects in Cypress are returned in response: `0-3c35f-12f-8c397340` and `0-6a7-191-8075d1f7`. You need to upload the data into the table. To do this, use the [write_table](https://ytsaurus.tech/api/commands.md#write_table) command. The tab-separated format is used as the data format: - The table rows are separated by a line advance ( `\n` ). - The columns are separated by a tab ( `\t` ). - The column name and its contents are separated by an equal sign. For example, a row: `lineno=1\tsize=6\tvalue=foobar` describes a row with the *lineno*, *size*, and *value* columns with the *1*, *6*, and *foobar* values, respectively. Tab characters are escaped. Uploading data to the table: ```bash # Downloading the text $ wget -O - http://lib.ru/BULGAKOW/whtguard.txt | iconv -f cp1251 -t utf-8 > source.txt # Reformatting and uploading the text $ cat source.txt | perl -e ' $n = 0; while(<>) { $n++; chomp; s/\t/\\t/g; print "lineno=$n\ttext=$_\n"; } ' > source.tsv $ HEAVY_YT_PROXY=$(curl -s -H "Accept: text/plain" "http://$YT_PROXY/hosts" | head -n1) $ curl -v -L -X PUT "http://$HEAVY_YT_PROXY/api/v3/write_table?path=$YT_HOME/input" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" \ -H "Content-Type: text/tab-separated-values" \ -H "Transfer-Encoding: chunked" \ -T source.tsv > PUT /api/v3/write_table?path=//tmp/yt_examples/input HTTP/1.1 > Expect: 100-continue < HTTP/1.1 100 Continue < HTTP/1.1 200 OK ``` {% note info "Note" %} Note that the input data format is also explicitly specified in this query: "text/tab-separated-values". {% endnote %} {% note info "Note" %} When creating a client, the user needs to occasionally query a list of heavy proxies using the `/hosts` query. A list of heavy proxies ordered by priority is returned in the response. The proxy priority is determined dynamically and depends on its load (CPU+I/O). A good strategy is to re-query the `/hosts` list every minute or every few queries and change the current proxy to which queries are made. {% endnote %} Getting a list of heavy proxies: ```bash $ curl -v -X GET "http://$YT_PROXY/hosts" > GET /hosts HTTP/1.1 < HTTP/1.1 200 OK < Content-Type: application/json < ["n0008-sas.cluster-name","n0025-sas.cluster-name",...] ``` To make sure that the data is written, look at the table attributes. Getting the table attributes: ```bash $ curl -v -X GET "http://$YT_PROXY/api/v3/get?path=$YT_HOME/input/@" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > GET /api/v3/get?path=//tmp/yt_examples/input/@ HTTP/1.1 < HTTP/1.1 200 OK { ... "uncompressed_data_size" : 993253, "compressed_size" : 542240, "compression_ratio" : 0.54592334480741566693, "row_count" : 8488, "sorted" : "false", ... "channels" : [ ["lineno"], ["text"] ] } ``` {% note info "Note" %} Don't be frightened by null values. null does not mean that there is no relevant data. It means that the given key contains a node of a special type (such as a table or a file) or that the data is lazily computable and needs to be explicitly accessed (the case above). {% endnote %} {% note info "Note" %} To upload, for example, JSON, you need to add a bit more options. For more information, see [Formats](https://ytsaurus.tech/docs/storage/formats.md#json). {% endnote %} Uploading data in JSON format: ```bash $ cat test.json { "color": "red", "value": "#f00" } { "color": "red", "value": "#f00" } $ curl -X POST "http://$YT_PROXY/api/v3/create?path=//tmp/test-json&type=table" \ -H "Accept: application/json" \ -H "Authorization: OAuth $YT_TOKEN" $ curl -L -X PUT "http://$HEAVY_YT_PROXY/api/v3/write_table?path=//tmp/test-json&encode_utf8=false" -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" -H "Content-Type: application/json" -H "Transfer-Encoding: chunked" -H "X-YT-Header-Format: yson" -H "X-YT-Input-Format: json" -T test.json ``` ## Uploading files To run a [Map](https://ytsaurus.tech/docs/data-processing/operations/map.md) or [Reduce](https://ytsaurus.tech/docs/data-processing/operations/reduce.md) operation, you will need to upload a script that must be executed into the system. The script for this task is placed on [GitHub](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/python-misc/http_proxy.py) (however, it is locally saved to `exec.py` ). The [write_file](https://ytsaurus.tech/api/commands.md#write_table) command is used to upload files to the YTsaurus system. Before uploading, create a node of the "file" type, similar to creating the table at the previous step. Uploading files: ```bash $ curl -v -X POST "http://$YT_PROXY/api/v3/create?path=$YT_HOME/exec.py&type=file" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > POST /api/v3/create?path=//tmp/yt_examples/exec.py&type=file HTTP/1.1 < HTTP/1.1 200 OK "0-efd-190-88fec34" $ curl -v -L -X PUT "http://$YT_PROXY/api/v3/write_file?path=$YT_HOME/exec.py" \ -H "Transfer-Encoding: chunked" -H "Authorization: OAuth $YT_TOKEN" \ -T exec.py > PUT /api/v3/write_file?path=//tmp/yt_examples/exec.py HTTP/1.1 > Expect: 100-continue < HTTP/1.1 100 Continue < HTTP/1.1 200 OK ``` For a file to have execution permissions, the *executable* attribute must be set for it. Setting attributes: ```bash $ curl -v -L -X PUT "http://$YT_PROXY/api/v3/set?path=$YT_HOME/exec.py/@executable" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" \ -H "Content-Type: application/json" \ --data-binary '"true"' > PUT /api/v3/set?path=//tmp/yt_examples/exec.py/@executable HTTP/1.1 < HTTP/1.1 200 OK ``` To check whether the uploaded file is correct, you can read it. Reading the uploaded file: ```bash $ curl -v -L -X GET "http://$YT_HEAVY_PROXY/api/v3/read_file?path=$YT_HOME/exec.py" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > GET /api/v3/read_file?path=//tmp/yt_examples/exec.py HTTP/1.1 < HTTP/1.1 200 OK #!/usr/bin/python import re import sys import itertools import operator ... ``` If you need to read a range of bytes from a file, you can use the additional *offset* and *length* parameters of the *read_file* command. The parameters are passed through the `X-YT-Parameters` header for variety. Reading a range of bytes from a file: ```bash $ curl -v -L -X GET "http://$YT_HEAVY_PROXY/api/v3/read_file" \ -H "Authorization: OAuth $YT_TOKEN" \ -H "X-YT-Parameters: {\"path\": \"$YT_HOME/exec.py\", \"offset\": 11, \"length\": 6}" > GET /api/v3/read_file HTTP/1.1 < HTTP/1.1 200 OK python ``` ## Running a Map operation { #launch_map } In the YTsaurus system, to run a Map operation, there is a [command](https://ytsaurus.tech/api/commands.md#map) with the same name. An important thing to consider is the requirement that an output table must exist **before** an operation is started. You can also create a table using the [create](https://ytsaurus.tech/api/commands.md#create) command. Creating an output table: ```bash $ curl -v -X POST "http://$YT_PROXY/api/v3/create?path=$YT_HOME/output_map&type=table" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > POST /api/v3/create?path=//tmp/yt_examples/output_map&type=table HTTP/1.1 < HTTP/1.1 200 OK "0-843-191-ae506abe" ``` Now you can run the Map operation. Pay attention to its specification: you can see that running the map command requires a structured set of arguments. In the HTTP interface of the system, you can encode arguments through a query string (which happened in the examples above, see the *path* and *type* arguments), as well as in the query body for POST queries. The example describes the specification for running in JSON format. Running a Map operation: ```bash $ cat < POST /api/v3/map HTTP/1.1 < HTTP/1.1 200 OK "381eb9-8eee9ec2-70a8370d-ea39f666" ``` {% note info "Note" %} Unlike the `set` and `write_file` operations, the returned GUID of the operation is needed to keep track of the operation status. A successful response from the HTTP interface about running the operation indicates that the operation was started, but not that it was completed correctly. {% endnote %} You can view the operation status in the web interface or by calling `get_operation`. Viewing the operation status: ```bash $ curl -v -X GET "http://$YT_PROXY/api/v3/get_operation?operation_id=381eb9-8eee9ec2-70a8370d-ea39f666" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > GET /api/v3/get_operation?operation_id=381eb9-8eee9ec2-70a8370d-ea39f666 HTTP/1.1 < HTTP/1.1 200 OK { "operation_type" : "map", "progress" : { "jobs" : { "total" : 1, "pending" : 0, "running" : 0, "completed" : 1, "failed" : 0 }, "chunks" : { "total" : 1,"running" : 0, "completed" : 1, "pending" : 0, "failed" : 0 }, ... }, "state" : "completed" } ``` To make sure that the operation was completed correctly, you can read its result. Reading data: ```bash $ curl -v -L -X GET "http://$YT_HEAVY_PROXY/api/v3/read_table?path=$YT_HOME/output_map" \ -H "Accept: text/tab-separated-values" -H "Authorization: OAuth $YT_TOKEN" \ | head > GET /api/v3/read?path=//tmp/yt_examples/output_map HTTP/1.1 < HTTP/1.1 202 Accepted word=html count=1 word=head count=1 word=title count=1 word=михаил count=1 word=булгаков count=1 word=белая count=1 word=гвардия count=1 word=title count=1 word=head count=1 word=body count=1 ``` ## Running sorting { #launch_sort } To run sorting, use the [sort](https://ytsaurus.tech/docs/data-processing/operations/sort.md) command which is a bit like [Map](https://ytsaurus.tech/docs/data-processing/operations/map.md) and [Reduce](https://ytsaurus.tech/docs/data-processing/operations/reduce.md) in its input specification. Running sorting: ```bash # Creating an output table $ curl -v -X POST "http://$YT_PROXY/api/v3/create?path=$YT_HOME/output_sort&type=table" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > POST /api/v3/create?path=//tmp/yt_examples/output_map&type=table HTTP/1.1 < HTTP/1.1 200 OK "0-95d-191-8901298a" # Running sorting $ cat < POST /api/v3/sort HTTP/1.1 < HTTP/1.1 200 OK "640310-da8d54f1-6eded631-31c91e76" ``` You can monitor the operation execution process in the web interface. ## Running a Reduce operation { #launch_reduce } The Reduce operation is started using the [command](https://ytsaurus.tech/api/commands.md#reduce) with the same name. Running a Reduce operation: ```bash # Creating an output table $ curl -v -X POST "http://$YT_PROXY/api/v3/create?path=$YT_HOME/output_reduce&type=table" \ -H "Accept: application/json" -H "Authorization: OAuth $YT_TOKEN" > POST /api/v3/create?path=//tmp/yt_examples/output_reduce&type=table HTTP/1.1 < HTTP/1.1 200 OK "0-95f-191-ba3a4055" # Running reduce $ cat < POST /api/v3/reduce HTTP/1.1 < HTTP/1.1 200 OK "658ad8-edf7650f-182e4fd0-a1a0fd03" ``` ## Reading the result { #read_results } After you complete the operation, you will get calculated statistics on the frequency of word usage in "The White Guard". Let's look at its result. Reading the result: ```bash $ curl -v -L -X GET "http://$YT_HEAVY_PROXY/api/v3/read_table?path=$YT_HOME/output_reduce" \ -H "Accept: text/tab-separated-values" -H "Authorization: OAuth $YT_TOKEN" \ | head > GET /api/v3/read_table?path=//tmp/yt_examples/output_reduce HTTP/1.1 < HTTP/1.1 200 OK count=1 word=0 count=1 word=04 count=1 word=05 count=4 word=1 count=2 word=10 count=4 word=11 count=4 word=12 count=4 word=13 count=3 word=14 count=2 word=15 ``` # RPC proxy This section contains examples of working with YT via RPC proxy for different programming languages: [C++](https://ytsaurus.tech/docs/en/#c_plus_plus), [Java](https://ytsaurus.tech/docs/en/#java), and [Python](https://ytsaurus.tech/docs/en/#python). ## С++ { #c_plus_plus } Example on [Github](https://github.com/ytsaurus/ytsaurus/blob/main/yt/examples/rpc_proxy_sample/main.cpp) The example implements a program in which you can work with dynamic tables in interactive mode. Call format: ```bash ~/ytbuild/bin/rpc_proxy_sample --config config.yson --user --token $(cat ~/.yt/token) 2> /dev/null ``` Working with Cypress: ```bash list //sys get //sys ``` Reading data using a query language (SelectRows): ```sql select timestamp, host, rack, utc_time, data FROM [//home/dev/autorestart_nodes_copy] LIMIT 10 ``` Querying rows by key (LookupRows): ```bash ulookup //home/dev/autorestart_nodes_copy timestamp;host;rack;utc_time;data 1486113922563016;"s04-sas.cluster-name";"SAS2.4.3-13" 1486113924172063;"s04-sas.cluster-name";"SAS2.4.3-13" 1486113992045484;"s04-sas.cluster-name";"SAS2.4.3-13" 1486113992591731;"s04-sas.cluster-name";"SAS2.4.3-13" 1486113997734536;"n4137-sas.cluster-name";"SAS2.4.3-13" ``` Inserting rows (aka WriteRows): ```bash upsert //home/dev/autorestart_nodes_copy timestamp;host;rack;utc_time;data 123;"host123";"rack123";"utc_time1";"data1" 567;"host567";"rack567";"utc_time2";"data2" ``` Deleting rows (DeleteRows): ```bash delete //home/dev/autorestart_nodes_copy timestamp;host;rack 123;"host123";"rack123" ``` Example of working with transactions: ```c++ if (ValidateSignature("delete", {"path", "columns", "..."}, tokens)) { auto path = tokens[1]; TPrepareRows prepareRows(tokens); auto tx = Client_->StartTransaction(NTransactionClient::ETransactionType::Tablet).Get(); if (!ValidateResult(tx)) return; tx.Value()->DeleteRows(path, prepareRows.NameTable, prepareRows.Rows); auto result = tx.Value()->Commit().Get(); if (!ValidateResult(result)) return; Cout << "Committed" << Endl; } ``` ### UserTag { #usertag } When working via the RPC proxy using the C++ client, you can specify an additional user tag `user_tag` — then the user’s graph identifier will be annotated not as ``, but as `:`. This can be useful for services that have many clients, but access YT on behalf of a single user. This additional tagging lets you link the load on YTsaurus to requests generated by external user actions. ```cpp auto connection = NYT::NApi::NRpcProxy::CreateConnection(rpcProxyConnectionConfig); auto options = TClientOptions::FromUser(user, userTag); auto client = connection->CreateClient(options); ``` ## Java { #java } The source code of the presented examples can be found on [GitHub](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/ytsaurus-client/src/test/java/tech/ytsaurus/client). Creating a client: ```java public static YtClient createYtClient(BusConnector connector, String user, String token) { return new YtClient(connector, "cluster-name", RpcCredentials(user, token)); } ``` Implementing a Select query: ```java package ru.yandex.yt.ytclient.examples; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.yandex.yt.ytclient.tables.ColumnSchema; import ru.yandex.yt.ytclient.wire.UnversionedRow; import ru.yandex.yt.ytclient.wire.UnversionedRowset; import ru.yandex.yt.ytclient.wire.UnversionedValue; import ru.yandex.yt.ytclient.ytree.YTreeMapNode; public class SelectRowsExample { private static final Logger logger = LoggerFactory.getLogger(SelectRowsExample.class); public static void main(String[] args) { ExamplesUtil.runExampleWithBalancing(client -> { long t0 = System.nanoTime(); UnversionedRowset rowset = client.selectRows( "timestamp, host, rack, utc_time, data FROM [//home/dev/autorestart_nodes_copy] LIMIT 10") .join(); long t1 = System.nanoTime(); logger.info("Request time: {}", (t1 - t0) / 1000000.0); logger.info("Result schema:"); for (ColumnSchema column : rowset.getSchema().getColumns()) { logger.info(" {}", column.getName()); } for (UnversionedRow row : rowset.getRows()) { logger.info("Row:"); for (UnversionedValue value : row.getValues()) { logger.info(" value: {}", value); } } for (YTreeMapNode row : rowset.getYTreeRows()) { logger.info("Row: {}", row); } }); } } ``` Working with Cypress: ```java package ru.yandex.yt.ytclient.examples; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class GetNodeExample { private static final Logger logger = LoggerFactory.getLogger(GetNodeExample.class); public static void main(String[] args) { ExamplesUtil.runExample(client -> { logger.info("Table dynamic: {}", client.getNode("//home/dev/autorestart_nodes_copy/@dynamic").join()); logger.info("Table schema: {}", client.getNode("//home/dev/autorestart_nodes_copy/@schema").join()); }); } } ``` Working with Cypress: ```java package ru.yandex.yt.ytclient.examples; import java.util.Random; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import ru.yandex.yt.rpcproxy.ETransactionType; import ru.yandex.yt.ytclient.misc.YtGuid; import ru.yandex.yt.ytclient.proxy.ApiServiceTransaction; import ru.yandex.yt.ytclient.proxy.ApiServiceTransactionOptions; import ru.yandex.yt.ytclient.proxy.request.ObjectType; public class CypressExample { public static void main(String[] args) { try { OptionParser parser = new OptionParser(); OptionSpec proxyOpt = parser.accepts("proxy", "proxy (see //sys/rpc_proxies)") .withRequiredArg().ofType(String.class); OptionSet option = parser.parse(args); String [] hosts = null; if (option.hasArgument(proxyOpt)) { String line = option.valueOf(proxyOpt); hosts = line.split(","); } else { parser.printHelpOn(System.out); System.exit(1); } Random rnd = new Random(); String host = hosts[rnd.nextInt(hosts.length)]; ExamplesUtil.runExample(client -> { try { ApiServiceTransactionOptions transactionOptions = new ApiServiceTransactionOptions(ETransactionType.MASTER); String node = "//tmp/test-node-cypress-example"; ApiServiceTransaction t = client.startTransaction(transactionOptions).get(); t.existsNode(node).thenAccept(result -> { try { if (result) { t.removeNode(node); } } catch (Throwable e) { throw new RuntimeException(e); } }).get(); YtGuid guid = t.createNode(node, ObjectType.Table).get(); /* Map data = new HashMap(); data.put("k1", new YTreeInt64Node(10, new HashMap<>())); data.put("k2", new YTreeInt64Node(31337, new HashMap<>())); data.put("str", new YTreeStringNode("stroka")); t.setNode(node, new YTreeMapNode(data)).get(); */ t.commit(); ApiServiceTransaction t2 = client.startTransaction(transactionOptions).get(); t2.linkNode(node, node + "-link"); t2.moveNode(node, node + "-moved"); t2.commit(); client.removeNode(node + "-link"); } catch (Throwable e) { System.out.println(e); e.printStackTrace(); System.exit(-1); } }, ExamplesUtil.getUser(), ExamplesUtil.getToken(), host); System.exit(0); } catch (Throwable e) { System.out.println(e); e.printStackTrace(); System.exit(-1); } } } ``` Executing LookupRows: ```java package ru.yandex.yt.ytclient.examples; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.yandex.yt.ytclient.proxy.LookupRowsRequest; import ru.yandex.yt.ytclient.tables.ColumnSchema; import ru.yandex.yt.ytclient.tables.ColumnValueType; import ru.yandex.yt.ytclient.tables.TableSchema; import ru.yandex.yt.ytclient.wire.UnversionedRow; import ru.yandex.yt.ytclient.wire.UnversionedRowset; import ru.yandex.yt.ytclient.wire.UnversionedValue; import ru.yandex.yt.ytclient.ytree.YTreeMapNode; public class LookupRowsExample { private static final Logger logger = LoggerFactory.getLogger(LookupRowsExample.class); public static void main(String[] args) { TableSchema schema = new TableSchema.Builder() .addKey("timestamp", ColumnValueType.INT64) .addKey("host", ColumnValueType.STRING) .addKey("rack", ColumnValueType.STRING) .addValue("utc_time", ColumnValueType.STRING) .addValue("data", ColumnValueType.STRING) .build(); ExamplesUtil.runExampleWithBalancing(client -> { long t0 = System.nanoTime(); LookupRowsRequest request = new LookupRowsRequest("//home/dev/autorestart_nodes_copy", schema.toLookup()) .addFilter(1486113922563016L, "s04-sas.cluster-name", "SAS2.4.3-13") .addFilter(1486113924172063L, "s04-sas.cluster-name", "SAS2.4.3-13") .addFilter(1486113992045484L, "s04-sas.cluster-name", "SAS2.4.3-13") .addFilter(1486113992591731L, "s04-sas.cluster-name", "SAS2.4.3-13") .addFilter(1486113997734536L, "n4137-sas.cluster-name", "SAS2.4.3-13") .addLookupColumns("utc_time", "data"); long t1 = System.nanoTime(); UnversionedRowset rowset = client.lookupRows(request).join(); long t2 = System.nanoTime(); logger.info("Request time: {}ms + {}ms", (t1 - t0) / 1000000.0, (t2 - t1) / 1000000.0); logger.info("Result schema:"); for (ColumnSchema column : rowset.getSchema().getColumns()) { logger.info(" {}", column.getName()); } for (UnversionedRow row : rowset.getRows()) { logger.info("Row:"); for (UnversionedValue value : row.getValues()) { logger.info(" value: {}", value); } } for (YTreeMapNode row : rowset.getYTreeRows()) { logger.info("Row: {}", row); } }); } } ``` Executing ModifyRows: ```java package ru.yandex.yt.ytclient.examples; import java.util.Arrays; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.yandex.yt.rpcproxy.ETransactionType; import ru.yandex.yt.ytclient.proxy.ApiServiceTransaction; import ru.yandex.yt.ytclient.proxy.ApiServiceTransactionOptions; import ru.yandex.yt.ytclient.proxy.LookupRowsRequest; import ru.yandex.yt.ytclient.proxy.ModifyRowsRequest; import ru.yandex.yt.ytclient.tables.ColumnSchema; import ru.yandex.yt.ytclient.tables.ColumnValueType; import ru.yandex.yt.ytclient.tables.TableSchema; import ru.yandex.yt.ytclient.wire.UnversionedRow; import ru.yandex.yt.ytclient.wire.UnversionedRowset; import ru.yandex.yt.ytclient.wire.UnversionedValue; import ru.yandex.yt.ytclient.ytree.YTreeBuilder; import ru.yandex.yt.ytclient.ytree.YTreeMapNode; public class ModifyRowsExample { private static final Logger logger = LoggerFactory.getLogger(ModifyRowsExample.class); public static void main(String[] args) { TableSchema schema = new TableSchema.Builder() .addKey("timestamp", ColumnValueType.INT64) .addKey("host", ColumnValueType.STRING) .addKey("rack", ColumnValueType.STRING) .addValue("utc_time", ColumnValueType.STRING) .addValue("data", ColumnValueType.STRING) .build(); ExamplesUtil.runExample(client -> { ApiServiceTransactionOptions transactionOptions = new ApiServiceTransactionOptions(ETransactionType.MASTER); try (ApiServiceTransaction transaction = client.startTransaction(transactionOptions).join()) { logger.info("Transaction started: {} (timestamp={}, ping={})", transaction.getId(), transaction.getStartTimestamp(), transaction.isPing()); transaction.ping().join(); logger.info("Transaction ping succeeded!"); ModifyRowsRequest request = new ModifyRowsRequest("//home/dev/autorestart_nodes_copy", schema) .addInsert(Arrays.asList(10, "myhost1", "myrack1", "utc_time1", "data1")) .addInsert(Arrays.asList(11, "myhost2", "myrack2", "utc_time2", "data2")) .addUpdate(new YTreeBuilder() .beginMap() .key("timestamp").value(1486190036109192L) .key("host").value("n0344-sas.cluster-name") .key("rack").value("SAS2.4.3-15") .key("data").value("XXX " + UUID.randomUUID().toString()) .buildMap() .mapValue()) .addUpdate(new YTreeBuilder() .beginMap() .key("timestamp").value(1486190037953802L) .key("host").value("s03-sas.cluster-name") .key("rack").value("SAS2.4.3-15") .key("data").value("XXX " + UUID.randomUUID().toString()) .buildMap() .mapValue()); long t0 = System.nanoTime(); transaction.modifyRows(request).join(); long t1 = System.nanoTime(); logger.info("Request time: {}ms", (t1 - t0) / 1000000.0); t0 = System.nanoTime(); LookupRowsRequest lookup = new LookupRowsRequest("//home/dev/autorestart_nodes_copy", schema.toLookup()) .addFilter(1486190036109192L, "n0344-sas.cluster-name", "SAS2.4.3-15") .addFilter(1486190037953802L, "s03-sas.cluster-name", "SAS2.4.3-15") .addLookupColumns("timestamp", "data"); t1 = System.nanoTime(); UnversionedRowset rowset = client.lookupRows(lookup).join(); long t2 = System.nanoTime(); logger.info("Request time: {}ms + {}ms", (t1 - t0) / 1000000.0, (t2 - t1) / 1000000.0); logger.info("Result schema:"); for (ColumnSchema column : rowset.getSchema().getColumns()) { logger.info(" {}", column.getName()); } for (UnversionedRow row : rowset.getRows()) { logger.info("Row:"); for (UnversionedValue value : row.getValues()) { logger.info(" value: {}", value); } } for (YTreeMapNode row : rowset.getYTreeRows()) { logger.info("Row: {}", row); } transaction.commit().join(); logger.info("Transaction committed!"); } }); } } ``` ## Python { #python } To work via Python, you need to install a bindings package, the pypi-package is called ytsaurus-rpc-driver. After that you should configure the client by specifying the backend type as `rpc` and, if necessary, specifying the connection via IPv4. {} Working with dynamic tables: ```python import yt.wrapper as yt def get_rpc_client(cluster_name, use_ipv4=False): # We will get the configuration for the client, considering the environment variables. config = yt.default_config.get_config_from_env() # Let's configure the RPC connection. config["backend"] = "rpc" if use_ipv4: config["driver_address_resolver_config"] = {"enable_ipv4": True, "enable_ipv6": False} return yt.YtClient(cluster_name, config=config) def main(): client = get_rpc_client("some_cluster", use_ipv4=True) schema = [ {"name": "x", "type": "int64", "sort_order": "ascending"}, {"name": "y", "type": "int64"}, {"name": "z", "type": "int64"} ] table = "//home/ignat/dynamic_table" client.create("table", table, attributes={"schema": schema, "dynamic": True}) client.mount_table(table, sync=True) client.insert_rows(table, [{"x": 0, "y": 99}]) for iter in xrange(5): with client.Transaction(type="tablet"): rows = list(client.lookup_rows(table, [{"x": 0}])) if len(rows) == 1 and rows[0]["y"] <= 100: rows[0]["y"] += 1 client.insert_rows(table, rows) print list(client.select_rows("* from [{}]".format(table))) if __name__ == "__main__": main() ``` In order for the client to use proxies with a certain role (such as `my_role`), you need to specify the `proxy_role` option in the config: ```python client = yt.YtClient("cluster-name", config={"backend": "rpc", "driver_config": {"proxy_role": "my_role"}}) ``` We also recommend enabling debug logs of the client library. Such logs will help a lot when problems arise. Besides a usual method, you can explicitly set up logging via the `YT_LOG_LEVEL=debug` environment variable in the rpc proxy client. To do this, pass the logging config via the `driver_logging_config` parameter: ```python logging_config = { 'rules': [{'min_level': 'debug', 'writers': ['file']}], 'writers': {'file': {'file_name': logging_file_name, 'type': 'file'}} } client = yt.YtClient("cluster-name", config={"backend": "rpc", "driver_logging_config": logging_config}) ``` # HTTP proxy reference ## Command execution structure { #commands_structure } When developing a library to work with YTsaurus, you should understand the command execution structure. Each executed command is a structure containing: - Information about the user, allowing for authentication (in case of working over HTTP, it is most often a token). - Information about the input and output data [format](https://ytsaurus.tech/docs/storage/formats.md) (represented as a YSON string with attributes). - Information about the command parameters (represented as a YSON dict). - Input (byte) data stream. - Output (byte) data stream. Below are the formal coding specifications for each item in the HTTP proxy, which include more features for interpreting HTTP queries (partially to support compatibility with such HTTP clients as web browsers). To work correctly with YTsaurus, you need to support a smaller set of features, namely: - Specify `X-YT-Header-Format` that defines the format for the `X-YT-Input-Format`, `X-YT-Output-Format`, and `X-YT-Parameters` headers (the header value is YSON). - Specify `X-YT-Input-Format` and `X-YT-Output-Format` encoded according to `X-YT-Header-Format` and defining the input and output data stream formats. - Pass all command parameters in the `X-YT-Parameters` header encoded according to `X-YT-Header-Format`. - Consider all HTTP responses 5xx as a transport-layer error. You must distinguish code 503 that is a clear signal of temporary unavailability from code 500 that signals an error on the YTsaurus side. In the first case, you can repeat the query. - In all other responses (2xx, 4xx) about the successful execution of the command, judge by `X-YT-Error` (preferably) or `X-YT-Response-Code` (extracted from the `X-YT-Error` error code) and `X-YT-Response-Message` (extracted from the `X-YT-Error` error troubleshooting) headers/trailers. ## Selecting an HTTP method for a command { #http_method } To select an HTTP method for a command, just use the following algorithm: 1. If the command has an input data stream, then PUT. 2. If the command is mutating, then POST. 3. Otherwise GET. ## Data formats for operations { #formats } Interpreting input and output byte streams (with a data type other than binary) is determined by the [data format](https://ytsaurus.tech/docs/storage/formats.md). A description of the format used is a YSON string possibly with additional attributes. A standard way to describe the formats used are the `X-YT-Input-Format` and `X-YT-Output-Format` headers. For better compatibility with HTTP libraries, the `Accept` and `Content-Type` headers are partially supported. Below is a detailed description of the format definition rules. ### Specifying an input data format { #set_input_format } An input data format is determined using the following rules. Each successive rule overlaps the previous one. 1. If the `Content-Type` header is specified and the specified MIME type is in the correspondence table, the format is selected from the table. 2. If the `X-YT-Input-Format` header is specified, the header content is interpreted as a JSON-encoded YSON string with attributes and it is used as an input format description. 3. If neither variant 1 nor variant 2 is successful, YSON is used. ### Specifying an output data format { #set_output_format } An output data format is determined using the following rules. Each successive rule overlaps the previous one. 1. If the `Accept` header is specified, the best MIME type from the table corresponding to the Accept header is selected and then the format is taken from the table. Content-Type is equal to the matching MIME type. 2. If the `X-YT-Output-Format` header is specified, the header content is interpreted as a JSON-encoded YSON string with attributes and it is used as an output format description. Content-Type is equal to `application/octet-stream`. 3. If neither variant 1 nor variant 2 is successful, Pretty YSON is used. Content-Type is equal to `text/plain`. ## HTTP return codes { #return_codes } If the web server realizes that there is an error before any data is sent to the client, return code 4xx or 5xx indicates an error. If the data started to be delivered to the client, the return code will be (`Accepted`) and there will be a YTsaurus return code in the `X-YT-Response-Code` trailer header. A non-zero `X-YT-Response-Code` indicates an error. In this case, an error message (as a JSON string) is specified in the `X-YT-Response-Message` trailer header. Table 1 — Return codes | Return code | Description | | ------------ | ------------------------------------------------------------ | | 200 | The command was successfully completed | | 202 | The command is executed, the response body will be sent over an HTTP stream, and internal return codes are written in the trailer headers | | 307 | Redirecting heavy queries from light to heavy proxies | | 400 | The command was executed, but an error was returned (detailed JSON-encoded error in the body) | | 401 | Unauthenticated query | | 404 | Unknown command | | 405 | Incorrect HTTP method | | 406 | Incorrect format in Accept | | 415 | Incorrect format in Accept-Encoding | | 429 | The limit on the number of queries from a user was exceeded | | 500 | Error on the proxy side | | 503 | Service unavailable. The query must be repeated later | ## Query debugging { #debugging } {% note info "Note" %} All queries support optional debug headers. Processing them is not mandatory, but recommended. {% endnote %} In the query: 1. Generate guid and set it in the `X-YT-Correlation-Id` header. This header helps you find the query by log even if the response to it did not come to the client. In the response: 1. `X-YT-Request-Id`: ID of the query generated by the proxy. You need it to find the query in the YTsaurus proxy log. 2. `X-YT-Proxy`: Hostname of the proxy from which the response came. It is important when the query passes through a balancer. ## Advanced features { #additional } ### Compression { #compression } You can use compression when transmitting data via an HTTP proxy. The proxy selects a codec for incoming data based on the `Content-Encoding` header and for outgoing data based on the `Accept-Encoding` header. Possible codecs are listed in the table. | **Content-Encoding**/**Accept-Encoding** | **Codec** | | ----------------------- | --------------------------- | | identity | None | | gzip, deflate | Standard zlib | ### Parameters { #parameters } Each command has a set of additional parameters represented as a YSON dict. A standard method to pass parameters is the `X-YT-Parameters` header interpreted as a JSON-encoded YSON dict. ### List of heavy proxies (/hosts) { #hosts } All YTsaurus commands are classified into two groups: light and heavy. Heavy commands are associated with a large I/O and, consequently, a network load. To isolate this load, light (controlling) commands are separated from the heavy ones. When you try to execute a heavy command, light proxies return code 503. The balancing of heavy commands among heavy proxies is performed by the client. Before you execute a heavy command, you need to query `/hosts` and get a list of proxies ordered by load. The load is estimated based on the current CPU and network load, as well as some planned future load on the proxies. The very first proxy in the resulting list is the least loaded, and that's the one you want to use in simple cases (= in 80% of cases). ### Available API versions (/api) { #api } The HTTP API is versioned (as you can see from the `/v4` prefix in the examples [from here](https://ytsaurus.tech/docs/en/http.md)). The API version is changed if there are backward-incompatible changes to the set of supported commands or to the semantics of any of the existing commands. Adding new commands does not usually change the API version. The HTTP proxy supports the two latest versions of the API. A list of supported API versions can be obtained from the URL `/api`. Getting a list of available APIs: ```bash $ curl -v -X GET "http://$YT_PROXY/api" > GET /api HTTP/1.1 < HTTP/1.1 200 OK ["v3","v4"] ``` ## The table of correspondence of MIME types and YT formats { #mime_to_yt_format } The correspondence of MIME types to YT formats is represented in the table. | **MIME type** | **YT format** | | ----------------------------------- | ----------------------------------- | | application/json | json | | application/x-yt-yson-binary | yson | | application/x-yt-yson-text | yson | | application/x-yt-yson-pretty | yson | | application/x-yamr-delimited | yamr | | application/x-yamr-lenval | yamr | | application/x-yamr-subkey-delimited | yamr | | application/x-yamr-subkey-lenval | yamr | | text/tab-separated-values | dsv | | text/x-tskv | dsv | ## Framing { #framing } Some commands may use a special protocol on top of the usual HTTP. If the client specifies the `X-YT-Accept-Framing: 1` header, the proxy can respond with the `X-YT-Framing: 1` header. In this case, the response body will consist of entries of the `
` type. Frame types: | **Name** | **Tag** | **Header** | **Frame** | **Comment** | |--------------|---------|------------------------------------------------|-------------|------------------| | Data | `0x01` | 4-byte little-endian number — frame size | frame body | | | Keep-alive | `0x02` | none | none | "the data is being prepared, please wait" | # gRPC proxy You can communicate with a YTsaurus cluster via the gRPC protocol. Advantages: 1. Full support for dynamic tables, you can work with transactions. 2. Support for all basic commands for working with Cypress. 3. gRPC is an industry standard for communicating with the service and it is easy to install it via pip or deb packages. Documentation is also available on the internet. Disadvantages: 4. The protocol is more low-level. More complicated work with tabular data, you need to serialize/deserialize rows manually. 5. There is currently no integration with off-the-shelf YTsaurus HTTP clients (C++ or Python). 6. Creating a full-fledged user-friendly client library on top of the API is a complicated and time-consuming task. For some languages (C++, Python, and Java), we have partially solved it. gRPC queries serve YTsaurus [RPC proxies](https://ytsaurus.tech/docs/en/rpc.md). As follows from the proxy name (RPC), the server expects a [protobuf](https://developers.google.com/protocol-buffers/) from the client. The server will also respond with a message. Each YTsaurus command, such as "create a node in Cypress" or "write rows", is represented as a pair of messages (with TReq and TRsp prefixes for a query and a response respectively). Example: `TReqCreateNode, TRspCreateNode`: Create a node in Cypress. You can view the messages and their descriptions in the proto file. ## Installing and compiling proto messages You can install compiled messages via pip: `pip install ytsaurus-proto`. You also need to install gRPC and protobuf of the following versions (newer versions are also allowed, but functionality has not been tested on newer packages): ``` protobuf>=3.2.1 grpcio==1.2.0rc1 ``` ## Examples A short example of a get query to Cypress via gRPC: ```python import yt_proto.yt.client.api.rpc_proxy.proto.api_service_pb2 as api_service_pb2 import yt.yson as yson import yt.wrapper as yt import grpc import random if __name__ == "__main__": # We will query the cluster yt.config["proxy"]["url"] = "cluster-name" # Getting a list of gRPC proxies, the "discover_proxies" command is available from the fourth version of the API. yt.config["api_version"] = "v4" proxies = yt.driver.make_formatted_request("discover_proxies", {"type": "grpc"}, format=None)["proxies"] # Creating a gRPC connection to an arbitrary proxy from the list. channel = grpc.insecure_channel(str(random.choice(proxies))) # Filling in the proto message, making a get on the path //home/username. get_req = api_service_pb2.TReqGetNode(path="//home/username") # Transmitting the minimum version of the protocol (learn more about this below) and the access token. metadata = [ ("yt-protocol-version", "1.0"), ("yt-auth-token", yt._get_token()) ] # Initiating a unary-unary query (that is, one message from the client (the query) and one message from the server (the response)). unary = channel.unary_unary( "/ApiService/GetNode", request_serializer=api_service_pb2.TReqGetNode.SerializeToString, response_deserializer=api_service_pb2.TRspGetNode.FromString) # Making a query. _, call = unary.with_call(get_req, metadata=metadata) # Typing a response. print yson.loads(call.result().value) ``` ## Message versioning and protocol version The client must pass `yt-protocol-version` in the headers of each query. This field is a row of the "Major.Minor" form. When receiving a gRPC query, the server checks the following: 1. The Major version of the client and the server must match exactly. 2. The Minor version of the server must be greater than or equal to the Minor version of the client. If the conditions are not met, an error will occur. The Major version may change in case of major protocol changes and such changes will be rare. Backward compatibility is supported within a single Major version: old clients will work with new versions of the server. The Minor version can change when a package with compiled proto files is rebuilt (for example, when optional fields are added to proto messages). The client is responsible for specifying the correct version of the protocol. Let's say there is an optional parameter in the message: ``` // Since 1.42 optional int64 some_important_value = 42; ``` As you can see from the comment, this parameter appeared in the server version "1.42". If you are using this parameter as a client, you need to specify `yt-protocol-version = "1.42"`, otherwise the old server may simply ignore this parameter. ## gRPC protocol in YTsaurus YTsaurus uses the gRPC protocol as follows: 1. In YTsaurus, gRPC descriptions of services in proto files are not used. 2. In YTsaurus, an arbitrary blob of binary data (the so called attachment, learn more about this below) can be added to the proto message. These differences do not require modification of the gRPC code, you can use an ordinary stock client. ### Attachment in gRPC To transfer binary data (such as rows to be inserted into a dynamic table), YTsaurus adds support for so called attachments. Attachment is a block of binary data attached to a single message. It can be used both in the query (such as rows to be inserted) and in the response. There may be multiple attachments in the response and the query. For a formal description, let's turn to the [description](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) of gRPC over HTTP/2 and consider an example of the query and response. **Query** ``` HEADERS (flags = END_HEADERS) :method = POST :scheme = http :path = /google.pubsub.v2.PublisherService/CreateTopic :authority = pubsub.googleapis.com grpc-timeout = 1S content-type = application/grpc+proto grpc-encoding = gzip authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v DATA (flags = END_STREAM) ``` **Response** ``` HEADERS (flags = END_HEADERS) :status = 200 grpc-encoding = gzip content-type = application/grpc+proto DATA HEADERS (flags = END_STREAM, END_HEADERS) grpc-status = 0 # OK trace-proto-bin = jher831yy13JHy3hc ``` Both the query and the response contain various metainformation (`grpc-encoding`, `content-type`, `grpc-status`, and others). The data section marked as `DATA ` is described in the [ABNF](https://en.wikipedia.org/wiki/Augmented_Backus–Naur_form) notation. ``` Length-Prefixed-Message → Compressed-Flag Message-Length Message Compressed-Flag → 0 / 1 # encoded as 1 byte unsigned integer Message-Length → {length of Message} # encoded as 4 byte unsigned integer Message → *{binary octet} ``` This is the compression flag (it takes one byte, in case of the YTsaurus RPC proxy, it must always be 0) followed by the message length encoded by four bytes and the message as an arbitrary byte sequence of `Message-Length`. YTsaurus specifies the Message, now it is not an arbitrary byte sequence. The message is described as follows: ``` Message → SerializedProtoMessage *Length-Prefixed-Attachment Length-Prefixed-Attachment → Attachment-Length [Attachment] Attachment-Length → {length of Attachment} # encoded as 4 byte unsigned integer, can be 0xFFFFFFFF if Attachment is omitted Attachment → *{binary octet} ``` First comes the `SerializedProtoMessage` part that is a serialized proto message. This is followed by the number of (possibly zero) attachments. Each attachment is a length (`Attachment-Length` encoded by four bytes) + an arbitrary byte sequence of `Attachment-Length`. Note that if there are no attachments, the protocol is completely identical to the stock gRPC protocol. The system cannot distinguish where `SerializedProtoMessage` ends. To do this, enter a special key in the metadata in the header: `yt-message-body-size`. Thus, the system reads `yt-message-body-size` bytes, turns them into a proto message, and considers the remaining bytes until the end of the stream as attachments. If the `yt-message-body-size` header is not specified, the entire stream is considered a serialized proto message for compatibility purposes. ## Wire format YTsaurus uses a wire format to transmit rows, serialized rows are transmitted to attachments. The partitioning of a serialized row stream into attachments is arbitrary, consider several consecutive attachments as a single byte stream. Each query that can accept or return rows has a [rowset descriptor](https://github.com/ytsaurus/ytsaurus/blob/main/yt/yt_proto/yt/client/api/rpc_proxy/proto/api_service.proto). It describes how rows should be deserialized or serialized. This section describes how an unversioned rowset (`RK_UNVERSIONED`) is arranged. If you want to use the `RK_VERSIONED` type, write to [ytsaurus@](https://ytsaurus.tech/docs/en/mailto:community@ytsaurus.tech). Use the familiar ABNF notation (endianness - little) for description: ``` UnversionedRowset → RowCount *UnversionedRow RowCount → {row count} # encoded as 8 byte unsigned integer UnversionedRow → ValueCount *UnversionedValue ValueCount → {value count} # encoded as 8 byte unsigned integer UnversionedValue → ValueIndex ValueType AggregateFlag Length Content ValueIndex → {index of column to which value belong, should match `columns` field in TColumnDescriptor} # encoded as 2 byte unsigned integer ValueType → 0x02 {Null} / 0x03 {Int64} / 0x04 {Uint64} / 0x05 {Double} / 0x06 {Boolean} / 0x10 {String} / 0x11 {Any} # encoded as unsigned byte AggregateFlag → {is value aggregate?} # encoded as unsigned byte Length → {length of Content} # encoded as 4 byte unsigned integer Content → *{binary octet} # must be 8-byte aligned! ``` Thus, the minimum `Content` length can be 8 bytes (for all values except String and Any). Let's consider the `{"a": 1, "b": {"x": "y"}}` dict as an example. This is one row (`UnversionedRow` in the terms above) consisting of two `UnversionedValue` — one (key `a`) of the `int64` type and the other (key `b`) of the `any` type. Column names are not passed in rows. They are passed in `TColumnDescriptor` in the proto message and the rows contain only their `ValueIndex`. An example of implementing this format in Python is available [here](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/yt/wire_format). # Task proxy YTsaurus operations often require deploying web services. These can be debugging UIs (such as Spark UI in [SPYT](https://ytsaurus.tech/docs/data-processing/spyt/overview.md)), ML model inference servers, or APIs inside jobs. Operation jobs run on cluster exec nodes, so services bind to network ports on these nodes — to receive incoming traffic. However, when attempting to directly access services inside a job, difficulties arise: - Network isolation: the user may not have direct network access to exec node IP addresses (they may be in a closed perimeter). - Dynamic addressing: even with network access, jobs can move between nodes, so the host and port of services constantly change. - Security: direct access to a port on a node bypasses YTsaurus authentication mechanisms. Access control to the operation is not enforced. _Task proxy_ solves these problems by providing a single entry point. It allocates stable domains (FQDN) for each service and verifies user access rights before redirecting the request inside the job. ## How it works { #how } Task proxy is based on the _service discovery_ mechanism. It automatically finds operations with web services in the cluster and [generates](https://ytsaurus.tech/docs/en/#access) domain names for them. Each service is described by entities: - _Operation_: The main unit of work in the YTsaurus cluster. - _Task_: Type of work within an operation. An operation can contain multiple tasks (for example, driver and executor). - _Job_: A specific process. Each task runs in one or more instances — jobs. - _Service_: A specific network service of a task. One task can expose multiple web services (for example, HTTP UI and REST API). Task proxy balances incoming traffic between all running jobs of one task, directing the request to a specific service of that job. If one of the jobs fails or moves to another node, the proxy automatically redirects traffic to available instances. ![](https://ytsaurus.tech/_images/task-proxy.png) Automatic discovery (service discovery) works for: - Vanilla operations with configured annotations. - SPYT operations (Standalone cluster and Direct Submit) — they are discovered automatically without additional configuration. ## Launching HTTP service {#quickstart} To launch an operation with a web service accessible through Task proxy: 1. Specify the `port_count` parameter in the task specification. YTsaurus will allocate free ports on the exec node and pass their numbers to environment variables `YT_PORT_0`, `YT_PORT_1`, etc. 2. Add `task_proxy={enabled=%true}` to the operation specification. Example command to launch two instances (`job_count=2`) of an HTTP server on one port (`port_count=1`): ```sh yt vanilla \ --tasks '{example_http_server={job_count=2; command="python3 -m http.server ${YT_PORT_0}"; port_count=1}}' \ --spec '{annotations={task_proxy={enabled=%true;}}}' ``` With this configuration, Task proxy will automatically discover the service, assign it the `HTTP` protocol and the name `port_0`. ## How to find out the service address {#access} After launching the operation, Task proxy will generate a unique domain for each service. The domain name consists of `baseDomain` (specified by the cluster administrator), to which a unique hash from the service descriptor is added. The service discovery mechanism scans operations and writes information about available entry points to the `//sys/task_proxies/services` table (the path can be changed by the administrator in the `dirPath` parameter). Below is an example of the table contents. Each row describes its own service: | __domain__ | __operation_id__ | __task_name__ | __service__ | __protocol__ | |------------------------------------------|-----------------------------------|---------------------|-------------|--------------| | 645236d8.my-cluster.ytsaurus.example.net | a6e04b98-bf982394-5103e8-55754a49 | example_http_server | port_0 | http | | ae5cf6f5.my-cluster.ytsaurus.example.net | a8ef7695-3de07913-5103e8-e29a6707 | example_grpc_server | server | grpc | | 2ef4261c.my-cluster.ytsaurus.example.net | a6e04b98-bf982394-5103e8-55754a49 | master | ui | http | | 51a6d485.my-cluster.ytsaurus.example.net | a6e04b98-bf982394-5103e8-55754a49 | history | ui | http | | 37a5f11c.my-cluster.ytsaurus.example.net | 6699a5a9-37c731e3-5103e8-b05d7dd0 | driver | ui | http | - The first row represents a [vanilla](https://ytsaurus.tech/docs/data-processing/operations/vanilla.md) operation with an HTTP server example. - The second is a [gRPC server example](https://github.com/ytsaurus/ytsaurus-task-proxy/tree/main/examples/grpc-service). On how to launch it using the extended Task proxy annotation format, read [further](https://ytsaurus.tech/docs/en/#exp). - The third and fourth rows correspond to a SPYT [standalone](https://ytsaurus.tech/docs/data-processing/spyt/cluster/cluster-start.md) cluster launched with history server. - The fifth row relates to SPYT [direct submit](https://ytsaurus.tech/docs/data-processing/spyt/direct-submit/desc.md). ## Methods of connecting to services {#connection-methods} To access a service, authentication is required. Supported: - **Tokens:** OAuth token or IAM token (in the `Authorization` header). - **Cookie:** Authentication cookie (cookie name is set by the administrator). ### Through browser {#browser} To access the UI (for example, Spark UI), simply open the service domain (`https://645236d8...`) in a browser. If you are authorized in the YTsaurus interface, access will be granted automatically via cookie. ### Through Ingress (public access) {#ingress} If an ingress controller is configured in the cluster, you can access the domain directly via the internet. In this case, DNS and TLS settings are not performed within Task proxy, but are carried out separately by your installation administrators. ```sh curl \ -H "Authorization: Bearer ${IAM_TOKEN}" \ "https://645236d8.my-cluster.ytsaurus.example.net" ``` ### Directly to Task proxy (inside K8s) {#k8s} If you are inside the network perimeter (for example, in Kubernetes where Task proxy is deployed), you can access the K8s service directly. In this case, you **must** specify the service domain in the `Host` header. ```sh curl \ -H "Host: 645236d8.my-cluster.ytsaurus.example.net" \ -H "Authorization: OAuth ${YT_TOKEN}" \ "http://task-proxy.${NAMESPACE}.svc.cluster.local:80" ``` ### Alternative to wildcard domains {#wildcard-domains} If the infrastructure does not support wildcard domains (like `*.ytsaurus.example.net`), use the `x-yt-taskproxy-id` header. Pass the left part of the domain (hash) as the value. ```sh curl \ -H "Authorization: Bearer ${IAM_TOKEN}" \ -H "x-yt-taskproxy-id: 645236d8" \ "https://task-proxy.my-cluster.ytsaurus.example.net" ``` ### Accessing services by alias (without a hash) {#alias} {% note info %} This feature is available in task-proxy version 0.3.0 and later. {% endnote %} By default, a service URL is generated using a hash (for example, `645236d8`) derived from the operation ID. Since the YTsaurus cluster generates this ID only when the operation starts, you cannot know the final service URL in advance. Instead, you have to look it up in a system table after the launch. If you need predictable URLs (for example, to automate requests to a job API), you can access the service using a unique combination of three parameters instead of the hash: `(operation_id_or_alias, task_name, service)`. This allows you to know the exact service URL before starting the operation. #### Setting an operation alias at launch {#set-alias} To make the URL predictable, assign an alias to the operation. The method depends on the type of operation: | Operation | Launch command | |--------------------|----------------------------------------------------------------------------| | Vanilla | `yt vanilla --spec '{alias="*myvanilla", ...}' ...` | | SPYT standalone | `spark-launch-yt --operation-alias *myspark ...` | | SPYT direct submit | `spark-submit --conf spark.ytsaurus.driver.operation.alias="*myspark" ...` | {% note warning %} When accessing the service (in the URL or HTTP headers), specify the alias **without the asterisk** (for example, `myspark`), even though it is defined as `*myspark` in the specification. {% endnote %} {% note info %} The `operation_alias`, `task_name`, and `service` values must match the `^[a-z0-9_]{1,30}$` regular expression. They can contain only lowercase letters, digits, and underscores (`_`), with a maximum length of 30 characters. {% endnote %} #### Accessing a service by alias {#access-by-alias} Once you know the operation alias, `task_name`, and `service`, you can access the service in one of two ways: via a domain name or HTTP headers. {% list tabs %} - Via a domain name You can combine the parameters using hyphens directly in the URL: ```sh curl \ -H "Authorization: Bearer ${IAM_TOKEN}" \ "https://${operation_alias}-${task_name}-${service}.my-cluster.ytsaurus.example.net" ``` - Via HTTP headers If you make a request to the base Task-proxy domain, you can pass the parameters using the `x-yt-taskproxy-*` headers: ```sh curl \ -H "Authorization: Bearer ${IAM_TOKEN}" \ -H "x-yt-taskproxy-operation-alias: ${operation_alias}" \ -H "x-yt-taskproxy-task-name: ${task_name}" \ -H "x-yt-taskproxy-service: ${service}" \ "https://task-proxy.my-cluster.ytsaurus.example.net" ``` {% endlist %} -------- #### Access by operation ID (if an alias is not set) {#access-by-operation-id} If you haven't set an alias but still want to avoid using the hash, you can access the service by passing its `operation_id` in the HTTP headers: ```sh curl \ -H "Authorization: Bearer ${IAM_TOKEN}" \ -H "x-yt-taskproxy-operation-id: ${operation_id}" \ -H "x-yt-taskproxy-task-name: ${task_name}" \ -H "x-yt-taskproxy-service: ${service}" \ "https://task-proxy.my-cluster.ytsaurus.example.net" ``` ## Extended annotation format {#exp} The `task_proxy={enabled=%true}` annotation applies default settings. This means that when using it, each service receives: - name `port_${PORT_INDEX}`; - port with an index corresponding to the order of task appearance in the operation specification; - HTTP protocol. If you want to set other settings — for example, use the gRPC protocol, set a custom service name, or select a specific port from the list — use the extended annotation format. To do this, you need to specify the `tasks_info` field in it. Examples can be found below. ### Launching gRPC service {#grpc} Let's launch a [gRPC server example](https://github.com/ytsaurus/ytsaurus-task-proxy/tree/main/examples/grpc-service) using the extended annotation: ```sh yt vanilla \ --tasks '{example_grpc_server={job_count=2; command="./yt-sample-grpc-service"; file_paths = ["//home/yt-sample-grpc-service"]; port_count=1}}' \ --spec '{annotations={task_proxy={enabled=%true; tasks_info={example_grpc_server={server={protocol="grpc"; port_index=0}}}}}}' ``` Breakdown of the `tasks_info` parameter: - `example_grpc_server` — task name. - `server` — service name (in this example there is one, but one task can contain multiple services). - `protocol="grpc"` — protocol specification. - `port_index=0` — use the first port from `port_count` (corresponds to `YT_PORT_0`). ### Accessing gRPC service {#grpcurl} gRPC requests also require authorization. #### Through Ingress (public access) {#grpc-ingress} Note that the port for gRPC on the ingress controller may differ (in the example below — `9090`). ```sh grpcurl \ -H "Cookie: YTCypressCookie ${AUTH_COOKIE_VALUE}" \ "ae5cf6f5.my-cluster.ytsaurus.example.net:9090" \ "helloworld.Greeter/SayHello" ``` #### Directly to Task proxy (inside K8s) {#grpc-task-proxy} ```sh grpcurl \ -plaintext \ -authority "ae5cf6f5.my-cluster.ytsaurus.example.net" \ -H "Authorization: Bearer ${IAM_TOKEN}" \ "task-proxy.${NAMESPACE}.svc.cluster.local:80" \ "helloworld.Greeter/SayHello" ``` # Troubleshooting This section covers diagnosing operations, jobs, and data reads. - [Debugging MapReduce programs](https://ytsaurus.tech/docs/en/mapreduce-debug.md) - [Jobshell and slow jobs](https://ytsaurus.tech/docs/en/jobshell-and-slowjobs.md) - [Slow reads](https://ytsaurus.tech/docs/en/slow-read.md) - [Job statistics](https://ytsaurus.tech/docs/en/jobstatistics.md) - [Debugging jobs locally](https://ytsaurus.tech/docs/en/jobtool.md) # Debugging MapReduce programs ## Emulating job startup locally You can emulate job startup on any computer. For example, you can use your development server. To learn more about local job emulation, see [Debugging jobs locally](https://ytsaurus.tech/docs/en/jobtool.md) and [Slow job troubleshooting](https://ytsaurus.tech/docs/en/jobshell-and-slowjobs.md). ## Getting the current stderr of a running job As an operation runs, there may be situations where there's still one incomplete job left while the rest have already finished. In YTsaurus, it's possible to get the stderr that the job has written up to a given moment. You can do that with a special mode implemented in the `yt` command: `yt get-job-stderr`. ```bash yt get-job-stderr --operation-id 35060d89-f9328e09-3f403e8-6f4eb4b5 --job-id fe270b54-a938652-3fc0384-2144 ``` Stderr in the output is truncated according to the same rules that apply when you save files in Cypress: the first few megabytes plus the last few megabytes are shaved off. ## Getting full stderr of all jobs of an operation In YTsaurus, you can save full stderr of all jobs to a table. You can export stderr of those jobs that were not aborted. To enable the described behavior: {% list tabs %} - In Python Use the `stderr_table` parameter. For example: ```python yt.wrapper.run_map_reduce( mapper, reducer, '//path/to/input', '//path/to/output', reduce_by=['some_key'], stderr_table='//path/to/stderr/table', ) ``` - In С++ Use the [StderrTablePath](https://github.com/ytsaurus/ytsaurus/blob/07c9c385ae116f56d8ecce9fa6765fa1a90e95cc/yt/cpp/mapreduce/interface/operation.h#L563) setting. - In SDKs in other languages You can pass the `stderr_table_path` setting directly to the operation specification. For a description of this option, see [Operation options](https://ytsaurus.tech/docs/data-processing/operations/operations-options.md). {% endlist %} A stderr table has the following columns: 1. `job_id`: Job ID. 2. `part_index`: If a job has stderr that is too large, it is split into parts. The `part_index` value indicates the index of a specific stderr part. 3. `data`: Stderr data itself. The table is sorted by `job_id`, `part_index`. You can read such a table using the [read_blob_table](https://ytsaurus.tech/docs/storage/blobtables.md#read_blob_table) command. # Slow job troubleshooting This section provides a description of using the `Job Shell` tool to troubleshoot slow jobs. On occasion, one or more operation jobs may run for a long time without visible progress. To figure out the reason, you can use the `Job Shell` tool. You can use this tool to open a console directly from the job environment on behalf of the same user the job is running under. Thereafter, you can explore the running job with calls to **gdb** or **strace**. The recommended work algorithm is shown in the figure: ## Starting Job Shell To launch Job Shell, you need to run `yt --proxy run-job-shell `, substituting a cluster name or a job ID for `<...>`. You can find out the job ID on the **Jobs** tab of the operation page. Detailed description of Job Shell launch: ```bash yt --proxy cluster-name run-job-shell fcebe6b9-e06ab2e1-3fe0384-d0b66453 Use ^F to terminate shell. Job environment: TMPDIR=/yt/disk2/cluster-data/slots/11/sandbox PYTHONPATH=/yt/disk2/cluster-data/slots/11/sandbox PYTHONUSERBASE=/yt/disk2/cluster-data/slots/11/sandbox/.python-site-packages PYTHON_EGG_CACHE=/yt/disk2/cluster-data/slots/11/sandbox/.python-eggs HOME=/yt/disk2/cluster-data/slots/11/sandbox YT_OPERATION_ID=78b048b5-8055fabe-3fe03e8-d5b74b2a YT_JOB_INDEX=120 YT_JOB_ID=fcebe6b9-e06ab2e1-3fe0384-d0b66453 YT_START_ROW_INDEX=202133578 UID PID PPID C STIME TTY TIME CMD 19911 257386 239675 0 02:24 pts/1 00:00:00 /bin/bash 19911 257397 257386 0 02:24 pts/1 00:00:00 \_ ps -fu 19911 --forest 19911 240043 239675 0 02:21 ? 00:00:00 /bin/bash -c :; ./my_mapper 19911 240073 240043 91 02:21 ? 00:02:37 \_ ./my_mapper yt_slot_11@n1757-sas:~$ ``` Command output displays all kinds of job information: - Path to the job's working directory: /yt/disk2/cluster-data/slots/11/sandbox. - Name of user job is running under: yt_slot_11. - List of processes running under current user. The first and the second processes in the example belong to Job Shell, the third process is a bash command-line shell session that the job in question is launched in, and the forth is the user process itself. ## User process analysis The first thing to do is to execute the `top -p ` command passing in the user process ID as an argument. Below is an example and a call output: ```bash yt_slot_11@n1757-sas:~$ top -p 240073 top - 02:30:52 up 6 days, 7:11, 0 users, load average: 41.26, 37.59, 39.23 Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie Cpu(s): 48.9%us, 5.6%sy, 0.0%ni, 31.1%id, 14.3%wa, 0.0%hi, 0.2%si, 0.0%st Mem: 132001796k total, 130937776k used, 1064020k free, 296488k buffers Swap: 0k total, 0k used, 0k free, 107424768k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 240073 yt_slot_ 20 0 4164 632 552 R 98 0.0 8:08.05 my_mapper ``` From CPU usage in this example (48.9%), it is obvious that the user process is performing computations, and the reason for the job's slow performance may be in the user code. You should connect to the job process using **gdb** and try and figure out what it is doing at the moment. If CPU usage is close to zero, this most likely means that the process is frozen in a system call. You should call **strace** on the user process. Example call: ```bash yt_slot_11@n1757-sas:~$ strace -p 240073 Process 240073 attached - interrupt to quit write(4 ``` If the process is making system calls all the time (rather than freezing in a single one), be prepared for the stream of information that the above command will output to the console. In this situation, this syntax may prove useful: ``strace -p 2>&1 | head``. This helps you see a single call. In addition, if your code is multi-treaded, it might be useful to add the ``-f`` option that helps capture system calls from all threads. If a process hangs up on a read or **write** system call, you should look at the value of the first argument, which is the file descriptor of the thread. The value of zero for the **read** system call and a small integer for the **write** system call (1, 4, 7) means that for some reason, the YTsaurus Job Proxy service process is not releasing or accepting the next batch of data to or from the user process. In this situation, you need to advise the system administrator of the problem enclosing detailed information on the troubleshooting you have done. For more information on troubleshooting, see [How to report](https://ytsaurus.tech/docs/en/howtoreport.md). In the event of system call hang-ups on **futex**, **connect**, **open**, **stat**, and others, the cause is not related to the operation of `Job Proxy` or the YTsaurus system overall. You should look for the problem inside user code. You need to use **gdb** to figure out what is causing the block. If the user code is multi-threaded, for instance, and blockage occurs for some reason, **strace** output could look like this: ```bash yt_slot_11@n1757-sas:~$ strace -p 240073 2>&1 | head Process 240073 attached - interrupt to quit futex(0x7ffff79b3e00, FUTEX_WAIT, 2, NULL ``` It is possible that the user process is not consuming any CPU, and **strace** is not showing any system calls. This means that the code is using **memory mapping** and reads from (or writes to) a file on disk. This results in a **major page fault**, and the process waits while the operating system uploads data from disk into memory. {% note warning "Attention!" %} The use of **memory mapping** in jobs is **strongly discouraged** while jobs doing this create heavy disk load with random reads and writes. Such jobs are recognizable from the high values of the [statistics](https://ytsaurus.tech/docs/en/jobstatistics.md) for `/user_job/block_io/[io_read,io_write]`. Please remember that a conventional hard drive is capable of no more than 100 random access operations per second while there are several dozen jobs running concurrently on a cluster node. {% endnote %} A specific example of a **memory mapped** file read by a job is an operation executable and its dependencies dynamically loaded by a linker as the operation is launched. The loader code is normally written to prevent the resulting **page faults** from causing problems. You should remember that reducing the executable and dependency size always has a salutary effect on performance. In particular, from the performance standpoint, it is better to use executables without debug symbols. ## Job Shell execute privilege To run Job Shell, you need to have the operation **read** and **manage** privileges. To get these, you need to include in the operation spec an [ACL](https://ytsaurus.tech/docs/storage/access-control.md) describing users and groups that are authorized to manipulate the operation and jobs when launching an operation. By default, the ACL list is empty, and operation privileges are only issued to the user that launched the operation as well as the YTsaurus system administrators. Example ACL configuration in Python: ```python spec={"acl": [{"permissions": ["read", "manage"], "subjects": ["user1", "user2"], "action": "allow"}]} ``` The user from the YT_TOKEN/YT_TOKEN_PATH (authenticated_user) environment variables is added automatically. To modify the access rights of an already running operation, you need to run the `update_operation_parameters` command with a new ACL. # Slow table and file reads Pattern for moving data for reading: ![](https://ytsaurus.tech/_images/read.svg) ## How reading is performed. Table chunks are hosted on node disks in a native format compressed by `compression_codec`. From there, they are moved to data proxies where they are unpacked, recoded as requested by the user (as YSON, Protobuf, or Skiff), compressed by the specified network codec as part of the HTTP response, and find their way to the user machine. ## Factors affecting read performance 1. [**Medium**](https://ytsaurus.tech/docs/storage/media.md). This is the type of disk table chunks are stored on. Held in the `@primary_medium` attribute. SSD speed is higher than HDD but their quota is more limited. To change the medium, you first need to make a new entry in the table's `@media` attribute, such as, `yt set //path/to/table/@media/ssd_blobs '{replication_factor=3}'`, and then move the table to the new medium: `yt set //path/to/table/@primary_medium ssd_blobs` 1. [**Compression algorithms**](https://ytsaurus.tech/docs/storage/compression.md) are a compromise between performance and compression quality. The fastest is `lz4`, the slower and the more compact ones range from `brotli_1` to `brotli_11` (the larger the number, the better the compression). You have to remember that there are two algorithms: - The one used to store the table (`yt get //path/to/table/@compression_codec`). To change this one, you need to set the `@compression_codec` attribute and re-compress all the table chunks (see [commands](https://ytsaurus.tech/docs/storage/compression.md#set_compression)). - The one used for network transmission. It depends on the client used. * In C++ API, you need to set the `TConfig::Get()->AcceptEncoding` global configuration option. The algorithm names from the [section](https://ytsaurus.tech/docs/storage/compression.md#compression_codecs) must be prefixed with `"z-"`, such as `"z-snappy"`. * In Python API, you need to set the `yt.wrapper.config["proxy"]["accept_encoding"]` option. The supported algorithms are `gzip`, `br` (brotli), and `identity`. 1. **Parallelism**. If you have reached the limit of disk read performance or node format/compression, parallel reads may help. - In Python API, use the `yt.wrapper.config["read_parallel"]` config section that includes `"enable"`, `"data_size_per_thread"`, and `"max_thread_count"`. - In C++ API, you need to use the `CreateParallelTableReader` function from a special library. 1. **Network**. If data is hosted in one data center, and data are read from a different one, you might discover that bandwidth is the bottleneck. 1. [**Format**](https://ytsaurus.tech/docs/storage/formats.md) The fastest is skiff, followed by Protobuf and YSON. 1. **Data proxy load**. The proxies are divided into groups (roles), and by default, everyone uses the `data` role; that is why, such proxies carry a higher load. 1. **Work on the cluster**. When cluster nodes are updating, for instance, disks may carry a higher load. ## Where do I start? 1. Increase parallelism (see above). This may solve most of your reading problems. If this does not help or does not help enough, you need to do further troubleshooting. 1. Look CPU load up in the [profiler](https://en.wikipedia.org/wiki/Perf_(Linux)) or [top-e](https://en.wikipedia.org/wiki/Top). If you can see activities related to reading, the possible reasons are: - HTTP codec. Python API, for instance, uses gzip by default, and that may prove to be the bottleneck because of compression speed. In the latter case, top-e may show high CPU load from the python process. You can try replacing it with `identity`. - Format. Such as parsing YSON and distributing it to objects in memory. 1. As an experiment, try moving some of the data to an SSD (see above) and reading from there. This will help you ascertain whether node disk limitations are your problem. 1. If the above steps failed to increase performance, please contact the system administrator. If the problem is associated with system capability limitations, write to the YTsaurus mailbox describing what you have done and the outcomes that you obtained and would like to obtain. # Job stats This section provides a list and descriptions of statistics collected by the YTsaurus system from operations and individual jobs. There are examples of issue troubleshooting based on the statistics collected. ## Overview While operations are running, YTsaurus collects various system and user statistics from jobs. Individual job statistics are entered into the scheduler log while aggregated statistics for an operation are available through the web interface by going to the **Statistics** tab on the operation page. The figure shows an example of a page with operation job statistics: ![](https://ytsaurus.tech/_images/jobstatistics.png) {% note info "Note" %} For a better understanding of job stats, you should be familiar with the notions of `job proxy` and `sandbox`, and have an idea of the way the YTsaurus system delivers user files to jobs. You can find out about these things in the Jobs section. {% endnote %} ## Statistic descriptions ### System stats { #system_stats } | Statistic | Description | | --- | --- | | `time/total` | Job running time from the moment it is created by the scheduler until the moment the scheduler is notified that the job is completed (successfully or otherwise). Milliseconds. | | `time/prepare` | Job set up time until a `job proxy` is started (files are uploaded into the chunk cache as required, a `sandbox` directory is created, files are created and copied to `tmpfs`, and requisite `cgroups` are set up). Milliseconds. | | `time/artifacts_download` | Time to upload files to the chunk cache (as required). Included in `time/prepare`. Milliseconds. | | `time/prepare_root_fs` | Time to prepare the file system in the porto container (as required). Included in `time/prepare`. Milliseconds. | | `time/gpu_check` | Time to run for the utility pre-checking node GPU functionality (relevant for some GPU operations). Included in `time/prepare`. Milliseconds. | | `time/exec` | Job running time from start to `job proxy` process exit. Milliseconds. | | `data/input/chunk_count` | Total number of data slices read by the job. A data slice is a continuous segment of a single chunk (for static tables) or a continuous range of rows between two keys (for dynamic tables). Pieces. | | `data/input/row_count` | Total number of table rows read by a job. Pieces. | | `data/input/compressed_data_size` | Total compressed block data read by a job (table data fed to operation input). Bytes. | | `data/input/uncompressed_data_size` | Total uncompressed block data read by a job. Bytes. | | `data/input/data_weight` | "Logical" amount of uncompressed data read by a job. Depends only on the values in the table cells and the number of rows. Not dependent on whether a table has a schema or the values of `optimize_for`, `compression_codec`, or `erasure_codec`. Calculated as `row_count + sum(data_weight(value))` for all values in the table cells. Bytes. | | `data/input/not_fully_consumed` | 1 if a job has not read the entire input, 0 otherwise. | | `data/output/K/chunk_count` | Number of chunks written to output table with index K. Pieces. | | `data/output/K/row_count` | Number of rows written to output table with index K. In units. | | `data/output/K/compressed_data_size` | Compressed block data written to output table K. In bytes. | | `data/output/K/uncompressed_data_size` | Uncompressed block data written to output table K. In bytes. | | `data/output/K/data_weight` | "Logical" uncompressed data amount written to output table K. Dependent only on the values in the table cells and the number of rows. Not dependent on whether a table has a schema or the values of `optimize_for`, `compression_codec`, or `erasure_codec`. Calculated as `row_count + sum(data_weight(value))` for all values in the table cells. Bytes. | | `data/output/K/regular_disk_space` | Only populated for tables with `erasure_codec == none` and equal to `compressed_data_size` + metadata chunk size. Bytes. | | `data/output/K/erasure_disk_space` | Populated only for tables with `erasure_codec != none`. In addition to `compressed_data_size`, includes the amount of data in the [parity](https://ytsaurus.tech/docs/en/*parity) blocks. For more information about erasure codecs, see the [section](https://ytsaurus.tech/docs/storage/replication.md). Bytes. | | `job_proxy/cpu/user` | User mode CPU time of the `job proxy` process. Computed based on the `cpu_usage` value of the porto container. Milliseconds. | | `job_proxy/cpu/system` | Kernel mode CPU time of the `job proxy` process. Computed based on the `cpu_usage_system` value of the porto container. Milliseconds. | | `job_proxy/cpu/wait` | Wait CPU time of the `job proxy` process. Computed based on the `cpu_wait` value of the porto container. Milliseconds. | | `job_proxy/cpu/throttled` | Throttled CPU time of the `job proxy` process. Computed based on the `cpu_throttled` value of the porto container. Milliseconds. | | `job_proxy/block_io/bytes_written` | Number of bytes written by the `job proxy` process to the local block device. The value is derived from the `Write` section of `blkio.io_serviced_bytes` for the relevant cgroup. Normally, a small value because `job proxy` only writes logs to the local disk. Chunks are written via (local and/or remote) nodes and do not count towards this statistic. | | `job_proxy/block_io/bytes_read` | Number of bytes read by the `job proxy` process from the local block device. The value is derived from the `Read` section of `blkio.io_serviced_bytes` for the relevant cgroup. In a typical case approaches zero because `job proxy` only reads its own configuration from disk. | | `job_proxy/block_io/io_read` | Number of reads from the local block device by the `job proxy` process. The value is derived from the `Read` section of `blkio.io_serviced` for the relevant cgroup. Pieces. | | `job_proxy/block_io/io_write` | Number of writes to the local block device by the `job proxy` process. The value is derived from the `Write` section of `blkio.io_serviced` for the relevant cgroup. Pieces. | | `job_proxy/block_io/io_total` | Total number of input/output transactions on the local block device by the `job proxy` process. Pieces. | | `job_proxy/max_memory` | Maximum amount of RAM used by the `job proxy` process while a job is running. Bytes. | | `job_proxy/memory_reserve` | Amount of RAM guaranteed to the `job proxy` at launch. If actual usage exceeds `memory_reserve`, the job may be aborted with a `resource_overdraft` message. Bytes. | | `job_proxy/traffic` | Amount of data transmitted over the network to/from the `job proxy`. Normally, matches the amount of job input/output data if it only reads and writes tables/files as a job usually does and does not generate any other traffic on its own. Contains fields looking like `A_to_B` where `A` and `B` are data center short names. The value in this field is equal to the amount of data transmitted in this direction. You will find the time it took to transmit the data in the `duration_ms` field. Normally matches the time the `job proxy` has been in existence. In addition, incoming/outgoing traffic is also tracked (`inbound` and `outbound` fields, respectively), itemized by data center as well. | | `exec_agent/traffic` | Same as `job_proxy/traffic` but for an exec agent. Normally displays the traffic generated during job artifact setup. | | `user_job/cpu/*` | Similar to `job_proxy/cpu/*` but applies to the user job processes. | | `user_job/block_io/bytes_read`, `user_job/block_io/bytes_written`, `user_job/block_io/io_read`, `user_job/block_io/io_write`, `user_job/block_io/io_total` | Block IO statistics for a user job. Similar to `job_proxy/block_io/*`. | | `user_job/cumulative_memory_mb_sec` | Integral of memory used, MB*s | | `user_job/current_memory/major_page_faults` | Number of [major page faults](https://en.wikipedia.org/wiki/Page_fault#Major) in a user process. The value is derived from the `pgmajfault` section of `memory.stat` from cgroup `memory`. May assist in investigating the elevated `user_job/block_io/read` statistic. Pieces. | | `user_job/current_memory/rss` | RSS size at job end. The value is derived from the `rss` section of `memory.stat` for the relevant cgroup. Bytes. | | `user_job/current_memory/mapped_file` | Memory mapped files size at job end. The value is derived from the `mapped_file` section of `memory.stat` for the relevant cgroup. Bytes. | | `user_job/tmpfs_size` | `tmpfs` currently being **used** by the running job. Computed as the difference between the values of `total` and `free` returned by a call to `statfs` on tmpfs mount point. Bytes. | | `user_job/max_tmpfs_size` | Maximum `tmpfs` amount **used** throughout the job running time. Bytes. | | `user_job/tmpfs_volumes/K/size`, `user_job/tmpfs_volumes/K/max_size` | Same as `user_job/tmpfs_size` and `user_job/max_tmpfs_size` for each requested tmpfs volume individually. | | `user_job/disk/usage` | Disk space being taken up by a job sandbox; computed through a recursive sandbox walk by adding up all the file sizes. Bytes. | | `user_job/disk/limit` | Requested limit on job sandbox size. Bytes. | | `user_job/max_memory` | Maximum amount of RAM taken up by a user job while running, **net** of `tmpfs`. Bytes. | | `user_job/memory_limit` | Limitation on RAM amount set in the operation spec at operation start. Duplicated in the stats for housekeeping reasons. Bytes. | | `user_job/memory_reserve` | Amount of RAM guaranteed for a user job at time of launch. When actual usage becomes greater (but less than `memory_limit`), a job may be aborted with a `resource_overdraft` message if a cluster node is short on memory. You can control this value using an [option](https://ytsaurus.tech/docs/data-processing/operations/operations-options.md) called `memory_reserve_factor` in the operation spec. Bytes. | | `user_job/pipes/input/bytes` | Number of bytes transmitted via the user process stdin (that is, the amount of input data converted to the specified input format with all the control modifiers like `table_index`, `row_index`, and so on). | | `user_job/pipes/input/idle_time` | Amount of time during which the `job proxy` transmitted no data to a user job via stdin because it was reading data. For instance, a data cluster node disk was busy, and the data were unavailable, or the `compression_codec` used was slow to uncompress. Milliseconds. | | `user_job/pipes/input/busy_time` | The amount of time during which the `job proxy` process was writing data to a user job stdin. The value can be large if the user code implements computationally intensive processing and does not have time to read data. In addition, slow reads could be the reason when user code hangs up on a write not reading new data as a consequence. Milliseconds. | | `user_job/pipes/output/K/bytes` | Number of bytes written by a user process to the descriptor corresponding to the Kth output table. For information on descriptors and their numbering, please see the dedicated section. | | `user_job/pipes/output/K/idle_time` | Time during which the `job proxy` process did not read the stream corresponding to the Kth output table because it was writing data already read from that stream. For example, the cluster node being written to was slow to respond, or a very slow compression algorithm is being used. Milliseconds. | | `user_job/pipes/output/K/busy_time` | Time during which the `job proxy` process was reading from the stream corresponding to the Kth output table. If this time value is large, the user code has not written anything to the stream for a long time. For instance, because it took a long time to process the input, or because the input was unavailable. Milliseconds. | | `user_job/gpu/cumulative_utilization_gpu` | Net amount of time during which there were running GPU computations. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_utilization_memory` | Net time spent accessing GPU memory. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_utilization_clocks_sm` | An integral of the board frequency over time with respect to the maximum frequency. Added up across all the GPUs used by a job. Milliseconds * share. | | `user_job/gpu/cumulative_utilization_power` | Integral of effective GPU board power over time with respect to the maximum power. Added up across all the GPUs used by a job. Milliseconds * share. | | `user_job/gpu/cumulative_load` | Time during which GPU was at non-zero load. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_memory` | Integral of GPU memory utilization. Added up across all the GPUs used by a job. Milliseconds * bytes. | | `user_job/gpu/cumulative_power` | Integral of utilized GPU power. Added up across all the GPUs used by a job. Milliseconds * power. | | `user_job/gpu/cumulative_clocks_sm` | Integral of utilized GPU frequency. Added up across all the GPUs used by a job. Milliseconds * frequency. | | `user_job/gpu/max_memory_used` | Maximum recorded GPU memory utilization. Added up across all the GPUs used by a job. Bytes. | | `user_job/gpu/memory_total` | Total available GPU memory. Added up across all the GPUs used by a job. Bytes. | | `user_job/gpu/cumulative_slowdown/hw` | Time during which GPU experienced hardware slowdown. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_slowdown/hw_power_brake` | Time during which GPU experienced hardware slowdown due to power break. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_slowdown/hw_thermal` | Time during which GPU experienced hardware slowdown due to overheating. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_slowdown/sw_thermal` | Time during which GPU experienced software slowdown due to overheating. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_tensor_activity"` | Time during which the GPU tensor pipeline (HMMA) was active. Added up across all the GPUs used by a job. Milliseconds. | | `user_job/gpu/cumulative_dram_activity"` | Time during which the GPU memory interface was active, sending or receiving data. Added up across all the GPUs used by a job. Milliseconds. | | `codec/cpu/decode` | Wall time spent uncompressing data. Milliseconds. | | `codec/cpu/encode` | Wall time spent compressing data. Milliseconds. | | `job_proxy/memory_reserve_factor_x10000`, `user_job/memory_reserve_factor_x10000` | Housekeeping parameters used to analyze the operation of the `memory_reserve` computational algorithm. | | `job_proxy/aggregated_max_cpu_usage_x100`, `job_proxy/aggregated_smoothed_cpu_usage_x100`, `job_proxy/aggregated_preemptible_cpu_x100`, `job_proxy/aggregated_preempted_cpu_x100`, `job_proxy/preemptible_cpu_x100`, `job_proxy/smoothed_cpu_usage_x100` | Job CPU monitor housekeeping statistics. | | `chunk_reader_statistics/data_bytes_read_from_disk` | Amount of data retrieved from disk during chunk reads. Bytes. | | `chunk_reader_statistics/data_bytes_transmitted` | Amount of data transmitted over the network during chunk reads. Bytes. | | `chunk_reader_statistics/data_bytes_read_from_cache` | Amount of data retrieved from cache while reading chunks. Bytes. | | `chunk_reader_statistics/meta_bytes_read_from_disk` | Amount of metadata retrieved from disk during chunk reads. Bytes. | | `chunk_reader_statistics/wait_time` | Time spent waiting for data during chunk reads. Milliseconds. | | `chunk_reader_statistics/read_time` | Time spent actively reading chunks, such as parsing strings from read blocks. Milliseconds. | | `chunk_reader_statistics/idle_time` | Time during which chunk reads were aborted for the processing of previously read data. Milliseconds. | ### Python API | Statistic | Description | | --- | --- | | `custom/python_job_preparation_time` | Time between program main entry and the start of input row processing. | ## Troubleshooting examples ### Slow jobs The most common use of statistics is to troubleshoot slow jobs. {% note info "Note" %} Job statistics are to be used alongside other troubleshooting methods available in the YTsaurus system. {% endnote %} #### Slow user code Simple case: if `user_job/cpu/user ~= time/exec`, user code is causing the job to run slowly. You should connect to the job via the [job shell](https://ytsaurus.tech/docs/en/jobshell-and-slowjobs.md) and look for the bottleneck using perf and gdb. #### Multithreading A more complex case: a user has analyzed the stats and retrieved the total utilized CPU time: ``` user_job/cpu/user + user_job/cpu/system+job_proxy/cpu/user+job_proxy/cpu/system = 303162 milliseconds time/exec = 309955 milliseconds ``` The user is expecting the numbers to match. The equality above is not totally proper since `time/exec` is wall time while `user_job/cpu/*` and `job_proxy/cpu/*` are CPU times spent computing. At the same time, both the `job proxy` and (potentially) the user process may use more than a single core because of multithreading, and on top of that, both the processes are running concurrently. If the user process is single-threaded, the following should be approximately true: ``` time/exec ~= user_job/cpu/* + user_job/pipes/input/idle_time + max(user_job/pipes/output/N/idle_time) ``` The above computation does not include the time to "close" output chunks: when the output stream is already closed, all its data read but the `job proxy` is still finalizing chunks. #### Long compression If an operation is taking much longer to run than user code running time, and an execution time measurement showed that the YTsaurus system is spending most of its time processing a `yield`, the problem is that inside `yield` the python wrapper is writing data to the output stream. If the `job proxy` write pipeline is overloaded, the `job proxy` process stops reading data and a `yield` becomes blocked. `user_job/pipes/output/N/idle_time` statistics showed "lost" minutes. There are two possible reasons why a `job proxy` process failed to read data: the `job proxy` took a long time processing data on its side or there was a cluster write. The `job_proxy/cpu/user` stats make it clear that 70% of the total job time, the `job proxy` process was keeping the CPU busy, which means that processing input data took a long time. The heaviest part of `job proxy` processing is compression, which is what the profiler indicated in the end. {% note info "Note" %} General rule for slow job troubleshooting: when analyzing running time, you need to find dominant metrics, then look for a cause of the problem in the identified component. {% endnote %} ### Exceeding memory guarantees Operations exceed memory limitations. Steps to resolve this issue: 1. When requesting tmpfs, you need to make sure that you are not ordering too much. To make sure, you need to look at the `user_job/tmpfs_size` statistics. 2. When selecting `memory_limit`, you need to proceed from the `user_job/max_memory` metric; if you are using `tmpfs`, you must add the required `tmpfs` amount to `user_job/max_memory`. If you use automatic `tmpfs` ordering via the C++ and the Python APIs, the APIs will make the addition on their own. 3. Most jobs do not use a lot of memory but certain jobs require significantly more memory, and you should set `memory_limit` to the maximum job memory requirement but set `memory_reserve_factor` lower (at the same time, you should be ready that jobs will be aborted for `resource_overdraft`). You can make the relevant decision by comparing the average and the maximum values of the `user_job/max_memory` statistic (the web interface includes appropriate switches): if average and maximum memory usage in a job differs by several dozen percent or by orders of magnitude, this is an indication that there are jobs whose usage is inordinately high. 4. Reverse situation: if too many jobs are being aborted for `resource_overdraft`, you should increase `memory_reserve_factor`. ### Operation `excessive io` message { #excessive_io } You must review the `user_job/block_io/io_read` statistic. If a job makes tens of thousands of reads, it is harming both itself and other users. You should check the `major page faults` counter. A large value is an indication of possible memory mapping use. {% note warning "Attention" %} The use of **memory mapping** in jobs is **strongly discouraged** while jobs doing this create heavy disk load with random reads and writes. Please remember that a conventional hard drive is capable of no more than 100 random access operations per second while there are several dozen jobs running concurrently on a cluster node. {% endnote %} Solution for this issue: reducing executable size (by removing the debugging symbols, for instance), ordering tmpfs for the job, and uploading large files into memory at job launch (`copy_files` option). For more information on these options, please see [Operation settings](https://ytsaurus.tech/docs/data-processing/operations/operations-options.md). ## Supplemental YTsaurus is unable to measure phase time inside a user process. For example, if user code uploads a large dictionary into memory at launch, you will not be able to see the time it took to load this dictionary in the system metrics. If this happens, you should take advantage of user statistics. You can use user statistics to compute simple aggregates even in a map operation. The number of user statistics is limited to 128 per job. You can build different plots from job metrics (in particular, visualize operation phases) to simplify troubleshooting. [*parity]: Additional blocks for redundancy and original block recovery in the event of a loss of any part. Computed using a logical XOR. # Debugging jobs locally To make job debugging more convenient (for example, with GDB), you can use a special utility, `yt job-tool`. It downloads the job environment along with its input data and generates a script to run it. The utility comes as part of the [Python API](https://ytsaurus.tech/api/python/start.md) package. All types of jobs that run custom code are supported. The binary file supports two commands: `prepare-job-environment` and `run-job`. To get detailed information, use the `yt job-tool --help` command. ## Job's input data By default, `job-tool` gets full job input, although it isn't available for all jobs: * The specification required to get full input is saved for a few failed and a few successfully completed jobs. It is also available for all running jobs. * Getting full input requires the data read by the job: input tables must be available and unmodified. * "reduce_combiner" and "reduce" jobs of a MapReduce operation read temporary data that is only available until the operation has completed. To debug such jobs, you can rewrite the operation as a combination of Map, Sort, and Reduce. Or you can debug them while the operation is still running (including while it is suspended). ## Example 1. Let's run this operation: ```python import yt.wrapper as yt def mapper(rec): raise RuntimeError("fail") if __name__ == "__main__": yt.run_map(mapper, "//home/user/tables/dsv", "//home/user/output", format="dsv") ``` 2. When the operation fails, we diagnose it: ```bash yt job-tool prepare-job-environment ffc8f462-7f68f8c3-3fe03e8-433fe11f ffe4ff5c-d2fac8c1-3fe0384-816a7fd0 ``` 3. Once the command has finished, the folder `job_ffe4ff5c-d2fac8c1-3fe0384-816a7fd0` will contain the following files: ```bash $ ls job_ffe4ff5c-d2fac8c1-3fe0384-816a7fd0 command input run_gdb.sh run.sh sandbox ``` Where: * The `command` file contains the command that runs the job. Since the operation was run using the [Python API](https://ytsaurus.tech/api/python/start.md), the code will look like this: ```bash cat job_ffe4ff5c-d2fac8c1-3fe0384-816a7fd0/command python _py_runner.py mapper.lV4l9c config_dump6RRYa4 _modulesEFMotR _main_moduleJALUsC.py _main_module PY_SOURCE% ``` * `input` is the job input; it could be worth analyzing in some cases. * `run.sh` is a shell script that runs the job locally. * `run_gdb.sh` is a shell script that runs the job with GDB, which is useful for debugging C++ programs. {% note info %} You can modify the `run_gdb.sh` and `run.sh` scripts, for example, to integrate a custom debugger. {% endnote %} * `sandbox` is the directory where the job was run, along with all the files necessary to run it. As you debug, you can try updating files in the directory. 4. Run the job locally using the `run.sh` script: ```bash ./run 2016-07-22 12:00:33,499 INFO Started job process User job exited with non-zero exit code 1 with stderr: Traceback (most recent call last): File "_py_runner.py", line 56, in main() File "_py_runner.py", line 53, in main yt.wrapper.py_runner_helpers.process_rows(__operation_dump_filename, __config_dump_filename, start_time=start_time) File "/home/user/yt/python/yt/wrapper/py_runner_helpers.py", line 154, in process_rows output_format.dump_rows(result, streams.get_original_stdout(), raw=raw) File "/home/user/yt/python/yt/wrapper/format.py", line 137, in dump_rows self._dump_rows(rows, stream) File "/home/user/yt/python/yt/wrapper/format.py", line 251, in _dump_rows for row in rows: File "/home/user/yt/python/yt/wrapper/py_runner_helpers.py", line 89, in process_frozen_dict for row in rows: File "/home/user/yt/python/yt/wrapper/py_runner_helpers.py", line 49, in generator result = func(*args) File "", line 2, in mapper RuntimeError: fail origin hostname in 2016-07-22T12:00:35.427161Z ``` # Importing data from Hive, S3, MongoDB, and other systems Data is imported from external systems to YTsaurus with SPYT. This page contains instructions for importing data using the [import.py](https://github.com/ytsaurus/ytsaurus/blob/main/connectors/import.py) script. With this script, you can import data from Hive, Hadoop, S3, and database management systems that support the JDBC protocol. To import data from other systems not supported by `import.py` — such as MongoDB — you can use SPYT directly, reading from the external system via the corresponding Spark Data Source. ## Configuring connection via Kerberos To connect to Hadoop, you can use the Kerberos network authentication protocol. You can set up Kerberos authentication by taking the following steps: 1. Create a token on the Hadoop side using the following command: ```bash $ fetchdt --renewer null /tmp/token ``` 2. Copy the token to Cypress: ```bash $ cat /tmp/token | yt write-file //home/spark/conf/token ``` 3. When you start the application, specify the path to the token in Cypress by using the `--files` option #### Client mode In `--deploy-mode client` mode, you need to specify the path to a local file with the token for the driver, since its process runs outside the YTsaurus cluster. For executors, specify `HADOOP_TOKEN_FILE_LOCATION=token````bash ```bash $ spark-submit \ --master ytsaurus:// \ --deploy-mode client \ --files yt://home/spark/conf/token \ --conf spark.executorEnv.HADOOP_TOKEN_FILE_LOCATION=/tmp/token \ ./script.py ``` #### Cluster mode In `--deploy-mode cluster` mode, the path to the file with the token should be specified in the `spark.ytsaurus.config.global.path` configuration parameter. You will also need to add the `kdc` and `realm` parameters for the driver. Example command: ```bash $ spark-submit \ --master ytsaurus:// \ --deploy-mode cluster \ --files yt://home/spark/conf/token \ --conf spark.ytsaurus.config.global.path=//home/spark/conf/global \ --conf spark.driver.extraJavaOption="-Djava.security.krb5.kdc=... -Djava.secutirykrb5.realm=..." \ ./script.py ``` ## Installing dependencies { #dependencies } jar dependencies for interacting with Hive are provided in the `pyspark` Python package. All of the packages that SPYT requires (including `pyspark`) must be installed on the system calling the import. To read data from a database system that supports JDBC, download the JDBC driver for that system. `connectors/pom.xml` is a maven config that includes JDBC drivers for MySQL and PostgreSQL as dependencies. To download these drivers, run the command: ```bash ~/yt/connectors$ mvn dependency:copy-dependencies ``` `mvn` will download jar files with JDBC drivers for PostgreSQL and MySQL to `target/dependency`. To import data from a different database, add the JDBC driver for that database to `pom.xml` and run `$ mvn dependency:copy-dependencies` {% note warning "JDBC driver must be visible to the driver process" %} The JDBC driver must be available not only to executors (via `--jars`), but also on the **driver process** classpath. In `--deploy-mode client` mode, Spark does not automatically pick up the driver from `--jars` on the driver side, and reads fail with `java.sql.SQLException: No suitable driver`. Place the jar in the `jars/` directory of the installed `pyspark` (`$SPARK_HOME/jars/`), or set `export SPARK_DIST_CLASSPATH=/path/to/driver.jar` before running. {% endnote %} ## Importing data { #how-to-import } To import data, run the `import.py` script: ```bash $ ./import.py \ # Import arguments. # ... ``` The arguments must identify: - Data source. - Path to the imported data. - Path within YTsaurus where the imported data should be written. You can find the complete list of arguments at the end of this page, under [Arguments](https://ytsaurus.tech/docs/en/#script-options). Below are examples showing how to import data from various systems. ### Client and cluster modes { #run-modes } `import.py` can be run in two modes: - **Client (default)** — `python import.py ...`: The Spark driver starts locally on the machine where the script runs, while executors run in the YTsaurus cluster. - **Cluster** — `import.py` runs as a Spark application via `spark-submit ... --deploy-mode cluster`, and the driver also starts inside the YTsaurus cluster. The mode is determined automatically (based on whether the script is started via `spark-submit`); no separate flag is needed: ```bash $ spark-submit \ --master ytsaurus:// \ --deploy-mode cluster \ --jars yt:///path/to/jars/driver.jar \ import.py \ --jdbc postgresql --jdbc-server pg_host:5432 --jdbc-user user --jdbc-password '' \ --input jdbc:database_name.table_name \ --output //path/in/yt/table ``` Cluster mode is convenient when submitting from an **external machine** (for example, outside the Kubernetes cluster where YTsaurus is deployed): the driver goes inside the cluster, and no network connectivity between the driver and executors is required from the external machine. #### Network configuration inside Kubernetes { #cluster-proxy } If you submit from an external machine and YTsaurus is deployed in Kubernetes, only the external proxy is typically available from the outside, while intra-cluster traffic (driver and executors to YTsaurus) must go through an internal proxy. To enable this, set the `spark.hadoop.yt.clusterProxy` parameter to the address of the proxy accessible from inside the cluster; at the same time, `--master` specifies the external address used for submission: ```bash $ spark-submit \ --master ytsaurus:// \ --deploy-mode cluster \ --conf spark.hadoop.yt.clusterProxy= \ import.py ... ``` In client mode, pass the same parameter via `--extra-conf spark.hadoop.yt.clusterProxy=...`. ### Hive To import data from Hive: 1. Upload the `hadoop-aws-*.jar` and `aws-java-sdk-bundle-*.jar` files from your local directory `target/dependency` to Cypress. 2. Run the data import command: ```bash $ ./import.py \ --metastore master_host:9083 \ --warehouse-dir /path/to/hive/warehouse \ --input hive:database_name.table_name \ --output //path/in/yt/table \ --proxy https://ytsaurus.company.net \ --num-executors 5 \ --executor-memory 4G \ --executor-memory-overhead 2G \ --jars yt:///path/to/jars/aws-java-sdk-bundle-1.11.901.jar \ yt:///path/to/jars/hadoop-aws-3.3.1.jar ``` [Go to argument descriptions](https://ytsaurus.tech/docs/en/#script-options). Alternatively, provide an SQL query to be executed by Hive using the `hive_sql` input specifier. Query results will be stored in YT. ```bash $ ./import.py \ ... --input hive_sql:database_name:SELECT * FROM action_log WHERE action_date > '2023-01-01' \ ... ``` ### HDFS To import files from HDFS, use a specifier with the file's format and the address of the HDFS NameNode: ```bash $ ./import.py \ ... --input text:hdfs://namenode/path/to/text/file ... ``` `import.py` supports the `text`, `parquet`, and `orc` file formats. ### Database systems with JDBC support To import data from JDBC-compatible systems, such as PostgreSQL, run the following command: ```bash $ ./import.py \ --jdbc postgresql \ --jdbc-server pg_host:5432 \ --jdbc-user user \ --jdbc-password '' \ # Get password from terminal prompt --input jdbc:database_name.table_name \ --output //path/in/yt/table ``` [Go to argument descriptions](https://ytsaurus.tech/docs/en/#script-options). To import the results of an SQL statement, use the `jdbc_sql` input specifier: ```bash $ ./import.py \ ... --input jdbc_sql:database_name:SELECT * FROM users WHERE signup_date > '2023-01-01' \ ... ``` #### Parallel reading of large tables By default, `import.py` reads a JDBC source in a single thread (with a single query). For large tables, this is slow and can lead to long-running query errors on the DBMS side (for example, `ORA-01555: snapshot too old` in Oracle). To read a table in parallel, specify a numeric or date partition column and its range: ```bash $ ./import.py \ --jdbc postgresql \ --jdbc-server pg_host:5432 \ --jdbc-user user \ --jdbc-password '' \ --input jdbc:database_name.table_name \ --jdbc-partition-column id \ --jdbc-lower-bound 1 \ --jdbc-upper-bound 1000000 \ --jdbc-num-partitions 16 \ --output //path/in/yt/table ``` SPYT splits the read into `--jdbc-num-partitions` ranges of the form `column >= a AND column < b` and reads them in parallel. The `--jdbc-lower-bound`/`--jdbc-upper-bound` bounds only define the split step — they do not filter the data (values outside the bounds land in the edge partitions). The column type must be numeric or date; preferably indexed or a table partitioning key, so that each query reads only its own portion. Partitioning options are global — they apply **identically to all** tables listed in `--input`. If you import multiple tables in a single run, the partition column and bounds must be suitable for each table; otherwise, run `import.py` separately for each table. ### S3 To import data from S3: 1. Upload the `hadoop-aws-*.jar` and `aws-java-sdk-bundle-*.jar` files from your local directory `target/dependency` to Cypress. 2. Run the data import command: ```bash ./import.py \ --input parquet:s3a://bucket/path/to/data/sample-parquet \ --output //home/tables/import_from_s3 \ --s3-access-key \ --s3-secret-key \ --s3-endpoint \ --proxy https://ytsaurus.company.net \ --num-executors 5 \ --executor-memory 4G \ --executor-memory-overhead 2G \ --jars yt:///path/to/jars/aws-java-sdk-bundle-1.11.901.jar \ yt:///path/to/jars/hadoop-aws-3.3.1.jar ``` The arguments are described below. ## Arguments { #script-options } `import.py` supports the following arguments: | **Argument** | **Description** | | ----------| --------- | | `--num-executors` | Number of executors for an import operation (1 by default). | | `--cores-per-executor` | Number of reserved CPU cores per executor (1 by default). | | `--ram-per-core` | Amount of RAM reserved, per core (2 GB by default). | | `--jdbc` | Type of JDBC driver. For example, `mysql` or `postgresql`. | | `--jdbc-server` | Database server host:port. | | `--jdbc-user` | Username to log in to the database. | | `--jdbc-password` | Password to log in to the database. If empty, read from terminal. | | `--jdbc-partition-column` | Numeric or date column for parallel reading of the source (Spark `partitionColumn`). Requires `--jdbc-lower-bound`, `--jdbc-upper-bound`, `--jdbc-num-partitions`. | | `--jdbc-lower-bound` | Lower bound of the `--jdbc-partition-column` range. | | `--jdbc-upper-bound` | Upper bound of the `--jdbc-partition-column` range. | | `--jdbc-num-partitions` | Number of parallel partitions (JDBC connections) for reading. | | `--jars` | Additional jar libraries. By default, `target/dependency/jar/*.jar` | | `--input` | Object to import, may be specified multiple times. | | `--output` | Path to write to in YTsaurus. For every `--input` flag, one output must be provided. | To configure the SPYT cluster started as part of an import operation, use the following arguments: | **Argument** | **Description** | | ----------| --------- | | `--proxy` | Path to the proxy in the YTsaurus cluster where SPYT should run. | | `--pool` | Resource pool in YTsaurus to run SPYT in. | | `--executor-timeout` | Idle timeout for Spark executors. | | `--executor-tmpfs-limit` | Size of tmpfs partition for Spark executors. | | `--executor-memory` | Amount of RAM to allocate to each executor. | | `--executor-memory-overhead` | Amount of additional RAM to allocate to executors beyond the primary amount. For example, if the main memory is 4 GB and the additional memory is 2 GB, then the total amount of memory requested from the cluster will be 6 GB. | If you're importing data from S3: | **Argument** | **Description** | | ----------| --------- | | `--s3-access-key` | Access Key ID identifying the user or app in S3. | | `--s3-secret-key` | Secret Access Key associated with the access key. | | `--s3-endpoint` | Endpoint URL of the S3 storage. | The following input specifiers are supported: | **Specifier** | **Description** | | ----------| --------- | | `hive` | Table in Hive, in `db_name.table_name` format. | | `hive_sql` | SQL query to run in Hive, in `db_name:sql statement` format. | | `jdbc` | Table in JDBC database, in `db_name.table_name` format. | | `jdbc_sql` | SQL query for JDBC database, in `db_name:sql statement` format. | | `text` | Text file in HDFS. | | `parquet` | Parquet file in HDFS. | | `orc` | ORC file in HDFS. | When writing a table to YTsaurus, the default assumption is that the table doesn't already exist there. If the table does exist, you can overwrite or append it using the `overwrite` or `append` specifiers. For example: `--output overwrite:/path/to/yt`. ## Type conversions Importing complex types is only supported partially. The YTsaurus type system doesn't exactly match its counterparts in other storage systems. When importing data, SPYT will try to keep the type on a best-effort basis. However, the value may get converted to a string when no matching type in YTsaurus could be inferred. When necessary, use SQL for proper type conversion. Value ranges for a single type may be different for YTsaurus and other systems. For example, the YTsaurus `date` type only stores calendar dates starting with the Unix epoch, January 1, 1970. An attempt to write earlier dates in YTsaurus will cause a runtime error. It is still possible to store earlier dates in YTsaurus as strings (for example, applying a `to_char(date_value, 'YYYY-MM-DD')` in PostgreSQL), or as integers (`date_value - '1970-01-01'` in PostgreSQL). # Working with Microsoft Excel tables The YTsaurus web interface allows to upload small Microsoft Excel spreadsheets into static YTsaurus tables and export data from strict-schema static tables as Microsoft Excel spreadsheets. ## Upload to a new table To upload data and create a YTsaurus table at the same time, use the **Create object** menu on the directory page. ![](https://ytsaurus.tech/_images/excel_upload_create.png){ .center } Be sure to set the **Column names** and the **Types** toggles to the correct values in the upload menu. With enabled **Column names**, column names are displayed in the first row of an Excel spreadsheet. If a spreadsheet doesn't have a header row, the Excel column names, such as A, B, C, and so on, are used as column names in the YTsaurus table. With enabled **Types**, there's a row with types. The row with types must follow the row with column names (if present). Types are defined in the `type` format (see [type mapping](https://ytsaurus.tech/docs/en/#type-mapping)). {% note warning "Attention" %} If no types are specified, all columns use `any` as their type. {% endnote %} ![](https://ytsaurus.tech/_images/excel_upload.png){ .center } ## Uploading into an existing table To upload data into an existing YTsaurus table, click the **Upload** button on the table page. ![](https://ytsaurus.tech/_images/excel_upload_existing.png){ .center } {% note warning "Attention" %} The Excel spreadsheet must include columns from the YTsaurus table schema. Additional columns in the Excel spreadsheet are ignored. {% endnote %} Use the **Append** toggle in the upload menu to choose the upload mode. The on state corresponds to rows being appended at the end of the YTsaurus table, the off state means that the table will be overwritten. ![](https://ytsaurus.tech/_images/excel_upload_append.png){ .center } ## Upload limitations * Only the first sheet of an Excel workbook is uploaded. * Maximum number of rows is `1,048,576`. * Maximum number of columns is `16,384`. * Maximum input file size is 50 MB. ## Downloading The interface provides a way to specify the required row and column subset. ![](https://ytsaurus.tech/_images/excel_download.png){ .center } The first row of the output contains column names, while the second one their types. ### Limitations * Strict-schema static tables. * Maximum number of rows is `1,048,574`. * Maximum number of columns is `16,384`. * Maximum output file size is 50 MB. * Maximum string length in a cell is `32,767`; Strings longer than `32,767` are truncated. ### Type mapping {#type-mapping} Microsoft Excel supports [4 data types](http://johnatten.com/2011/10/03/microsoft-excel-basics-part-ii-data-types-in-excel/): `Logical`, `Number`, `Text`, and `Error`. | **YTsaurus data type description** | **Representation in `type`** | **Representation in `type_v3`** | **Representation in Excel** | |----------------|--------------------|-----------------------|---------------------| | An integer within the range `[-2^63, 2^63-1]` | `int64` | `int64` | [Number*](https://ytsaurus.tech/docs/en/#number*) | | An integer within the range `[-2^31, 2^31-1]` | `int32` | `int32` | `Number` | | An integer within the range `[-2^15, 2^15-1]` | `int16` | `int16` | `Number` | | An integer within the range `[-2^7, 2^7-1]` | `int8` | `int8` | `Number` | | An integer within the range `[0, 2^64-1]` | `uint64` | `uint64` | [Number*](https://ytsaurus.tech/docs/en/#number*) | | An integer within the range `[0, 2^32-1]` | `uint32` | `uint32` | `Number` | | An integer within the range `[0, 2^16-1]` | `uint16` | `uint16` | `Number` | | An integer within the range `[0, 2^8-1]` | `uint8` | `uint8` | `Number` | | A 4-byte real number | `float` | `float` | [Number*](https://ytsaurus.tech/docs/en/#number*) | | An 8-byte real number | `double` | `double` | [Number*](https://ytsaurus.tech/docs/en/#number*) | | Standard `true/false` Boolean | `boolean` | `bool` (different from `type`) | `Logical` | | A random sequence of bytes | `string` | `string` | [Text*](https://ytsaurus.tech/docs/en/#text*) | | A valid UTF-8 sequence | `utf8` | `utf8` | [Text*](https://ytsaurus.tech/docs/en/#text*) | | An integer within the range `[0, 49673 - 1]`
that represents the number of days since the Unix epoch,
with a representable date range of `[1970-01-01, 2105-12-31]` | `date` | `date` | [Number**](https://ytsaurus.tech/docs/en/#number**) | | An integer within the range `[0, 49673 * 86400 - 1]`
that represents the number of seconds since the Unix epoch,
with a representable time range of
`[1970-01-01T00:00:00Z, 2105-12-31T23:59:59Z]` | `datetime` | `datetime` | [Number**](https://ytsaurus.tech/docs/en/#number**) | | An integer within the range `[0, 49673 * 86400 * 10^6 - 1]`
that represents the number of microseconds since the Unix epoch,
with a representable time range of
`[1970-01-01T00:00:00Z, 2105-12-31T23:59:59.999999Z]` | `timestamp` | `timestamp` | [Number***](https://ytsaurus.tech/docs/en/#number***) | | An integer within the range
`[-49673 * 86400 * 10^6 + 1, 49673 * 86400 * 10^6 - 1]`
that represents the number of microseconds between two timestamps | `interval` | `interval` | [Number*](https://ytsaurus.tech/docs/en/#number*) | | An arbitrary YSON structure
that is represented as a byte sequence
and can't have a `required=%true` attribute | `any` | `yson` (different from `type`) | [Text**](https://ytsaurus.tech/docs/en/#text**) | #### Number* {#number*} `Number` is a Double-Precision Floating Point value. The number can have only [15 digits](https://stackoverflow.com/questions/38522197/converting-the-text-data-to-a-int64-format-in-excel). When entering `99999999999999999` (`10^17-1`), the cell displays `99999999999999900`. Numbers exceeding this limit are exported as strings. #### Number** {#number**} The value from the YTsaurus table is written to an Excel table cell as the `Number` type, without loss of accuracy. The `date` and `datetime` types are exported as `Number` with special display formats and appear in Excel as: * `date` — `2020-12-05` * `datetime` — `2000-12-10 10:22:17` #### Number*** {#number***} The value can't fit in `Number`. `Timestamp` in milliseconds is exported as `Number` with special display formats (`1969-12-30 00:00:00`). For smaller units, it comes out as a string in the following format: `2006-01-02T15:04:05.999999Z`. #### Text* {#text*} `Text` is a string type. Maximum strings length in a cell is `32,767`. In YTsaurus, a string may be longer: up to `128 * 10^6`. Long strings are truncated. #### Text** {#text**} Values are serialized as [YSON](https://ytsaurus.tech/docs/en/storage/yson.md). Long strings are truncated like in `Text*`. ### Missing values For missing values of `optional` types, an empty string is added to the cell. # Query Tracker ## Introduction Query Tracker is a YTsaurus component that allows working with the system using human-readable queries in SQL-like languages. The user sends queries to Query Tracker, they are executed by the engine, and the execution result is returned to the user. With Query Tracker, you can: + Send ad hoc queries for execution. + Track query execution. + Save query results. + View query history. + Share queries with others. See the [example of working with Query Tracker](https://ytsaurus.tech/docs/en/#example). Queries are defined by engine and text. The engine controls the execution of the query. The query text is engine-specific. In this way, you can process data using different languages and runtime environments whilst honoring the guarantees and performance targets. ## Engines {#engines} Currently supported execution engines include: + [YT QL](https://ytsaurus.tech/docs/dynamic-tables/dyn-query-language.md) + A query language built in YT. Only supports dynamic tables. + [YQL](https://ytsaurus.tech/yql/index.md) + Executes the query on YQL agents, which break the query into individual YT operations (map, reduce, ...), start them, then retrieve and return the result. + [CHYT](https://ytsaurus.tech/docs/data-processing/chyt/about-chyt.md) + Executes the query on a clique. + [SPYT](https://ytsaurus.tech/docs/data-processing/spyt/overview.md) + Executes the query on the Spark cluster. ## API {#api} All operations accept the optional `query_tracker_stage` parameter that can be used to select the Query Tracker installation where you want your queries to be run. The default value is `production`. ### start_query {#start-query} Sends the query for execution. Returns the query ID. Required parameters: + `engine`: Query execution engine. Supported values are `ql`, `yql`, `chyt`, and `spyt`. + `query`: Query text. Optional parameters: + `files`: List of files for the query in YSON format. + `settings`: Additional query parameters in YSON format. + In [CHYT](https://ytsaurus.tech/docs/data-processing/chyt/about-chyt.md), you must set a clique alias using the `clique` parameter. Default value is the public clique. + In [SPYT](https://ytsaurus.tech/docs/data-processing/spyt/overview.md), you must set the housekeeping directory of an existing Spark cluster using the `discovery_path` parameter. + In [YQL](https://ytsaurus.tech/yql/index.md), you can run queries in different modes using the `execution_mode` parameter. Acceptable values are `run`, `validate`, and `optimize`. The default value is `run`. + `draft`: Used to mark draft queries. These queries are terminated automatically without execution. + `annotations`: Arbitrary annotations to the query. They can make it easier to search for queries. Specified in YSON format. + `access_control_objects`: List of objects at `//sys/access_control_object_namespaces/queries/` that controls access to the query for other users. Example: `start_query(engine="yql", query="SELECT * FROM my_table", access_control_objects=["my_aco"])` ### abort_query {#abort-query} Aborts query execution. Doesn't return anything. Required parameters: + `query_id`: Query ID. Optional parameters: + `message`: Abort message. Example: `abort_query(query_id="my_query_id")`. ### get_query_result {#get-query-result} Returns meta information about query execution results. Required parameters: + `query_id`: Query ID. + `result_index`: Result ID. Example: `get_query_result(query_id="my_query_id", result_index=0)`. ### read_query_result {#read-query-result} Returns the query results. ``` {% note warning %} Query Tracker stores only the first 10,000 rows of the response internally — all subsequent ones are truncated, and the `is_truncated` flag is set. If the expected query result is larger, this result should be inserted into a table using the engine and read from there. For example, in the case of YQL, you can use the `insert` operation. {% endnote %} ``` Required parameters: + `query_id`: Query ID. + `result_index`: Result ID. Optional parameters: + `columns`: List of columns to read. + `lower_row_index`: First row from which to start reading the results. + `upper_row_index`: Last row at which to stop reading the results. Example: `read_query_result(query_id="my_query_id", result_index=0)`. ### get_query {#get-query} Returns information about the query. Required parameters: + `query_id`: Query ID. Optional parameters: + `attributes`: Filters the returned attributes. + `timestamp`: Returned query information will be consistent with the system as at the specified point in time. Example: `get_query(query_id="my_query_id")`. ### list_queries {#list-queries} Gets a list of queries that match the set filters. Optional parameters: + `from_time`: Lower threshold for the query start time. + `to_time`: Upper threshold for the query start time. + `cursor_direction`: Direction in which to sort queries by start time. + `cursor_time`: Cursor stop time. Only applicable if `cursor_direction` is specified. + `user_filter`: Filter by query creator. + `state_filter`: Filter by query state. + `engine_filter`: Filter by query engine. + `substr_filter`: Filter by query ID, annotations, and [access control object](https://ytsaurus.tech/docs/en/#access-control). + `limit`: Number of items to return. The default value is 100. + `attributes`: Filters the returned attributes. Example: `list_queries()`. ### alter_query {#alter-query} Modifies the query. Doesn't return anything. Required parameters: + `query_id`: Query ID. Optional parameters: + `annotations`: New annotations for the query. + `access_control_objects`: New access control object list for the query. Example: `alter_query(query_id="my_query_id", access_control_objects=["my_new_aco"])`. ## Access control {#access-control} To manage access to queries and their results, the query includes a list of `access_control_objects` strings that point to `//sys/access_control_object_namespaces/queries/[access_control_object]`. An Access Control Object (ACO) is an object with the `@principal_acl` attribute. It sets access rules in the same manner as `@acl` does for Cypress nodes. For more information, see [Access control](https://ytsaurus.tech/docs/storage/access-control.md). You can create an ACO through the user interface or by calling the [create](https://ytsaurus.tech/docs/storage/cypress-example.md#create) command: `yt create access_control_object --attr '{namespace=queries;name=my_aco}'`. All APIs use ACOs to verify access: + `Use` grants access to the query. + `Read` grants access to the query results. + `Administer` grants the permission to modify and stop the query. Detailed information on each API method: + `start_query` checks whether the passed ACO exists. + `alter_query` checks whether the query ACO grants the `Administer` permission to the user and whether the passed ACO exists. + `get_query` checks whether the query ACO grants the `Use` permission to the user. + `list_queries` checks whether the query ACO grants the `Use` permission to the user. + `get_query_result` checks whether the query ACO grants the `Read` permission to the user. + `read_query_result` checks whether the query ACO grants the `Read` permission to the user. + `abort_query` checks whether the query ACO grants the `Administer` permission to the user. A few things to keep in mind: + The query creator always has access to their queries. + If a query has multiple ACOs, you only need to have access to one of them to get access to the query. For convenience, "ytsaurus-k8s-operator" creates the following ACOs: + `nobody`. Doesn't grant any permissions. + `everyone`. Grants `Use` and `Read` permissions to all users. + `everyone-use`. Grants `Use` permissions to all users. + `everyone-share`. The same as "everyone", but is not returned in `list_queries`. ## Example {#example} A typical usage scenario for Query Tracker is as follows: 1. Sending a query for execution. + `start_query(engine="yql", query="SELECT * from //home/me/my_table", access_control_objects=["my_team_aco"])`. 2. Getting a list of our available queries. + `list_queries()`. 3. Getting information about our query. + `get_query(query_id="my_query_id")`. 4. Waiting for the query to complete. + `get_query(query_id="my_query_id").state == "completed"`. 5. Getting meta information about the results (result size, table schema). + `get_query_result(query_id="my_query_id", result_index=0)`. 6. Reading the result. + `read_query_result(query_id="my_query_id", result_index=0)`. 7. Changing the ACO of the query to make it accessible to everyone. + `alter_query(query_id="my_query_id", access_control_objects=["everyone"])`. # Python user-defined functions (UDFs) in YQL ## Introduction Sometimes it can be difficult to describe data processing tasks using only a declarative language like SQL. You may find it more convenient to use an imperative language as well. For this purpose, YQL supports the creation and use of user-defined functions (UDFs). ## Python UDF With the Python UDF feature, you can define and use Python functions inside a query. ## How to use To use a Python UDF in a YQL query, follow these steps: 1. Define your function in a Python script and include this script as a string in your query. This function will be called as a UDF. 2. In your query, declare the function name and its signature (input and output data types). 3. Call the function in the query body and pass the necessary arguments to it. Each of these steps is explained in detail below. ### Write Python script You can add your script as: - Regular string literal. For example, `$script = "def foo(): return 'bar'"`. - Multi-line string literal (YQL extension). You must enclose this literal in the `@@` characters, which are similar to triple quotes in Python. The `#py` comment enables Python syntax highlighting in the YQL editor: ```yql $script = @@#py def foo(): return b'bar' @@ ``` - Named file attached to the query. You can include the file content in your query by using the [FileContent](https://ytsaurus.tech/yql/builtins/basic.md#file-content-path) function. For example, `$script = FileContent("foo.py")`. ### Declare function name and signature Since Python is a dynamically typed language and YQL is statically typed, you need to define the types in advance. The name and signature of the function called from the Python script must be known before executing a YQL query. If the types you declare in the function signature do not match the actual data that the function returns, you get a runtime error. Example of declaring a function name and signature: `$f = SystemPython3_8::foo(Callable<()->String>, $script);`. In this example: - `$f` and `$script`: [Named YQL expressions](https://ytsaurus.tech/yql/syntax/expressions.md#named-nodes). - `$f`: Ready-to-use function. - `$script`: Python script mentioned in the previous section. - `SystemPython3_8::`: Fixed prefix for declaring a Python function. `System` means that Python will be taken from the environment at runtime. `3_8` indicates the Python version to be used. - As of the time of writing this article, versions `3_8` – `3_12` are available. - `foo`: Name of the function to be called, defined in the `$script`. - `Callable<()->String>`: Function signature description. The empty brackets indicate that the function doesn't accept any arguments. `String` after the arrow means that the function returns a string. Example of a function signature with arguments: `Callable<(String, Uint32)->Double>`. ### Use UDF in your query You can call the function obtained after declaring the signature in the same way as any other built-in YQL function. For example, `$f()` or `$udf("test", 123)`. ## Data types To find out which data types are allowed in the function signature, see the [YQL data types](https://ytsaurus.tech/yql/types/index.md) section. ### Containers Containers are converted into Python objects according to the following rules: | **Name** | **Signature declaration** | **Signature example** | **Representation in Python** | | --- | --- | --- | --- | | List | `List` | `List` | `list-like object` [(learn more)](https://ytsaurus.tech/docs/en/#python-container-wrappers) | | Dictionary | `Dict` | `Dict` | `dict-like object` [(learn more)](https://ytsaurus.tech/docs/en/#python-container-wrappers) | | Tuple | `Tuple` | `Tuple` | `tuple` | | Structure | `Struct` | `Struct` | `StructSequence` | | Stream | `Stream` | `Stream` | `generator` | | Variant on tuple | `Variant` | `Variant` | `tuple with index and object` | | Variant on structure | `Variant` | `Variant` | `tuple with field name and object` | You can nest containers into each other. For example, `List>`. ### Specifics of objects for lists and dictionaries passed to functions {#python-container-wrappers} When you use `List` and `Dict`, the read-only `yql.TList` and `yql.TDict` objects are passed to the function arguments. The object's specifics: - They are read-only. This means that you can't make any changes to these objects without copying them because another part of the current query may use the same object. For example, a neighboring function. - At the cost of potentially slow copying, you can get a real list object from them by iterating for partial copying. For dictionaries, you also need to call `iteritems()`. By setting the `_yql_lazy_input` attribute value to False on the function itself, you can enable automatic copying of lists and dictionaries to "list" and "dict". This mechanism works recursively for nested containers. - To get the number of elements, you can call `len(my_arg)`. - To get the latest set of available methods, call `dir(my_arg)`. [Example](https://ytsaurus.tech/docs/en/#tlist-methods). The `yql.TList` methods: - `has_fast_len()`: Indicates whether you can get the list length quickly. If False is returned, the list is "lazy". - `has_items()`: Checks whether the list is empty. - `reversed()`: Returns a reversed copy of the list. - `skip(n)` and `take(n)`: Similar to `[n:]` and `[:n]` slices, respectively. - `to_index_dict()`: Returns a dictionary with element numbers as keys. This allows you to access these elements by their indices. Example with the `_yql_lazy_input` attribute: ```yql $u = SystemPython3_8::list_func(Callable<(List)->Int32>, @@#py def list_func(lst): return lst.count(1) list_func._yql_lazy_input = False @@); SELECT $u(AsList(1,2,3)); ``` ## Changing the environment {#environment} ### Interaction with Python from the environment The `SystemPython` UDF is called in a separate operation within a custom script. The interaction with Python is done via the dynamic `libpython3.N.so` library, which is linked at runtime. If you replace this library in the environment, it will change the Python used and available libraries. To change the environment from a YQL operation, use the `yt.DockerImage` and [yt.LayerPaths](https://ytsaurus.tech/yql/syntax/pragma/yt.md#ytlayerpaths) pragmas. They will affect the corresponding user script parameters described in the [Operation options - User script options](https://ytsaurus.tech/docs/data-processing/operations/operations-options.md#user_script_options) section. {% note info "Default environment" %} The default environment used for jobs is configured by the cluster administrator. It may not include the required Python version. {% endnote %} Example of using the TensorFlow library with `pragma yt.DockerImage`: ```yql pragma yt.DockerImage = "docker.io/tensorflow/tensorflow:2.16.1"; $script = @@#py import sys import tensorflow def foo(): return "Python version: " + sys.version + ".\nTensorflow version: " + tensorflow.__version__ @@; $udf = SystemPython3_11::foo(Callable<()->String>, $script); SELECT $udf(); -- Python version: 3.11.0rc1 (main, Aug 12 2022, 10:02:14) [GCC 11.2.0]. -- Tensorflow version: 2.16.1 ``` ## Specifics When using UDFs, the standard output (stdout) is used for system purposes. Therefore, you must not use it. ## Examples ### Hello World ```yql $script = @@#py def hello(name): return b'Hello, %s!' % name @@; $udf = SystemPython3_8::hello(Callable<(String)->String>, $script); SELECT $udf("world"); -- "Hello, world!" ``` ### String and number concatenation ```yql $script = @@#py def concat(a, b): return a.decode("utf-8") + str(b) @@; $concat = SystemPython3_8::concat(Callable<(String?, Int64?)->String>, $script); SELECT $concat(name, age) FROM `//tmp/sample`; ``` ### Retrieving the latest list of `yql.TList` methods {#tlist-methods} ```yql $u = SystemPython3_8::get_dir(Callable<(List)->List>, @@#py def get_dir(lst): return dir(lst) @@); SELECT $u(AsList()); ``` ### Inferencing Llama You need an appropriate environment that you can get using the following Dockerfile: ```dockerfile FROM ollama/ollama:0.3.9 RUN apt install -y python3.11 libpython3.11 python3-pip RUN python3.11 -m pip install ollama ``` Use the specified image to call Llama from Python: ```yql pragma yt.DockerImage = "my.docker.registry/ollama/ollama:0.3.9-with-python3.11-2"; pragma yt.DefaultMemoryLimit = "30G"; pragma yt.OperationSpec = "{max_failed_job_count=1;mapper={cpu_limit=64}}"; $script = @@#py from ollama import Client import subprocess def infer_llama(prompt): proc = subprocess.Popen(["/bin/ollama", "serve"], stdout=subprocess.DEVNULL) client = Client(host='http://localhost:11434') client.pull('llama3.1') response = client.chat(model='llama3.1', messages=[ { 'role': 'user', 'content': str(prompt), }, ]) proc.kill() return response['message']['content'] @@; $infer_llama = SystemPython3_11::infer_llama(Callable<(String?)->String>, $script); select $infer_llama("Why is the sky blue?"); ``` ## FAQ ### I get the error `Module not loaded for script type: SystemPython3_8` or `Module SystemPython3_8 is not registered` This indicates that the cluster doesn't support this functionality. The cluster must have QueryTracker and YqlAgents with an image version no older than [YTsaurus QueryTracker 0.0.8](https://github.com/ytsaurus/ytsaurus/releases/tag/docker%2Fquery-tracker%2F0.0.8). Ask your cluster administrators to update these components. ### I get the error `libpython3.8.so.1.0: cannot open shared object file: No such file or directory` This indicates that the environment of the job that is executing the function is missing the dynamic library `libpython3.8.so.1.0`. Make sure that you're using the same Python version that is used in the job environment. [Read more](https://ytsaurus.tech/docs/en/#environment). If you're using the [yt.DockerImage](https://ytsaurus.tech/yql/syntax/pragma/yt.md#ytdockerimage) pragma, make sure that the cluster supports this method of changing the environment. [Read more](https://ytsaurus.tech/admin-guide/prepare-spec.md#job-environment). # Using Jupyter Notebooks in YTsaurus {% note warning %} This feature is under development and may be unstable. It is not recommended for use in production workflows or for serious development and debugging. {% endnote %} YTsaurus allows to launch Jupyter Notebooks on the computational resources of the cluster. Functionality can be used on clusters using CRI job environment. Following actions are required to launch a notebook: 1. Set the environment variable `JUPYT_CTL_ADDRESS` to the address of JUPYT strawberry controller. ```bash export JUPYT_CTL_ADDRESS=jupyt.test.yt.mycloud.net ``` 1. Select docker-image of the jupyter-notebook. You can use [jupyter stacks](https://jupyter-docker-stacks.readthedocs.io/en/latest/) as an image or as a base of the image. Example of such an image is [minimal-notebook](https://quay.io/jupyter/minimal-notebook). 1. Create a notebook. To create a notebook you should specify docker-image with the notebook and scheduler pool for the operation. ```bash yt jupyt ctl create --speclet-options '{jupyter_docker_image="quay.io/jupyter/minimal-notebook"; pool=my-pool}' test-jupyt ``` 1. Launch the notebook. ```bash yt jupyt ctl start test-jupyt ``` 1. Wait until notebook is started and get the link to it using CLI. ```bash foo@bar:~$ yt jupyt ctl get-endpoint test-jupyt { "address" = "http://some-node.yt.mycloud.net:27042"; "operation_id" = "60b8ec86-6f123a7d-134403e8-ee6b88de"; "job_id" = "16f76ac9-2a20ab96-13440384-14e"; } ``` {% note warning "Warning" %} Notebooks and files created on filesystem are not preserved after job restart. For the reliable storage of the files you should store them in [Cypress](https://ytsaurus.tech/docs/en/storage/cypress.md). {% endnote %} # API and reference This section describes YTsaurus access interfaces, client libraries, and request reference information. - [Request parameters](https://ytsaurus.tech/docs/en/query-parameters.md) - [Error codes](https://ytsaurus.tech/docs/en/error-codes.md) - [Command-line interface](https://ytsaurus.tech/docs/en/cli/cli.md) - [Python API](https://ytsaurus.tech/docs/en/python/start.md) - [C++ API](https://ytsaurus.tech/docs/en/cpp/description.md) - [Java API](https://ytsaurus.tech/docs/en/java/examples.md) # Overview This section contains information about the YTsaurus command line: [description](https://ytsaurus.tech/docs/en/#description), [installation methods](https://ytsaurus.tech/docs/en/install.md), and [use cases](https://ytsaurus.tech/docs/en/examples.md). ## Description { #description } The YTsaurus CLI (Command Line Interface) is the most convenient way to interact with the YTsaurus system from the console. The CLI fully implements the features of all the [commands](https://ytsaurus.tech/docs/commands.md) supported by YTsaurus. The name of the command is specified as the first argument followed by its options. To get help on the CLI, just run the command: ```bash yt --help yt read --help ``` To work with a cluster, you need to get an access token and put it in the `~/.yt/token` file or the `YT_TOKEN` environment variable. For more information about the tokens and authentication, see the [Authentication](https://ytsaurus.tech/user-guide/storage/auth.md) section. The `yt` CLI does not read the `YT_USER` environment variable. When the CLI uses an HTTP proxy, as in the standard setup, the token determines the user; the RPC and native drivers follow separate rules. Run `yt whoami` to check the authenticated username. For details, see [Selecting a username](https://ytsaurus.tech/user-guide/storage/auth.md#user-selection). In addition to the access token, you need to specify the cluster to work with. To do this, you can specify the proxy server name in the `YT_PROXY` environment variable: `export YT_PROXY=` or pass the proxy server name to each command individually via the `proxy` option, such as `--proxy `. Below are some features of the CLI: - All underscores in command and option names were replaced by hyphens. - `input_table_paths` and `output_table_paths` are specified via the `--src` and `--dst` options. - To specify multiple columns for sort or reduce, pass the `sort-by` or `reduce-by` option several times, for example, `--sort-by a --sort-by b` will sort by composite key `a b`. {% note warning "Attention!" %} If you write `--sort-by a,b`, sorting by one column named `a,b` will be performed. {% endnote %} - The `write` command writes data chunk-by-chunk and under the transaction. - There is a `find` command (implemented on top of get) which partially replicates the unix find utility. - By default, the `list` command (without specifying the `--format` parameter) just enters a list of node elements, each on a separate string. It also has the `-l` option that will enter useful information about the nodes along with their names. - By default, any files uploaded into YTsaurus are uploaded without the executable flag. If you want to upload a binary file that will run in a job, you must specify the `--executable` option when uploading or set the `/@executable=true` attribute after uploading. - [The formats of the data](https://ytsaurus.tech/user-guide/storage/formats.md) received at the command input and appearing at the command output can be specified through the following environment variables: - `YT_ARGUMENTS_FORMAT`: Manages the format for the values of the options of the `--spec`, `--attributes` type. The default value is `yson`. - `YT_STRUCTURED_DATA_FORMAT`: Manages the format of the get/set command. The default value is `yson`. - `YT_TABULAR_DATA_FORMAT`: Manages the format of reading/writing tables, as well as starting operation jobs. There is no default value, the variable value must be set explicitly or the `--format` option must be specified. - There is a method to create aliases for commands. Aliases are specified in the `~/.yt/aliases` file in `name=opts` format. Example: ```bash mkdir=create map_node ls=list rm=remove mv=move cp=copy ``` - Auto-completion of binary file commands and paths for bash is supported. # YTsaurus CLI ## Installation There are several ways to install the CLI. The CLI consists of several parts. The main part is written in Python, it is compatible with Python 2&3, and it is platform-independent. To use the [YSON](https://ytsaurus.tech/user-guide/storage/yson.md) format of data representation, you can install the so called YSON bindings. They are a separate library available only for Linux and MacOS and written in C++. You can find out the version of the installed CLI by calling the `yt --version` command, which is the easiest way to check if the CLI is working. {% note warning "Attention!" %} We do not recommend installing the library and YSON bindings in different ways at the same time. This can lead to problems that are difficult to diagnose. {% endnote %} {% note info "Note" %} If you have any problems, check the [FAQ](https://ytsaurus.tech/faq/index.md) section. {% endnote %} The package is called `ytsaurus-client`. Before installing the package, you can install the `wheel` package to be able to install a version other than the system one or install the YTsaurus CLI without sudo). By default, the latest stable version of the package is installed from PyPI. All test versions have the "a1" suffix and can be installed via pip by adding the `--pre` option. The command for installing from pypi: ```bash # Install the YTsaurus CLI pip install ytsaurus-client # Install YSON bindings pip install ytsaurus-yson ``` ## Autocompletion { #autocompletion } The Deb and PyPI packages also come with a bash autocompletion script. In the case of a pip package, you can enable autocompletion using the `register-python-argcomplete` utility, which comes with the argcomplete package. ```bash register-python-argcomplete yt | sudo tee /etc/bash_completion.d/yt >/dev/null ``` You can check whether it is operating correctly by entering this in the console: ```bash yt abort-job abort-op abort-tx add-member alter-table alter-table-replica check-permission ... ``` The main advantage of this feature is supplementing paths in Cypress. For this function to work, the cluster you are working with must be specified in the environment variables, for example, using the `export YT_PROXY=` command. Example: ```bash export YT_PROXY= yt list // //@ //home/ //porto_layers/ //statbox/ //test_q_roc_auc //trash //userfeat/ //userstats/ //cooked_logs/ //logs //projects/ //sys //tmp/ //userdata/ //user_sessions/ ``` If you see something else (either nothing or paths in the current directory) instead of Cypress command names or paths, then autocompletion is not enabled. Check that the following conditions are met: - The main bash_completion package must be installed. On Ubuntu-like systems, this can be achieved by running the `sudo apt-get install bash-completion` command. - Bash-completion must be initiated when a bash session is started. For example, by default, Ubuntu contains a `. /etc/bash_completion` string. If you don't see it, you should add it to .bashrc. - The `yt_completion` file that configures autocompletion for the yt command must be placed to /etc/bash_completion.d. If all the contents of /etc/bash_completion.d do not run automatically on your system for some reason, you can run this script yourself at the end of your .bashrc. # Examples ## Running a simple operation For brevity, in the examples below, the ```--proxy ``` parameter is omitted or it is assumed that the ```YT_PROXY``` environment variable is set. ```bash # Creating a directory in which output tables will be stored yt create map_node //tmp/yt_cli_examples # Running grep based on the data from logs. You must specify the format in which the data will come to the operation input. yt map 'grep "domain.com" || exit 0' --src //home/logs/search/2017-Q3 --dst //tmp/yt_cli_examples/grep_result_table --format dsv .... # Viewing the number of rows in the output table yt get //tmp/yt_cli_examples/grep_result_table/@row_count # Looking at the first 10 rows. The format must be specified. yt read '//tmp/yt_cli_examples/grep_result_table[:#10]' --format dsv # Reading rows 6 and 7 (numbering from zero) — in square brackets, the half-interval is set which includes the left boundary and does not include the right one yt read '//tmp/yt_cli_examples/grep_result_table[#5:#7]' --format dsv # Column-by-column reading: yt read '//tmp/yt_cli_examples/grep_result_table{column1,column2}' --format dsv # **Only for a sorted table** # Viewing a range of keys in the table from "key_begin" to "key_end" (half-interval which includes the left boundary and does not include the right one). yt read '//tmp/yt_cli_examples/grep_result_table["key_begin":"key_end"]' --format dsv ``` ## The get, set, and remove commands ```bash yt get //home/@count 36 yt set //home/@custom_attribute '{x=y}' yt get //home/@custom_attribute { "x" = "y" } yt remove //home/@custom_attribute ``` ## The find and list commands ```bash yt find / --name "dev" //home/dev //tmp/images/dev //tmp/music/dev ... yt list //home abc_feedback abt abt-dev abt-viewers acid ... yt list //home -l map_node abc_feedback 0.0B 2017-02-28 09:40 abc_feedback map_node abt 0.0B 2019-05-28 08:37 abt map_node abt-dev 0.0B 2019-04-28 20:23 abt-dev map_node abt-viewers 0.0B 2019-03-26 12:55 abt-viewers map_node dev 0.0B 2016-10-21 11:34 acid ... ``` ## The read and write commands ```bash echo "x=10" | yt write //tmp/yt_cli_examples/table --format dsv yt read //tmp/yt_cli_examples/table --format dsv x=10 ``` ```bash echo -e "x=10\ty=20" | yt write //tmp/yt_cli_examples/table --format dsv yt read //tmp/yt_cli_examples/table --format dsv x=10 y=20 ``` The `--format` option is mandatory and regulates the [format](https://ytsaurus.tech/user-guide/storage/formats.md) in which data will be read. Using ` --format "yson" --control-attributes '{enable_row_index=%true}'`, you can find out the number of the first row in the ordered range. ## The write-file and read-file commands ```bash cat binary | yt write-file //tmp/yt_cli_examples/binary --executable yt read-file //tmp/yt_cli_examples/binary > binary_copy diff binary binary_copy ``` ## Running operations ```bash yt map "./binary" --src //tmp/yt_cli_examples/table --dst //tmp/yt_cli_examples/output --format dsv --local-file binary 2013-03-28 16:23:35.797 ( 0 min): operation 1535429-8d795980-9f7f5a9f-44bec919 initializing ... # Sorting by column x (in ascending order) yt sort --src //tmp/yt_cli_examples/table --dst //tmp/yt_cli_examples/output --sort-by "x" ... # Sorting by column x (in descending order, available only on clusters with masters version 21.2+) yt sort --src //tmp/yt_cli_examples/table --dst //tmp/yt_cli_examples/output --sort-by "{name=x; sort_order=descending;}" ... # Sorting by column x in descending order, and in the event of equality, by column y in ascending order (sorting in descending order is available only on clusters with masters version 21.2+) yt sort --src //tmp/yt_cli_examples/table --dst //tmp/yt_cli_examples/output --sort-by "{name=x; sort_order=descending;}" --sort-by "y" ... yt map-reduce --mapper cat --reducer cat --src //tmp/yt_cli_examples/table --dst //tmp/yt_cli_examples/output --format dsv --reduce-by "x" ... yt map "cat" --spec "{pool=example_pool; weight=10.0}" --src //tmp/yt_cli_examples/table --dst //tmp/yt_cli_examples/output --format yson ... yt vanilla --tasks '{task={job_count=10; command="sleep 60"; cpu_limit=2};}' --spec '{resource_limits={user_slots=2}}' ... ``` [//]: # "Autogenerated by yt/yt/scripts/python_sdk/docs/cli_commands" [//]: # "yandex-yt Version: 0.15.13" # YTsaurus CLI Commands ## Cypress commands ### concatenate concatenates cypress nodes. This command applicable only to files and tables
```bash usage: yt concatenate [-h] [--params PARAMS] --src SOURCE_PATHS --dst DESTINATION_PATH ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--src` (Required) Source paths Accepted multiple times. > `--dst` (Required) Destination paths ### copy copies Cypress node ```bash usage: yt copy [-h] [--params PARAMS] [--source-path SOURCE_PATH] [--destination-path DESTINATION_PATH] [--preserve-account | --no-preserve-account] [--preserve-owner | --no-preserve-owner] [--preserve-acl | --no-preserve-acl] [--preserve-creation-time] [--preserve-modification-time] [--preserve-expiration-time] [--preserve-expiration-timeout] [-r] [-i] [-l] [-f] [--no-pessimistic-quota-check] [source_path] [destination_path] ``` #### Positional Arguments > `source_path` source address, path must exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `destination_path` destination address, path must not exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--source-path` source address, path must exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--destination-path` destination address, path must not exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--preserve-account` > `--no-preserve-account` > `--preserve-owner` > `--no-preserve-owner` > `--preserve-acl` > `--no-preserve-acl` > `--preserve-creation-time` preserve creation time of node > `--preserve-modification-time` preserve modification time of node > `--preserve-expiration-time` preserve expiration time of node > `--preserve-expiration-timeout` preserve expiration timeout of node > `-r, --recursive` > `-i, --ignore-existing` > `-l, --lock-existing` > `-f, --force` > `--no-pessimistic-quota-check` ### create creates Cypress node ```bash usage: yt create [-h] [--params PARAMS] [--type TYPE] [--path PATH] [-r] [-i] [-l] [-f] [--attributes ATTRIBUTES] [type] [path] ``` #### Positional Arguments > `type` one of table, file, document, account, user, list_node, map_node, string_node, int64_node, uint64_node, double_node, … > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--type` one of table, file, document, account, user, list_node, map_node, string_node, int64_node, uint64_node, double_node, … > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `-r, --recursive` > `-i, --ignore-existing` > `-l, --lock-existing` > `-f, --force` > `--attributes` structured attributes in yson format ### create-account . creates account ```bash usage: yt create-account [-h] [--params PARAMS] [--name NAME] [--parent-name PARENT_NAME] [--resource-limits RESOURCE_LIMITS] [-i] [--allow-children-limit-overcommit] [--attributes ATTRIBUTES] [name] ``` #### Positional Arguments > `name` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--name` > `--parent-name` > `--resource-limits` structured resource-limits in yson format > `-i, --ignore-existing` > `--allow-children-limit-overcommit` > `--attributes` structured attributes in yson format ### create-pool . creates scheduler pool ```bash usage: yt create-pool [-h] [--params PARAMS] [--name NAME] [--pool-tree POOL_TREE] [--parent-name PARENT_NAME] [--weight WEIGHT] [--mode MODE] [--fifo-sort-parameters FIFO_SORT_PARAMETERS] [--max-operation-count MAX_OPERATION_COUNT] [--max-running-operation-count MAX_RUNNING_OPERATION_COUNT] [--forbid-immediate-operations] [--resource-limits RESOURCE_LIMITS] [--min-share-resources MIN_SHARE_RESOURCES] [--strong-guarantee-resources STRONG_GUARANTEE_RESOURCES] [--create-ephemeral-subpools] [--ephemeral-subpool-config EPHEMERAL_SUBPOOL_CONFIG] [-i] [--attributes ATTRIBUTES] [name] [pool_tree] ``` #### Positional Arguments > `name` > `pool_tree` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--name` > `--pool-tree` > `--parent-name` > `--weight` > `--mode` fifo or fair_share > `--fifo-sort-parameters` structured fifo-sort-parameters in yson format > `--max-operation-count` > `--max-running-operation-count` > `--forbid-immediate-operations` > `--resource-limits` structured resource-limits in yson format > `--min-share-resources` deprecated, use –strong-guarantee-resources instead. structured min-share-resources in yson format > `--strong-guarantee-resources` structured strong-guarantee-resources in yson format > `--create-ephemeral-subpools` > `--ephemeral-subpool-config` structured ephemeral-subpool-config in yson format > `-i, --ignore-existing` > `--attributes` structured attributes in yson format ### exists checks if Cypress node exists ```bash usage: yt exists [-h] [--params PARAMS] [--path PATH] [--suppress-transaction-coordinator-sync] [--read-from READ_FROM] [--cache-sticky-group-size CACHE_STICKY_GROUP_SIZE] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--suppress-transaction-coordinator-sync` suppress transaction coordinator sync > `--read-from` Can be set to «cache» to enable reads from system cache > `--cache-sticky-group-size` Size of sticky group size for read_from=»cache» mode ### externalize externalize cypress node ```bash usage: yt externalize [-h] [--params PARAMS] [--path PATH] --cell-tag CELL_TAG [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--cell-tag` (Required) ### find searches for some nodes in Cypress subtree ```bash usage: yt find [-h] [--params PARAMS] [--path PATH] [--name NAME] [--type TYPE] [--account ACCOUNT] [--owner OWNER] [--follow-links] [--attribute-filter ATTRIBUTE_FILTER] [--depth DEPTH] [-l] [--recursive-resource-usage] [--time-type {access_time,modification_time,creation_time}] [--read-from READ_FROM] [--cache-sticky-group-size CACHE_STICKY_GROUP_SIZE] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--name, -name` pattern of node name, use shell-style wildcards: `*, ?, [seq], [!seq]` > `--type` one of table, file, document, account, user, list_node, map_node, string_node, int64_node, uint64_node, double_node, … > `--account` > `--owner` > `--follow-links` follow links > `--attribute-filter` yson map fragment with filtering attributes, e.g. k1=v1;k2=v2 > `--depth` recursion depth (infinite by default) > `-l, --long-format` print some extra information about nodes > `--recursive-resource-usage` use recursive resource usage for in long format mode > `--time-type` Possible choices: access_time, modification_time, creation_time > type of time to use in long-format, default: ``'modification_time'`` > `--read-from` Can be set to «cache» to enable reads from system cache > `--cache-sticky-group-size` Size of sticky group size for read_from=»cache» mode ### get gets Cypress node content (attribute tree) ```bash usage: yt get [-h] [--params PARAMS] [--path PATH] [--max-size MAX_SIZE] [--format FORMAT] [--attribute ATTRIBUTES] [--suppress-transaction-coordinator-sync] [--read-from READ_FROM] [--cache-sticky-group-size CACHE_STICKY_GROUP_SIZE] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--max-size` maximum size of entries returned by get; if actual directory size exceeds that value only subset of entries will be listed (it’s not specified which subset); default value is enough to list any nonsystem directory. > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `--attribute` desired node attributes in the response Accepted multiple times. > `--suppress-transaction-coordinator-sync` suppress transaction coordinator sync > `--read-from` Can be set to «cache» to enable reads from system cache > `--cache-sticky-group-size` Size of sticky group size for read_from=»cache» mode ### internalize internalize cypress node ```bash usage: yt internalize [-h] [--params PARAMS] [--path PATH] --cell-tag CELL_TAG [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--cell-tag` (Required) ### link makes link to Cypress node ```bash usage: yt link [-h] [--params PARAMS] [--target-path TARGET_PATH] [--link-path LINK_PATH] [-r] [-i] [-l] [-f] [--attributes ATTRIBUTES] [target_path] [link_path] ``` #### Positional Arguments > `target_path` address of original node to link, path must exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `link_path` address of resulting link, path must not exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--target-path` address of original node to link, path must exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--link-path` address of resulting link, path must not exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `-r, --recursive` create parent nodes recursively > `-i, --ignore-existing` > `-l, --lock-existing` > `-f, --force` force create link even if destination already exists (supported only on cluster with 19+ version) > `--attributes` structured attributes in yson format ### list lists directory (map_node) content. Node type must be «map_node» ```bash usage: yt list [-h] [--params PARAMS] [--path PATH] [-l] [--format FORMAT] [--attribute ATTRIBUTES] [--max-size MAX_SIZE] [--recursive-resource-usage] [--suppress-transaction-coordinator-sync] [--read-from READ_FROM] [--cache-sticky-group-size CACHE_STICKY_GROUP_SIZE] [--absolute] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `-l, --long-format` print some extra information about nodes > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--attribute` node attributes to add into response Accepted multiple times. > `--max-size` maximum size of entries returned by list; if actual directory size exceeds that value only subset of entries will be listed (it’s not specified which subset); default value is enough to list any nonsystem directory. > `--recursive-resource-usage` use recursive resource usage for in long format mode > `--suppress-transaction-coordinator-sync` suppress transaction coordinator sync > `--read-from` Can be set to «cache» to enable reads from system cache > `--cache-sticky-group-size` Size of sticky group size for read_from=»cache» mode > `--absolute` print absolute paths ### move moves (renames) Cypress node ```bash usage: yt move [-h] [--params PARAMS] [--source-path SOURCE_PATH] [--destination-path DESTINATION_PATH] [--preserve-account | --no-preserve-account] [--preserve-owner | --no-preserve-owner] [--preserve-acl | --no-preserve-acl] [--preserve-creation-time] [--preserve-modification-time] [--preserve-expiration-time] [--preserve-expiration-timeout] [-r] [-f] [--no-pessimistic-quota-check] [source_path] [destination_path] ``` #### Positional Arguments > `source_path` old node address, path must exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `destination_path` new node address, path must not exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--source-path` old node address, path must exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--destination-path` new node address, path must not exist. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--preserve-account` > `--no-preserve-account` > `--preserve-owner` > `--no-preserve-owner` > `--preserve-acl` > `--no-preserve-acl` > `--preserve-creation-time` preserve creation time of node > `--preserve-modification-time` preserve modification time of node > `--preserve-expiration-time` preserve expiration time of node > `--preserve-expiration-timeout` preserve expiration timeout of node > `-r, --recursive` > `-f, --force` > `--no-pessimistic-quota-check` ### remove removes Cypress node ```bash usage: yt remove [-h] [--params PARAMS] [--path PATH] [-r] [-f] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `-r, --recursive` > `-f, --force` ### remove-attribute removes attribute at given path ```bash usage: yt remove-attribute [-h] [--params PARAMS] [--path PATH] [-r] [path] name ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `name` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `-r, --recursive` ### set sets new value to Cypress node ```bash usage: yt set [-h] [--params PARAMS] [--path PATH] [--format FORMAT] [-r] [-f] [--suppress-transaction-coordinator-sync] [--value VALUE] [path] [value] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `value` new node attribute value, in yson format. You can specify in bash pipe: «cat file_with_value | yt set //tmp/my_node» #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `-r, --recursive` > `-f, --force` > `--suppress-transaction-coordinator-sync` suppress transaction coordinator sync > `--value` new node attribute value, in yson format. You can specify in bash pipe: «cat file_with_value | yt set //tmp/my_node» ### set-attribute sets attribute at given path ```bash usage: yt set-attribute [-h] [--params PARAMS] [--path PATH] [-r] [path] name value ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `name` > `value` structured value in yson format #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `-r, --recursive` ## File commands ### download downloads file from path in Cypress to local machine ```bash usage: yt download [-h] [--params PARAMS] [--path PATH] [--file-reader FILE_READER] [--offset OFFSET] [--length LENGTH] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--file-reader` structured file-reader in yson format > `--offset` offset in input file in bytes, 0 by default > `--length` length in bytes of desired part of input file, all file without offset by default ### read-file downloads file from path in Cypress to local machine ```bash usage: yt read-file [-h] [--params PARAMS] [--path PATH] [--file-reader FILE_READER] [--offset OFFSET] [--length LENGTH] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--file-reader` structured file-reader in yson format > `--offset` offset in input file in bytes, 0 by default > `--length` length in bytes of desired part of input file, all file without offset by default ### upload uploads file to destination path from stream on local machine ```bash usage: yt upload [-h] [--params PARAMS] [--destination DESTINATION] [--file-writer FILE_WRITER] [--compressed] [--executable] [--compute-md5] [--no-compression] [destination] ``` #### Positional Arguments > `destination` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--destination` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--file-writer` structured file-writer in yson format > `--compressed` expect stream to contain compressed file data. Warning! This option disables retries. Data is passed directly to proxy without recompression. > `--executable` do file executable > `--compute-md5` compute md5 of file content > `--no-compression` disable compression ### write-file uploads file to destination path from stream on local machine ```bash usage: yt write-file [-h] [--params PARAMS] [--destination DESTINATION] [--file-writer FILE_WRITER] [--compressed] [--executable] [--compute-md5] [--no-compression] [destination] ``` #### Positional Arguments > `destination` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--destination` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--file-writer` structured file-writer in yson format > `--compressed` expect stream to contain compressed file data. Warning! This option disables retries. Data is passed directly to proxy without recompression. > `--executable` do file executable > `--compute-md5` compute md5 of file content > `--no-compression` disable compression ## Table commands ### alter-table performs schema and other table meta information modifications ```bash usage: yt alter-table [-h] [--params PARAMS] [--path PATH] [--schema [SCHEMA]] [--dynamic | --static] [--upstream-replica-id UPSTREAM_REPLICA_ID] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--schema` new schema value, in yson format. > `--dynamic` > `--static` > `--upstream-replica-id` upstream replica id ### create-temp-table creates temporary table by given path with given prefix and return name ```bash usage: yt create-temp-table [-h] [--params PARAMS] [--path PATH] [--name-prefix NAME_PREFIX] [--expiration-timeout EXPIRATION_TIMEOUT] [--attributes ATTRIBUTES] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` path where temporary table will be created. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--name-prefix` prefix of table name > `--expiration-timeout` expiration timeout in ms > `--attributes` structured attributes in yson format ### dirtable Upload/download to file system commands ```bash usage: yt dirtable [-h] dirtable_command ... ``` #### Positional Arguments > `dirtable_command` Possible choices: upload, download, list-files, append-single-file #### Sub-commands ##### upload Upload directory to YTsaurus ```bash yt dirtable upload [-h] --directory DIRECTORY [--store-full-path] [--part-size PART_SIZE] [--recursive] [--no-recursive] --yt-table YT_TABLE [--process-count PROCESS_COUNT] [--exact-filenames EXACT_FILENAMES] [--filter-by-regexp FILTER_BY_REGEXP] [--exclude-by-regexp EXCLUDE_BY_REGEXP] [--force] [--prepare-for-sky-share] ``` ###### Named Arguments > `--directory` > `--store-full-path` Store full path to the uploaded file. Makes –yt-name equal to the –fs-path value > `--part-size` Default: ``4194304`` > `--recursive` > `--no-recursive` > `--yt-table` > `--process-count` Default: ``4`` > `--exact-filenames` Files to upload (separated by comma) > `--filter-by-regexp` Files with name matching that regexp will be uploaded > `--exclude-by-regexp` Files with name matching that regexp will not be uploaded > `--force` > `--prepare-for-sky-share` ##### download Download directory from YTsaurus ```bash yt dirtable download [-h] --directory DIRECTORY --yt-table YT_TABLE [--process-count PROCESS_COUNT] [--exact-filenames EXACT_FILENAMES] [--filter-by-regexp FILTER_BY_REGEXP] [--exclude-by-regexp EXCLUDE_BY_REGEXP] ``` ###### Named Arguments > `--directory` > `--yt-table` > `--process-count` Default: ``4`` > `--exact-filenames` Files to extract (separated by comma) > `--filter-by-regexp` Files with name matching that regexp will be extracted > `--exclude-by-regexp` Files with name matching that regexp will not be extracted ##### list-files List files from YTsaurus ```bash yt dirtable list-files [-h] --yt-table YT_TABLE [--raw] ``` ###### Named Arguments > `--yt-table` > `--raw` Displays only file names list. If omitted, prints filenames and sizes in human-readable format ##### append-single-file Append single file to table ```bash yt dirtable append-single-file [-h] --yt-table YT_TABLE [--yt-name YT_NAME] --fs-path FS_PATH [--store-full-path] [--process-count PROCESS_COUNT] ``` ###### Named Arguments > `--yt-table` > `--yt-name` > `--fs-path` > `--store-full-path` Storing file’s full path as provided in –directory > `--process-count` Default: ``4`` ### get-table-columnar-statistics gets columnar statistics of tables listed in paths ```bash usage: yt get-table-columnar-statistics [-h] [--params PARAMS] --path PATHS ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` (Required) Path to source table. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) Accepted multiple times. ### read reads rows from table and parse (optionally) ```bash usage: yt read [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--table-reader TABLE_READER] [--control-attributes CONTROL_ATTRIBUTES] [--unordered] [--as-json-list] [--omit-inaccessible-columns] [--omit-inaccessible-rows] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` output format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--table-reader` structured table-reader in yson format > `--control-attributes` structured control-attributes in yson format > `--unordered` > `--as-json-list` In case of JSON format output stream as JSON list instead of JSON lines format > `--omit-inaccessible-columns` Skip columns the user has no access to > `--omit-inaccessible-rows` Skip rows the user has no access to ### read-blob-table reads file from blob table ```bash usage: yt read-blob-table [-h] [--params PARAMS] [--table TABLE] [--part-index-column-name PART_INDEX_COLUMN_NAME] [--data-column-name DATA_COLUMN_NAME] [--part-size PART_SIZE] [--table-reader TABLE_READER] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--part-index-column-name` name of column with part indexes > `--data-column-name` name of column with data > `--part-size` size of each blob > `--table-reader` structured table-reader in yson format ### read-table reads rows from table and parse (optionally) ```bash usage: yt read-table [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--table-reader TABLE_READER] [--control-attributes CONTROL_ATTRIBUTES] [--unordered] [--as-json-list] [--omit-inaccessible-columns] [--omit-inaccessible-rows] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` output format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--table-reader` structured table-reader in yson format > `--control-attributes` structured control-attributes in yson format > `--unordered` > `--as-json-list` In case of JSON format output stream as JSON list instead of JSON lines format > `--omit-inaccessible-columns` Skip columns the user has no access to > `--omit-inaccessible-rows` Skip rows the user has no access to ### write writes rows from input_stream to table ```bash usage: yt write [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--table-writer TABLE_WRITER] [--compressed] [--no-compression] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` input format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--table-writer` structured table-writer in yson format > `--compressed` expect stream to contain compressed file data. Warning! This option disables retries. Data is passed directly to proxy without recompression. > `--no-compression` disable compression ### write-table writes rows from input_stream to table ```bash usage: yt write-table [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--table-writer TABLE_WRITER] [--compressed] [--no-compression] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` input format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--table-writer` structured table-writer in yson format > `--compressed` expect stream to contain compressed file data. Warning! This option disables retries. Data is passed directly to proxy without recompression. > `--no-compression` disable compression ## Dynamic table commands ### alter-table-replica changes mode and enables or disables a table replica or replicated table tracker for table replica ```bash usage: yt alter-table-replica [-h] [--params PARAMS] [--enable | --disable] [--enable-replicated-table-tracker | --disable-replicated-table-tracker] [--mode MODE] replica_id ``` #### Positional Arguments > `replica_id` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--enable` enable table replica > `--disable` disable table replica > `--enable-replicated-table-tracker` enable replicated table tracker for table replica > `--disable-replicated-table-tracker` disable replicated table tracker for table replica > `--mode` alternation mode, can be «sync» or «async» ### balance-tablet-cells reassign tablets evenly among tablet cells ```bash usage: yt balance-tablet-cells [-h] [--params PARAMS] [--bundle BUNDLE] [--tables [TABLES ...]] [--sync] [bundle] ``` #### Positional Arguments > `bundle` tablet cell bundle #### Named Arguments > `--params` specify additional params. structured params in yson format > `--bundle` tablet cell bundle > `--tables` tables to balance. If omitted, all tables of bundle will be balanced > `--sync` ### delete . Use "delete-rows" instead of "delete" ```bash usage: yt delete [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ### delete-rows deletes rows with keys from input_stream from dynamic table ```bash usage: yt delete-rows [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--atomicity {full,none}] [--durability {sync,async}] [--require-sync-replica | --no-require-sync-replica] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` input format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--atomicity` Possible choices: full, none > `--durability` Possible choices: sync, async > `--require-sync-replica` > `--no-require-sync-replica` ### explain-query explains a SQL-like query on dynamic table ```bash usage: yt explain-query [-h] [--params PARAMS] [--query QUERY] [--timestamp TIMESTAMP] [--input-row-limit INPUT_ROW_LIMIT] [--output-row-limit OUTPUT_ROW_LIMIT] [--allow-full-scan | --forbid-full-scan] [--allow-join-without-index | --forbid-join-without-index] [--execution-pool EXECUTION_POOL] [--format FORMAT] [--syntax-version SYNTAX_VERSION] [--udf-registry-path UDF_REGISTRY_PATH] [query] ``` #### Positional Arguments > `query` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--query` > `--timestamp` > `--input-row-limit` > `--output-row-limit` > `--allow-full-scan` > `--forbid-full-scan` > `--allow-join-without-index` > `--forbid-join-without-index` > `--execution-pool` > `--format` > `--syntax-version` > `--udf-registry-path` ### freeze-table freezes the table ```bash usage: yt freeze-table [-h] [--params PARAMS] [--path PATH] [--first-tablet-index FIRST_TABLET_INDEX] [--last-tablet-index LAST_TABLET_INDEX] [--sync] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--first-tablet-index` > `--last-tablet-index` > `--sync` ### get-tablet-errors returns dynamic table tablet and replication errors ```bash usage: yt get-tablet-errors [-h] [--params PARAMS] [--path PATH] [--limit LIMIT] [--format FORMAT] [path] ``` #### Positional Arguments > `path` path to dynamic table. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` path to dynamic table. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--limit` number of tablets with errors > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### get-tablet-infos returns various runtime tablet information ```bash usage: yt get-tablet-infos [-h] [--params PARAMS] [--path PATH] [--tablet-indexes TABLET_INDEXES [TABLET_INDEXES ...]] [--format FORMAT] [path] ``` #### Positional Arguments > `path` path to dynamic table. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` path to dynamic table. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--tablet-indexes` > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### insert . Use "insert-rows" instead of "insert" ```bash usage: yt insert [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ### insert-rows inserts rows from input_stream to dynamic table ```bash usage: yt insert-rows [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--atomicity {full,none}] [--durability {sync,async}] [--require-sync-replica | --no-require-sync-replica] [--update | --no-update] [--aggregate | --no-aggregate] [--lock_type LOCK_TYPE] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` input format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--atomicity` Possible choices: full, none > `--durability` Possible choices: sync, async > `--require-sync-replica` > `--no-require-sync-replica` > `--update` > `--no-update` > `--aggregate` > `--no-aggregate` > `--lock_type` ### lookup . Use "lookup-rows" instead of "lookup" ```bash usage: yt lookup [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ### lookup-rows lookups rows in dynamic table ```bash usage: yt lookup-rows [-h] [--params PARAMS] [--table TABLE] [--format FORMAT] [--timestamp TIMESTAMP] [--versioned] [--column-name COLUMN_NAMES] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` input format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--timestamp` > `--versioned` return all versions of the requested rows > `--column-name` column name to lookup Accepted multiple times. ### mount-table mounts the table ```bash usage: yt mount-table [-h] [--params PARAMS] [--path PATH] [--first-tablet-index FIRST_TABLET_INDEX] [--last-tablet-index LAST_TABLET_INDEX] [--freeze] [--sync] [--cell-id CELL_ID | --target-cell-ids TARGET_CELL_IDS [TARGET_CELL_IDS ...]] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--first-tablet-index` > `--last-tablet-index` > `--freeze` > `--sync` > `--cell-id` tablet cell id where the tablets will be mounted to, if omitted then an appropriate cell is chosen automatically > `--target-cell-ids` tablet cell id for each tablet in range [first-tablet-index, last-tablet-index]. Should be used if exact destination cell for each tablet is known. ### remount-table remounts the table ```bash usage: yt remount-table [-h] [--params PARAMS] [--path PATH] [--first-tablet-index FIRST_TABLET_INDEX] [--last-tablet-index LAST_TABLET_INDEX] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--first-tablet-index` > `--last-tablet-index` ### reshard-table changes pivot keys separating tablets of a given table ```bash usage: yt reshard-table [-h] [--params PARAMS] [--path PATH] [--first-tablet-index FIRST_TABLET_INDEX] [--last-tablet-index LAST_TABLET_INDEX] [--tablet-count TABLET_COUNT] [--sync] [--uniform] [--slicing-accuracy SLICING_ACCURACY] [--trimmed-row-counts TRIMMED_ROW_COUNTS [TRIMMED_ROW_COUNTS ...]] [--enable-slicing | --no-enable-slicing] [path] [pivot_keys ...] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `pivot_keys` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--first-tablet-index` > `--last-tablet-index` > `--tablet-count` > `--sync` > `--uniform` > `--slicing-accuracy` > `--trimmed-row-counts` > `--enable-slicing` > `--no-enable-slicing` ### reshard-table-automatic automatically balance tablets of a mounted table according to tablet balancer config ```bash usage: yt reshard-table-automatic [-h] [--params PARAMS] [--path PATH] [--sync] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--sync` ### select . Use "select-rows" instead of "select" ```bash usage: yt select [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ### select-rows executes a SQL-like query on dynamic table ```bash usage: yt select-rows [-h] [--params PARAMS] [--query QUERY] [--timestamp TIMESTAMP] [--input-row-limit INPUT_ROW_LIMIT] [--output-row-limit OUTPUT_ROW_LIMIT] [--verbose-logging] [--enable-code-cache | --disable-code-cache] [--allow-full-scan | --forbid-full-scan] [--allow-join-without-index | --forbid-join-without-index] [--execution-pool EXECUTION_POOL] [--format FORMAT] [--print-statistics] [--syntax-version SYNTAX_VERSION] [--udf-registry-path UDF_REGISTRY_PATH] [--merge-versioned-rows | --do-not-merge-versioned-rows] [query] ``` #### Positional Arguments > `query` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--query` > `--timestamp` > `--input-row-limit` > `--output-row-limit` > `--verbose-logging` > `--enable-code-cache` > `--disable-code-cache` > `--allow-full-scan` > `--forbid-full-scan` > `--allow-join-without-index` > `--forbid-join-without-index` > `--execution-pool` > `--format` > `--print-statistics` > `--syntax-version` > `--udf-registry-path` > `--merge-versioned-rows` > `--do-not-merge-versioned-rows` ### trim-rows trim rows of the dynamic table ```bash usage: yt trim-rows [-h] [--params PARAMS] [--path PATH] [path] tablet_index trimmed_row_count ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `tablet_index` > `trimmed_row_count` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ### unfreeze-table unfreezes the table ```bash usage: yt unfreeze-table [-h] [--params PARAMS] [--path PATH] [--first-tablet-index FIRST_TABLET_INDEX] [--last-tablet-index LAST_TABLET_INDEX] [--sync] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--first-tablet-index` > `--last-tablet-index` > `--sync` ### unmount-table unmounts the table ```bash usage: yt unmount-table [-h] [--params PARAMS] [--path PATH] [--first-tablet-index FIRST_TABLET_INDEX] [--last-tablet-index LAST_TABLET_INDEX] [--force] [--sync] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--first-tablet-index` > `--last-tablet-index` > `--force` > `--sync` ## Run operation commands ### erase erases table or part of it ```bash usage: yt erase [-h] [--params PARAMS] [--table TABLE] [--print-statistics] [--async] [--spec SPEC] [table] ``` #### Positional Arguments > `table` path to table to erase. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` path to table to erase. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--print-statistics` > `--async` do not track operation progress > `--spec` structured spec in yson format ### join-reduce runs join-reduce operation ```bash usage: yt join-reduce [-h] [--params PARAMS] [--binary command] --src SOURCE_TABLE [SOURCE_TABLE ...] --dst DESTINATION_TABLE [--file YT_FILES] [--local-file LOCAL_FILES] [--job-count JOB_COUNT] [--memory-limit MEMORY_LIMIT] [--spec SPEC] [--format FORMAT] [--input-format INPUT_FORMAT] [--output-format OUTPUT_FORMAT] [--print-statistics] [--async] --join-by JOIN_BY [command] ``` #### Positional Arguments > `command` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--binary` > `--src` (Required) Accepted multiple times. > `--dst` (Required) Accepted multiple times. > `--file` > Accepted multiple times. > `--local-file` > Accepted multiple times. > `--job-count` > `--memory-limit` in MB > `--spec` structured spec in yson format > `--format` > `--input-format` > `--output-format` > `--print-statistics` > `--async` do not track operation progress > `--join-by` (Required) Columns to join by. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" ### map runs map operation ```bash usage: yt map [-h] [--params PARAMS] [--binary command] --src SOURCE_TABLE [SOURCE_TABLE ...] --dst DESTINATION_TABLE [--file YT_FILES] [--local-file LOCAL_FILES] [--job-count JOB_COUNT] [--memory-limit MEMORY_LIMIT] [--spec SPEC] [--format FORMAT] [--input-format INPUT_FORMAT] [--output-format OUTPUT_FORMAT] [--print-statistics] [--async] [--ordered] [command] ``` #### Positional Arguments > `command` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--binary` > `--src` (Required) Accepted multiple times. > `--dst` (Required) Accepted multiple times. > `--file` > Accepted multiple times. > `--local-file` > Accepted multiple times. > `--job-count` > `--memory-limit` in MB > `--spec` structured spec in yson format > `--format` > `--input-format` > `--output-format` > `--print-statistics` > `--async` do not track operation progress > `--ordered` Force ordered input for mapper. ### map-reduce runs map (optionally), sort, reduce and reduce-combine (optionally) operations ```bash usage: yt map-reduce [-h] [--params PARAMS] [--mapper MAPPER] --reducer REDUCER [--reduce-combiner REDUCE_COMBINER] --src SOURCE_TABLE --dst DESTINATION_TABLE [--map-file MAP_YT_FILES] [--map-local-file MAP_LOCAL_FILES] [--reduce-file REDUCE_YT_FILES] [--reduce-local-file REDUCE_LOCAL_FILES] [--reduce-combiner-file REDUCE_COMBINER_YT_FILES] [--reduce-combiner-local-file REDUCE_COMBINER_LOCAL_FILES] [--mapper-memory-limit MAPPER_MEMORY_LIMIT] [--reducer-memory-limit REDUCER_MEMORY_LIMIT] [--reduce-combiner-memory-limit REDUCE_COMBINER_MEMORY_LIMIT] --reduce-by REDUCE_BY [--sort-by SORT_BY] [--spec SPEC] [--format FORMAT] [--map-input-format MAP_INPUT_FORMAT] [--map-output-format MAP_OUTPUT_FORMAT] [--reduce-input-format REDUCE_INPUT_FORMAT] [--reduce-output-format REDUCE_OUTPUT_FORMAT] [--reduce-combiner-input-format REDUCE_COMBINER_INPUT_FORMAT] [--reduce-combiner-output-format REDUCE_COMBINER_OUTPUT_FORMAT] [--async] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--mapper` > `--reducer` (Required) > `--reduce-combiner` > `--src` (Required) Accepted multiple times. > `--dst` (Required) Accepted multiple times. > `--map-file` > Accepted multiple times. > `--map-local-file` > Accepted multiple times. > `--reduce-file` > Accepted multiple times. > `--reduce-local-file` > Accepted multiple times. > `--reduce-combiner-file` > Accepted multiple times. > `--reduce-combiner-local-file` > Accepted multiple times. > `--mapper-memory-limit, --map-memory-limit` in MB > `--reducer-memory-limit, --reduce-memory-limit` in MB > `--reduce-combiner-memory-limit` in MB > `--reduce-by` (Required) Columns to reduce by. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" > `--sort-by` Columns to sort by. Must be superset of reduce-by columns. By default is equal to –reduce-by option. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" > `--spec` structured spec in yson format > `--format` (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--map-input-format` see –format help > `--map-output-format` see –format help > `--reduce-input-format` see –format help > `--reduce-output-format` see –format help > `--reduce-combiner-input-format` see –format help > `--reduce-combiner-output-format` see –format help > `--async` do not track operation progress ### merge merges source tables to destination table ```bash usage: yt merge [-h] [--params PARAMS] --src SOURCE_TABLE [SOURCE_TABLE ...] --dst DESTINATION_TABLE [--mode {unordered,ordered,sorted,auto}] [--print-statistics] [--async] [--spec SPEC] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--src` (Required) Accepted multiple times. > `--dst` (Required) path to destination table. For append mode add before path. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--mode` Possible choices: unordered, ordered, sorted, auto > use sorted mode for saving sortedness. unordered mode by default, ordered for saving order of chunks. Mode auto chooses from sorted and unordered modes depending on sortedness of source tables., default: ``'auto'`` > `--print-statistics` > `--async` do not track operation progress > `--spec` structured spec in yson format ### reduce runs reduce operation ```bash usage: yt reduce [-h] [--params PARAMS] [--binary command] --src SOURCE_TABLE [SOURCE_TABLE ...] --dst DESTINATION_TABLE [--file YT_FILES] [--local-file LOCAL_FILES] [--job-count JOB_COUNT] [--memory-limit MEMORY_LIMIT] [--spec SPEC] [--format FORMAT] [--input-format INPUT_FORMAT] [--output-format OUTPUT_FORMAT] [--print-statistics] [--async] [--reduce-by REDUCE_BY] [--sort-by SORT_BY] [--join-by JOIN_BY] [command] ``` #### Positional Arguments > `command` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--binary` > `--src` (Required) Accepted multiple times. > `--dst` (Required) Accepted multiple times. > `--file` > Accepted multiple times. > `--local-file` > Accepted multiple times. > `--job-count` > `--memory-limit` in MB > `--spec` structured spec in yson format > `--format` > `--input-format` > `--output-format` > `--print-statistics` > `--async` do not track operation progress > `--reduce-by` Columns to reduce by. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" > `--sort-by` Columns to sort by. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" > `--join-by` Columns to join by. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" ### remote-copy copies source table from remote cluster to destination table on current cluster ```bash usage: yt remote-copy [-h] [--params PARAMS] --src SOURCE_TABLE [SOURCE_TABLE ...] --dst DESTINATION_TABLE --cluster CLUSTER_NAME [--network NETWORK_NAME] [--copy-attributes] [--print-statistics] [--async] [--cluster-connection CLUSTER_CONNECTION] [--spec SPEC] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--src` (Required) path to source tables in remote cluster Accepted multiple times. > `--dst` (Required) path to destination table in current cluster > `--cluster` (Required) remote cluster proxy, like smith > `--network` > `--copy-attributes` specify this flag to coping node attributes too > `--print-statistics` > `--async` do not track operation progress > `--cluster-connection` structured cluster-connection in yson format > `--spec` structured spec in yson format ### shuffle shuffles table randomly ```bash usage: yt shuffle [-h] [--params PARAMS] --table TABLE [--temp-column-name TEMP_COLUMN_NAME] [--print-statistics] [--async] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` (Required) > `--temp-column-name` > `--print-statistics` > `--async` do not track operation progress ### sort sorts source tables to destination table ```bash usage: yt sort [-h] [--params PARAMS] --src SOURCE_TABLE [SOURCE_TABLE ...] --dst DESTINATION_TABLE --sort-by SORT_BY [--print-statistics] [--async] [--spec SPEC] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--src` (Required) Accepted multiple times. > `--dst` (Required) > `--sort-by` (Required) Columns to sort by. In order to choose descending sort order, provide a map of form "{name=foo; sort_order=descending}" > `--print-statistics` > `--async` do not track operation progress > `--spec` structured spec in yson format ### vanilla run vanilla operation ```bash usage: yt vanilla [-h] [--params PARAMS] [--print-statistics] [--async] [--tasks TASKS] [--spec SPEC] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--print-statistics` > `--async` do not track operation progress > `--tasks` task descriptions. structured tasks in yson format > `--spec` structured spec in yson format ## Operation commands ### abort-op aborts operation ```bash usage: yt abort-op [-h] [--params PARAMS] [--message MESSAGE] [--operation OPERATION] [--operation-alias OPERATION_ALIAS] [operation] ``` #### Positional Arguments > `operation` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--message, --reason` abort reason message > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) ### check-operation-permission check if user has permission for operation ```bash usage: yt check-operation-permission [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] --user USER --permission PERMISSION [--format FORMAT] [operation_id] ``` #### Positional Arguments > `operation_id` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--user` (Required) user name > `--permission` (Required) permission name (e.g. read, manage) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### complete-op completes operation ```bash usage: yt complete-op [-h] [--params PARAMS] [--operation OPERATION] [--operation-alias OPERATION_ALIAS] [operation] ``` #### Positional Arguments > `operation` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) ### get-operation get operation attributes through API ```bash usage: yt get-operation [-h] [--params PARAMS] [--attribute ATTRIBUTES] [--include-runtime] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--format FORMAT] [operation_id] ``` #### Positional Arguments > `operation_id` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--attribute` desired attributes in the response Accepted multiple times. > `--include-runtime` request runtime operation information > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### list-operation-events list events of given operation ```bash usage: yt list-operation-events [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--event-type EVENT_TYPE] [--format FORMAT] [operation_id] ``` #### Positional Arguments > `operation_id` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--event-type` filter events by type > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### list-operations list operations that satisfy given options ```bash usage: yt list-operations [-h] [--params PARAMS] [--user USER] [--state STATE] [--type TYPE] [--filter FILTER] [--pool-tree POOL_TREE] [--pool POOL] [--with-failed-jobs | --without-failed-jobs] [--from-time FROM_TIME] [--to-time TO_TIME] [--cursor-time CURSOR_TIME] [--cursor-direction CURSOR_DIRECTION] [--include-archive] [--no-include-counters] [--limit LIMIT] [--attribute ATTRIBUTES] [--format FORMAT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--user` filter operations by user > `--state` filter operations by state > `--type` filter operations by operation type > `--filter` filter operation by some text factor. For example, part of the title can be passed to this option > `--pool-tree` filter operations by pool tree > `--pool` filter operations by pool. If –pool-tree is set, filters operations with this pool in specified pool tree > `--with-failed-jobs` show only operations with failed jobs > `--without-failed-jobs` show only operations without failed jobs > `--from-time` lower limit for operations start time. Time is accepted as unix timestamp or time string in YTsaurus format > `--to-time` upper limit for operations start time. Time is accepted as unix timestamp or time string in YTsaurus format > `--cursor-time` cursor time. Used in combination with –cursor-direction and –limit. Time is accepted as unix timestamp or time string in YTsaurus format > `--cursor-direction` cursor direction, can be one of («none», «past», «future»). Used in combination with –cursor-time and –limit > `--include-archive` include operations from archive in result > `--no-include-counters` do not include operation counters in result > `--limit` maximum number of operations in output > `--attribute` desired attributes in the response Accepted multiple times. > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### patch-op-spec patches operation spec ```bash usage: yt patch-op-spec [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [operation_id] patches ``` #### Positional Arguments > `operation_id` operation id or alias > `patches` structured patches in yson format #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) ### resume-op continues operation after suspending ```bash usage: yt resume-op [-h] [--params PARAMS] [--operation OPERATION] [--operation-alias OPERATION_ALIAS] [operation] ``` #### Positional Arguments > `operation` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) ### suspend-op suspends operation ```bash usage: yt suspend-op [-h] [--params PARAMS] [--operation OPERATION] [--operation-alias OPERATION_ALIAS] [--abort-running-jobs] [--reason REASON] [operation] ``` #### Positional Arguments > `operation` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--abort-running-jobs` abort running jobs > `--reason` suspend reason ### track-op synchronously tracks operation, prints current progress and finalize at the completion ```bash usage: yt track-op [-h] [--params PARAMS] [--operation OPERATION] [operation] ``` #### Positional Arguments > `operation` operation id #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id ### update-op-parameters updates operation runtime parameters ```bash usage: yt update-op-parameters [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [operation_id] parameters ``` #### Positional Arguments > `operation_id` operation id or alias > `parameters` structured parameters in yson format #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) ## Job commands ### abort-job interrupts running job with preserved result ```bash usage: yt abort-job [-h] [--params PARAMS] [--interrupt-timeout INTERRUPT_TIMEOUT] job_id ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--interrupt-timeout` try to interrupt job before abort during timeout (in ms) ### get-job get job of operation ```bash usage: yt get-job [-h] [--params PARAMS] [--job-id JOB_ID] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--format FORMAT] [job_id] [operation_id] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 > `operation_id` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### get-job-input get full input of the specified job ```bash usage: yt get-job-input [-h] [--params PARAMS] [--job-id JOB_ID] [job_id] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 ### get-job-input-paths get input paths of the specified job ```bash usage: yt get-job-input-paths [-h] [--params PARAMS] [--job-id JOB_ID] [job_id] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 ### get-job-spec get spec of the specified job ```bash usage: yt get-job-spec [-h] [--params PARAMS] [--job-id JOB_ID] [--omit-node-directory | --no-omit-node-directory] [--omit-input-table-specs | --no-omit-input-table-specs] [--omit-output-table-specs | --no-omit-output-table-specs] [job_id] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 > `--omit-node-directory` Whether node directory should be removed from job spec (true by default) > `--no-omit-node-directory` Whether node directory should be removed from job spec (true by default) > `--omit-input-table-specs` Whether input table specs should be removed from job spec (false by default) > `--no-omit-input-table-specs` Whether input table specs should be removed from job spec (false by default) > `--omit-output-table-specs` Whether output table specs should be removed from job spec (false by default) > `--no-omit-output-table-specs` Whether output table specs should be removed from job spec (false by default) ### get-job-stderr gets stderr of the specified job ```bash usage: yt get-job-stderr [-h] [--params PARAMS] [--job-id JOB_ID] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--stderr-type {user_job_stderr,gpu_check_stderr}] [job_id] [operation_id] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 > `operation_id` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--stderr-type` Possible choices: user_job_stderr, gpu_check_stderr > return stderr of specified type ### list-jobs list jobs of operation ```bash usage: yt list-jobs [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--job-type JOB_TYPE] [--job-state JOB_STATE] [--address ADDRESS] [--job-competition-id JOB_COMPETITION_ID] [--sort-field {type,state,start_time,finish_time,address,duration,progress,id}] [--sort-order SORT_ORDER] [--limit LIMIT] [--offset OFFSET] [--operation-incarnation OPERATION_INCARNATION] [--monitoring-descriptor MONITORING_DESCRIPTOR] [--with-spec] [--with-stderr] [--with-fail-context] [--with-competitors] [--with-monitoring-descriptor] [--with-interruption-info] [--include-cypress] [--include-runtime] [--include-archive] [--data-source {auto,runtime,archive,manual}] [--attribute ATTRIBUTES] [--format FORMAT] [operation_id] ``` #### Positional Arguments > `operation_id` operation id or alias #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--job-type` filter jobs by job type > `--job-state` filter jobs by job state > `--address` filter jobs by node address > `--job-competition-id` filter jobs by job competition id > `--sort-field` Possible choices: type, state, start_time, finish_time, address, duration, progress, id > field to sort jobs by > `--sort-order` sort order. Can be either «ascending» or «descending» > `--limit` output limit > `--offset` offset starting from zero > `--operation-incarnation` filter jobs by operation incarnation > `--monitoring-descriptor` filter jobs by monitoring descriptor > `--with-spec` > `--with-stderr` > `--with-fail-context` > `--with-competitors` with competitive jobs > `--with-monitoring-descriptor` > `--with-interruption-info` > `--include-cypress` include jobs from Cypress in result. Have effect only if –data-source is set to «manual» > `--include-runtime` include jobs from controller agents in result. Have effect only if –data-source is set to «manual» > `--include-archive` include jobs from archive in result. Have effect only if –data-source is set to «manual» > `--data-source` Possible choices: auto, runtime, archive, manual > data sources to list jobs from > `--attribute` desired attributes in the response Accepted multiple times. > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### run-job-shell runs interactive shell in the job sandbox ```bash usage: yt run-job-shell [-h] [--params PARAMS] [--shell-name SHELL_NAME] [--timeout TIMEOUT] [--command COMMAND] job_id [command] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 > `command` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--shell-name` name of the job shell to start > `--timeout` inactivity timeout in milliseconds after job has finished, by default 60000 milliseconds > `--command` ## Transaction commands ### abort-tx aborts transaction. All changes will be lost ```bash usage: yt abort-tx [-h] [--params PARAMS] [--transaction TRANSACTION] [transaction] ``` #### Positional Arguments > `transaction` transaction id, for example: 5c51-24e204-1-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--transaction` transaction id, for example: 5c51-24e204-1-9f3f6437 ### commit-tx saves all transaction changes ```bash usage: yt commit-tx [-h] [--params PARAMS] [--transaction TRANSACTION] [transaction] ``` #### Positional Arguments > `transaction` transaction id, for example: 5c51-24e204-1-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--transaction` transaction id, for example: 5c51-24e204-1-9f3f6437 ### lock tries to lock the path. Do not forget about global –tx option ```bash usage: yt lock [-h] [--params PARAMS] [--path PATH] [--mode {snapshot,shared,exclusive}] [--waitable] [--wait-for WAIT_FOR] [--child-key CHILD_KEY] [--attribute-key ATTRIBUTE_KEY] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--mode` Possible choices: snapshot, shared, exclusive > blocking type, exclusive by default > `--waitable` wait for lock if node is under blocking > `--wait-for` wait interval in milliseconds > `--child-key` child key of shared lock > `--attribute-key` attribute key of shared lock ### ping-tx prolongs transaction lifetime ```bash usage: yt ping-tx [-h] [--params PARAMS] [--transaction TRANSACTION] [transaction] ``` #### Positional Arguments > `transaction` transaction id, for example: 5c51-24e204-1-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--transaction` transaction id, for example: 5c51-24e204-1-9f3f6437 ### start-tx starts transaction ```bash usage: yt start-tx [-h] [--params PARAMS] [--attributes ATTRIBUTES] [--timeout TIMEOUT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--attributes` structured attributes in yson format > `--timeout` transaction lifetime since last ping in milliseconds ### unlock tries to unlock the path ```bash usage: yt unlock [-h] [--params PARAMS] [--path PATH] [path] ``` #### Positional Arguments > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ## ACL (permission) commands ### add-member adds member to Cypress node group ```bash usage: yt add-member [-h] [--params PARAMS] [--member MEMBER] [--group GROUP] [member] [group] ``` #### Positional Arguments > `member` > `group` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--member` > `--group` ### check-permission checks permission for Cypress node ```bash usage: yt check-permission [-h] [--params PARAMS] [--user USER] [--permission PERMISSION] [--path PATH] [--read-from READ_FROM] [--cache-sticky-group-size CACHE_STICKY_GROUP_SIZE] [--columns COLUMNS] [--format FORMAT] [user] [permission] [path] ``` #### Positional Arguments > `user` > `permission` one of read, write, administer, create, use > `path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--user` > `--permission` one of read, write, administer, create, use > `--path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--read-from` Can be set to «cache» to enable reads from system cache > `--cache-sticky-group-size` Size of sticky group size for read_from=»cache» mode > `--columns` structured columns in yson format > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` ### issue-token issues a new token for user, reading password interactively and securely from console ```bash usage: yt issue-token [-h] [--params PARAMS] user ``` #### Positional Arguments > `user` user to issue token #### Named Arguments > `--params` specify additional params. structured params in yson format ### list-user-tokens lists sha256-encoded user tokens, reading password interactively and securely from console ```bash usage: yt list-user-tokens [-h] [--params PARAMS] user ``` #### Positional Arguments > `user` user to revoke token #### Named Arguments > `--params` specify additional params. structured params in yson format ### remove-member removes member from Cypress node group ```bash usage: yt remove-member [-h] [--params PARAMS] [--member MEMBER] [--group GROUP] [member] [group] ``` #### Positional Arguments > `member` > `group` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--member` > `--group` ### revoke-token revokes user token, reading password and maybe token value interactively and securely from console ```bash usage: yt revoke-token [-h] [--params PARAMS] [--token-sha256 TOKEN_SHA256] user ``` #### Positional Arguments > `user` user to revoke token #### Named Arguments > `--params` specify additional params. structured params in yson format > `--token-sha256` sha256-encoded token to revoke; if omitted, token value is requested interactively ### set-user-password updates user password, reading it and optionally a current password ```bash usage: yt set-user-password [-h] [--params PARAMS] user ``` #### Positional Arguments > `user` user to set password #### Named Arguments > `--params` specify additional params. structured params in yson format ### whoami invokes whoami command at YTsaurus API and returns login ```bash usage: yt whoami [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ## Diagnostics ### download-core-dump downloads core dump for a given operation_id and job_id from a given core_table_path. Tool for downloading job core dumps ```bash usage: yt download-core-dump [-h] [--params PARAMS] [--operation-id OPERATION_ID] [--core-table-path CORE_TABLE_PATH] [--job-id JOB_ID] [--core-index CORE_INDICES] [--output-directory OUTPUT_DIRECTORY] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation-id` Operation id (should be specified if proper core file naming is needed) > `--core-table-path` A path to the core table > `--job-id` Id of a job that produced core dump. If not specified, an arbitrary job with core dumps is taken > `--core-index` Indices of core dumps to download (indexing inside single job). Several indices may be specified. If not specified, all core dumps will be downloaded. Requires –job-id to be specified Accepted multiple times. > `--output-directory` A directory to save the core dumps. Defaults to the current working directory, default: ``'.'`` ### get-job-trace get traces of the specified job ```bash usage: yt get-job-trace [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--job-id JOB_ID] [--trace-id TRACE_ID] [--from-time FROM_TIME] [--to-time TO_TIME] [operation_id] [job_id] ``` #### Positional Arguments > `operation_id` operation id or alias > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 > `--trace-id` trace id > `--from-time` lower limit for event’s timestamp. Time is accepted as unix timestamp or time string in YTsaurus format > `--to-time` upper limit for event’s timestamp. Time is accepted as unix timestamp or time string in YTsaurus format ### job-tool Tool helps to debug user job code by preparing job environment on local machine. It downloads all necessary job files, fail context (small portion of job input data) and prepares run script. ```bash usage: yt job-tool [-h] [--params PARAMS] command ... ``` #### Positional Arguments > `command` Possible choices: prepare-job-environment, run-job #### Named Arguments > `--params` specify additional params. structured params in yson format #### Sub-commands ##### prepare-job-environment prepare all necessary stuff for job ```bash yt job-tool prepare-job-environment [-h] [--operation-id OPERATION_ID] [--job-id JOB_ID] [--job-path JOB_PATH] [--run] [--full-input | --context] [operation_id] [job_id] ``` ###### Positional Arguments > `operation_id` > `job_id` ###### Named Arguments > `--operation-id` > `--job-id` > `--job-path` output directory to store job environment. Default: `/job_` > `--run` run job when job environment is prepared > `--full-input, --full` download input context of a job > `--context` download fail context of a job ##### run-job runs job binary ```bash yt job-tool run-job [-h] [--job-path JOB_PATH] [--env ENV] [job_path] ``` ###### Positional Arguments > `job_path` path to prepared job environment ###### Named Arguments > `--job-path` path to prepared job environment > `--env` environment to use in script run in YSON format ### list-job-traces list traces of the specified job ```bash usage: yt list-job-traces [-h] [--params PARAMS] [--operation OPERATION_ID] [--operation-alias OPERATION_ALIAS] [--job-id JOB_ID] [--per-process] [--limit LIMIT] [--format FORMAT] [operation_id] [job_id] ``` #### Positional Arguments > `operation_id` operation id or alias > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 #### Named Arguments > `--params` specify additional params. structured params in yson format > `--operation` operation id > `--operation-alias` operation alias (e.g. `*my_alias`) > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 > `--per-process` get traces info about each process > `--limit` maximum number of traces to return > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### run-job-shell-command runs a command in the job sandbox and returns the output ```bash usage: yt run-job-shell-command [-h] [--params PARAMS] [--job-id JOB_ID] [--command COMMAND] [--shell-name SHELL_NAME] [job_id] [command] ``` #### Positional Arguments > `job_id` job id, for example: 5c51-24e204-384-9f3f6437 > `command` command to execute in job sandbox #### Named Arguments > `--params` specify additional params. structured params in yson format > `--job-id` job id, for example: 5c51-24e204-384-9f3f6437 > `--command` command to execute in job sandbox > `--shell-name` name of the job shell to use ## Parquet commands ### dump-orc dump table with a strict schema as ORC file ```bash usage: yt dump-orc [-h] [--params PARAMS] [--table TABLE] --output-file OUTPUT_FILE [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--output-file` (Required) ### dump-parquet dump table with a strict schema as Parquet file ```bash usage: yt dump-parquet [-h] [--params PARAMS] [--table TABLE] [--output-path OUTPUT_PATH] [--output-file OUTPUT_FILE] [--enable-several-files] [--unordered] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--output-path` > `--output-file` > `--enable-several-files` allowing parquet to be written to multiple files > `--unordered` ### upload-orc upload ORC file as a table ```bash usage: yt upload-orc [-h] [--params PARAMS] [--table TABLE] --input-file INPUT_FILE [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--input-file` (Required) ### upload-parquet upload Parquet file as a table ```bash usage: yt upload-parquet [-h] [--params PARAMS] [--table TABLE] --input-file INPUT_FILE [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--input-file` (Required) ## Queue ### advance-consumer advances consumer offset for the given queue ```bash usage: yt advance-consumer [-h] [--params PARAMS] [--consumer-path CONSUMER_PATH] [--queue-path QUEUE_PATH] --partition-index PARTITION_INDEX [--old-offset OLD_OFFSET] --new-offset NEW_OFFSET [consumer_path] [queue_path] ``` #### Positional Arguments > `consumer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--consumer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--partition-index` (Required) > `--old-offset` > `--new-offset` (Required) ### advance-queue-consumer advances consumer offset for the given queue ```bash usage: yt advance-queue-consumer [-h] [--params PARAMS] [--consumer-path CONSUMER_PATH] [--queue-path QUEUE_PATH] --partition-index PARTITION_INDEX [--old-offset OLD_OFFSET] --new-offset NEW_OFFSET [consumer_path] [queue_path] ``` #### Positional Arguments > `consumer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--consumer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--partition-index` (Required) > `--old-offset` > `--new-offset` (Required) ### create-queue-producer-session creates queue producer session ```bash usage: yt create-queue-producer-session [-h] [--params PARAMS] [--producer-path PRODUCER_PATH] [--queue-path QUEUE_PATH] --session-id SESSION_ID [--format FORMAT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--producer-path` Path to producer in Cypress; cluster may be specified. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` Path to queue in Cypress; cluster may be specified. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--session-id` (Required) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### list-queue-consumer-registrations lists queue consumer registrations ```bash usage: yt list-queue-consumer-registrations [-h] [--params PARAMS] [--queue-path QUEUE_PATH] [--consumer-path CONSUMER_PATH] [--format FORMAT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--queue-path` Path to queue in Cypress; cluster may be specified. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--consumer-path` Path to consumer in Cypress; cluster may be specified. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### pull-consumer reads rows from a single partition of a queue (i.e. any ordered dynamic table) with authorization via consumer ```bash usage: yt pull-consumer [-h] [--params PARAMS] [--consumer-path CONSUMER_PATH] [--queue-path QUEUE_PATH] --offset OFFSET --partition-index PARTITION_INDEX [--max-row-count MAX_ROW_COUNT] [--max-data-weight MAX_DATA_WEIGHT] [--replica-consistency {none,sync}] [--format FORMAT] [consumer_path] [queue_path] ``` #### Positional Arguments > `consumer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--consumer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--offset` (Required) > `--partition-index` (Required) > `--max-row-count` > `--max-data-weight` > `--replica-consistency` Possible choices: none, sync > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### pull-queue reads rows from a single partition of a queue (i.e. any ordered dynamic table) ```bash usage: yt pull-queue [-h] [--params PARAMS] [--queue-path QUEUE_PATH] --offset OFFSET --partition-index PARTITION_INDEX [--max-row-count MAX_ROW_COUNT] [--max-data-weight MAX_DATA_WEIGHT] [--replica-consistency {none,sync}] [--format FORMAT] [queue_path] ``` #### Positional Arguments > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--offset` (Required) > `--partition-index` (Required) > `--max-row-count` > `--max-data-weight` > `--replica-consistency` Possible choices: none, sync > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### pull-queue-consumer reads rows from a single partition of a queue (i.e. any ordered dynamic table) with authorization via consumer ```bash usage: yt pull-queue-consumer [-h] [--params PARAMS] [--consumer-path CONSUMER_PATH] [--queue-path QUEUE_PATH] --offset OFFSET --partition-index PARTITION_INDEX [--max-row-count MAX_ROW_COUNT] [--max-data-weight MAX_DATA_WEIGHT] [--replica-consistency {none,sync}] [--format FORMAT] [consumer_path] [queue_path] ``` #### Positional Arguments > `consumer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--consumer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--offset` (Required) > `--partition-index` (Required) > `--max-row-count` > `--max-data-weight` > `--replica-consistency` Possible choices: none, sync > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### push-queue-producer push rows to queue via queue producer ```bash usage: yt push-queue-producer [-h] [--params PARAMS] [--producer-path PRODUCER_PATH] [--queue-path QUEUE_PATH] --session-id SESSION_ID --epoch EPOCH [--require-sync-replica | --no-require-sync-replica] [--input-format INPUT_FORMAT] [--output-format OUTPUT_FORMAT] [producer_path] [queue_path] ``` #### Positional Arguments > `producer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--producer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--session-id` (Required) > `--epoch` (Required) > `--require-sync-replica` > `--no-require-sync-replica` > `--input-format` input format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--output-format` output format. response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### register-queue-consumer registers queue consumer ```bash usage: yt register-queue-consumer [-h] [--params PARAMS] [--queue-path QUEUE_PATH] [--consumer-path CONSUMER_PATH] (--vital | --non-vital) [--partitions [PARTITIONS ...]] [queue_path] [consumer_path] ``` #### Positional Arguments > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `consumer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--consumer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--vital` Whether the consumer is vital > `--non-vital` Whether the consumer is vital > `--partitions` ### remove-queue-producer-session removes queue producer session ```bash usage: yt remove-queue-producer-session [-h] [--params PARAMS] [--producer-path PRODUCER_PATH] [--queue-path QUEUE_PATH] --session-id SESSION_ID ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--producer-path` Path to producer in Cypress; cluster may be specified. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--queue-path` Path to queue in Cypress; cluster may be specified. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--session-id` (Required) ### unregister-queue-consumer unregisters queue consumer ```bash usage: yt unregister-queue-consumer [-h] [--params PARAMS] [--queue-path QUEUE_PATH] [--consumer-path CONSUMER_PATH] [queue_path] [consumer_path] ``` #### Positional Arguments > `queue_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `consumer_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--queue-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--consumer-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ## Query tracker ### abort-query abort query ```bash usage: yt abort-query [-h] [--params PARAMS] [--message MESSAGE] [--stage STAGE] query_id ``` #### Positional Arguments > `query_id` query id #### Named Arguments > `--params` specify additional params. structured params in yson format > `--message` optional abort message > `--stage` query tracker stage, defaults to «production» ### alter-query alter query ```bash usage: yt alter-query [-h] [--params PARAMS] [--annotations ANNOTATIONS] [--access-control-objects ACCESS_CONTROL_OBJECTS] [--stage STAGE] query_id ``` #### Positional Arguments > `query_id` query id #### Named Arguments > `--params` specify additional params. structured params in yson format > `--annotations` a YSON map of annotations. structured annotations in yson format > `--access-control-objects` access control objects, a YSON list of ACO names. structured access-control-objects in yson format > `--stage` query tracker stage, defaults to «production» ### get-query get query ```bash usage: yt get-query [-h] [--params PARAMS] [--attribute ATTRIBUTES] [--stage STAGE] [--format FORMAT] query_id ``` #### Positional Arguments > `query_id` query id #### Named Arguments > `--params` specify additional params. structured params in yson format > `--attribute` desired attributes in the response Accepted multiple times. > `--stage` query tracker stage, defaults to «production» > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### get-query-result get query result ```bash usage: yt get-query-result [-h] [--params PARAMS] [--result-index RESULT_INDEX] [--stage STAGE] [--format FORMAT] query_id ``` #### Positional Arguments > `query_id` query id #### Named Arguments > `--params` specify additional params. structured params in yson format > `--result-index` index of query result, defaults to 0 > `--stage` query tracker stage, defaults to «production» > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### get-query-tracker-info get query tracker info ```bash usage: yt get-query-tracker-info [-h] [--params PARAMS] [--stage STAGE] [--format FORMAT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--stage` query tracker stage, defaults to «production» > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### list-queries list operations that satisfy given options ```bash usage: yt list-queries [-h] [--params PARAMS] [--user USER] [--engine ENGINE] [--state STATE] [--filter FILTER] [--from-time FROM_TIME] [--to-time TO_TIME] [--cursor-time CURSOR_TIME] [--cursor-direction CURSOR_DIRECTION] [--limit LIMIT] [--attribute ATTRIBUTES] [--stage STAGE] [--use-full-scan-search | --search-by-token-prefix] [--tutorials] [--sort-order SORT_ORDER] [--format FORMAT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--user` filter queries by user > `--engine` filter queries by engine > `--state` filter queries by state > `--filter` filter queries by some text factor. For example, part of the query can be passed to this option > `--from-time` lower limit for operations start time. Time is accepted as unix timestamp or time string in YTsaurus format > `--to-time` upper limit for operations start time. Time is accepted as unix timestamp or time string in YTsaurus format > `--cursor-time` cursor time. Used in combination with –cursor-direction and –limit. Time is accepted as unix timestamp or time string in YTsaurus format > `--cursor-direction` cursor direction, can be one of («none», «past», «future»). Used in combination with –cursor-time and –limit > `--limit` maximum number of operations in output > `--attribute` desired attributes in the response Accepted multiple times. > `--stage` query tracker stage, defaults to «production» > `--use-full-scan-search` forces the use of full-scan search for exact substring match > `--search-by-token-prefix` can’t be used with «–use-full-scan-search». If set, search will be performed not by exact token match but by prefix match > `--tutorials` show tutorial queries > `--sort-order` result sort order by query start time, can be one of («cursor», «ascending», «descending»). Sorting is not applied in «cursor» mode, rows will be listed in the order they are read. > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### query run query and track its progress (unless sync = false) ```bash usage: yt query [-h] [--params PARAMS] [--settings SETTINGS] [--files FILES] [--access-control-objects ACCESS_CONTROL_OBJECTS] [--stage STAGE] [--structured] [--async] [--format FORMAT] engine query ``` #### Positional Arguments > `engine` engine of a query, one of «ql», «yql», «chyt», «spyt» > `query` query text #### Named Arguments > `--params` specify additional params. structured params in yson format > `--settings` additional settings of a query in structured form. structured settings in yson format > `--files` query files, a YSON list of files, each of which is represented by a map with keys «name», «content», «type». Field «type» is one of «raw_inline_data», «url». structured files in yson format > `--access-control-objects` access control objects, a YSON list of ACO names. structured access-control-objects in yson format > `--stage` query tracker stage, defaults to «production» > `--structured` output all query results together with their metainformation in structured form > `--async` do not track query progress > `--format` output format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### read-query-result read query result ```bash usage: yt read-query-result [-h] [--params PARAMS] [--result-index RESULT_INDEX] [--stage STAGE] [--format FORMAT] query_id ``` #### Positional Arguments > `query_id` query id #### Named Arguments > `--params` specify additional params. structured params in yson format > `--result-index` index of query result, defaults to 0 > `--stage` query tracker stage, defaults to «production» > `--format` output format. (yson string), one of «yson», «json», «yamr», «dsv», «yamred_dsv», «schemaful_dsv» with modifications. See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### start-query start query ```bash usage: yt start-query [-h] [--params PARAMS] [--settings SETTINGS] [--files FILES] [--annotations ANNOTATIONS] [--access-control-object ACCESS_CONTROL_OBJECT] [--access-control-objects ACCESS_CONTROL_OBJECTS] [--stage STAGE] [--do-not-index] engine query ``` #### Positional Arguments > `engine` engine of a query, one of «ql», «yql», «chyt», «spyt» > `query` query text #### Named Arguments > `--params` specify additional params. structured params in yson format > `--settings` additional settings of a query in structured form. structured settings in yson format > `--files` query files, a YSON list of files, each of which is represented by a map with keys «name», «content», «type». Field «type» is one of «raw_inline_data», «url». structured files in yson format > `--annotations` additional annotations of a query in structured form. Query title might be specified here. structured annotations in yson format > `--access-control-object` optional access control object name (deprecated) > `--access-control-objects` access control objects, a YSON list of ACO names. structured access-control-objects in yson format > `--stage` query tracker stage, defaults to «production» > `--do-not-index` makes query invisible for list_queries ## Other commands ### add-maintenance adds maintenance request for a given node ```bash usage: yt add-maintenance [-h] [--params PARAMS] [-c COMPONENT] [-a ADDRESS] [-t TYPE] [--comment COMMENT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `-c, --component` > `-a, --address` Component address > `-t, --type` Maintenance type > `--comment` ### admin Administer commands ```bash usage: yt admin [-h] admin_command ... ``` #### Positional Arguments > `admin_command` Possible choices: switch-leader, describe, logs, metrics, remove-master-unrecognized-options #### Sub-commands ##### switch-leader Switch master cell leader ```bash yt admin switch-leader [-h] [--cell-id CELL_ID] [--new-leader-address NEW_LEADER_ADDRESS] ``` ###### Named Arguments > `--cell-id` > `--new-leader-address` ##### describe Describe cluster or table. This functionality is experimental and the interface may change. ```bash yt admin describe [-h] {cluster,table} ... ``` ###### Sub-commands ####### cluster General information about cluster components and their configs. This functionality is experimental and the interface may change. ```bash yt admin describe cluster [-h] [--nodes] [--bundles] [--dynamic-configs] [--static-configs] [--format {yson,json,yaml}] [-o dir] ``` ######## Named Arguments > `--nodes` Fetch cluster nodes information > `--bundles` Fetch tablet cell bundles information > `--dynamic-configs` Fetch dynamic configurations > `--static-configs` Fetch static node configurations > `--format` Possible choices: yson, json, yaml, default: ``'yson'`` > `-o, --output` Output directory path, default: ``'describe'`` ####### table Attributes of certain tables. This functionality is experimental and the interface may change. ```bash yt admin describe table [-h] [--format {yson,json,yaml}] [-o dir] paths [paths ...] ``` ######## Positional Arguments > `paths` Table paths ######## Named Arguments > `--format` Possible choices: yson, json, yaml, default: ``'yson'`` > `-o, --output` Output directory path, default: ``'describe'`` ##### logs Fetch cluster logs. This functionality is experimental and the interface may change. ```bash yt admin logs [-h] {k8s} ... ``` ###### Sub-commands ####### k8s Fetch YTsaurus cluster logs via Kubernetes API. This functionality is experimental and the interface may change. ```bash yt admin logs k8s [-h] [-n NAMESPACE] [-p PODS] [--exec-slot-index EXEC_SLOT_INDEX] [--from-ts ISO8601] [--to-ts ISO8601] [-w WRITER] [--writer-force regex] [-o dir] [--grep regex] [--yes] cluster_name {discovery,primary_masters,master_caches,http_proxies,rpc_proxies,data_nodes,exec_nodes,tablet_nodes,queue_agents,tcp_proxies,kafka_proxies,schedulers,controller_agents,query_trackers,yql_agents,cypress_proxies,bundle_controllers} [group_name] ``` ######## Positional Arguments > `cluster_name` YTsaurus cluster name (see: kubectl get ytsaurus) > `component_name` Possible choices: discovery, primary_masters, master_caches, http_proxies, rpc_proxies, data_nodes, exec_nodes, tablet_nodes, queue_agents, tcp_proxies, kafka_proxies, schedulers, controller_agents, query_trackers, yql_agents, cypress_proxies, bundle_controllers > Component name > `group_name` Component group name, default: ``'default'`` ######## Named Arguments > `-n, --namespace` Kubernetes namespace (default: `from kubeconfig or "default")` > `-p, --pods` Specific pod name (can be specified multiple times) > `--exec-slot-index` Slot index for job proxy logs (useful only for exec nodes) > `--from-ts` Filter logs from this time (i.e. 2026-09-21T21:58:13Z) > `--to-ts` Filter logs up to this time (i.e. 2026-09-21T21:58:13Z) > `-w, --writer` Filter by writer name (can be specified multiple times) > `--writer-force` Force add regex pattern for filename filtering > `-o, --output` Output directory path, default: ``'logs'`` > `--grep` Filter logs content by regex > `--yes, -y` Skip confirmation prompt ##### metrics Metrics tools (validate / dump / replay). This functionality is experimental and the interface may change. ```bash yt admin metrics [-h] {validate,dump,replay} ... ``` ###### Sub-commands ####### validate Validate metrics spec and print unique selectors. This functionality is experimental and the interface may change. ```bash yt admin metrics validate [-h] --spec SPEC [--target TARGET] ``` ######## Named Arguments > `--spec` Path to spec.yaml > `--target` Additional inline target as JSON, e.g. "{«type»:»metric»,»query»:»yt_x»}" ####### dump Dump metrics from Prometheus into an archive. Auth via PROMETHEUS_USER/PROMETHEUS_PASSWORD env. This functionality is experimental and the interface may change. ```bash yt admin metrics dump [-h] --spec SPEC --from-ts ISO8601 --to-ts ISO8601 --prometheus-url PROMETHEUS_URL [--step DURATION] [--target TARGET] [--output OUTPUT] [--max-series MAX_SERIES] [--force] [--max-points-per-series MAX_POINTS_PER_SERIES] ``` ######## Named Arguments > `--spec` Path to spec.yaml > `--from-ts` Start timestamp (i.e. 2026-09-21T21:58:13Z) > `--to-ts` End timestamp (i.e. 2026-09-21T21:58:13Z) > `--prometheus-url` Prometheus base URL > `--step` Step, e.g. 15s or 5m (overrides spec defaults.step) > `--target` Additional inline target as JSON, e.g. "{«type»:»metric»,»query»:»yt_x»}" > `--output` Output archive path, default: ``'metrics.zip'`` > `--max-series` Confirmation threshold for total series count, default: ``100000`` > `--force` Skip confirmation when over –max-series > `--max-points-per-series` Maximum points per series a single query may return (0 to disable), default: ``11000`` ####### replay Run local Prometheus and Grafana containers from a dumped metrics archive. This functionality is experimental and the interface may change. ```bash yt admin metrics replay [-h] [--prometheus-port PROMETHEUS_PORT] [--grafana-port GRAFANA_PORT] archive ``` ######## Positional Arguments > `archive` Path to metrics archive (.zip) ######## Named Arguments > `--prometheus-port` Port to expose Prometheus on (default: `Docker picks a free port)` > `--grafana-port` Port to expose Grafana on (default: `Docker picks a free port)` ##### remove-master-unrecognized-options Remove unrecognized options from master dynamic config. ```bash yt admin remove-master-unrecognized-options [-h] [--dry] [--do-not-print-config] ``` ###### Named Arguments > `--dry` Just print unrecognized options without actually removing them > `--do-not-print-config` Do not print [//sys/@config](https://ytsaurus.tech/docs/en/mailto://sys/@config) before removing anything (it is printed by default just in case) ### chyt ClickHouse over YTsaurus commands ```bash usage: yt chyt [-h] clickhouse_command ... ``` #### Positional Arguments > `clickhouse_command` Possible choices: start-clique, execute, ctl #### Sub-commands ##### start-clique starts a CHYT clique consisting of a given number of instances ```bash yt chyt start-clique [-h] --instance-count INSTANCE_COUNT [--alias ALIAS] [--cypress-ytserver-clickhouse-path CYPRESS_YTSERVER_CLICKHOUSE_PATH] [--cypress-clickhouse-trampoline-path CYPRESS_CLICKHOUSE_TRAMPOLINE_PATH] [--cypress-ytserver-log-tailer-path CYPRESS_YTSERVER_LOG_TAILER_PATH] [--cypress-base-config-path CYPRESS_BASE_CONFIG_PATH] [--cpu-limit CPU_LIMIT] [--cypress-geodata-path CYPRESS_GEODATA_PATH] [--abort-existing] [--artifact-path ARTIFACT_PATH] [--skip-version-compatibility-validation] [--spec SPEC] [--clickhouse-config CLICKHOUSE_CONFIG] [--memory-config MEMORY_CONFIG] ``` ###### Named Arguments > `--instance-count` (Required) > `--alias, --operation-alias` Alias for clique; may be also specified via CHYT_PROXY env variable > `--cypress-ytserver-clickhouse-path` > `--cypress-clickhouse-trampoline-path` > `--cypress-ytserver-log-tailer-path` > `--cypress-base-config-path` Default: ``'//sys/clickhouse/config'`` > `--cpu-limit` > `--cypress-geodata-path` > `--abort-existing` Abort existing operation under same alias > `--artifact-path` path for artifact directory; by default equals to //sys/clickhouse/kolkhoz/ > `--skip-version-compatibility-validation` (For developer use only) > `--spec` structured spec in yson format > `--clickhouse-config` ClickHouse configuration patch. structured clickhouse-config in yson format > `--memory-config` Memory configuration. structured memory-config in yson format ##### execute executes ClickHouse query in given CHYT clique ```bash yt chyt execute [-h] [--alias ALIAS] [--query QUERY] [--format FORMAT] [--setting SETTING] [query] ``` ###### Positional Arguments > `query` Query to execute; do not specify FORMAT in query, use –format instead ###### Named Arguments > `--alias, --operation-alias` Alias for clique; may be also specified via CHYT_PROXY env variable > `--query` Query to execute; do not specify FORMAT in query, use –format instead > `--format` ClickHouse data format; refer to [https://clickhouse.tech/docs/en/interfaces/formats/](https://clickhouse.tech/docs/en/interfaces/formats/); default is TabSeparated, default: ``'TabSeparated'`` > `--setting` Add ClickHouse setting to query in format =. Accepted multiple times. ##### ctl CHYT controller ```bash yt chyt ctl [--address ADDRESS] [--stage STAGE] [--cluster-name CLUSTER_NAME] command ... ``` ###### Positional Arguments > `command` ###### Named Arguments > `--address` controller service address; may contain {stage}, {family} and {host_suffix} parameters; the default value is fetched from the remote client config > `--stage` controller stage, default: ``'production'`` > `--cluster-name` cluster name under which the cluster is configured in the strawberry controller; the default value is fetched from the remote client config ### clickhouse ClickHouse over YTsaurus commands ```bash usage: yt clickhouse [-h] clickhouse_command ... ``` #### Positional Arguments > `clickhouse_command` Possible choices: start-clique, execute, ctl #### Sub-commands ##### start-clique starts a CHYT clique consisting of a given number of instances ```bash yt clickhouse start-clique [-h] --instance-count INSTANCE_COUNT [--alias ALIAS] [--cypress-ytserver-clickhouse-path CYPRESS_YTSERVER_CLICKHOUSE_PATH] [--cypress-clickhouse-trampoline-path CYPRESS_CLICKHOUSE_TRAMPOLINE_PATH] [--cypress-ytserver-log-tailer-path CYPRESS_YTSERVER_LOG_TAILER_PATH] [--cypress-base-config-path CYPRESS_BASE_CONFIG_PATH] [--cpu-limit CPU_LIMIT] [--cypress-geodata-path CYPRESS_GEODATA_PATH] [--abort-existing] [--artifact-path ARTIFACT_PATH] [--skip-version-compatibility-validation] [--spec SPEC] [--clickhouse-config CLICKHOUSE_CONFIG] [--memory-config MEMORY_CONFIG] ``` ###### Named Arguments > `--instance-count` (Required) > `--alias, --operation-alias` Alias for clique; may be also specified via CHYT_PROXY env variable > `--cypress-ytserver-clickhouse-path` > `--cypress-clickhouse-trampoline-path` > `--cypress-ytserver-log-tailer-path` > `--cypress-base-config-path` Default: ``'//sys/clickhouse/config'`` > `--cpu-limit` > `--cypress-geodata-path` > `--abort-existing` Abort existing operation under same alias > `--artifact-path` path for artifact directory; by default equals to //sys/clickhouse/kolkhoz/ > `--skip-version-compatibility-validation` (For developer use only) > `--spec` structured spec in yson format > `--clickhouse-config` ClickHouse configuration patch. structured clickhouse-config in yson format > `--memory-config` Memory configuration. structured memory-config in yson format ##### execute executes ClickHouse query in given CHYT clique ```bash yt clickhouse execute [-h] [--alias ALIAS] [--query QUERY] [--format FORMAT] [--setting SETTING] [query] ``` ###### Positional Arguments > `query` Query to execute; do not specify FORMAT in query, use –format instead ###### Named Arguments > `--alias, --operation-alias` Alias for clique; may be also specified via CHYT_PROXY env variable > `--query` Query to execute; do not specify FORMAT in query, use –format instead > `--format` ClickHouse data format; refer to [https://clickhouse.tech/docs/en/interfaces/formats/](https://clickhouse.tech/docs/en/interfaces/formats/); default is TabSeparated, default: ``'TabSeparated'`` > `--setting` Add ClickHouse setting to query in format =. Accepted multiple times. ##### ctl CHYT controller ```bash yt clickhouse ctl [--address ADDRESS] [--stage STAGE] [--cluster-name CLUSTER_NAME] command ... ``` ###### Positional Arguments > `command` ###### Named Arguments > `--address` controller service address; may contain {stage}, {family} and {host_suffix} parameters; the default value is fetched from the remote client config > `--stage` controller stage, default: ``'production'`` > `--cluster-name` cluster name under which the cluster is configured in the strawberry controller; the default value is fetched from the remote client config ### detect-porto-layer ```bash usage: yt detect-porto-layer [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ### devtools Devtools commands ```bash usage: yt devtools [-h] {image} ... ``` #### Sub-commands ##### image Run operations in docker ```bash yt devtools image [-h] {get-cluster-env,prepare,run,list,install} ... ``` ###### Sub-commands ####### get-cluster-env Get cluster OS/Python version ```bash yt devtools image get-cluster-env [-h] ``` ####### prepare Create dockerfile for cluster OS/Python version and build image ```bash yt devtools image prepare [-h] [--image-name IMAGE_NAME] [--base-image BASE_IMAGE] [--python-version PYTHON_VERSION] [--print-dockerfile-only] [--with-modules [WITH_MODULES ...]] [--with-local-modules] ``` ######## Named Arguments > `--image-name` Target image name > `--base-image` Base image. For example - «docker.io/library/ubuntu:trusty». By default - as on target cluster > `--python-version` Python version. For example «3.13». By default - as on target cluster > `--print-dockerfile-only` Only create Dockerfile > `--with-modules` Python module to install into container > `--with-local-modules` Collect installed modules from current environment ####### run Run script in specific docker contaner ```bash yt devtools image run [-h] [--image-name IMAGE_NAME] script_path ``` ######## Positional Arguments > `script_path` Path to script ######## Named Arguments > `--image-name` Docker image ####### list List images for scrupt run (by default - for current cluster) ```bash yt devtools image list [-h] [--all] ``` ######## Named Arguments > `--all` Show images for all clusters ####### install List images for scrupt run ```bash yt devtools image install [-h] [--image-name IMAGE_NAME] [--pip [PACKAGES ...]] [--bash BASH] ``` ######## Named Arguments > `--image-name` Docker image name to modify > `--pip` Python package to intsall > `--bash` Bash command to execute ### dq DQ clique commands ```bash usage: yt dq [-h] dq_command ... ``` #### Positional Arguments > `dq_command` Possible choices: ctl #### Sub-commands ##### ctl DQ controller ```bash yt dq ctl [--address ADDRESS] [--stage STAGE] [--cluster-name CLUSTER_NAME] command ... ``` ###### Positional Arguments > `command` ###### Named Arguments > `--address` controller service address; may contain {stage}, {family} and {host_suffix} parameters; the default value is fetched from the remote client config > `--stage` controller stage, default: ``'production'`` > `--cluster-name` cluster name under which the cluster is configured in the strawberry controller; the default value is fetched from the remote client config ### execute . execute your command ```bash usage: yt execute [-h] [--params PARAMS] command_name execute_params ``` #### Positional Arguments > `command_name` > `execute_params` structured execute_params in yson format #### Named Arguments > `--params` specify additional params. structured params in yson format ### execute-batch executes requests in parallel as one batch request ```bash usage: yt execute-batch [-h] [--params PARAMS] requests [requests ...] ``` #### Positional Arguments > `requests` Request description #### Named Arguments > `--params` specify additional params. structured params in yson format ### explain-id ```bash usage: yt explain-id [-h] [--params PARAMS] [--local] id ``` #### Positional Arguments > `id` id (GUID like string) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--local` Do not gather info from cluster ### flow YTsaurus Flow commands ```bash usage: yt flow [-h] flow_command ... ``` #### Positional Arguments > `flow_command` Possible choices: start-pipeline, stop-pipeline, pause-pipeline, get-pipeline-spec, set-pipeline-spec, remove-pipeline-spec, get-pipeline-dynamic-spec, set-pipeline-dynamic-spec, remove-pipeline-dynamic-spec, get-pipeline-state, get-flow-view, show-logs, execute, describe-pipeline, read-states, delete-states #### Sub-commands ##### start-pipeline start YTsaurus Flow pipeline. Start YTsaurus Flow pipeline ```bash yt flow start-pipeline [-h] [--pipeline-path PIPELINE_PATH] [--sync] [--wait-timeout WAIT_TIMEOUT] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--sync` Wait for the pipeline to start > `--wait-timeout` Timeout in seconds to wait for the pipeline to start, default: ``600`` ##### stop-pipeline stop YTsaurus Flow pipeline. Stop YTsaurus Flow pipeline ```bash yt flow stop-pipeline [-h] [--pipeline-path PIPELINE_PATH] [--sync] [--wait-timeout WAIT_TIMEOUT] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--sync` Wait for the pipeline to stop > `--wait-timeout` Timeout in seconds to wait for the pipeline to stop, default: ``600`` ##### pause-pipeline pause YTsaurus Flow pipeline. Pause YTsaurus Flow pipeline ```bash yt flow pause-pipeline [-h] [--pipeline-path PIPELINE_PATH] [--sync] [--wait-timeout WAIT_TIMEOUT] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--sync` Wait for the pipeline to pause > `--wait-timeout` Timeout in seconds to wait for the pipeline to pause, default: ``600`` ##### get-pipeline-spec get YTsaurus Flow pipeline spec. Get YTsaurus Flow pipeline spec ```bash yt flow get-pipeline-spec [-h] [--pipeline-path PIPELINE_PATH] [--format FORMAT] [--spec-path SPEC_PATH] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `--spec-path` Path inside pipeline spec yson struct, starting with / ##### set-pipeline-spec set YTsaurus Flow pipeline spec. Set YTsaurus Flow pipeline spec ```bash yt flow set-pipeline-spec [-h] [--pipeline-path PIPELINE_PATH] [--format FORMAT] [--expected-version EXPECTED_VERSION] [--force] [--spec-path SPEC_PATH] [--value VALUE] [pipeline_path] [value] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `value` New spec attribute value; if it is not set, it will be read from stdin ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `--expected-version` Pipeline spec expected version > `--force` Set spec even if pipeline is paused > `--spec-path` Path inside pipeline spec yson struct, starting with / > `--value` New spec attribute value; if it is not set, it will be read from stdin ##### remove-pipeline-spec remove YTsaurus Flow pipeline spec. Remove YTsaurus Flow pipeline spec ```bash yt flow remove-pipeline-spec [-h] [--pipeline-path PIPELINE_PATH] [--expected-version EXPECTED_VERSION] [--force] [--spec-path SPEC_PATH] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--expected-version` Pipeline spec expected version > `--force` Remove spec even if pipeline is paused > `--spec-path` Path inside pipeline spec yson struct, starting with / ##### get-pipeline-dynamic-spec get YTsaurus Flow pipeline dynamic spec. Get YTsaurus Flow pipeline dynamic spec ```bash yt flow get-pipeline-dynamic-spec [-h] [--pipeline-path PIPELINE_PATH] [--format FORMAT] [--spec-path SPEC_PATH] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `--spec-path` Path inside pipeline dynamic spec yson struct, starting with / ##### set-pipeline-dynamic-spec set YTsaurus Flow pipeline dynamic spec. Set YTsaurus Flow pipeline dynamic spec ```bash yt flow set-pipeline-dynamic-spec [-h] [--pipeline-path PIPELINE_PATH] [--format FORMAT] [--expected-version EXPECTED_VERSION] [--spec-path SPEC_PATH] [--value VALUE] [pipeline_path] [value] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `value` New spec attribute value; if it is not set, it will be read from stdin ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `--expected-version` Pipeline spec expected version > `--spec-path` Path inside pipeline dynamic spec yson struct, starting with / > `--value` New spec attribute value; if it is not set, it will be read from stdin ##### remove-pipeline-dynamic-spec remove YTsaurus Flow pipeline dynamic spec. Remove YTsaurus Flow pipeline dynamic spec ```bash yt flow remove-pipeline-dynamic-spec [-h] [--pipeline-path PIPELINE_PATH] [--expected-version EXPECTED_VERSION] [--spec-path SPEC_PATH] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--expected-version` Pipeline spec expected version > `--spec-path` Path inside pipeline dynamic spec yson struct, starting with / ##### get-pipeline-state get YTsaurus Flow pipeline state. Get YTsaurus Flow pipeline state ```bash yt flow get-pipeline-state [-h] [--pipeline-path PIPELINE_PATH] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ##### get-flow-view get YTsaurus Flow flow view. Get YTsaurus Flow flow view ```bash yt flow get-flow-view [-h] [--pipeline-path PIPELINE_PATH] [--format FORMAT] [--view-path VIEW_PATH] [--cache CACHE] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md), default: ``'yson'`` > `--view-path` Path inside flow view yson struct, starting with / > `--cache` Use controller cache ##### show-logs get YTsaurus Flow controller logs. Show YTsaurus FLow logs ```bash yt flow show-logs [-h] [--pipeline-path PIPELINE_PATH] [--count COUNT] [--attach] [--rate RATE] [--print-host] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--count` The number of last logs, default: ``25`` > `--attach` Attach to the logs stream > `--rate` Logs reading period in seconds, default: ``1.0`` > `--print-host` Print controller’s hostname ##### execute execute YTsaurus Flow specific command. Execute YTsaurus Flow specific command ```bash yt flow execute [-h] [--pipeline-path PIPELINE_PATH] [--flow-command FLOW_COMMAND] [--flow-argument FLOW_ARGUMENT] [--input-format INPUT_FORMAT] [--output-format OUTPUT_FORMAT] [pipeline_path] [flow_command] [flow_argument] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `flow_command` Name of the command to execute; use command "list" to list all available commands > `flow_argument` Argument of the command (optional) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--flow-command` Name of the command to execute; use command "list" to list all available commands > `--flow-argument` Argument of the command (optional) > `--input-format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) > `--output-format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ##### describe-pipeline execute YTsaurus Flow describe-pipeline command. Execute YTsaurus Flow describe command ```bash yt flow describe-pipeline [-h] [--pipeline-path PIPELINE_PATH] [--status-only] [--output-format OUTPUT_FORMAT] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--status-only` Describe only the pipeline status and its messages > `--output-format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ##### read-states read state rows matching the supplied filters. Read state rows matching the supplied filters ```bash yt flow read-states [-h] [--pipeline-path PIPELINE_PATH] [--computation-id COMPUTATION_ID] [--partition-id PARTITION_ID] [--key KEY] [--name NAME] [--target {all,key_state,partition_state}] [--limit LIMIT] [--output-format OUTPUT_FORMAT] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--computation-id` Filter by computation id; mutually exclusive with –partition-id > `--partition-id` Filter by partition id; mutually exclusive with –computation-id > `--key` TKey value as a YSON list ([v0, v1, …]) or a named map ({col: value, …}); requires –computation-id > `--name` Optional state name filter > `--target` Possible choices: all, key_state, partition_state > Which table(s) to read (default all) > `--limit` Upper bound on rows scanned per table, default: ``10`` > `--output-format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ##### delete-states delete state rows matching the supplied filters. Delete state rows matching the supplied filters; pipeline must be stopped; dry-run unless –commit ```bash yt flow delete-states [-h] [--pipeline-path PIPELINE_PATH] [--computation-id COMPUTATION_ID] [--partition-id PARTITION_ID] [--key KEY] [--name NAME] [--target {all,key_state,partition_state}] [--force] [--commit] [--output-format OUTPUT_FORMAT] [pipeline_path] ``` ###### Positional Arguments > `pipeline_path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) ###### Named Arguments > `--pipeline-path` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--computation-id` Filter by computation id; mutually exclusive with –partition-id > `--partition-id` Filter by partition id; mutually exclusive with –computation-id > `--key` TKey value as a YSON list or named map; requires –computation-id > `--name` Optional state name filter > `--target` Possible choices: all, key_state, partition_state > Which table(s) to delete from (default all) > `--force` Delete states even if the pipeline is only paused, not stopped > `--commit` Actually delete; without it the call only previews the affected rows > `--output-format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### generate-timestamp generates timestamp ```bash usage: yt generate-timestamp [-h] [--params PARAMS] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format ### get-features retrieves supported cluster features (data types, codecs etc.). Get cluster features (types, codecs etc.) ```bash usage: yt get-features [-h] [--params PARAMS] [--format FORMAT] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--format` response or input format: yson or json, for example: «yson». See also: [Formats](https://ytsaurus.tech/user-guide/storage/formats.md) ### jupyt Jupyter over YTsaurus commands ```bash usage: yt jupyt [-h] jupyter_command ... ``` #### Positional Arguments > `jupyter_command` Possible choices: ctl #### Sub-commands ##### ctl JUPYT controller ```bash yt jupyt ctl [--address ADDRESS] [--stage STAGE] [--cluster-name CLUSTER_NAME] command ... ``` ###### Positional Arguments > `command` ###### Named Arguments > `--address` controller service address; may contain {stage}, {family} and {host_suffix} parameters; the default value is fetched from the remote client config > `--stage` controller stage, default: ``'production'`` > `--cluster-name` cluster name under which the cluster is configured in the strawberry controller; the default value is fetched from the remote client config ### remove-maintenance removes maintenance requests from given node by id or filter ```bash usage: yt remove-maintenance [-h] [--params PARAMS] [-c COMPONENT] [-a ADDRESS] [--id ID] [--ids IDS] [-t TYPE] [-u USER] [--mine] [--all] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `-c, --component` > `-a, --address` Component address > `--id` > `--ids` structured ids in yson format > `-t, --type` Maintenance type > `-u, --user` > `--mine` > `--all` ### run-command-with-lock . Run command under lock ```bash usage: yt run-command-with-lock [-h] [--params PARAMS] [--shell] [--poll-period POLL_PERIOD] [--term-delay TERM_DELAY] [--kill-delay KILL_DELAY] [--conflict-exit-code CONFLICT_EXIT_CODE] [--set-address] [--address-path ADDRESS_PATH] [--recursive] path command [command ...] ``` #### Positional Arguments > `path` Path to the lock in Cypress > `command` Command to execute #### Named Arguments > `--params` specify additional params. structured params in yson format > `--shell` Run command in subshell > `--poll-period` Poll period for command process in seconds, default: ``1.0`` > `--term-delay` Delay before term signal in seconds, default: ``1.0`` > `--kill-delay` Delay before kill signal in seconds, default: ``1.0`` > `--conflict-exit-code` Exit code in case of lock conflict, default: ``1`` > `--set-address` Set address of current host (in lock attribute by default) > `--address-path` Path to set host address > `--recursive` Create lock path recursively ### run-compression-benchmarks Gets input table and recompresses it in all available codecs. For each codec prints compression ratio, cpu_write and cpu_read. ```bash usage: yt run-compression-benchmarks [-h] [--params PARAMS] [--table TABLE] [--all-codecs] [--sample-size SAMPLE_SIZE] [--format {json,csv}] [--max-operations MAX_OPERATIONS] [--time-limit-sec TIME_LIMIT_SEC] [table] ``` #### Positional Arguments > `table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--table` address in Cypress. See also: [YPATH](https://ytsaurus.tech/user-guide/storage/ypath.md) > `--all-codecs` benchmark every level of codecs with levels > `--sample-size` approximate table’s sample fragment size in bytes, default: ``1000000000`` > `--format` Possible choices: json, csv > output format, default: ``'json'`` > `--max-operations` max count of parallel operations, default: ``10`` > `--time-limit-sec` time limit for one operation in seconds, default: ``200`` ### show-default-config returns default configuration of python API ```bash usage: yt show-default-config [-h] [--params PARAMS] [--with-env-patch] [--with-remote-patch] [--only-remote-patch] ``` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--with-env-patch` with patch from environment variables > `--with-remote-patch` with patch from cluster > `--only-remote-patch` show only patch from cluster ### show-spec shows available spec options of the operation ```bash usage: yt show-spec [-h] [--params PARAMS] operation ``` #### Positional Arguments > `operation` operation type #### Named Arguments > `--params` specify additional params. structured params in yson format ### spark SPYT commands ```bash usage: yt spark [-h] spark_command ... ``` #### Positional Arguments > `spark_command` Possible choices: find-cluster, livy #### Sub-commands ##### find-cluster print Spark urls. Print URLs of running Spark cluster ```bash yt spark find-cluster [-h] [--discovery-path DISCOVERY_PATH] ``` ###### Named Arguments > `--discovery-path` Cypress path for discovery files and logs, the same path must be used in start-spark-cluster. SPARK_YT_DISCOVERY_PATH env variable is used by default ##### livy SPYT Livy commands ```bash yt spark livy [-h] livy_command ... ``` ###### Positional Arguments > `livy_command` Possible choices: ctl ###### Sub-commands ####### ctl LIVY controller ```bash yt spark livy ctl [--address ADDRESS] [--stage STAGE] [--cluster-name CLUSTER_NAME] command ... ``` ######## Positional Arguments > `command` ######## Named Arguments > `--address` controller service address; may contain {stage}, {family} and {host_suffix} parameters; the default value is fetched from the remote client config > `--stage` controller stage, default: ``'production'`` > `--cluster-name` cluster name under which the cluster is configured in the strawberry controller; the default value is fetched from the remote client config ### spyt SPYT commands ```bash usage: yt spyt [-h] spark_command ... ``` #### Positional Arguments > `spark_command` Possible choices: find-cluster, livy #### Sub-commands ##### find-cluster print Spark urls. Print URLs of running Spark cluster ```bash yt spyt find-cluster [-h] [--discovery-path DISCOVERY_PATH] ``` ###### Named Arguments > `--discovery-path` Cypress path for discovery files and logs, the same path must be used in start-spark-cluster. SPARK_YT_DISCOVERY_PATH env variable is used by default ##### livy SPYT Livy commands ```bash yt spyt livy [-h] livy_command ... ``` ###### Positional Arguments > `livy_command` Possible choices: ctl ###### Sub-commands ####### ctl LIVY controller ```bash yt spyt livy ctl [--address ADDRESS] [--stage STAGE] [--cluster-name CLUSTER_NAME] command ... ``` ######## Positional Arguments > `command` ######## Named Arguments > `--address` controller service address; may contain {stage}, {family} and {host_suffix} parameters; the default value is fetched from the remote client config > `--stage` controller stage, default: ``'production'`` > `--cluster-name` cluster name under which the cluster is configured in the strawberry controller; the default value is fetched from the remote client config ### transfer-account-resources transfers resources between accounts ```bash usage: yt transfer-account-resources [-h] [--params PARAMS] [--source-account SOURCE_ACCOUNT] [--destination-account DESTINATION_ACCOUNT] [--resource-delta RESOURCE_DELTA] [source_account] [destination_account] ``` #### Positional Arguments > `source_account` > `destination_account` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--source-account, --src` > `--destination-account, --dst` > `--resource-delta` structured resource-delta in yson format ### transfer-pool-resources transfers resources between pools ```bash usage: yt transfer-pool-resources [-h] [--params PARAMS] [--source-pool SOURCE_POOL] [--destination-pool DESTINATION_POOL] [--pool-tree POOL_TREE] [--resource-delta RESOURCE_DELTA] [source_pool] [destination_pool] [pool_tree] ``` #### Positional Arguments > `source_pool` > `destination_pool` > `pool_tree` #### Named Arguments > `--params` specify additional params. structured params in yson format > `--source-pool, --src` > `--destination-pool, --dst` > `--pool-tree` > `--resource-delta` structured resource-delta in yson format ### transform transforms source table to destination table writing data with given compression and erasure codecs ```bash usage: yt transform [-h] [--params PARAMS] [--src SOURCE_TABLE] [--dst DESTINATION_TABLE] [--erasure-codec ERASURE_CODEC] [--compression-codec COMPRESSION_CODEC] [--optimize-for OPTIMIZE_FOR] [--desired-chunk-size DESIRED_CHUNK_SIZE] [--check-codecs] [--spec SPEC] [source_table] [destination_table] ``` #### Positional Arguments > `source_table` source table > `destination_table` destination table (if not specified source table will be overwritten) #### Named Arguments > `--params` specify additional params. structured params in yson format > `--src` source table > `--dst` destination table (if not specified source table will be overwritten) > `--erasure-codec` desired erasure codec for table > `--compression-codec` desired compression codec for table > `--optimize-for` desired chunk format for table. Possible values: [«scan», «lookup»] > `--desired-chunk-size` desired chunk size in bytes > `--check-codecs` check if table already has proper codecs before transforming > `--spec` structured spec in yson format ## Python API {% note info "Note" %} Before you start, install the Python client from the pip repository using the command: ```bash pip install ytsaurus-client ``` {% endnote %} What becomes available after installing the package: - The Python yt library. - The CLI binary [yt](https://ytsaurus.tech/docs/cli/cli.md). Current package version requires **Python 3.8+** ### Installation { #install } #### YSON libraries To use the YSON format to work with tables, you need C++ bindings installed as a separate package. Installing [YSON bindings](https://ytsaurus.tech/docs/en/userdoc.md#yson_bindings): ```bash pip install ytsaurus-yson ``` {% note warning "Attention!" %} It is currently impossible to install YSON bindings on Windows. {% endnote %} {% note info "For Apple M1 platform users" %} There are currently no YSON bindings built for the Apple platform. You can use [Rosetta 2](https://en.wikipedia.org/wiki/Rosetta_(software)) as a temporary solution and install the Python version for the x86_64 architecture. Learn more [here](https://stackoverflow.com/questions/71691598/how-to-run-python-as-x86-with-rosetta2-on-arm-macos-machine). {% endnote %} To learn more about YSON, see [Formats](https://ytsaurus.tech/docs/en/userdoc.md#formats). To find out the version of the installed Python wrapper, print the `yt.VERSION` variable or call the `yt --version` command. If you encounter a problem, check the [FAQ](https://ytsaurus.tech/docs/en/#faq) section. If the problem persists, write to the [chat](https://t.me/ytsaurus_ru). [Library source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/yt/wrapper). {% note warning "Attention!" %} We do not recommend installing the library and its dependent packages in different ways at the same time. This can lead to problems that are difficult to diagnose. {% endnote %} #### Additional dependencies (extras) { #extras } Some features require the additional dependencies that are not installed by default. They are declared in the extras sets of the `ytsaurus-client` package and are installed by specifying the set in square brackets: | Set | Dependencies | Purpose | | --- | --- | --- | | `recommended` | `brotli`, `cryptography` | Recommended optional dependencies | | `admin` | `kubernetes`, `docker` | [CLI admin commands](https://ytsaurus.tech/admin-guide/cli-admin.md) (`yt admin logs k8s`, `yt admin metrics replay`) | ```bash pip install "ytsaurus-client[recommended]" pip install "ytsaurus-client[admin]" ``` ### User documentation { #userdoc } * [General](https://ytsaurus.tech/docs/en/userdoc.md#common) - [Agreements used in the code](https://ytsaurus.tech/docs/en/userdoc.md#agreements) - [Client](https://ytsaurus.tech/docs/en/userdoc.md#client) - [Thread safety](https://ytsaurus.tech/docs/en/userdoc.md#threadsafety) - [Asynchronous client based on gevent](https://ytsaurus.tech/docs/en/userdoc.md#gevent) - [Configuration](https://ytsaurus.tech/docs/en/userdoc.md#configuration) - [Shared config](https://ytsaurus.tech/docs/en/userdoc.md#configuration_common) - [Logging setup](https://ytsaurus.tech/docs/en/userdoc.md#configuration_logging) - [Token setup](https://ytsaurus.tech/docs/en/userdoc.md#configuration_token) - [Setting up retries](https://ytsaurus.tech/docs/en/userdoc.md#configuration_retries) - [Errors](https://ytsaurus.tech/docs/en/userdoc.md#errors) - [Formats](https://ytsaurus.tech/docs/en/userdoc.md#formats) - [YPath](https://ytsaurus.tech/docs/en/userdoc.md#ypath) * [Teams](https://ytsaurus.tech/docs/en/userdoc.md#commands) - [Working with Cypress](https://ytsaurus.tech/docs/en/userdoc.md#cypress_commands) - [Working with files](https://ytsaurus.tech/docs/en/userdoc.md#file_commands) - [Working with tables](https://ytsaurus.tech/docs/en/userdoc.md#table_commands) - [Data classes](https://ytsaurus.tech/docs/en/userdoc.md#dataclass) - [Schemas] { ../../../api/python/userdoc.md#table_schema } - [TablePath](https://ytsaurus.tech/docs/en/userdoc.md#tablepath_class) - [Teams](https://ytsaurus.tech/docs/en/userdoc.md#table_commands) - [Parallel table reading](https://ytsaurus.tech/docs/en/userdoc.md#parallel_read) - [Parallel table writing](https://ytsaurus.tech/docs/en/userdoc.md#parallel_write) - [Working with transactions and locks](https://ytsaurus.tech/docs/en/userdoc.md#transaction_commands) - [Running operations](https://ytsaurus.tech/docs/en/userdoc.md#run_operation_commands) - [SpecBuilder](https://ytsaurus.tech/docs/en/userdoc.md#spec_builder) - [Working with operations and jobs](https://ytsaurus.tech/docs/en/userdoc.md#operation_and_job_commands) - [Operation](https://ytsaurus.tech/docs/en/userdoc.md#operation_class) - [OperationsTracker](https://ytsaurus.tech/docs/en/userdoc.md#operations_tracker_class) - [OperationsTrackerPool](https://ytsaurus.tech/docs/en/userdoc.md#operations_tracker_pool_class) - [Working with access permissions](https://ytsaurus.tech/docs/en/userdoc.md#acl_commands) - [Working with dynamic tables](https://ytsaurus.tech/docs/en/userdoc.md#dyntables_commands) - [Other commands](https://ytsaurus.tech/docs/en/userdoc.md#etc_commands) * [Python objects as operations](https://ytsaurus.tech/docs/en/userdoc.md#python_operations) - [General information](https://ytsaurus.tech/docs/en/userdoc.md#python_operations_intro) - [Preparing an operation from a job](https://ytsaurus.tech/docs/en/userdoc.md#prepare_operation) - [Decorators](https://ytsaurus.tech/docs/en/userdoc.md#python_decorators) - [Pickling functions and environments](https://ytsaurus.tech/docs/en/userdoc.md#pickling) - [Running Python workloads in open-source environments](https://ytsaurus.tech/docs/en/userdoc.md#python_open_source_environment) - [Running the local script inside Docker with `respawn_in_docker`](https://ytsaurus.tech/docs/en/userdoc.md#respawn_in_docker) - [General structure](https://ytsaurus.tech/docs/en/userdoc.md#pickling_description) - [Link to a post with tips](https://ytsaurus.tech/docs/en/userdoc.md#pickling_advises) - [Porto layers](https://ytsaurus.tech/docs/en/userdoc.md#porto_layers) - [tmpfs in jobs](https://ytsaurus.tech/docs/en/userdoc.md#tmpfs_in_jobs) - [Statistics in jobs](https://ytsaurus.tech/docs/en/userdoc.md#python_jobs_statistics) * [Untyped Python operations](https://ytsaurus.tech/docs/en/userdoc.md#python_operations_untyped) - [Decorators](https://ytsaurus.tech/docs/en/userdoc.md#python_decorators_untyped) - [Formats](https://ytsaurus.tech/docs/en/userdoc.md#python_formats) - [Structured data representation](https://ytsaurus.tech/docs/en/userdoc.md#structured_data) - [Control attributes](https://ytsaurus.tech/docs/en/userdoc.md#control_attributes) - [Other formats](https://ytsaurus.tech/docs/en/userdoc.md#other_formats) * [Other](https://ytsaurus.tech/docs/en/userdoc.md#other) - [gRPC](https://ytsaurus.tech/docs/en/userdoc.md#grpc) - [YSON bindings](https://ytsaurus.tech/docs/en/userdoc.md#yson_bindings) * [Deprecated](https://ytsaurus.tech/docs/en/userdoc.md#legacy) - [Python3 and byte strings](https://ytsaurus.tech/docs/en/userdoc.md#python3_strings) ### Help { #pydoc } The most up-to-date help on specific functions and their parameters is in the code. To view a description of functions and classes in the interpreter, proceed as follows: ```bash python >>> import yt.wrapper as yt >>> help(yt.run_sort) ``` ### Examples { #examples } * [Basic level](https://ytsaurus.tech/docs/en/examples.md#base) - [Reading and writing tables](https://ytsaurus.tech/docs/en/examples.md#read_write) - [Table schemas](https://ytsaurus.tech/docs/en/examples.md#table_schema) - [Simple map](https://ytsaurus.tech/docs/en/examples.md#simple_map) - [Sorting a table and a simple reduce operation](https://ytsaurus.tech/docs/en/examples.md#sort_and_reduce) - [Reduce with multiple input tables](https://ytsaurus.tech/docs/en/examples.md#reduce_multiple_output) - [Reduce with multiple input and output tables](https://ytsaurus.tech/docs/en/examples.md#reduce_multiple_input_output) - [mapreduce](https://ytsaurus.tech/docs/en/examples.md#map_reduce) - [MapReduce with multiple intermediate tables](https://ytsaurus.tech/docs/en/examples.md#map_reduce_multiple_intermediate_streams) - [Decorators for job classes](https://ytsaurus.tech/docs/en/examples.md#job_decorators) - [Working with files on the client and in operations](https://ytsaurus.tech/docs/en/examples.md#files) - [Grep](https://ytsaurus.tech/docs/en/examples.md#grep) * [Advanced level](https://ytsaurus.tech/docs/en/examples.md#advanced) - [Batch queries](https://ytsaurus.tech/docs/en/examples.md#batch_queries) - [RPC](https://ytsaurus.tech/docs/en/examples.md#rpc) * [Miscellaneous](https://ytsaurus.tech/docs/en/examples.md#misc) - [Data classes](https://ytsaurus.tech/docs/en/examples.md#dataclass) - [Context and managing writes to output tables](https://ytsaurus.tech/docs/en/examples.md#table_switches) - [Spec builders](https://ytsaurus.tech/docs/en/examples.md#spec_builder) - [Using gevent](https://ytsaurus.tech/docs/en/examples.md#gevent) * [Untyped API](https://ytsaurus.tech/docs/en/examples.md#untyped_tutorial) - [Reading and writing tables](https://ytsaurus.tech/docs/en/examples.md#read_write_untyped) - [Simple map](https://ytsaurus.tech/docs/en/examples.md#simple_map_untyped) - [Sorting a table and a simple reduce operation](https://ytsaurus.tech/docs/en/examples.md#sort_and_reduce_untyped) - [Reduce with multiple input tables](https://ytsaurus.tech/docs/en/examples.md#reduce_multiple_output_untyped) - [Reduce with multiple input and output tables](https://ytsaurus.tech/docs/en/examples.md#reduce_multiple_input_output_untyped) - [MapReduce operation](https://ytsaurus.tech/docs/en/examples.md#map_reduce_untyped) - [Decorators for job classes and functions](https://ytsaurus.tech/docs/en/examples.md#job_decorators_untyped) - [Table switches and context](https://ytsaurus.tech/docs/en/examples.md#table_switches_untyped) - [Working with strings in Python3](https://ytsaurus.tech/docs/en/examples.md#yson_string_proxy) ### FAQ { #faq } This section contains answers to a number of frequently asked questions about the Python API. Answers to other frequently asked questions are in the [FAQ](https://ytsaurus.tech/faq/index.md) section. **Q: I installed the package via pypi, but I get the `yt: command not found` error.** A: Try running the `pip install ytsaurus-client --force-reinstall` command , the log will most likely display a warning like `The script yt is installed in '...' which isn't on your PATH`. To solve the problem, you need to add the specified path to the PATH environment variable. To do this, run the following command: ``` echo 'export PATH="$PATH:"' >> ~/.bashrc source ~/.bashrc ``` Depending on the shell, the file may have a different name. The most common name on Mac is `~/.zshrc`. **Q: Reading with retry ends with an error because of timeout.** A: Most likely there are too many chunks in the table, you need to enlarge them. Use `yt merge --src table --dst table --spec "{combine_chunks=true}"` **Q: The operation ends with a YSON error (for example: `YsonError: Premature end of stream`) and the web interface displays a YSON parsing error.** A: The operation most likely writes to `stdout`. This is prohibited from being done explicitly in Python via `print, sys.stdout.write()` if the operation is not marked as `raw_io`, but it can be done by a third-party program, such as an archiver. **Q: The Python library writes too much to stderr, how do I increase the level of logging?** A: You can increase the level by setting the `YT_LOG_LEVEL="ERROR"` environment variable or by setting up the YTsaurus logger: `logging.getLogger("Yt").setLevel(logging.ERROR)`. **Q: I start an operation on Mac OS X, but jobs end with errors like `ImportError: ./tmpfs/modules/_ctypes.so: invalid ELF header`.** A: Since the Python wrapper takes all Python operation dependencies with it to the cluster, binary .so and .pyc files arrive there too, which then cannot be loaded. Use a porto layer with your local environment and enable filtering of these files so that they do not end up on the cluster. For more information, see the [section](https://ytsaurus.tech/docs/en/userdoc.md#porto_layers). **Q: Jobs end with the `Invalid table index N: expected integer in range [A,B]` error.** A: The message means that you output a table index in the records and there is no corresponding table. This most often means that you have several input tables and one output table. The `@table_index` fields appear in the input records by default. To disable them, you can change the format: `yt.config["tabular_data_format"] = yt.YsonFormat(process_table_index=None)`. To learn more about the format, see the [section](https://ytsaurus.tech/docs/en/userdoc.md#python_formats). As an alternative, explicitly indicate in the specification (example for a map operation): `{"mapper": {"enable_input_table_index": False}}`. **Q: The (ReadTimeout, HTTPConnectionPool(....): Read timed out.) error appears after the operation is completed.** The message means that the operation stderr could not be downloaded due to network problems and even repeated queries didn't help. In that case, you should use the `ignore_stderr_if_download_failed` option which enables you to ignore stderr if you can't download it. We recommend using this option when writing production processes. **Q: I get the `Yson bindings required` error.** This means that YSON was selected as the input (output) format and bindings could not be imported in the job. To learn more about YSON and bindings, see the [section](https://ytsaurus.tech/docs/en/userdoc.md#yson). You need to install the bindings package and check that YSON bindings are not filtered out using `module_filter`. This is a dynamic yson_lib.so library that can easily be accidentally filtered out when filtering out all .so files. In addition, so that `yt_yson_bindings` that came in modules are not deleted, write `config["pickling"]["ignore_yson_bindings_for_incompatible_platforms"] = False` in the configuration file. ## General information { #common } ### Conventions { #agreements } Options of system commands have the default value of `None`. This means that the option value won't be transmitted at the command execution, and the system will use the default value. For the `format` option, the default value of `None` usually has another meaning. The command must return a parsed Python structure or expect a Python structure as input. If the `format` option isn't specified, the command must return an unparsed result or expect unparsed input. All timeouts or time periods are accepted in milliseconds by default. In some cases, `datetime.timedelta` can be used as time interchangeably with milliseconds. All the functions have the `client` object as the last option. This behavior is implementation-specific. We do not recommend using this feature on your own. For brevity, in the below examples, we assume that `import yt.wrapper as yt` has already been performed. ### Client and global client { #client } The functions and clauses are available from the yt.wrapper global environment of the module library and can change its global status. For example, they save the current transaction there. By changing yt.config, you're also changing the global configuration. If you want to have an option to work from several independent (that is, differently configured) clients, use the [YtClient](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient) class. This class provides almost all the functions from the [yt.wrapper](https://pydoc.ytsaurus.tech/yt.wrapper.html) module: you can call `client.run_map`, `client.read_table_structured`, `with client.Transaction()`, and so on. Note that the `YT_PROXY`, `YT_TOKEN`, and other environment variables only set the configuration of the global client. That is, they affect only [yt.wrapper.config](https://pydoc.ytsaurus.tech/yt.wrapper.html#module-yt.wrapper.config), but not the configuration of explicitly created `YtClient` instances. ```python from yt.wrapper import YtClient client = YtClient(proxy=, config={"backend": "rpc"}) print(client.list("/")) ``` {% note warning "Attention" %} Note that the library is not thread-safe: in order to work from different threads, you need to create a client instance on each thread. {% endnote %} #### Command parameters { #commandparams } You can transmit a specified set of parameters to all the requests set up through the client (for example, `trace_id`). To do this, you can use the special [create_client_with_command_params](https://pydoc.ytsaurus.tech/yt.wrapper.html?highlight=create_client_with#yt.wrapper.client.create_client_with_command_params) method that enables you to specify an arbitrary set of options to be transmitted to all the API calls. #### Thread safety { #threadsafety } This tip remains valid if you are using multiprocessing, even if you aren't using threading. One of the reasons is that if you have already set up a request for the cluster in your main process, a connection pool to this cluster has already been initiated within the global client. So, when the processes fork, they're going to use the same sockets when communicating with the cluster. This might result in various issues. {% note warning "Attention" %} When using a native driver, use fork + exec (via `Popen`), avoiding even `multiprocessing`, because the driver service threads aren't preserved at fork, and the driver becomes incapable of executing requests. You should also avoid overusing clients after forks, because they might have a non-trivial state that poorly survives forks. {% endnote %} #### Asynchronous client based on gevent { #gevent } If `gevent` is present in your code, you can use `yt.wrapper` in the asynchronous mode. This functionality hasn't been covered by tests yet, but should be operable in simple scenarios. Key points: - Before getting started, run monkey patching to replace blocking network calls. - Create an independent [YtClient](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient) inside each [greenlet](https://pypi.org/project/greenlet/). This is important. If you neglect this point, you will get errors. You can find an example in this [section](https://ytsaurus.tech/docs/en/examples.md#gevent). ### Configuring { #configuration } The library supports rich configuration options determining its behavior in different scenarios. For example, you can change the path in Cypress where your temporary tables will be created by default: `yt.config["remote_temp_tables_directory"] = "//home/my_home_dir/tmp"`. To learn about options and their detailed descriptions, see the [code](http://pydoc.ytsaurus.tech/_modules/yt/wrapper/default_config.html). The library is configured via one of the following methods: - Updating the `yt.config` object: `yt.config["proxy"]["url"] = ""`. - Calling the function `yt.update_config`: `yt.update_config({"proxy": {"url": ""}})`. - Setting an environment variable: `export YT_CONFIG_PATCHES='{url=""}'`; - Using the file whose path is stored in the `YT_CONFIG_PATH` environment variable, the default value is `~/.yt/config`. The file should be in the [YSON](https://ytsaurus.tech/user-guide/storage/formats.md#yson) format (using the `YT_CONFIG_FORMAT` environment variable, you can change this behavior; the YSON and JSON formats are supported). Sample file contents: `{proxy={url=""}}`. You can change some configuration options using environment variables. Such variables are: `YT_TOKEN`, `YT_PROXY`, `YT_PREFIX`. And also options for setting up the logging options (that are separate from the config): `YT_LOG_LEVEL` and `YT_LOG_PATTERN`. When using [CLI](https://ytsaurus.tech/docs/cli/cli.md), you can pass the configuration patch using the `--config` option. Some options can be specified in the [cluster client config](https://ytsaurus.tech/docs/en/#remote_client_config). Note that the library configuration doesn't affect the client configuration: by default, a config with the default values will be used when you're creating a client. To pass a config based on environment variables to the client: `client = yt.YtClient(..., config=yt.default_config.get_config_from_env())`. You can also update an existing config with values from the environment variables using the `update_config_from_env(config)` function. Keep an eye on the priority order. When you import the library, the configurations transmitted through `YT_CONFIG_PATCHES` are applied. This environment variable expects list_fragment: you can pass multiple configurations separated by a semicolon. These patches are applied last-to-first. Then, the values of the options specified by specific environment variables are applied. For example, this can be done using `YT_PROXY`. Only after that, the configurations explicitly specified in the code (or passed in the `--config` option) are applied. When the config is applied, all the dict nodes are merged rather than overwritten. Setting up a global configuration to work with a cluster in your home directory. ```python yt.config["prefix"] = "//home/username/" yt.config["proxy"]["url"] = "cluster-name" ``` Creating a client that will create more and more new tables with the `brotli_3` codec. ```python client = yt.YtClient(proxy=, config={"create_table_attributes": {"compression_codec": "brotli_3"}}) ``` Setting up a global client to collect the archives of dependencies in Python operations using a local directory that is different from `/tmp`. Then creating a client with an additional max_row_weight setting of 128 MB. ```python my_config = yt.default_config.get_config_from_env() my_config["local_temp_directory"] = "/home/ignat/tmp" my_config["table_writer"] = {"max_row_weight": 128 * 1024 * 1024} client = yt.YtClient(config=my_config) ``` #### Shared config { #configuration_common } #### Profiles { #configuration_profiles } In the configuration file, you can specify multiple profiles for working with YTsaurus. A profile is a set of settings with a common name. Profiles can be hepful: 1. In different usage scenarios of a single cluster (e.g., you can have a profile for lengthy processes and a profile for quick one-off operations). 2. To specify default settings for working with different clusters. 3. To give a short and fitting name to a cluster. To use profiles, enable the second format version: `config_version=2`. A configuration file in the second version has the following structure: * `profiles`: A dict where a key is a profile name and a value is a profile configuration in standard [format](https://ytsaurus.tech/docs/en/userdoc.md#configuration). * `default_profile`: A string that contains the name of the default profile. * `config_version`: A number, version of the configuration file format. Profiles are supported only in the second version. By default, the first version is used. Example of a configuration with profiles in YSON format: ```yson { "default_profile"="dev"; "profiles"={ "main"={ "token_path"="/etc/token"; "proxy"={ "url"="cluster-url"; }; "prefix"="//main"; }; "dev"={ "token_path"="/etc/token-dev"; "proxy"={ "url"="cluster-url"; }; "prefix"="//dev"; "pool"="dev"; }; "external"={ "token_path"="/etc/token-external"; "proxy"={ "url"="external-url"; }; }; }; "config_version"=2; } ``` You can specify a profile in several ways: 1. Via the `YT_CONFIG_PROFILE` environment variable. 2. In the python sdk `client = yt.YtClient(..., config=yt.default_config.get_config_from_env(profile="my_profile"))`. 3. The `default_profile` key value is used by default. #### Logging setup { #configuration_logging } This is how logging works in ytsaurus-client and all the tools that use this library. There is a special logger implemented in the `yt.logger` module both as a global `LOGGER` variable and as aliases that enable logging at the module level. To change the logging settings, update the `LOGGER`. The initial logger setup (when loading the module) is regulated by the `YT_LOG_LEVEL`, `YT_LOG_PATTERN`, and `YT_LOG_PATH` environment variables. The `YT_LOG_LEVEL` variable regulates the logging level and accepts one of the following values: `DEBUG`, `INFO`, `WARNING`, `ERROR`. The `YT_LOG_PATTERN` variable regulates the formatting of log messages and accepts the logger's format string. For more information, see the Python [documentation](https://docs.python.org/3/library/logging.html#logging.Formatter). You can use the `YT_LOG_PATH` environment variable to specify the path to the log file. By default, the logging level is INFO, and log messages are written to stderr. #### Token setup { #configuration_token } You can retrieve the token from the following places (listed in priority order). 1. From the `config["token"]`option that gets to the global configuration from the `YT_TOKEN` environment variable. 2. From the file specified in `config["token_path"]`; the default value of this option is `~/.yt/token`. This option can also be overridden using the `YT_TOKEN_PATH` environment variable (the environment variable only applies to the global client configuration; for details, see the [ section](https://ytsaurus.tech/docs/en/#configuration)). #### Setting up configuration retries { #configuration_retries } Commands in YTsaurus are classified into light and heavy (to get the system's view on the list of commands, see [Commands](https://ytsaurus.tech/docs/commands.md)). Light commands are commands like `create, remove, set, list, get, ping`, and other similar ones. Before each request, the client also accesses a special proxy to get a list of commands that it supports. We recommend retrying such commands when errors or timeouts occur. You can set up the retry parameters in the `proxy/retries` configuration section. There are two categories of heavy requests. - Reading from and writing to tables or files (that is, read_table, write_table, read_blob_table, and other requests). The retries for such commands are set up in the `read_retries` and `write_retries` sections (for read requests and write requests, respectively). - Reading from and writing to dynamic tables (that is, with heavy queries, such a select_rows, lookup_rows, insert_rows, and others). For such commands, the retries are set up individually in the `dynamic_table_retries` section (the regular options read_retries and write_retries won't work because when reading static tables, 60-second intervals between retries are considered normal, but that's inappropriate for dynamic tables where low latency is critical). The above-mentioned retries affect the processes of data uploads or exports from the cluster in the case of network faults, chunk unavailability, and other issues. Below is a description of actions that are initiated inside the cluster (running a MapReduce operation or a batch request). 1. The `start_operation_retries` section regulates the retries of the operation start command, that is, instead of dealing with network issues, here we handle the errors like "concurrent operation limit exceeded," when many operations are running and the scheduler won't start new ones. In case of such errors, the client makes retries with big sleep intervals to allow enough time for some operations to complete. 2. The `concatenate_retries` section regulates the retries of the concatenate command (see [API](https://ytsaurus.tech/docs/en/start.md)). This isn't a light command because it may access different master cells and spend a long time there. That's why you can't use the retry settings for light commands in this case. 3. The `batch_requests_retries` section regulates the retries made from inside a batch request (see the description of the `execute_batch`) command. The client retries the requests that failed with such errors as the "request rate limit exceeded". That is, the client sends a batch of requests, of which some have completed and some have failed with the "request queue size per user exceeded" errors. In this case, the requests are delivered again with a new batch. This section regulates the policy of such retries. ### Errors { #errors } [YtError](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtError): The parent class of all the errors in the library. It has the following fields: - `code` (int type): HTTP error code (see the [section](https://ytsaurus.tech/docs/error-codes.md)). If omitted, it's equal to 1. - `inner_errors` (the list type): Errors that preceded the given error when executing the request (the proxy accessed the driver, an error occurred inside the driver: this error will be added to `inner_errors`). - `attributes` (the dict type): Attributes of a generic error (for example, the request date). The following errors describe more specific issues. [YtOperationFailedError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtOperationFailedError): The operation has failed. This error stores the following information in the `attributes` field. - `id`: Operation ID. - `url`: Operation URL. - `stderrs`: List of dicts with details about the jobs that failed or jobs with stderr. This dict has the `host` field; it can also have the `stderr` and `error` fields, depending on whether the job had a stderr or had failed. - `state`: Operation status (for example, `failed`). To print the stderr output when an operation fails, you need to process the exception and explicitly print error messages. Otherwise, you will see them truncated in backtrace. You can use the non-public decorator [add_failed_operation_stderrs_to_error_message](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.add_failed_operation_stderrs_to_error_message) (though it might be renamed in the future) that intercepts the [YtOperationFailedError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtOperationFailedError) exception and enriches its message about the stderr error. [YtResponseError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtResponseError): The command (that is, the request to YTsaurus) has failed. This class has the `error` field that stores the structured response describing the error cause. It provides the following useful methods: - [is_resolve_error](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_resolve_error) (for example, it returns`True` on an attempt to commit a non-existing transaction or request a non-existing node). - [is_access_denied](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_access_denied); - [is_concurrent_transaction_lock_conflict](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_concurrent_transaction_lock_conflict); - [is_request_queue_size_limit_exceeded](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_request_queue_size_limit_exceeded); - [is_chunk_unavailable](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_chunk_unavailable); - [is_request_timed_out](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_request_timed_out); - [is_concurrent_operations_limit_reached](https://pydoc.ytsaurus.tech/yt.html#yt.common.YtResponseError.is_concurrent_operations_limit_reached). Other subclasses of `YtError`: - [YtHttpResponseError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtHttpResponseError): Subclass of [YtResponseError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtResponseError), it includes the `headers` field. - [YtProxyUnavailable](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtProxyUnavailable): The proxy is currently unavailable - for example, it's overwhelmed with requests. - [YtTokenError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtTokenError): An invalid token has been passed. - [YtFormatError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtFormatError): The format is invalid. - [YtIncorrectResponse](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtIncorrectResponse): An invalid response has been received from the proxy (for example, JSON is invalid). - [YtTimeoutError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtTimeoutError): The operation has been terminated by a timeout. You can look up a full list of subclasses in the code. ### Formats { #formats } To learn more about the formats, see the [section](https://ytsaurus.tech/user-guide/storage/formats.md). There is a separate class for each format: - [YsonFormat](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.YsonFormat); - [JsonFormat](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.JsonFormat); - [DsvFormat](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.DsvFormat); - [SchemafulDsvFormat](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.SchemafulDsvFormat); - [YamrFormat](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.YamrFormat). In the constructor, these classes accept the parameters that are specific for the given formats. If the format option isn't explicitly available in the constructor, you can pass it in the `attributes` option that accepts a `dict`. Moreover, each class has a set of methods for serialization or deserialization of records from the stream: - [load_row](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.Format.load_row): Reads a single row from the stream. Some formats (for example, Yson) do not support loading a single record. That's why, when you use `load_rows` on them, they will throw an exception. - [load_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.Format.load_rows): Reads all the records from the stream, processes the table switches, and returns a record iterator. If `raw==True`, the method returns rows without parsing them. This method is a generator. - [dump_row](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.Format.dumps_row): Writes a single record to the stream. - [dump_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.Format.dumps_rows): Writes a set of records to the stream. It adds table switches to the output stream. There is also the [create_format](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.create_format) function that creates a format based on the specified name and a given set of attributes. The format name can also include attributes in YSON format: you can create a format as follows: `create_format("schemaful_dsv")`. You can use the [create_table_switch](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.create_table_switch) function to create a record that functions as a switch to the table with the specified index. The functions `read_table, write_table, select_rows, lookup_rows, insert_rows, delete_rows` have the `raw` parameter that enables you to get/send records in an unparsed format (as strings). If you set `raw` to `False`, this means that the records will be deserialized to `dict` at reading and serialized from `dict` at writing. Example: ```python yt.write_table("//home/table", [b"x=value\n"], format=yt.DsvFormat(), raw=True) assert list(yt.read_table("//home/table", format=yt.DsvFormat(), raw=False)) == [{"x": "value"}] # Ok ``` A similar option exists for operations. By default, this operation deserializes records from the format specified at operation startup. If you want the operation to accept unparsed strings as input, use the `raw` decorator and specify the format when running the operation: ```python @yt.raw def mapper(rec): ... yt.run_map(...., format=yt.JsonFormat()) ``` {% note warning "Attention" %} To use YSON format to work with tables (operations, table read/write), additionally install the package with YSON C++ bindings (see [yson bindings](https://ytsaurus.tech/docs/en/#yson_bindings)). Otherwise, you will see the "Yson bindings required" exception. {% endnote %} Specifics of using JSON format: the JSON module is written in Python, that's why it's very slow. The library tries to use different modules that support bindings written in faster languages such as ujson. To enable it, use the [enable_ujson](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.format.JsonFormat) parameter: `JsonFormat(..., enable_ujson=True)`. The ujson package is disabled by default because in certain cases its default behavior is faulty: ```python import ujson s = '{"a": 48538100000000000000.0}' ujson.loads(s) {u'a': 1.1644611852580897e+19} ``` If you have read the ujson documentation, and you are sure you that your input data will be deserialized correctly, then parse using this module. ### YPath { #ypath } Paths in YTsaurus are represented by [YPath](https://ytsaurus.tech/user-guide/storage/ypath.md). In code, this is enabled by the [YPath](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.YPath) class and its subclasses, [TablePath](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.TablePath) and [FilePath](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.FilePath). Among the constructors of the last two classes, you can specify relevant YPath attributes, for example, `schema`, `start_index`, `end_index` (for `TablePath`) and `append` and `executable` (for `FilePath`). To learn more about `TablePath`, see the [section](https://ytsaurus.tech/docs/en/#tablepath_class). Use these classes for working with paths in your code instead of formatting YPath literals manually. There are other useful functions in [YPath](https://pydoc.ytsaurus.tech/yt.wrapper.html#module-yt.wrapper.ypath) modules, here are some of them: - [ypath_join](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.ypath_join) joins several paths into one path (a counterpart of `os.path.join`). - [ypath_split](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.ypath_split): Returns the `(head, tail)` pair, where `tail` is the trailing path component and`head` is the remaining path (a counterpart of `os.path.split`). - [ypath_dirname](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.ypath_dirname): Returns the path without the trailing component (a counterpart of `os.path.dirname`). - [escape_ypath_literal](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.escape_ypath_literal): Escapes the string so that it can be used as a YPath component. ### Python3 and byte strings { #python3_strings } In YTsaurus, all the strings are byte strings. However, in Python, regular strings are Unicode-based. That's why the following feature for structured data (dicts) has been introduced in Python 3. When writing to and reading from tables in `raw` mode, the library uses binary strings both for input and output. In non-`raw` mode, the following logic is enabled. When read, strings are automatically utf8-encoded, including dict keys (to select a different encoding, use the `encoding` parameter in the format, see below). If a byte string is read, a special [YsonStringProxy](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.yson_types.YsonStringProxy) object is returned. If you try to treat such an object as a string, you will get a typical error: [NotUnicodeError](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.yson_types.NotUnicodeError). You can call two functions on such an object: [yt.yson.is_unicode](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.yson_types.is_unicode) and [yt.yson.get_bytes](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.yson_types.get_bytes). The first function will return `False` for `YsonStringProxy`, and the second function will return raw bytes that couldn't be decoded. You can call the same functions on regular strings, and for them `is_unicode` will return `True` as expected. When calling `get_bytes` on regular strings, as the second argument, you can specify the encoding (`utf-8` by default) and get `s.encode(encoding)`. Here's the assumed scenario for mixed, regular, and byte structures when reading tables or running operations: ```python ## if you simply need bytes b = yt.yson.get_bytes(s) ## if you need to do respond differently to byte strings and regular strings if yt.yson.is_unicode(s): # Process a regular string else: b = yt.yson.get_bytes(s) # Process a byte string ``` When writing data, you can both leave the `YsonStringProxy` object as it is (it will be automatically converted to a byte string) or return a byte string or Unicode string. Unicode strings will be encoded in UTF-8 (or other encoding). **Note that** you can't mix `bytes` and `str` in dict keys. When `encoding != None`, the only way to specify a byte key is to use the [yt.yson.make_byte_key](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.yson_types.make_byte_key) function. The reason is that in Python 3, the strings `"a"` and `b"a"` are not equal. It is unacceptable for a dict in the format `{"a": 1, b"a": 1}` to be sent to the system implicitly, converted to a string with two identical `a` keys. If needed, you can disable the decoding logic or select another encoding. Use the `encoding` parameter when creating a format in this case. If the `encoding` parameter is specified and is `None`, the library works with the records where all the rows are expected to be binary rows (both keys and values, both on write and on read). If you attempt to serialize a dict that includes Unicode strings with `encoding=None`, in most cases, you'll see an error. If the `encoding` parameter is specified and is not `None`, the library handles Unicode strings, but expects the specified encoding from all data (instead of UTF-8 used by default). An example of use with comments is [in a separate section](https://ytsaurus.tech/docs/en/examples.md#yson_string_proxy). ### Batch requests Low-level API: - [execute_batch](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.execute_batch): Accepts a set of request descriptions in the list format and returns a set of results. A simple wrapper for the [API command](https://ytsaurus.tech/docs/commands.md#execute_batch). High level API: - [create_batch_client](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.batch_helpers.create_batch_client): Creates a batch client. This function is available both on the client side and globally. The batch client has as its methods all the API functions supported in the batch requests. - [batch_apply](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.batch_apply): This method enables you to apply a function to a dataset, in batch mode. Returns a list of results. Example: ```python client = yt.YtClient("cluster-name") batch_client = client.create_batch_client() list_rsp = batch_client.list("/") exists_rsp = batch_client.exists("/") batch_client.commit_batch() print(list_rsp.get_result()) print(exists_rsp.get_result()) ``` For more information about batch requests, see the [tutorial](https://ytsaurus.tech/user-guide/storage/batch-requests.md). Specifics of working with the batch client: - The [create_batch_client](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.create_batch_client) method and the client configuration have the `max_batch_size` parameter with a default value of 100. When the [commit_batch](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.batch_client.BatchClient.commit_batch) method is called on the client side, the requests are broken down into parts, `max_batch_size` each, and are executed in a step-by-step manner. This is because of natural restrictions on the request size. - All the requests are sent with the client transaction from which the batch-client has been constructed. If the client is used inside a transaction, all its requests are executed in the context of this transaction. The batch client behaves similarly. - By default, you can handle all the errors that arise in requests by looking at [BatchResponse](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.batch_response.BatchResponse) returned by the batch client methods. Example: ```python client = yt.YtClient("cluster-name") batch_client = client.create_batch_client() list_rsp = batch_client.list("//some/node") if list_rsp.is_ok(): # ... else: error = yt.YtResponseError(list_rsp.get_error()) if error.is_resolve_error(): # Handle the situation when the node for which the list has been created doesn't exist # ... ``` The [create_batch_client](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.create_batch_client) method has the `raise_errors` parameter that you can set to `True`. Then, if at least one request fails, the [YtBatchRequestFailedError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.batch_execution.YtBatchRequestFailedError) exception will be thrown with all the errors. Example: ```python client = yt.YtClient("cluster-name") batch_client = client.create_batch_client(raise_errors=True) list_rsp = batch_client.list("//some/node") try: batch_client.commit_batch() except yt.YtBatchRequestFailedError as err: # Print the error message print err.inner_errors[0]["message"] # "Error resolving path //some/node" ``` ## Commands { #commands } The yt library allows running the commands in the system using the Python API. The public part of the library includes only the methods that are in `yt/wrapper/__init__.py` and `yt/yson/__init__.py`. Some command options are shared by command classes. For more information, see [Commands](https://ytsaurus.tech/docs/commands.md). ### Working with Cypress { #cypress_commands } - [get](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.get): Get the value of the Cypress node. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#get). - [set](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.set): Write a value to the Cypress node. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#set). - [create](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.create): Create an empty Cypress node of the `type` and with `attributes`. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#create). - [exists](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.exists): Check whether the Cypress node exists. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#exists). - [remove](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.remove): Delete the Cypress node. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#remove). - [list](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.list): Get a list of children for the `path` node. The `absolute` option enables the output of absolute paths instead of relative paths. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#list). Examples: ```python yt.create("table", "//home/table", attributes={"mykey": "myvalue"}) # Output: yt.get("//home/table/@mykey") # Output: "myvalue" yt.create("map_node", "//home/dir") # Output: yt.exists("//home/dir") # Output: True yt.set("//home/string_node", "abcde") yt.get("//home/string_node") # Output: "abcde" yt.get("//home/string_node", format="json") # Output: '"abcde"' yt.set("//home/string_node/@mykey", "value") # Set the attribute yt.get("//home/string_node/@mykey") # Output: "value" ## Create a document and use it as a dictonary yt.create("document", "//home/doc") yt.set("//home/doc", {"number": 7, "vegetable": "tomato"}) yt.get("//home/doc") # Output: [7L, "tomato"] yt.set("//home/doc/vegetable", "cabbage") yt.get("//home/doc") # Output: [7L, "cabbage"] ## Create a document and append number and a string to it yt.create("document", "//home/doc-list") yt.set("//home/doc-list", []) yt.set("//home/doc-list/end", 7) yt.set("//home/doc-list/end", "cabbage") yt.get("//home/doc-list") # Output: [7L, "cabbage"] ``` - [copy](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.copy) and [move](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.move): Copy/move the Cypress node. To learn more about the option value, see [section](https://ytsaurus.tech/user-guide/storage/cypress-example.md#copy_move). - [link](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.link): create a symlink to a Cypress node. [Read more](https://ytsaurus.tech/user-guide/storage/cypress-example.md#link). To learn where the symlink points, read the value from the `@path` attribute. To access a `link` object, add `&` at the end of the path. Examples: ```python yt.create("table", "//home/table") yt.copy("//home/table", "//tmp/test/path/table") ## error yt.copy("//home/table", "//tmp/test/path/table", recursive=True) yt.get("//home/table/@account") ## Output: sys yt.get("//tmp/test/path/table/@account") ## Output: tmp yt.move("//home/table", "//tmp/test/path/table") ## error yt.move("//home/table", "//tmp/test/path/table", force=True) yt.get("//tmp/test/path/table/@account") ## Output: sys yt.link("//tmp/test/path/table", "//home/table") yt.get("//home/table/@path") ##Output: "/tmp/test/path/table" ``` An alias function for creating a directory. - [mkdir](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.mkdir): Creates a directory, that is, a node of the `map_node` type. {% cut "Alias functions for working with attributes" %} - [get_attribute](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.get_attribute) - [has_attribute](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.has_attribute) - [set_attribute](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.set_attribute) - [remove_attribute](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.remove_attribute) Note that these functions do not support access to nested attributes by design. To access nested attributes, use regular Cypress verbs and navigation using [YPath](https://ytsaurus.tech/user-guide/storage/ypath.md). {% endcut %} Merging files/tables: - [concatenate](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.concatenate): Merges chunks from tables or files. Other commands: - [find_free_subpath](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.find_free_subpath): Searches a free node whose path begins with `path`. - [search](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.search): Recursively traverses a subtree growing from the root node. By default, it outputs the absolute paths of all the nodes of the subtree. There's also a number of filters that allow you to select specific records. The `attributes` option specifies a list of attributes that must be retrieved with each node. The retrieved attributes are available in the `.attributes` field on the returned paths. Example: ```python for table in yt.search( "//home", node_type=["table"], object_filter=lambda obj: obj.attributes.get("account") == "dev", attributes=["account"], ): print(table) ``` ### Working with files { #file_commands } For more information about files in Cypress, see the [section](https://ytsaurus.tech/user-guide/storage/files.md). - [read_file](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.read_file) Read a file from Cypress to a local machine. Returns the [ResponseStream](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.response_stream.ResponseStream) object, which is a line iterator that has the following additional methods: - [read](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.response_stream.ResponseStream.read): Read `length` bytes from the stream. If `length==None`, read the data to the end of the stream. - [readline](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.response_stream.ResponseStream.readline): Read a line (including "\n"). - [chunk_iter](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.response_stream.ResponseStream.chunk_iter): An iterator by response chunks. The command supports retries (enabled by default). To enable/disable or increase the number of retries, use the `read_retries` configuration option (see `read_table`). To enable read retries, use the `YT_RETRY_READ=1` variable. - [write_file](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.write_file) Write a file to Cypress. The command accepts a stream from which it reads data. The command supports retries. To set up retries, use the `write_retries` configuration option (more in [write_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.write_table)). There is the `is_stream_compressed` parameter that says that the stream data has already been compressed, and you can transmit it without compressing. - Files can be transmitted as arguments of an operation. In that case, they are written to the root of the directory where your jobs will be run. For more information, see the [section](https://ytsaurus.tech/docs/en/#run_operation_commands) and the [example](https://ytsaurus.tech/docs/en/examples.md#files). - [get_file_from_cache](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.get_file_from_cache) Returns a path to the cached file based on the specified md5 sum. - [put_file_to_cache](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.put_file_to_cache) Upload the file existing at the given Cypress path to the cache. Note that the file should be uploaded to Cypress with a special option that enables md5 calculation. ### Working with tables { #table_commands } For more information about tables, see [Static tables](https://ytsaurus.tech/user-guide/storage/static-tables.md). #### Data classes { #dataclass } The main method used to represent table rows is to use classes with fields annotated by types (similar to [dataclasses](https://docs.python.org/3/library/dataclasses.html)). With this approach, you can effectively (de)serialize data, avoid errors, and work with complex types (structures, lists, and others) more conveniently. To define a data class, use the `yt_dataclass` decorator. For example: ```python @yt.yt_dataclass class Row: id: int name: str robot: bool = False ``` The field type comes after the colon. This might be a regular Python type or a type from the [typing](https://docs.python.org/3/library/typing.html) module or a special type such as `OtherColumns`. For more information, see [Data classes](https://ytsaurus.tech/docs/en/dataclass.md#types). You can create objects of this class as follows: `row = Row(id=123, name="foo")`. For all the fields without default values (such as `robot: bool = False`), you need to pass relevant fields to the constructor. Otherwise, it will throw an exception. The data classes support inheritance. For more information, see [Data classes](https://ytsaurus.tech/docs/en/dataclass.md). See also the [example](https://ytsaurus.tech/docs/en/examples.md#dataclass). #### Schemas { #table_schema } Each table in YTsaurus has a [schema](https://ytsaurus.tech/user-guide/storage/static-schema.md). The Python  API has a dedicated [TableSchema](https://pydoc.ytsaurus.tech/yt.wrapper.schema.html#yt.wrapper.schema.table_schema.TableSchema) class. The primary way to create a schema is from a [data class](https://ytsaurus.tech/docs/en/#dataclass): `schema = TableSchema.from_row_type(Row)`. In particular, this happens automatically when writing a table. However, sometimes you need to build a schema manually. To do this easily, use a builder interface. For example: ```python import yt.type_info.typing as ti schema = yt.schema.TableSchema() \ .add_column("id", ti.Int64, sort_order="ascending") \ .add_column("name", ti.Utf8) \ .add_column("clicks", ti.List[ ti.Struct[ "url": ti.String, "ts": ti.Optional[ti.Timestamp], ] ]) ``` The column type should be a type from the [type_info](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/yt/type_info) library. Composite types (`Optional`, `List`, `Struct`, `Tuple`, etc.) are set up using square brackets. You can specify a schema when creating or writing to (an empty) table (in the `schema` attribute of the [TablePath](https://ytsaurus.tech/docs/en/#tablepath_class) class). To get a table schema, use: ```python schema = TableSchema.from_yson_type(yt.get("//path/to/table/@schema")) ``` Each schema has a corresponding ID. To retrieve it from a table, use the following command: ```python schema = TableSchema.from_yson_type(yt.get("//path/to/table/@schema_id")) ``` You can also retrieve a materialized schema by ID: ```python schema = TableSchema.from_yson_type(yt.get("#/@value")) ``` Note that the relationship between schemas and their IDs is one‑to‑many, not one‑to‑one. Two tables with the same schema might have different schema IDs if the tables have different native cells. Additionally, the lifetime of schemas depends on the lifetime of tables and chunks that reference the schema. That is, if all tables using a schema are deleted, the schema is also deleted. When you create a new table with a previously deleted schema, the schema gets a new ID. The main use case for IDs is schema deduplication, since schema objects can be quite heavy. Note also that working with schema IDs is supported in the `create` and `alter` verbs. #### TablePath { #tablepath_class } All the commands used with tables (including operations), are not only accepting string as the input and output tables, but also the [TablePath](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.TablePath) class (where appropriate). This class represents a path to a table with certain modifiers (a wrapper on [YPath](https://ytsaurus.tech/user-guide/storage/ypath.md) for tables). Its constructor accepts: - `name`: Path to a table in Cypress. - `append`: Append records to the table instead of overwriting it. - `sorted_by`: Set of columns by which the table should be sorted when written to. - `columns`: List of selected columns. - `lower_key, upper_key, exact_key`: Lower/upper/exact read boundary defined by a key. Used with sorted tables only. - `start_index, end_index, exact_index`: Lower/upper/exact read boundary defined by row indexes. - `ranges`: Specify an arbitrary set of ranges to be read. - `schema`: A [table schema](https://ytsaurus.tech/docs/en/#table_schema); it makes sense when creating a table or writing to an empty or non-existing table. - `attributes`: Set any additional attributes. Ranges are semi-intervals (that is, they do not include the upper boundary). Note that some modifiers make sense only when you read data from a table (all the attributes related to ranges or columns), and some modifiers can only be used when writing to tables (append, sorted_by). As `name`, you can pass a string with ypath modifiers and ypath attributes, they will be read correctly and put to the `attributes` field. In the [TablePath](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.ypath.TablePath) object, the `attributes` field is both readable and writable. Example: ```python @yt.yt_dataclass class Row: x: str table = "//tmp/some-table" yt.write_table_structured(table, Row, [Row(x="a"), Row(x="c"), Row(x="b")]) yt.run_sort(table, sort_by=["x"]) ranged_path = yt.TablePath(table, lower_key="b") list(yt.read_table_structured(ranged_path, Row)) ## Output: [Row(x='b'), Row(x='c')] ``` #### Commands { #table_commands } - [create_temp_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.create_temp_table) Creates a temporary table in the `path` directory with the `prefix`. If `path` is omitted, the directory will be taken from the config: `config["remote_temp_tables_directory"]`. For convenience, there's a wrapper that supports with_statement and accepts the same parameters as it. Example: ```python with yt.TempTable("//home/user") as table1: with yt.TempTable("//home/user", "my") as table2: yt.write_table_structured(table1, Row, [Row(x=1)]) yt.run_map(..., table1, table2, ...) ``` - [write_table_structured](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.write_table_structured) It writes the rows of `row_type` (it must be a [`yt_dataclass`](https://ytsaurus.tech/docs/en/#dataclass)) from `input_stream` to the `table`. If the table is missing, first it is created together with a schema. The command supports retries. You can set up retries using the `write_retries` config option. {% note warning "Attention" %} Writing with retries consumes more memory than regular writing because the write operation buffers the rows written into chunks before writing (if a chunk fails to be written, a retry occurs). The default size of each chunk is 520 MB (see the [configuration option](https://github.com/ytsaurus/ytsaurus/blob/49f99ef8659f108f94d2d086f5f1dacfddb6b553/yt/python/yt/wrapper/default_config.py#L535)). {% endnote %} With the `table_writer` options, you can specify a number of system [write parameters](https://ytsaurus.tech/user-guide/storage/io-configuration.md). To write raw or compressed data, use the `write_table` function. Example: ```python @yt.yt_dataclass class Row: id: str ts: int yt.write_table_structured("//path/to/table", Row, [Row(id="a", ts=10)]) ``` When writing to an empty or non-existing table, the schema is created automatically. In more complex cases, you might need to build the schema manually. For more information, see [this section](https://ytsaurus.tech/docs/en/#table_schema) and the [example](https://ytsaurus.tech/docs/en/examples.md#table_schema). - [read_table_structured](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.read_table_structured) Read the table as a sequence of rows of `row_type` (it must belong to [`yt_dataclass`](https://ytsaurus.tech/docs/en/#dataclass)). The command supports retries (enabled by default). You can set up retries using the `read_retries` configuration option. The `table_reader` (dict) option enables you to specify a number of system [read parameters](https://ytsaurus.tech/user-guide/storage/io-configuration.md#table_reader). The `unordered` (bool) option enables you to request unordered reading. In that case, the data might be read faster, but the read order isn't guaranteed. You can use the `response_parameters` (dict) option to pass a dict. This dict will be appended with special read command parameters (currently, there are two such parameters: `start_row_index` and `approximate_row_count`). The iterator returned supports the `.with_context()` method that returns an iterator on the `(row, ctx)` pairs. The second item enables you to get the indexes of the current row and range using the `ctx.get_row_index()` and `ctx.get_range_index()` methods (a similar iterator inside the job also enables you to get the table index: `ctx.get_table_index()`). See examples in the tutorial showing the context in [regular reading](https://ytsaurus.tech/docs/en/examples.md#read_write) and [inside operations](https://ytsaurus.tech/docs/en/examples.md#table_switches). A few more words about reading with retries: in the event of retries, a transaction is created in the current context and a snapshot lock is taken on the table. The lock holds until the whole data stream is read or `.close()` is called on the stream or iterator. This behavior can result in different errors. For example, the following code **won't** work: because of the nested read transaction you won't be able to commit the transaction that has been explicitly created within the code (the nested read transaction hasn't been completed because `read_table_structured` created an iterator that hasn't been used). ```python with Transaction(): write_table_structured(table, Row, rows) read_table_structured(table, Row) ``` Examples: ```python @yt.yt_dataclass class Row: id: str ts: int yt.write_table_structured("//path/to/table", Row, [Row(id="a", ts=1)]) assert list(yt.read_table_structured("//path/to/table", Row) == [Row(id="a", ts=1)]) ranges = [ {"lower_limit": {"key": ["a"]}, "upper_limit": {"key": ["b"]}}, {"lower_limit": {"key": ["x"]}, "upper_limit": {"key": ["y"]}}, ] path = yt.TablePath("//other/table", columns=["time", "value"], ranges=ranges) rows = yt.read_table_structured(path, Row) for row, ctx in rows.with_context(): # ctx.get_row_index() – index of row in table. # ctx.get_range_index() – index of range from requested ranges. # ... ``` Alias functions for working with tables: - [row_count](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.row_count): Returns the number of records in the table. - [is_empty](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.is_empty): Checks whether the table is empty. - [is_sorted](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.is_sorted): Checks whether the table is sorted. Examples: ```python yt.write_table_structured("//home/table", Row, [Row(id="a", ts=2), Row(id="b", ts=3)]) sum = 0 for row in yt.read_table_structured("//home/table", Row): sum += row.ts print(sum) # Output: 5 yt.is_empty("//home/table") # Output: False yt.row_count("//home/table") # Output: 2 yt.is_sorted("//home/table") # Output: False ``` - [write_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.write_table) A non-typed analog of `write_table_structured`, **should be avoided**. {% cut "Info" %} Writes rows from `input_stream` to the `table`. If the table is missing, it is created first. You can set up retries using the `write_retries` config option. Learn the [example](https://ytsaurus.tech/docs/en/examples.md#read_write_untyped) in the dedicated section. {% note warning "Attention" %} Writing with retries consumes more memory than regular writing because the write operation buffers the rows written into chunks before writing (if a chunk fails to be written, then a retry is made). The default size of each chunk is 520 MB (see the [configuration option](https://github.com/ytsaurus/ytsaurus/blob/49f99ef8659f108f94d2d086f5f1dacfddb6b553/yt/python/yt/wrapper/default_config.py#L535)). {% endnote %} With the `table_writer` options, you can specify a number of system [write parameters](https://ytsaurus.tech/user-guide/storage/io-configuration.md). There is also the `is_stream_compressed` parameter that says that the stream data has already been compressed, and you can transmit them without prior compression. Keep in mind that when transmitting compressed data, you need to specify `Content-Encoding` using the configuration: `config["proxy"]["content_encoding"] = "gzip"`, as well as set the `raw=True` option. The `raw` option regulates in which format the data is expected. If `raw=False`, an iterator by Python structures is expected. If `raw=True`, then a string is expected (or a string iterator) or a stream with the data in the `format`. Examples: ```python yt.write_table("//path/to/table", ({"a": i, "b": i * i} for i in xrange(100))) yt.write_table("//path/to/table", open("my_file.json"), format="json", raw=True) yt.write_table("//path/to/table", "a=1\na=2\n", format="dsv", raw=True) ``` To write a table with a schema, you need to create it separately first. ```python schema = [ {"name": "id", "type": "string"}, {"name": "timestamp", "type": "int64"}, {"name": "some_json_info", "type": "any"}, ] yt.create("table", "//path/to/table", attributes={"schema": schema}) yt.write_table("//path/to/table", rows) ``` {% endcut %} - [read_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.read_table) A non-typed analog of `read_table_structured`, **should be avoided**. {% cut "Info" %} Reads the table using the specified format. The value returned depends on the `raw` option. If `raw=False` (default value), an iterator by a list of records is returned. One record is a `dict` or `Record` (in the case of the `yamr` format). If `raw=True`, a stream-like object is returned from which you can read data in the `format`. The command supports retries (enabled by default). You can set up retries using the `read_retries` configuration option. When retries are enabled, reading is slower because of the need to parse the stream as you need to count the number of previously read rows. The `table_reader` (dict) option enables you to specify a number of system [read parameters](https://ytsaurus.tech/user-guide/storage/io-configuration.md#table_reader). With the `control_attributes` (dict) option, you can request a number of [control attributes](https://ytsaurus.tech/user-guide/storage/io-configuration.md#control_attributes) when reading data. The `unordered` (bool) option enables you to request unordered reading. In that case, the data might be read faster, but the read order isn't guaranteed. The `response_parameters` (dict) option enables you to send a dict to it. This dict will be appended by special read command parameters (in the current implementation, the two parameters are: start_row_index and approximate_row_count). See the [example](https://ytsaurus.tech/user-guide/storage/examples.md#read_write_untyped) in the dedicated section. A few more words about reading with retries: in this case, a transaction is created in the current context and a snapshot lock is taken on the table. This lock holds until you've read the entire data stream or call `.close()` on the stream or the iterator returned. Such a behavior can result in errors. For example, the following code **won't** work: because of the nested read transaction, you won't be able to commit the transaction explicitly created within the code (the nested read transaction hasn't been completed because [read_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.read_table) created an iterator that hasn't been used). ```python with Transaction(): write_table(table, rows) read_table(table, format="yson") ``` {% note warning "Attention" %} Reading with retries usually works slower than without them because you need to parse a stream of records. {% endnote %} {% endcut %} #### Parallel reading of tables and files { #parallel_read } The table is broken down into smaller ranges, assuming that the data is evenly distributed across rows. Each range is considered a separate stream. When you enable retries, the entire range, rather than individual table rows, will be retried. This approach enables you to skip data parsing and streamline the reading process. The files are simply split into chunks of the specified size and read in parallel. The configuration of parallel reading are stored in the `read_parallel` configuration section that includes the following keys: - `enable`: Enable parallel reading. - `max_thread_count`: Maximum number of threads. - `data_size_per_thread`: Amount of data loaded by each data thread. Specifics of parallel reading of data: - Requesting control attributes is not supported. - Table rows with limits set by keys aren't supported. - Parallel reading might be ineffective on tables that substantially vary in row sizes. Acceleration example: ```bash $ time yt read-table //sys/scheduler/event_log.2[:#1000000] --proxy cluster-name --format yson > /dev/null real 1m46.608s user 1m39.228s sys 0m4.216s $ time yt read-table //sys/scheduler/event_log.2[:#1000000] --proxy cluster-name --format yson --config "{read_parallel={enable=%true;max_thread_count=50;}}" > /dev/null real 0m14.463s user 0m12.312s sys 0m4.304s ``` {% cut "Measuring the speed" %} ```bash $ export YT_PROXY=cluster-name $ yt read //sys/scheduler/event_log.2[:#20000000] --format json --config "{read_parallel={enable=%true;max_thread_count=50;}}" > scheduler_log_json $ yt read //sys/scheduler/event_log.2[:#20000000] --format yson --config "{read_parallel={enable=%true;max_thread_count=50;}}" > scheduler_log_yson $ ls -lah total 153G drwxrwxr-x 2 user group 4.0K Sep 29 21:23 . drwxrwxr-x 7 user group 4.0K Sep 28 15:20 .. -rw-r--r-- 1 user group 51G Sep 29 21:25 scheduler_log_json -rw-r--r-- 1 user group 51G Sep 29 21:22 scheduler_log_yson -rw-r--r-- 1 user group 51G Sep 29 21:20 test_file $ time cat scheduler_log_yson | yt write-table //tmp/test_yson --format yson real 36m51.653s user 31m34.416s sys 1m5.256s $ time cat scheduler_log_json | yt write-table //tmp/test_json --format json real 88m38.745s user 21m7.188s sys 1m1.400s $ time cat test_file | yt write-file //tmp/test_file real 35m50.723s user 17m31.232s sys 1m39.132s $ time cat scheduler_log_yson | yt write-table //tmp/test_yson --format yson --config "{write_parallel={enable=%true;max_thread_count=30;}}" real 13m37.545s user 37m20.516s sys 4m16.436s $ time cat scheduler_log_json | yt write-table //tmp/test_json --format json --config "{write_parallel={enable=%true;max_thread_count=30;}}" real 3m53.308s user 23m21.152s sys 2m57.400s $ time cat test_file | yt write-file //tmp/test_file --config "{write_parallel={enable=%true;max_thread_count=30;}}" real 1m49.368s user 18m30.904s sys 1m40.660s ``` {% endcut %} #### Parallel writing of tables and files { #parallel_write} To use this option, do the following: 1. Set `zlib_fork_safe` if you run python2. 2. Enable the following option in the config file: `config["write_parallel"]["enable"] = True`. After that, the standard commands [write_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.write_table) and [write_file](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.write_file) will work in multithreading mode. Now, the client configuration includes a new `write_parallel` section with the following keys: - `enable` (False by default) enables the option to write to tables and files in YTsaurus in parallel. - `max_thread_count` (10 by default): The maximum number of threads the data is written to in parallel. - `unordered` (False by default): Enables you to write rows to a table in an arbitrary order, streamlining the writing process. - `concatenate_size` (20 by default): a limit on the number of tables or files that the concatenate command receives as input. #### How it works The entire process of parallel writing looks like this: 1. The input thread is split into rows, and the rows are grouped into chunks whose size is regulated by the `/write_retries/chunk_size` configuration option (the default value is 512 MB). 2. The chunks are delivered to ThreadPool. 3. The threads receive a group of rows to be written as input, compress this data, and upload it to to a temporary table or file on the server. 4. The main thread accumulates temporary tables and files that have already been written to. 5. As soon as the amount of written tables or files becomes equal to `/write_parallel/concatenate_size`, the data is merged. #### Limitations - Parallel writing is unavailable when a thread of compressed data is delivered as input. - If the path to the output table has the `sorted_by` attribute, parallel writing will also be unavailable. - Parallel writing won't be effective in Python2 unless you install `zlib_fork_safe` (this is because of GIL). 4. Parallel writing won't be effective if the parser used for your format is slow (for example, in the event of [schemaful_dsv](https://ytsaurus.tech/user-guide/storage/formats.md#schemaful_dsv)). 5. Because the input thread is split into chunks for parallel uploading to the cluster, the custom script will consume the memory proportionally to `max_thread_count * chunk_size` (in practice, the multiplier is about 2). **Q: Why is multithreaded writing many times faster for JSON vs. YSON, but is the other way around for singlethreaded writing?** **A:** There are two reasons: You need to break down the input thread into rows. In JSON, you can do this easily by splitting by `\n`. Doing this in YSON requires much more effort. As this operation is single-threaded, it becomes a weak spot and locks the entire writing process. ### Working with transactions and locks { #transaction_commands } - [Transaction](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.transaction.Transaction): **Single-thread** wrapper class for creating, committing, or aborting transactions. It supports the syntax of context manager (the `with` statement), that is, if the transaction exits the scope successfully, the transaction is committed; otherwise, it is aborted. All the commands in the scope run within the specified transaction. You can create nested scopes. The `ping` parameter (the default value is `True`) in the builder is responsible for running a pinging thread. If there is no ping, the operation will be forcibly aborted on timeout expiry. Examples: ```python with yt.Transaction(): yt.write_table_structured(table, [Row(x=5)]) # the data will be written within the transaction # Once it exits the with, the transaction is committed. with yt.Transaction(): yt.write_table_structured(table, [Row(x=6)]) raise RuntimeError("Something went wrong") # The exception will abort the transaction and no one will see the changes ## Nested transactions with yt.Transaction(): yt.lock("//home/table", waitable=True, wait_for=60000) with yt.Transaction(): yt.set("//home/table/@attr", "value") ``` If the pinging thread fails when trying to ping the transaction, it calls `thread.interrupt_main()`. You can change this behavior using the option `config["ping_failed_mode"]`. Available options: 1. `pass`: Do nothing 2. `call_function`: Calls the function specified in the `ping_failed_function` field of the config 3. `interrupt_main`: Throw the `KeyboardInterrupt` exception in the main thread 4. `send_signal`: Send the `SIGUSR1` signal to the process. 5. `terminate_process`: Terminate the process. You can also manage transactions at a lower level (unlike with the `Transaction` context manager, the commands won't automatically be called under the given transaction). - [start_transaction](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.start_transaction): Create a new transaction with the specified timeout. - [abort_transaction](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.abort_transaction): Abort the transaction with the specified ID. - [commit_transaction](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.commit_transaction): Commit the transaction with the specified ID. - [ping_transaction](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.ping_transaction): Ping the transaction to extend its TTL. These functions produce the [YtResponseError](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.errors.YtResponseError) exception with `.is_resolve_error() == True` when the transaction is not found. - [lock](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.lock): Acquires a lock on the specified node within the current transaction. In the event of a waitable lock and the specified `wait_for`, it waits for the lock to be taken within `wait_for` milliseconds. If the lock isn't taken within this time, it returns an exception. - [unlock](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.unlock): Removes all the [explicit](https://ytsaurus.tech/user-guide/storage/transactions.md#implicit_locks) locks on the node acquired by the current transaction. This includes already acquired locks as well as enqueued locks. If there are no locks, it has no effect. When the unlocking is impossible (because the locked node version includes changes compared to the original version), it returns an exception. {% note info "Note" %} When the transaction is completed (either successfully or not), all the locks taken by it are released. This means that you only have to `unlock` when you need to release a node without completing the transaction. {% endnote %} ### Running operations { #run_operation_commands } To learn about the operations available for table data, see the [section](https://ytsaurus.tech/user-guide/data-processing/operations/overview.md). All the functions that run operations start with the `run_` prefix. These functions often have numerous parameters. Next, we will describe the parameters that are shared by all the operation run functions, as well as the parameter selection logic. For a full list of parameters for each specific function, see [pydoc](http://pydoc.ytsaurus.tech/yt.wrapper.html). Operation run commands - [run_erase](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_erase): [The operation that deletes data from a table](https://ytsaurus.tech/user-guide/data-processing/operations/erase.md) - [run_merge](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_merge): [The operation to merge/process data in the table without running the custom code](https://ytsaurus.tech/user-guide/data-processing/operations/merge.md) - [run_sort](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_sort): [Data sorting operation](https://ytsaurus.tech/user-guide/data-processing/operations/sort.md). If the destination table is omitted, inplace sorting of the table is performed. - [run_map_reduce](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_map_reduce): [MapReduce operation](https://ytsaurus.tech/user-guide/data-processing/operations/mapreduce.md). - [run_map](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_map): [Map operation](https://ytsaurus.tech/user-guide/data-processing/operations/map.md). - [run_reduce](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_reduce): [Reduce operation](https://ytsaurus.tech/user-guide/data-processing/operations/reduce.md). - [run_join_reduce](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_join_reduce): [JoinReduce operation](https://ytsaurus.tech/user-guide/data-processing/operations/reduce.md#foreign_tables). - [run_operation](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_operation): Runs the operation. By default, the command starts the operation as it is. The `enable_optimizations` option enables the `treat_unexisting_as_empty`, `run_map_reduce_if_source_is_not_sorted`, `run_merge_instead_of_sort_if_input_tables_are_sorted` optimizations if they are enabled in the config - [run_remote_copy](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_remote_copy): [RemoteCopy operation](https://ytsaurus.tech/user-guide/data-processing/operations/remote-copy.md) Coping the table from one YTsaurus cluster to another. #### SpecBuilder { #spec_builder } We recommend using [spec builders](https://ytsaurus.tech/docs/en/#spec_builder) to specify the operation's runtime parameters.  The following classes are provided for populating the operation specifications: - [MapSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MapSpecBuilder); - [ReduceSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.ReduceSpecBuilder); - [MapReduceSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MapReduceSpecBuilder); - [JoinReduceSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.JoinReduceSpecBuilder); - [MergeSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MergeSpecBuilder); - [SortSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.SortSpecBuilder); - [RemoteCopySpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.RemoteCopySpecBuilder); - [EraseSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.EraseSpecBuilder); - [VanillaSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.VanillaSpecBuilder). The names of the methods correspond to option names in the specification listed [here](https://ytsaurus.tech/user-guide/data-processing/operations/operations-options.md). The builders used to populate the specification for an operation with custom jobs provide the `begin_mapper`, `begin_reducer`, and `begin_reduce_combiner` methods. Similarly, there are the `begin_job_io`, `begin_map_job_io`, `begin_sort_job_io`, `begin_reduce_job_io`, `begin_partition_job_io`, `begin_merge_job_io` methods in the appropriate spec builders. Example: ```python import yt.wrapper as yt if __name__ == "__main__": spec_builder = yt.spec_builders.MapSpecBuilder() \ .input_table_paths("//tmp/input_table") \ .output_table_paths("//tmp/output_table") \ .begin_mapper() \ .command("cat") \ .format(yt.YsonFormat()) \ .end_mapper() yt.run_operation(spec_builder) ``` Another two types of spec builders are provided for convenience: 1. Spec builders for filling out [I/O parameters](https://ytsaurus.tech/user-guide/storage/io-configuration.md): - [PartitionJobIOSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.PartitionJobIOSpecBuilder); - [SortJobIOSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.SortJobIOSpecBuilder); - [MergeJobIOSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MergeJobIOSpecBuilder); - [ReduceJobIOSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.ReduceJobIOSpecBuilder); - [MapJobIOSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MapJobIOSpecBuilder). 2. Spec builders for filling out the specification of a custom job: - [MapperSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MapperSpecBuilder); - [ReducerSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.ReducerSpecBuilder); - [ReduceCombinerSpecBuilder](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.ReduceCombinerSpecBuilder). see the [example in the tutorial](https://ytsaurus.tech/docs/en/examples.md#spec_builder). #### Other parameters of the operation run commands. The `sync` parameter is shared by all the operation run functions (`True` by default). If `sync=True`, when the function is called, the operation completion will be awaited synchronously. If `sync=False`, the [Operation](https://ytsaurus.tech/docs/en/#operation_class) object will be returned to the client. If the relevant parameter is missing in the appropriate spec builder, you can use the `spec` parameter of the `dict` type that enables you to explicitly set arbitrary options for the operation specification (the specified parameter has the highest priority when generating a specification). For example, you can specify the desired amount of data per job: `yt.run_map(my_mapper, "//tmp/in", "//tmp/out", spec={"data_weight_per_job": 1024 * 1024})` (but in this particular case, it's more appropriate to use a [builder method](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.spec_builders.MapSpecBuilder.data_size_per_job)). All the required specification parameters exist as individual options. Such parameters include the input and output table paths and the command (for operations with the custom code). For the sort, reduce, map_reduce, join_reduce operations, the `sort_by`, `reduce_by`, and `join_by` parameters are specified as individual options. The `source_table` and `destination_table` parameters can accept as their input both a table path and a list of paths to tables, and correctly handle both cases, depending on whether the operation expects a table path or a set of table paths at the given point. Both the strings and [TablePath](https://ytsaurus.tech/docs/en/#tablepath_class) objects are accepted as table paths. Some of the included parameters have to be modified fairly regularly. This includes `job_io` and its `table_writer` part. These parameters will be added to the appropriate parts of the specification. If the operation includes multiple steps, then the parameters specified in `job_io` will propagate to all the steps. For all the operations that have the `job_count` parameter in the specification, this parameter is defined as a separate option. For all the operations that include the custom code, a separate option is created for `memory_limit`, as well as the `format`, `input_format`, and `output_format` parameters that enable you to specify the [input and output data format](https://ytsaurus.tech/user-guide/storage/formats.md). There are the `yt_files` and `local_files` parameters (and the `files` alias) enabling you to set the file paths in Cypress and the paths to local files (respectively) that will be passed to the job environment. For the map_reduce operation, all the parameters of the custom script described in the previous paragraph are copied separately for mapper, reducer, and reduce-combiner. As the `binary`, you can pass both the [run command](https://ytsaurus.tech/user-guide/data-processing/operations/overview.md) (as a string) and a callable Python object. The operations that access callable objects as `binary` are referred to as [Python operations](https://ytsaurus.tech/docs/en/#python_operations). If a run command is passed as `binary`, the [format](https://ytsaurus.tech/docs/en/#python_formats) parameter is mandatory. However, the format is optional in the case of a callable object. Any operation writing data to destination_table has the option that skips deleting the existing table but appends data to it. For this purpose, as destination_table, you can specify a [TablePath object](https://ytsaurus.tech/docs/en/#tablepath_class) with `append=True`. When running a `map_reduce` operation with multiple input tables, keep in mind that the YTsaurus client can add table switches to input records. As a result, the switches might end up in the output stream (if you pass this data to the output stream as is, such as a mapper from some `yield row`). This might result in the error of writing the record to a non-existing table. To avoid this situation, you should either disable the table index using the `enable_input_table_index` option or manually delete the table index before writing data to the output stream: `del row["@table_index"]`. For the [run_map_reduce](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_map_reduce) function, all the options specific to a given job exist in three copies: for the mapper, reducer, and reduce_combiner. That is, the function has the options `map_local_files`, `reduce_local_files`, `reduce_combiner_local_files`, etc. For a full list of options, see [pydoc](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_map_reduce). ## Commands { #commands } - [get_operation_state](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.get_operation_state) — returns the state of the current operation. The returned object is a class that has a single field `name` and methods `is_finished`, `is_running`, `is_unsuccessfully_finished`. - [abort_operation](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.abort_operation) — abort the operation without saving its result. - [suspend_operation](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.suspend_operation) — suspend the operation. - [resume_operation](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.resume_operation) — resume a suspended operation. - [complete_operation](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.complete_operation) — complete the operation with the current available result. When you run an operation with the `sync=False` flag, it’s more convenient to use the `abort`, `suspend`, `resume`, `complete` methods of the `Operation` class instead of the methods listed above. See the [Operation](https://ytsaurus.tech/docs/en/#operation_class) section. - [run_job_shell](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_job_shell) — run a job-shell for a job. It’s easier to use this function via the equivalent command in the [CLI](https://ytsaurus.tech/docs/cli/examples.md). #### Getting information about jobs and operations { #operation_and_job_info_commands } An operation has a fairly non‑trivial lifecycle, and at different times you can get information about an operation from different sources: - Cypress (contains information about operations that are running and not yet archived). - Orchid controller agent (contains all up‑to‑date information about running operations). - Operation archive (contains information about completed operations). Because you can get operation information from different sources (and those sources can change both in composition and internal structure), the following methods are available. They can collect operation information from the listed sources at any time. - [get_operation](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.get_operation) — get information about an operation by its ID. Returns a `dict` with fields similar to the response fields of [get_operation](https://ytsaurus.tech/docs/commands.md#get_operation). - [list_operations](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.list_operations) — get information about a set of operations based on filters. The meaning of the fields is similar to `get_operation`. See the list of filters in the [Commands](https://ytsaurus.tech/docs/commands.md#list_operations) section. - [iterate_operations](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.iterate_operations) — get an iterator for a set of operations. This function is similar to `list_operations`, but it has no limit on the number of operations you can request. Example: print the types of the three latest operations started by the user `username`: ```python from datetime import datetime import yt.wrapper as yt result = yt.list_operations( user="username", cursor_time=datetime.utcnow(), cursor_direction="past", limit=3, ) print([op["type"] for op in result["operations"]]) ## Output: ['sort', 'sort', 'sort'] ``` Another example: find all running operations started in user ephemeral pools: ```python for op in client.iterate_operations(state="running"): if "$" in op.get("pool"): print(op["id"], op["start_time"]) ``` Information about a job’s operations is stored in the Scheduler and in the archive. The following methods let you get information about jobs: - [get_job](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.get_job) — get information about a job. Returns a `dict` with fields similar to the response fields of [get_job](https://ytsaurus.tech/docs/commands.md#get_job). - [list_jobs](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.list_jobs) — get information about a set of jobs for an operation. The meaning of the fields in the response is similar to `get_job`. See the list of filters in the [Commands](https://ytsaurus.tech/docs/commands.md#list_jobs) section. - [get_job_stderr](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.get_job_stderr) — get a job’s stderr. - [get_job_input](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.get_job_input) — get a job’s full input. - [get_job_input_paths](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.get_job_input_paths) — get the list of input tables (with row ranges) for a job. - [get_job_spec](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.get_job_spec) — get a job’s specification. {% note warning "Warning" %} Calls to the functions in this section may internally access Master servers, the Scheduler, and cluster nodes. Because calls to Master servers and the Scheduler don’t scale, you shouldn’t use these functions too often. {% endnote %} To debug failed jobs, it’s convenient to use the [job tool](https://ytsaurus.tech/user-guide/problems/jobstatistics.md). This utility lets you set up an environment similar to a job’s environment and run it with the same input data. #### Operation { #operation_class } The commands that start operations (`run_*`) return an object of this class. It provides a [small API](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.Operation) for working with an already started (or completed) operation. #### OperationsTracker { #operations_tracker_class } You can use [OperationsTracker](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTracker) to work conveniently with multiple operations. You can also use it to work with operations started on different clusters. OperationsTracker has the following interface: - [add](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTrackerBase.add) — add an `Operation` object to the set of tracked operations. - [add_by_id](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTracker.add_by_id) — add an operation by ID to the set of tracked operations. - [wait_all](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTrackerBase.wait_all) — wait for all operations to complete. - [abort_all](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTrackerBase.abort_all) — abort all tracked operations. Example: ```python with yt.OperationsTracker() as tracker: op1 = yt.run_map("sleep 10; cat", "//tmp/table1", "//tmp/table1_out", sync=False) op2 = yt.run_map("sleep 10; cat", "//tmp/table2", "//tmp/table2_out", sync=False) tracker.add(op1) tracker.add(op2) tracker.abort_all() tracker.add(yt.run_map("true", table, TEST_DIR + "/out", sync=False)) ``` When you exit the `with` block, the tracker waits for all operations to complete. If an exception occurs, all started operations are aborted. {% note warning "Warning" %} Using the tracker without `with` is outdated and not recommended. {% endnote %} #### OperationsTrackerPool { #operations_tracker_pool_class } The [OperationsTrackerPool](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTrackerPool) class is similar to [OperationsTracker](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTracker), but it also guarantees that no more than `pool_size` operations run at the same time. The class methods accept one or more spec builders as input (see the corresponding [section](https://ytsaurus.tech/docs/en/#spec_builder)). This class creates a background thread that gradually starts all operations from the queue, ensuring that no more than the specified number of operations run simultaneously. Interface: - [add](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTrackerPool.add) — accepts a spec builder as input and adds it to the launch queue. - [map](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operations_tracker.OperationsTrackerPool.map) — accepts a list of spec builders as input. The other parameters have the same meaning as the `add` method. ### Working with access rights { #acl_commands } - [check_permission](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.check_permission) - [add_member](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.add_member) - [remove_member](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.remove_member) Examples: ```python yt.create("user", attributes={"name": "tester"}) yt.check_permission("tester", "read", "//sys") ## Output: {"action": "allow", ...} yt.create("group", attributes={"name": "test_group"}) yt.add_member("tester", "test_group") yt.get_attribute("//sys/groups/testers", "members") ## Output: ["tester"] yt.remove_member("tester", "test_group") yt.get_attribute("//sys/groups/testers", "members") ## Output: [] ``` ### Working with dynamic tables { #dyntables_commands } [Dynamic tables](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md) implement an interface for point‑wise reading and writing data by key, support transactions, and provide their own SQL dialect. List of commands for working with the contents of dynamic tables: - [insert_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.insert_rows) — [insert](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md#insert_rows) rows into a dynamic table. - [lookup_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.lookup_rows) — [select](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md#chtenie-stroki) rows with specified keys. - [delete_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.delete_rows) — [delete](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md#udalenie-stroki) rows with specified keys. - [select_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.select_rows) — [select](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md#vypolnenie-zaprosa) rows that match a query in the [SQL dialect](https://ytsaurus.tech/user-guide/dynamic-tables/dyn-query-language.md). - [lock_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.lock_rows) — [lock](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md#blokirovka-stroki) rows with specified keys. - [trim_rows](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.trim_rows) — [delete](https://ytsaurus.tech/user-guide/dynamic-tables/ordered-dynamic-tables#trim) a specified number of rows from an ordered table. Commands related to [mounting](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md#mount_table) tables: - [mount_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.mount_table) — mount a dynamic table. - [unmount_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.unmount_table) — unmount a dynamic table. - [remount_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.remount_table) — remount a dynamic table. Commands related to [sharding](https://ytsaurus.tech/user-guide/dynamic-tables/resharding.md): - [reshard_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.reshard_table) — reshard a table by the specified pivot keys. - [reshard_table_automatic](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.reshard_table_automatic) — shard mounted tablets according to the tablet balancer configuration. Other commands: - [freeze_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.freeze_table) — [freeze](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md#zamorozka) a table. - [unfreeze_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.unfreeze_table) — [unfreeze](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md#zamorozka) a table. - [balance_tablet_cells](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.balance_tablet_cells) — distribute tablets evenly across [tablet cells](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md#tablet_cells). - [get_tablet_infos](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.get_tablet_infos) — get the attributes of a [tablet](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md#tablets). - [get_tablet_errors](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.get_tablet_errors) — get the errors of a [tablet](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md#tablets). - [alter_table_replica](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.dynamic_table_commands.alter_table_replica) — [change the attributes](https://ytsaurus.tech/user-guide/dynamic-tables/replicated-dynamic-tables.md#nastrojki-replik) of a replica of a [replicated table](https://ytsaurus.tech/user-guide/dynamic-tables/replicated-dynamic-tables.md). ### Other commands { #etc_commands } #### Table transformations **Transform.** Sometimes you need to move a table to a new compression codec or erasure, or to a [new chunk format](https://ytsaurus.tech/user-guide/storage/chunks.md). The [transform](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.transform) function helps with these tasks. The function starts a `merge` operation on the input table, recompressing the data according to the specified parameters. If the `check_codecs` option is set to `True`, the function checks whether the data is already compressed; if it is, the operation doesn’t start. If you specify the `optimize_for` option, the operation always starts (the `check_codecs` option is ignored), because there’s no way to check what format the table’s chunks have. **Shuffle.** To shuffle the rows of a table in random order, use the [shuffle_table](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.shuffle_table) function. The function starts a `map_reduce` operation that sorts the table by an added column with a random number. ## Typed operations { #python_operations } ### General information { #python_operations_intro } To run an operation, you need to define a special subclass of `yt.wrapper.TypedJob` and pass an object of this class to the [operation run function](https://ytsaurus.tech/docs/en/#run_operation_commands) (or specify it in the corresponding field of the [SpecBuilder](https://ytsaurus.tech/docs/en/#spec_builders)). Your job class must define the `__call__(self, row)` method (for a mapper) or the `__call__(self, rows)` method (for a reducer). This method receives table rows as input (in the case of a reducer, one `__call__` invocation corresponds to a set of rows with the same key). It must return (using `yield`) the rows that need to be written to the output table. If there are multiple output tables, use the wrapper class `yt.wrapper.OutputRow`, whose constructor accepts the row to be written and `table_index` as a named parameter (see the [example](https://ytsaurus.tech/docs/en/examples.md#table_switches) in the tutorial). You can also define the following methods: - `start(self)` — this runs exactly once before the job processes any records. - `finish(self)` — this runs once after the job processes all records. These methods, like `__call__`, can generate new records using `yield`. This lets you implement buffering or data accumulation. These methods aren’t called for operations marked with `@yt.aggregator` that receive the entire input at once. You can also define the [.prepare_operation(self, context, preparer)](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.TypedJob.prepare_operation) method. It’s used to specify the row types for input and output tables and to modify the operation spec. For more details, see the [Prepare an operation from a job](https://ytsaurus.tech/docs/en/#prepare_operation) section, and check out the examples: - [{#T}](https://ytsaurus.tech/docs/en/examples.md#prepare_operation) - [{#T}](https://ytsaurus.tech/docs/en/examples.md#grep) ### Data type description { #prepare_operation } To specify input and output row types in the job class, you can use type hints (see examples in the tutorial: [one](https://ytsaurus.tech/docs/en/examples.md#simple_map), [two](https://ytsaurus.tech/docs/en/examples.md#multiple_input_reduce), [three](https://ytsaurus.tech/docs/en/examples.md#multiple_input_multiple_output_reduce), and [four](https://ytsaurus.tech/docs/en/examples.md#map_reduce_multiple_intermediate_streams)), or override the [.prepare_operation(self, context, preparer)](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.TypedJob.prepare_operation) method. You specify types via methods of the `preparer` object of type [`OperationPreparer`](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.OperationPreparer). Useful methods include: 1. [`inputs`](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.OperationPreparer.inputs): lets you specify the input row type for multiple input tables (it must be a class with the [`@yt.wrapper.yt_dataclass`](https://ytsaurus.tech/docs/en/#dataclass) decorator), the list of column names the job needs, and column renames. 2. [`outputs`](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.OperationPreparer.outputs): lets you specify the output row type for multiple output tables (it must be a class with the [`@yt.wrapper.yt_dataclass`](https://ytsaurus.tech/docs/en/#dataclass) decorator), and the schema you want to output for these tables (by default, the schema is derived from the dataclass). 3. `input` and `output` — these are similar to the corresponding methods but accept a single index. The [`context`](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.OperationPreparationContext) object lets you get information about input and output streams: their count, schemas, and table paths. Check the examples in the tutorial: [one](https://ytsaurus.tech/docs/en/examples.md#prepare_operation) and [two](https://ytsaurus.tech/docs/en/examples.md#grep). If you’re running a MapReduce with multiple intermediate streams, you also need to override the [.get_intermediate_stream_count(self)](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.prepare_operation.TypedJob.get_intermediate_stream_count) method and return the number of intermediate streams from it. See the [example](https://ytsaurus.tech/docs/en/examples.md#map_reduce_multiple_intermediate_streams). ## Working with operations { #python_operations_misc } ### Decorators { #python_decorators } You can mark job functions or classes with special decorators that change the expected interaction interface with the jobs. - [aggregator](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.aggregator) — this decorator marks a mapper as an aggregator, meaning it accepts an iterator over rows as input instead of a single row. - [raw](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.raw) — this decorator marks a function as accepting a raw data stream as input instead of parsed records. - [raw_io](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.raw_io) — this decorator marks a function as reading records (rows) from `stdin` and writing to `stdout`. - [reduce_aggregator](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.reduce_aggregator) — this decorator marks a reducer as an aggregator, meaning it accepts a generator of pairs as input, where each pair is (key, records with that key), instead of an iterator over records with a single key. - [with_context](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.with_context) — this decorator lets you request a context for the function. This context will contain the control attributes requested when starting the operation. Note that decorators are implemented by setting an attribute on the function. So, for example, you can’t declare a function with a decorator and then wrap it with `functools.partial`. If you want to pass some parameters to the function directly at call time, create a class with the decorator (see the last example below). You can find examples in the [tutorial](https://ytsaurus.tech/docs/en/examples.md#job_decorators). ### tmpfs in jobs { #tmpfs_in_jobs } tmpfs support in jobs has two parts: 1. For Python operations, tmpfs is enabled by default. It’s allocated to a special tmpfs directory, and the module archive is unpacked there. The extra memory for tmpfs is added to the limit you specified. The behavior is controlled by the `pickling/enable_tmpfs_archive` and `pickling/add_tmpfs_archive_size_to_memory_limit` options. 2. You can automatically request tmpfs for all job files. The option is called `mount_sandbox_in_tmpfs/enable` and is disabled by default. Enabling it means your operations will have `tmpfs_path="."` and `tmpfs_size` (equal to the total file size) specified in the spec. Also, `tmpfs_size` will be added to `memory_limit`. Note that if you’re using table files, the system can’t determine their size on disk after formatting, so you must specify the size in the path attributes using the `disk_size` attribute. You can also request extra space in tmpfs if your job generates some files during execution. Specify the required number of bytes via the `mount_sandbox_in_tmpfs/additional_tmpfs_size` option. ### Statistics in jobs { #python_jobs_statistics } While a job runs, you can export your own statistics (for example, to track the runtime of individual stages within the job). The library provides the following function: - [write_statistics](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.user_statistics.write_statistics) — writes a dictionary with collected statistics to the specified file descriptor. You must call this function only from within a job. The `Operation` class also has the [get_job_statistics](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.operation_commands.Operation.get_job_statistics) method for quick access to the operation’s statistics. Example: ```python class WriteStatistics(yt.wrapper.TypedJob): def __call__(self, row: Row) -> Row: yt.write_statistics({"row_count": 1}) yield row yt.write_table_structured(table, [Row(x=1)]) op = yt.run_map(WriteStatistics(), table, table, sync=False) op.wait() print(op.get_job_statistics()["custom"]) ## Output: {"row_count": {"$": {"completed": {"map": {"count": 1, "max": 1, "sum": 1, "min": 1}}}}} ``` ### Porto layers { #porto_layers } When you start an operation, you can specify which [filesystem image](https://ytsaurus.tech/user-guide/data-processing/layers/layer-paths.md) to prepare before running the jobs. There’s a set of [prebuilt layers](https://ytsaurus.tech/user-guide/data-processing/layers/layer-paths.md#gotovye-sloi-v-kiparise) located at `//porto_layers`. You can specify the path to the required layer via the `layer_paths` parameter in the job spec, for example: ```python spec_builder = ReduceSpecBuilder() \ .begin_reducer() \ .command(reducer) \ .layer_paths(["//porto_layers/ubuntu-precise-base.tar.xz"]) \ .end_reducer() \ ... yt.run_operation(spec_builder) ``` ### Configuring dependency collection (pickling) { #pickling } #### General structure { #pickling_description } When you start an operation that’s a Python function, the following happens: 1. The library uses the [dill](https://github.com/uqfoundation/dill) module to convert the object being run (a class or function) into a byte stream. 2. It collects the local environment where the function runs, passes it into the job on the cluster, and uses it there to correctly run your function. After that, a special code from the `_py_runner.py` module runs on the cluster. In each job, it locally unpacks all dependencies, converts the function from the byte stream into a Python object, and runs it correctly, reading data from stdin. #### Link to the post with tips { #pickling_advises } {% cut "Examples of module filtering" %} For example, here’s how to filter out the transfer of .so and .pyc files while keeping the yt module transfer (this also lets you run scripts on Mac OS X): ```python yt.config["pickling"]["module_filter"] = lambda module: hasattr(module, "__file__") and \ not module.__file__.endswith(".so") yt.config["pickling"]["force_using_py_instead_of_pyc"] = True ## When working via a client, you can set this in its config: client = yt.wrapper.client.Yt(config={"pickling": {"module_filter": lambda ...}}) ``` You can also write your own dependency collection function, using the [default implementation here](http://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.create_modules_archive_default) as a base: ```python def my_create_modules_archive(): ... yt.config["pickling"]["create_modules_archive_function"] = my_create_modules_archive ``` If you’re using binary modules that are dynamically linked to other libraries that aren’t present on the cluster, you should manually bring these libraries to the cluster (and remember to set the `LD_LIBRARY_PATH` environment variable), or use the automatic collection of dynamic libraries for Python modules. With automatic collection, `ldd` is called for each .so file, and the found dependencies are packaged. You enable it like this: ```python yt.config["pickling"]["dynamic_libraries"]["enable_auto_collection"] = True ``` You can also specify a filter if you don’t need to include some libraries. Example: ```python yt.config["pickling"]["dynamic_libraries"]["library_filter"] = lambda lib: not lib.startswith("/lib") ``` By default, it’s recommended to filter out libraries located in the `/lib` and `/lib64` directories, as they contain various system libraries (for example, `libcgroup`) and using them on the cluster can lead to strange errors. You can filter them out with the function from the example above. Recipes for filters for different cases: - If you get the error: `AttributeError: 'module' object has no attribute 'openssl_md_meth_names'`, filter out hashlib: `yt.config["pickling"]["module_filter"] = lambda module: "hashlib" not in getattr(module, "__name__", "") ` - If you’re using Anaconda, filter out hashlib (see the filter example above) and .so libraries. To filter .so libraries, you can use the following filter: ```python yt.config["pickling"]["module_filter"] = ( lambda module: hasattr(module, "__file__") and not module.__file__.endswith(".so") ) ``` {% note warning "Attention" %} This filter also filters out YSON [bindings](https://ytsaurus.tech/docs/en/#yson). If you’re using YSON, add the library to the exceptions: ```python yt.config["pickling"]["module_filter"] = ( lambda module: hasattr(module, "__file__") and (not module.__file__.endswith(".so") or module.__file__.endswith("yson_lib.so") ) ``` {% endnote %} - You’re running a fresh Python 2.7 and getting an error like: `ImportError: No module named urllib3` or `ImportError: cannot import name _compare_digest`, or you can’t import the `hmac` module. To fix this, filter out hmac from the modules you’re bringing with you (it imports a method from the operator module that isn’t available in the Python 2.7.3 installed on the cluster). ```python yt.config["pickling"]["module_filter"] = ... and getattr(module, "__name__", "") != "hmac" ``` Automatic filtering of .pyc and .so files is also supported if the Python version or operating system differs between the cluster and the client. You must first enable the option in the config: ```python yt.config["pickling"]["enable_modules_compatibility_filter"] = True ``` When you run a Python function in a job, all modules in the dependencies, including your main module, are unpacked and imported. Because of this, all business logic should be wrapped under `__main__`. The correct way to do this is: ```python class Mapper(yt.TypedJob): ... if __name__ == "__main__": yt.run_map(mapper, ...) ``` {% endcut %} ## Running operations in different environments { #python_operations_environments } ### Running a local script inside Docker with `yt devtools` { #python_open_source_environment_docker } Running operations on the cluster via the Python SDK can be tricky because you deliver the code to the cluster along with the script’s runtime dependencies (from your local machine). If the differences in OS type, OS version, or Python version are significant, the code won’t run. One way to solve this problem is to ensure that the OS and Python versions match between the launch point (your local machine) and the execution point (the cluster). You do this by running your local code inside a Docker container with the correct environment. This approach lets you run scripts from different OS setups, doesn’t require CRI support on the cluster, and doesn’t require you to push Docker images to any registry. To do this: #### 0. Set up Docker - For ARM platforms (Mac M1/M2), you need to run Docker in x86 mode. ``` # Install Rosetta to support x86 softwareupdate --install-rosetta --agree-to-license # If you’re using Colima, you need to restart it: colima stop colima start --arch x86_64 --vm-type=vz --vz-rosetta ``` - If your cluster uses IPv6 addresses, you need to configure their routing from the container. See https://docs.docker.com/engine/daemon/ipv6/ Note: Colima doesn’t support IPv6. #### 1. Create a Docker image that matches the cluster settings ```(bash) # Build a Docker image for the target cluster (the OS and Python version in the image will match the cluster) yt devtools image prepare --proxy --with-modules ytsaurus-client ytsaurus-yson ``` If the Python or OS version on the cluster is too old and blocks installing the dependencies you need, you can specify the version you want (this requires CRI support on the cluster). ```(bash) # Check the OS/Python version on the cluster yt devtools image get-cluster-env --proxy # Build a Docker image with the desired OS (for example, Ubuntu Trusty) and Python yt devtools image prepare --proxy --base-image docker.io/library/ubuntu:trusty --python 3.13 ``` Make sure you use the exact same `proxy` for all commands; the resulting image name is derived from it (`cluster_env__:latest`). You can always override this behavior with the `--image-name ` parameter. You can view the list of all images with this command: ```(bash) yt devtools image list ``` #### 2. Install additional packages in the image You can install missing packages in the image at any time. ```(bash) # Install pip packages yt devtools image install --proxy --pip # Or run a custom command yt devtools image install --proxy --bash '' ``` #### 3. Run the script If needed, set the environment variables: ``` export YT_TOKEN=... export YT_TOKEN_PATH=... export YT_PROXY=my_cluster.fqdn ``` Then run the script: ``` yt devtools image run --proxy # Or manually: # docker run --platform linux/amd64 --rm -e USER=$USER -e YT_TOKEN_PATH="${YT_TOKEN_PATH:-$HOME/.yt/token}" -e YT_TOKEN="$YT_TOKEN" -e YT_PROXY="$YT_PROXY" -v $HOME:$HOME -it my_cluster_image:latest bash -ic "python $PWD/my_script.py" ``` ### Using Python in an open-source environment { #python_open_source_environment } In open-source installations of YTsaurus, if you want to ensure that your local Python operation and local environment are correctly pickled and run successfully on the cluster, the local environment and the remote job environment should be as similar as possible. For Python operations, we strongly recommend the following configuration: - Always specify a Docker image for the user job. - Always enable `ignore_system_modules` in the Python wrapper config: ```python yt.config["pickling"]["ignore_system_modules"] = True ``` The `ignore_system_modules` option prevents the wrapper from packaging Python modules that are considered system modules on the client machine. In open-source environments, relying on host system modules often leads to hard-to-diagnose discrepancies between the local machine and the job environment. Example: ```python spec_builder = yt.spec_builders.MapSpecBuilder() \ .begin_mapper() \ .command(MyMapper()) \ .docker_image("docker.io/library/python:3.11") \ .end_mapper() \ .input_table_paths(["//tmp/in"]) \ .output_table_paths(["//tmp/out"]) yt.config["pickling"]["ignore_system_modules"] = True yt.run_operation(spec_builder) ``` The Python version in the Docker image and the local Python version must match. This is especially important for serialized Python code, imported modules, and native extensions. We also strongly recommend using the same operating system environment locally and in the Docker image. For example, if the Docker image is built on Ubuntu 24.04, you should also use Ubuntu 24.04 locally. The setup might still work with a different local environment, but the likelihood of issues with packaging dependencies or execution increases. ### Running a local script inside Docker with `respawn_in_docker` { #respawn_in_docker } The wrapper provides the `respawn_in_docker` decorator, which restarts the local entry script inside a Docker container before submitting the operation. ```python import yt.wrapper as yt @yt.respawn_in_docker("docker.io/library/python:3.11") def main(): yt.config["pickling"]["ignore_system_modules"] = True yt.run_map(MyMapper(), "//tmp/in", "//tmp/out") if __name__ == "__main__": main() ``` This approach helps eliminate discrepancies between the local runtime environment and the remote job environment because: - The script is rerun inside the specified Docker image. - Wrapper operations automatically inherit the same image via `YT_BASE_LAYER`. - The container mounts the script directory, the current working directory, the home directory, and the paths to Python libraries. This approach has important limitations: - Docker must be available on the local machine. - By default, only environment variables with the `YT_` prefix are passed to the container. If your script depends on other environment variables, pass them explicitly via the `env` argument. - The image must contain a compatible Python interpreter. By default, `respawn_in_docker` runs `python3` inside the container, but you can override this via the `python` argument. - The local paths that are mounted into the container must be accessible to Docker. - The decorator aligns the local startup environment with the job image, but it doesn’t eliminate all possible host-specific differences. Use `respawn_in_docker` when you need maximum assurance that the script submitting the operation runs in the same Docker environment as the jobs themselves. This is the preferred approach when Docker is available locally. ## Untyped Python operations { #python_operations_untyped } In addition to typed jobs, untyped jobs are also supported. Instead of a `TypedJob` subclass, the user job is a Python function or any callable object. The function accepts a record as an argument (in the case of a mapper) or a key plus an iterator over records (in the case of a reducer). In the latter case, the key is a `dict`-like object where only the key columns are populated. The function itself must be a generator that yields records to be written to the output table. Example of a filtering mapper: ```python def mapper(row): if row.get("type") == "job_started": yield row ``` Example of a reducer that calculates the sum of the `value` column: ```python def reducer(key, rows): row = dict(key.iteritems()) row["sum"] = sum((row["value"] for row in rows)) yield row ``` ### Decorators { #python_decorators_untyped } In the untyped API, the meaning of some decorators changes slightly compared to the [typed one](https://ytsaurus.tech/docs/en/#python_decorators). - [reduce_aggregator](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.py_wrapper.reduce_aggregator) — the reducer receives not a single (key, records) pair but an iterator of pairs, where each pair is (key, records with that key). Note that decorators are implemented by setting an attribute on the function. Therefore, for example, you can’t define a function with a decorator and then wrap it with `functools.partial`. If you want to pass some parameters to the function directly at call time, create a class with the decorator (see the last example below). You can find examples in the [tutorial](https://ytsaurus.tech/docs/en/examples.md#job_decorators_untyped). ### Formats { #python_formats } By default, you don’t need to specify a format when working with Python operations. YTsaurus stores structured data; it has no predefined text representation. All jobs run in streaming mode, and you must explicitly specify the form in which your script expects to see data in the input stream. In contrast, when working with data in a Python function, you also get it in structured form. You can map the structured data representation in YTsaurus to Python, and the intermediate format used to transfer data to the job becomes less important. It’s important to understand that there’s no perfect one-to-one mapping between structured data in YTsaurus and Python dicts, so there are various specifics that will be discussed below. #### Structured data representation { #structured_data } There are the following important specifics of representing structured data in Python: - Objects in table records in YTsaurus can have [attributes](https://ytsaurus.tech/user-guide/storage/attributes.md) (this applies only to columns of type any). To represent them, the library defines [special types](http://pydoc.ytsaurus.tech/yt.yson.html#module-yt.yson.yson_types) that inherit from standard Python types but also have an `attributes` field. Since creating custom objects is expensive, by default such an object is created only if it has attributes. To control this behavior, there’s the `always_create_attributes` option. YSON type comparison works as follows: first, the values of the base types are compared, then the attributes are compared. If the attributes aren’t equal (including cases where one object has attributes and the other doesn’t), the objects are considered unequal. Keep this in mind when comparing with base Python types: to avoid dependence on the object’s attributes, explicitly cast the object to the base type. Illustrative example: ```python import yt.yson as yson s = yson.YsonString(b"a") s == b"a" # True s.attributes["b"] = "c" s == b"a" # False bytes(s) == b"a" # True other_s = yson.YsonString(b"a") other_s == s # False other_s.attributes["b"] = "c" other_s == s # True ``` - The YSON format has two integer types: [int64 and uint64](https://ytsaurus.tech/user-guide/storage/yson.md). In Python, from a data model perspective, there’s only one type with no restrictions. Therefore, when reading data, the unsigned type is represented as `YsonUint64`, while the signed type is represented as a regular `int`. When writing ints, automation handles it: numbers in the range [-2^63, 2^63) are represented as signed, and numbers in the range [2^63, 2^64) as unsigned. But you can always explicitly specify the type by creating a YSON object directly. - Unicode strings. Since all strings in YTsaurus are byte strings, Python Unicode strings are by default encoded into UTF‑8 byte strings. When reading in Python 3, the system attempts to decode byte strings using the UTF‑8 decoder; if this fails, it returns a special [YsonStringProxy](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.yson_types.YsonStringProxy) object. For more details, see the corresponding [section](https://ytsaurus.tech/docs/en/#python3_strings). When writing data, `YsonFormat` correctly distinguishes between elementary Python types and YSON types. #### Control attributes { #control_attributes } In addition to the record stream, when reading from a table (or in a job), you can request various [control attributes](https://ytsaurus.tech/user-guide/storage/io-configuration.md#control_attributes). Working with control attributes depends on the format; for example, in formats other than Yamr, JSON, and YSON, most control attributes aren’t representable. In the Yamr format, the Python API correctly supports parsing the `row_index` and `table_index` attributes, which will be represented as the `tableIndex` and `recordIndex` fields of `Record` objects. In the JSON format, no special processing of control attributes is performed, so if you request them, you’ll need to handle the control records in the stream yourself. In the YSON format, there are several modes for automatic processing of control attributes. The mode is controlled by the `control_attributes_mode` option, which can take the following values (important: for historical reasons, for the option to work correctly, you must also specify `process_table_index=None` when creating the format; by default, `process_table_index` is `True`, which forces `control_attributes_mode=row_fields`): - `iterator` (default) — when parsing the stream, the format returns an iterator over records; to get control attributes in the job, you’ll need to use the context. - `row_fields` — requested control attributes will be written as fields on each record. For example, if `row_index` is requested, each record will have an `@row_index` field containing that record’s number. - `none` — no special processing of control attributes will be performed, meaning the client will receive a record stream that includes entity records with control attributes. You can find examples of switching between output tables with `table_index` when `control_attributes_mode` is set to `iterator` and `row_fields`, as well as an example of getting the current table row index in a reducer using `context`, [here](https://ytsaurus.tech/docs/en/examples.md#table_switches_untyped). #### Other formats { #other_formats } By default, the library performs serialization via the [YSON format](https://ytsaurus.tech/user-guide/storage/formats.md#yson), as it provides a correct and unambiguous representation of any data stored in YTsaurus tables. To work efficiently with the YSON format in Python, you need the [native library](https://ytsaurus.tech/docs/en/#yson_bindings). If this library isn’t available on the cluster, you’ll get an error when running the job (there’s no fallback to the Python library for YSON because it’s very slow and inefficient). In this situation, you can switch to using the [JSON format](https://ytsaurus.tech/user-guide/storage/formats.md#json). Most other formats are text-based (meaning numbers aren’t distinguished from strings), so you’ll lose type information. When using formats other than YSON and Yamr, you’ll always receive `dict` as records. For example, no automatic conversion from the JSON representation of records to the YSON representation will occur. ## Other { #other } ### gRPC { #grpc } There’s no support for RPC proxy using the gRPC transport. If you can’t install the binary package with RPC bindings, you can use the gRPC transport yourself. ### YSON { #yson } Along with the library for working with YTsaurus clusters, a library for working with the [YSON format](https://ytsaurus.tech/user-guide/storage/yson.md) is also provided. The library is available in the `yt.yson` module and implements the standard `load`, `loads`, `dump`, and `dumps` functions. It also provides access to [YSON types](https://ytsaurus.tech/docs/en/#structured_data). Additionally, the library implements the following useful functions: - [to_yson_type](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.convert.to_yson_type) — creates a YSON type from a Python object. - [json_to_yson](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.convert.json_to_yson) — recursively converts a Python object from its JSON representation (see the specifics of representing structured data in YTsaurus in the [JSON format](https://ytsaurus.tech/user-guide/storage/formats.md#json)) to its YSON representation. - [yson_to_json](https://pydoc.ytsaurus.tech/yt.yson.html#yt.yson.convert.yson_to_json) — recursively converts a Python object from its YSON representation to its JSON representation. #### YSON bindings { #yson_bindings } The YSON library has two implementations: pure Python and C++ bindings. The native YSON parser and writer written in Python are very slow and are suitable only for working with small amounts of data. **Important**: in particular, you won’t be able to run operations or read tables in the YSON format. C++ bindings are provided as Debian and pip packages. The packages are built as a universal .so library that includes libcxx, so they should work on any Debian system. #### Cluster client config { #remote_client_config } Some client settings are delivered from the cluster at the moment of initialization. - To see what settings come from the cluster: `yt show-default-config --proxy --only-remote-patch`. - To completely disable applying cluster settings, use the environment variable: `YT_APPLY_REMOTE_PATCH_AT_START=none`. How to add a config to the cluster: - Create a `default` node of type **document** in the `//sys/client_config` directory (`//sys/client_config/default`). - Add the required parameters to the document (see the parameters supported in `default_config.py` — they’re initialized via `RemotePatchable`). - You can verify that the config is applied in the client’s debug logs (`YT_LOG_LEVEL=DEBUG`) and via the CLI — `show-default-config`. # Data classes {% note warning "Attention" %} Data classes are only supported in Python 3. {% endnote %} The main method used to represent table rows is to create classes with fields marked by types (similar to [dataclasses](https://docs.python.org/3/library/dataclasses.html)). See also the [example](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/dataclass_typed). ## Motivation { #motivation } 1. Convenience: since the YTsaurus scalar type system is richer than in Python, marking by types helps explicitly express the desired column types. The convenience can be seen even better when working with composite types, such as structures. 2. Strong typing helps you detect errors in the code before they result in incorrect data and other negative effects. To do this, linters and IDEs can be used. 3. Speed: due to the [Skiff](https://ytsaurus.tech/user-guide/storage/skiff.md) format and some other optimizations, CPU consumption is several times lower than for untyped data. ## Installation To work with the `yt.wrapper.schema` module, install the packages: * `typing_extensions` * `ytsaurus-yson` * `six` ## Introduction { #introduction } To work with tabular data, you must declare a class with the `yt.wrapper.yt_dataclass` decorator and mark its fields with types. The field type comes after the colon. It can be: - Python built-in types: `int`, `float`, `str`, and others. - Custom classes with the `@yt_dataclass` decorator. - Composite types from the [typing](https://docs.python.org/3/library/typing.html) module. `List`, `Dict`, `Optional`, and `Tuple` are currently supported. We're also planning to add support for a number of other types in the future. - Special types from `yt.wrapper.schema`: `Int8`, `Uint16`, `OtherColumns`, and others (a more complete list is given [below](https://ytsaurus.tech/docs/en/#types)). Example: ```python @yt.wrapper.yt_dataclass class Row: id: int name: str is_robot: bool = False ``` For the class described in this way, a constructor and other service methods will be generated. In particular, `__eq__` and `__repr__`. You can specify a default value for some fields. It will get into the constructor signature. You can create objects of this class as follows: `row = Row(id=123, name="foo")`. For all the fields without default values (such as `robot: bool = False`), you need to pass relevant fields to the constructor. Otherwise, it will throw an exception. The data classes support inheritance. When you have a class like this, you can: 1. Create a table with a relevant schema (you can just start writing to an empty or a non-existent table or use the `TableSchema.from_row_type()` function). 2. Write and read [tables](https://ytsaurus.tech/docs/en/userdoc.md#table_commands), [example](https://ytsaurus.tech/docs/en/examples.md#read_write). 3. Run [operations](https://ytsaurus.tech/docs/en/userdoc.md#python_operations), [example](https://ytsaurus.tech/docs/en/examples.md#simple_map). Data classes can also be created based on table schemas explicitly or automatically, by reading structured data. ```python yt_table_schema = client.get(f"{table_path}/@schema") dataclass_type = yt.schema.make_dataclass_from_table_schema(yt.schema.TableSchema.from_yson_type(yt_table_schema)) # or typed_table_data = list(client.read_table_structured(table=table_path, row_type=None)) ``` ## Special types { #types } | `yt.wrapper.schema` | Python | Diagram | |-------------------------------|----------------|-------------| | `Int8,` `Int16`, `Int32`, `Int64` | `int` | `int8`, `int16`, `int32`, `int64` | | `Uint8`, `Uint16`, `Uint32`, `Uint64` | `int` | `uint8`, `uint16`, `uint32`, `uint64` | | `YsonBytes` | `bytes` | `yson`/`any` | | `OtherColumns` | `OtherColumns` | Corresponds to several columns. | # Python API with examples Before using the examples, read the [instructions](https://ytsaurus.tech/user-guide/storage/auth.md) for obtaining the token. We recommend running all programs on `Linux`. ## Basic level { #base } The modern method of working with the Python API is typed. Data stored in tables and processed by operations is represented in the code by [classes](https://ytsaurus.tech/docs/en/userdoc.md#dataclass) with typed fields (similar to [dataclasses](https://docs.python.org/3/library/dataclasses.html)). An unrecommended (but sometimes unavoidable, especially when working with old tables) method is untyped when table rows are represented by dicts. This method is much slower, fraught with errors, and inconvenient when working with composite types. Therefore, this section of the documentation provides examples of working with the typed API and the untyped examples can be found in the [corresponding section](https://ytsaurus.tech/docs/en/#untyped_tutorial). ### Reading and writing tables { #read_write } YTsaurus enables you to write data to tables, as well as to append data to the end of existing tables. Several modes are available for reading tables: reading the entire table, reading individual ranges by row number or key. The example is located at [yt/python/examples/table_read_write_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/table_read_write_typed). ### Simple map { #simple_map } Suppose there is a table with the login, name, and uid columns at //home/dev/tutorial/staff_unsorted. You need to make a table with email addresses, calculating them from the login: `email = login + "@ytsaurus.tech"`. A simple mapper is suitable for this job. The example is located at [yt/python/examples/simple_map_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/simple_map_typed) ### Sorting a table and a simple reduce operation { #sort_and_reduce } Using the same table as in the previous example, you can calculate the statistics: how many times you can see this or that name or which is the longest username among all people with the same name. A Reduce operation is well suited for this job. Since the Reduce operation can only be run on sorted tables, you must first sort the original table. The example is located at [yt/python/examples/simple_reduce_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/simple_reduce_typed). ### Reduce with multiple input tables { #reduce_multiple_output } Suppose there is another table besides the table with users and it records which user is a service one. The is_robot field can be true or false. The following program generates a table in which only service users remain. The example is located at [yt/python/examples/multiple_input_reduce_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/multiple_input_reduce_typed). ### Reduce with multiple input and output tables { #reduce_multiple_input_output } This example repeats the previous one, with the difference that two output tables will be written to at once, both with human users and service users. The example is located at [yt/python/examples/multiple_input_multiple_output_reduce_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/multiple_input_multiple_output_reduce_typed). ### Table schemas { #table_schema } All tables in YTsaurus have a [schema](https://ytsaurus.tech/docs/en/userdoc.md#table_schema). The code demonstrates examples of working with the table schema. The example is located at [yt/python/examples/table_schema_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/table_schema_typed). ### MapReduce { #map_reduce } YTsaurus implements the MapReduce merge operation that works faster than Map + Sort + Reduce. Let's try to use the table with users once again to calculate the statistics of how many times you can see this or that name. Before calculating the statistics, we will normalize the names by converting them to lowercase so that people with the names `ARCHIE` and `Archie` are merged in our statistics. The example is located at [yt/python/examples/map_reduce_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/map_reduce_typed). ### MapReduce with multiple intermediate tables { #map_reduce_multiple_intermediate_streams } The intermediate data (between the map and reduce stages) in a MapReduce operation can "flow" in multiple streams and have different types. In this example, there are two tables on the operation input: the first displays uid in the name and the second contains information about the events associated with the user with that uid. The mapper selects events like "click" and sends them to one output stream, and all users to another. The reducer counts all clicks on this user. The example is located at [yt/python/examples/map_reduce_multiple_intermediate_streams_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/map_reduce_multiple_intermediate_streams_typed). ### Decorators for job classes { #job_decorators } You can mark functions or job classes with special decorators that change the expected interface of interaction with jobs. Examples of decorators: `with_context`, `aggregator`, `reduce_aggregator`, `raw`, or `raw_io`. You can find a full description in the [documentation](https://ytsaurus.tech/docs/en/userdoc.md#python_decorators) The example is located at [yt/python/examples/job_decorators_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/job_decorators_typed). ### Working with files on the client and in operations { #files } For more information, see the [documentation](https://ytsaurus.tech/docs/en/userdoc.md#file_commands). For more information about files in Cypress, see the [section](https://ytsaurus.tech/user-guide/storage/files.md). The example is located at [yt/python/examples/files_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/files_typed). ### Grep { #grep } The typed API enables you to work with fairly arbitrary data using a single data class and a single operation class. As an example, let's consider the job of filtering a table based on a regular expression matching a given row field. The example is located at [yt/python/examples/grep_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/grep_typed). ## Advanced level { #advanced } ### Batch queries { #batch_queries } You can execute "light" queries (create/delete a table, check its existence, and others) in groups. It is reasonable to use this method if you need to perform a large number of single-type operations: batch queries can significantly save execution time. The example is located at [yt/python/examples/batch_client](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/batch_client). ### Using the RPC { #rpc } Using the RPC with the CLI. ```bash yt list / --proxy cluster_name --config '{backend=rpc}' cooked_logs home ... ``` A similar example (full code at [yt/python/examples/simple_rpc](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/simple_rpc)) can be built in Python (with ya-make build): Pay attention to the additional `PEERDIR(yt/python/client_with_rpc)` in `ya.make`. You can work with dynamic tables using the RPC. The example is located at [yt/python/examples/dynamic_tables_rpc](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/dynamic_tables_rpc). ### Specifying row types using prepare_operation { #prepare_operation } In addition to using type hints, the `prepare_operation` method where all types are specified using special methods can be defined to specify Python table row types. If there is a `prepare_operation` method in the job class, the library will use the types specified inside the method and there will be no attempts to derive row types from type hints. The example is located at [yt/python/examples/prepare_operation_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/prepare_operation_typed). ## Different examples { #misc } ### Data classes { #dataclass } This example demonstrates the features and peculiarities of working with [data classes](https://ytsaurus.tech/docs/en/userdoc.md#dataclass) in more detail. The example is located at [yt/python/examples/dataclass_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/dataclass_typed). ### Context and managing writes to output tables { #table_switches } To select which output table to write a row to, you must use the `OutputRow` wrapper class, namely the `table_index` argument of its constructor. Having any iterator on data classes (returned from `read_table_structured()` or passed to the `__call__()` method of the job), you can use the `.with_context()` method to make an iterator on pairs `(row, context)` from it. The `context` object has the `.get_table_index()`, `.get_row_index()`, and `.get_range_index()` methods. When writing a job class to whose `__call__()` method a separate row (for example, mappers) is passed rather than an iterator, you can add the `@yt.wrapper.with_context` decorator to the class. In this case, the `__call__()` method must take a third `context` argument (see the [documentation](https://ytsaurus.tech/docs/en/userdoc.md#python_decorators)). In the reducer, mapper aggregators, and when reading tables — whenever there is an iterator on rows, use the `with_context` method of the iterator. The example is located at [yt/python/examples/table_switches_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/table_switches_typed). ### Spec builders { #spec_builder } Use the [spec builder](https://ytsaurus.tech/docs/en/userdoc.md#spec_builder) to describe the specification of operations to avoid errors. The example is located at [yt/python/examples/spec_builder_typed](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/spec_builder_typed). ### Using gevent { #gevent } The documentation is contained [in the separate section](https://ytsaurus.tech/docs/en/userdoc.md#gevent). The example is located at [yt/python/examples/gevent](https://github.com/ytsaurus/ytsaurus/tree/main/yt/python/examples/gevent). # YTsaurus Airflow Provider [Apache Airflow](https://airflow.apache.org/) is a platform for creating, scheduling, and monitoring workflows. YTsaurus Airflow Provider enables you to integrate Airflow with YTsaurus and efficiently manage data processing tasks. This section provides instructions on how to connect to a YTsaurus cluster in Apache Airflow and how to use operators to interact with Cypress, work with static tables, and execute queries in [Query Tracker](https://ytsaurus.tech/user-guide/query-tracker/about.md). ## Installation {% note info %} The current package version requires: - Python 3.8+ - Airflow 2.9.0+ {% endnote %} 1. Install the Python client from the PyPi repository: ```bash pip install ytsaurus-airflow-provider ``` {% cut "If you encounter the error 'Building wheel for xmlsec (pyproject.toml) did not run successfully.'" %} Install the packages required to build `xmlsec`: ```bash sudo apt install libxmlsec1-dev pkg-config build-essential python3-dev ``` {% endcut %} 2. Verify that the provider was installed successfully: ```bash $ python3 -c "import ytsaurus_airflow_provider; print(ytsaurus_airflow_provider.__version__)" 0.1.1 ``` As a result, the following will be available: - Python library for writing DAGs. - Provider for [Apache Airflow](https://airflow.apache.org). - `ytsaurus-client` and `ytsaurus-yson` tools. ## Getting started { #howtostart } In the Apache Airflow web interface, create a connection to your YTsaurus cluster: 1. **Admin** -> **Connections** -> **+** 2. **Connection Id**: `ytsaurus_cluster_default` 3. **Connection Type**: YTsaurus Cluster 4. **Cluster Proxy**: Cluster proxy 5. **Cluster Token**: Cluster connection token 6. **Extra**: Optionally, you can specify [Client Config](https://ytsaurus.tech/docs/python/userdoc#configuration): ``` { "client_config": { "create_table_attributes": { "compression_codec": "brotli_3" } } } ``` If necessary, you can create a connection to the object storage: 1. **Admin** -> **Connections** -> **+** 2. **Connection Id**: `aws_default` 3. **Connection Type**: Amazon Web Service 4. **AWS Access Key ID** and **AWS Secret Access Key**: Access keys for the object storage #### How to specify a connection { #selectconn } Each operator accepts the optional `ytsaurus_conn_id` parameter, which by default is set to `ytsaurus_cluster_default`. If you need to use a different connection, you should specify it in the `ytsaurus_conn_id` parameter. ## Operators { #operators } All operators, except for the read operator with a specified ObjectStorage, write data to XCom. It is not recommended to pass large volumes of data through XCom. You can find out below which operators write data to which XComs. {% note info "Note" %} All operators use the Python SDK. If an error occurs during operator execution, see the [Python SDK documentation](https://ytsaurus.tech/docs/python/userdoc.md). {% endnote %} ### Working with Cypress { #operators_cypress } #### GetOperator Operator for retrieving the contents of a Cypress node. It implements the [get](https://ytsaurus.tech/docs/commands.md#get) functionality from the Python SDK. **Parameters:** - `path: str | YPath` - `max_size: None | int = None` - `attributes: None | dict[str, Any] = None` - `read_from: None | str = None` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `path` - `return_value` #### SetOperator Operator for writing new contents to a Cypress node. It implements the [set](https://ytsaurus.tech/docs/commands.md#set) functionality from the Python SDK. **Parameters:** - `path: str | YPath` - `value: Any` - `set_format: None | str | Format = None` - `recursive: bool = False` - `force: None | bool = None` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `path` #### RemoveOperator Operator for removing a Cypress node. It implements the [remove](https://ytsaurus.tech/docs/commands.md#remove) functionality from the Python SDK. **Parameters:** - `path: str | YPath` - `recursive: bool = False` - `force: bool = False` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `path` #### CreateOperator Operator for creating an empty Cypress node of the `node_type` with `attributes`. It implements the [create](https://ytsaurus.tech/docs/commands.md#create) functionality from the Python SDK. **Parameters:** - `node_type: Literal["table", "file", "map_node", "document", "string_node", "int64_node", "uint64_node", "double_node", "boolean_node", "link"]` - `path: str | YPath` - `recursive: bool = False` - `ignore_existing: bool = False` - `lock_existing: None | bool = None` - `force: None | bool = None` - `attributes: None | dict[str, Any] = None` - `ignore_type_mismatch: bool = False` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `object_id` - `path` #### ListOperator Operator for getting a list of descendants of the `path` node. The `absolute` option enables the output of absolute paths instead of relative paths. It implements the [list](https://ytsaurus.tech/docs/commands.md#list) functionality from the Python SDK. **Parameters:** - `path: str | YPath` - `max_size: None | int = None` - `absolute: None | bool = None` - `attributes: None | dict[str, Any] = None` - `sort: bool = True` - `read_from: None | str = None` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `path` - `return_value` ### Working with tables { #operators_tables } #### WriteTableOperator Operator for writing data to a static table. It implements the [write_table](https://ytsaurus.tech/docs/commands.md#write_table) functionality from the Python SDK. Input data can be passed through the `input_data` parameter or the `object_storage_path` parameter, which is an object of the [ObjectStorage](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/objectstorage.html) type. You cannot pass both `input_data` and `object_storage_path` simultaneously. When writing data from the object storage, you need to specify the corresponding `object_storage_format` (see [Table data representation formats](https://ytsaurus.tech/user-guide/storage/formats#table_formats)). **Parameters:** - `path: str | YPath` - `input_data: Any | None = None` - `object_storage_path: None | UPath = None` - `object_storage_format: None | str | Format = None` - `table_writer: dict[str, Any] | None = None` - `max_row_buffer_size: int | None = None` - `force_create: None | bool = None` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `path` - `object_storage_path` (if writing through `object_storage_path`) #### ReadTableOperator Operator for reading data from a static table. It implements the [read_table](https://ytsaurus.tech/docs/commands.md#read_table) functionality from the Python SDK. This operator can write data to the object storage if you pass the `object_storage_path` parameter, which is an object of the [ObjectStorage](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/objectstorage.html) type. Otherwise, the table will be written to XCom. When writing data to the object storage, you need to specify the desired `object_storage_format` (see [Table data representation formats](https://ytsaurus.tech/user-guide/storage/formats#table_formats)). **Parameters:** - `path: str | YPath` - `object_storage_path: None | UPath = None` - `object_storage_format: None | str | Format = None` - `table_reader: None | dict[str, Any] = None` - `control_attributes: None | dict[str, Any] = None` - `unordered: None | bool = None` - `response_parameters: None | dict[str, Any] = None` - `enable_read_parallel: None | bool = None` - `omit_inaccessible_columns: None | bool = None` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `path` - `object_storage_path` (if writing to `object_storage_path`) - `return_value` (if writing to XCom) ### Query Tracker { #operators_query_tracker } #### RunQueryOperator Operator for executing queries in Query Tracker with different syntax. You can learn more about Query Tracker [here](https://ytsaurus.tech/user-guide/query-tracker/about.md). It implements the [run_query](https://pydoc.ytsaurus.tech/yt.wrapper.html#yt.wrapper.client_impl.YtClient.run_query) functionality from the Python SDK. This operator can be run in asynchronous mode, then it will not wait for the query to complete. Query execution results can be written to XCom and the object storage. If the query returns three results, and `object_storage_paths` are passed as `[None, ObjectStorage("s3://bucket/path"), None]`, then results No. 1 and 3 will be written to XCom, and result No. 2 will be written to the object storage. **Parameters:** - `engine: Literal["ql", "yql", "chyt", "spyt"]` - `query: str` - `settings: None | dict[str, Any] | yt.yson.yson_types.YsonType = None` - `files: None | list[dict[str, Any]] | list[yt.yson.yson_types.YsonType] = None` - `stage: None | str = None` - `annotations: None | dict[str, Any] | yt.yson.yson_types.YsonType = None` - `access_control_objects: None | list[str] = None` - `sync: bool = True` - `object_storage_paths: list[None | UPath] | None = None` - `ytsaurus_conn_id: str = "ytsaurus_cluster_default"` **XComs:** - `meta` - `query_id` - `result_{i}`, where i is the number of the returned result, counted from 0. If `object_storage_paths[i]` is passed for the i-th result, the result will not be written to `result_{i}`. ## Examples { #examples } Examples of working with operators are presented in ready-made DAGs [example_dags](https://github.com/ytsaurus/ytsaurus-airflow-provider/tree/main/ytsaurus_airflow_provider/example_dags). # Description To use the features of YTsaurus, you can use the [C++ client](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce). See [Usage examples](https://ytsaurus.tech/docs/en/examples.md). {% note warning %} The C++ client is currently provided as is, so: - The client interface may undergo non-backward compatible changes. - The current version only supports static linking. - Contact info@ytsaurus.tech if you have any questions about building the client. {% endnote %} ## Overview - The entire code is in the `NYT` namespace. - All the interfaces that the user might need can be found [here](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/interface). - The implementation of all the interfaces is available [here](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/client). Libraries need to be linked with `interface`, and executable binaries must be linked with `client`. - `ISomethingPtr` is usually defined as `TIntrusivePtr` for interfaces. - All mandatory parameters are explicitly present in the signatures of the corresponding functions. All optional command parameters are presented as auxiliary structures within `TOptions`. These structures have a builder-like interface that allows for specifying parameters as `TWhateverOptions().Parameter1(value1).Parameter2(value2)` for single-line writing, if convenient. ## Logging { #logging } Logging is toggled with the `YT_LOG_LEVEL` environment variable, which can take one of the following values: `ERROR`, `WARNING` (equivalent to `ERROR` for compatibility with the [Python API](https://ytsaurus.tech/docs/python/userdoc.md)), `INFO`, `DEBUG`. In addition, you need to call the `NYT::Initialize()` function at the start of the program (see the next section). If possible, saving logs at the `DEBUG` level is recommended, especially for production processes. Should there be any issues with the API, such logs will make fixing them much easier and faster. Attaching these logs to emails about API-related problems is recommended. ## Initializing { #init } You need to call the `Initialize` method before starting client actions. This method performs initialization of required subsystems (logging and so on), as well as checks if binary is launched on the YTsaurus node inside the job and, if it is the case, switches execution to the job mode. The client entry point is the `CreateClient(serverName)` function, which returns a pointer to the `IClient` interface. Example of a minimum program (`main.cpp` file): ```c++ #include int main() { NYT::Initialize(); auto client = NYT::CreateClient("cluster_name"); client->Create("//tmp/table", NYT::NT_TABLE); } ``` If you need to have clients working under different [authorization tokens](https://ytsaurus.tech/user-guide/storage/auth.md) within the same program, you can use the following options: ``` auto client = CreateClient(serverName, TCreateClientOptions().Token(userToken)); ``` ## Transactions { #transactions } There are two interfaces (`IClient` and `ITransaction`) that have almost the same set of methods inherited from `IClientBase`. In the first case, the corresponding commands are executed without any client transaction; in the second case, they are executed under a transaction of the specified class. To get an `ITransaction` instance, use the `IClientBase::StartTransaction()` method. When using the `YT_TRANSACTION` environment variable, even`IClient` starts working under the parent transaction. Example of working with transactions: ```c++ #include int main() { NYT::Initialize(); auto client = NYT::CreateClient("cluster_name"); auto tx = client->StartTransaction(); tx->Remove("//tmp/table"); tx->Commit(); } ``` The transaction has explicit methods `Commit()` and `Abort()`. If the transaction is destroyed without one of these methods being explicitly called, an attempt will be made in the destructor to call `Abort()`, but without guarantees that the method will actually execute. Transactions can be hierarchical: ```c++ auto tx = client->StartTransaction(); auto innerTx = tx->StartTransaction(); innerTx->Create("//tmp/double", NT_DOUBLE); innerTx->Commit(); tx->Commit(); ``` The `ITransaction::GetId()` method returns `TGUID` — the ID of this transaction. The transaction created using `StartTransaction()` is automatically pinged and managed by the C++ client. There is a way to work under an externally created transaction. To do this, call the `IClient::AttachTransaction(transactionId)` method. All commands called in such an object will be executed in the context of the transaction with this ID, but this transaction will not be pinged by the client. For more information on transactions, see the [Transactions](https://ytsaurus.tech/user-guide/storage/transactions.md) section. ## TNode { #tnode } [TNode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/interface/node.h) is the main class that provides for a dynamic DOM representation of [YSON documents](https://ytsaurus.tech/user-guide/storage/yson-docs.md). It is used for working with Cypress, transferring additional operation specifications, as one of the ways to encode table rows, and more. Usage examples: ```c++ TNode i = 1; // all signed types -> int64 TNode u = 8u; // all unsigned types -> uint64 TNode b = true; // boolean TNode d = 2.5; // double TNode s = "foo"; // string TNode l = TNode::CreateList(); l.Add(5); l.Add(false); Cout << l[1].AsBool() << Endl; TNode m = TNode::CreateMap(); m["abc"] = 255u; m("foo", "bar"); Cout << m["foo"].AsString() << Endl; TNode e = TNode::CreateEntity(); if (e.IsEntity()) { Cout << "entity!" << Endl; } ``` - Values of primitive types are retrieved using the `TNode::AsType()` methods. - Types can be checked using the `TNode::IsType()` methods. By default, the constructor creates `TNode` with the `TNode::Undefined` type: such an object cannot be serialized in YSON without explicit initialization. For shortening purposes, the methods for inserting in map and list immediately change `TNode` to the right type: ```c++ auto mapNode = TNode()("key1", "value1")("key2", "value2"); auto listNode = TNode().Add(100).Add(500); ``` ## Working with Cypress { #cypress } For more information about the commands, see [Working with the meta information tree](https://ytsaurus.tech/user-guide/storage/cypress-example.md). Implemented as the `ICypressClient` interface, from which `IClientBase` is inherited. Usage examples: ```c++ TString node("//tmp/node"); client->Create(node, NYT::NT_STRING); Cout << client->Exists(node) << Endl; client->Set(node, "foo"); Cout << client->Get(node).AsString() << Endl; TString otherNode("//tmp/other_node"); client->Copy(node, otherNode); client->Remove(otherNode); client->Link(node, otherNode); ``` Example of working with table attributes: ```c++ client->Set("//tmp/table/@user_attr", 25); Cout << client->Get("//tmp/table/@row_count").AsInt64() << Endl; Cout << client->Get("//tmp/table/@sorted").AsBool() << Endl; ``` ### Create [Create](https://ytsaurus.tech/docs/commands.md#create) a node. [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#create). ```c++ TNodeId Create( const TYPath& path, ENodeType type, const TCreateOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `path` | `TYPath` | - | Node path | | `type` | `ENodeType` | - | [Type](https://ytsaurus.tech/user-guide/storage/objects.md#object_types) of node created:
`NT_STRING` — string (`string_node`);
`NT_INT64` — signed integer (`int64_node`);
`NT_UINT64` — unsigned integer (`uint64_node`);
`NT_DOUBLE` — real number (`double_node`);
`NT_BOOLEAN` — Boolean value (`boolean_node`);
`NT_MAP` — map in Cypress (keys are strings, and values are other nodes, `map_node`);
`NT_FILE` — [file](https://ytsaurus.tech/user-guide/storage/objects.md#files) (`file`);
`NT_TABLE` — [table](https://ytsaurus.tech/user-guide/storage/objects.md#tables) (`table`);
`NT_DOCUMENT` — [document](https://ytsaurus.tech/user-guide/storage/objects.md#yson_doc) (`document`);
`NT_REPLICATED_TABLE` — [replicated table](https://ytsaurus.tech/user-guide/dynamic-tables/replicated-dynamic-tables.md) (`replicated_table`);
`NT_TABLE_REPLICA` — (`table_replica`); | | `options` — optional settings: | | | | | `Recursive` | `bool` | `false` | Whether to create intermediate directories. | | `IgnoreExisting` | `bool` | `false` | If the node exists, do nothing and show no error. | | `Force` | `bool` | `false` | If the node exists, re-create it instead of an error. | | `Attributes` | `TNode` | none | [Attributes](https://ytsaurus.tech/user-guide/storage/attributes.md) of the created node. | ### Remove [Remove](https://ytsaurus.tech/docs/commands.md#remove) the node. [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#remove). ```c++ void Remove( const TYPath& path, const TRemoveOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `path` | `TYPath` | - | Path to the node. | | `options` — optional settings: | | | | | `Recursive` | `bool` | `false` | Recursively remove the children of a composite node. | | `Force` | `bool` | `false` | Do not stop if the specified node is no longer there. | ### Exists [Checking the existence](https://ytsaurus.tech/docs/commands.md#exists) of the object at the specified path. ```c++ bool Exists( const TYPath& path) ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `path` | `TYPath` | - | Path to the node. | | `options` — optional settings | | | | ### Get [Get the contents of the Cypress node](https://ytsaurus.tech/docs/commands.md#get) in `TNode` format. [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#get). ```c++ TNode Get( const TYPath& path, const TGetOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `path` | `TYPath` | - | Path to the node or attribute. | | `options` — optional settings: | | | | | `AttributeFilter` | `TMaybe` | none | List of attributes to be obtained. | | `MaxSize` | `TMaybe` | none | Limit on the number of children (for composite nodes). | ### Set [Write new Cypress node content.](https://ytsaurus.tech/docs/commands.md#set) [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#set). ```c++ void Set( const TYPath& path, const TNode& value, const TSetOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `path` | `TYPath` | - | Path to the node or attribute. | | `value` | `TNode` | - | Written subtree (content). | | `options` — optional settings: | | | | | `Recursive` | `bool` | `false` | Whether to create intermediate nodes. | ### List [Get a list of the node's descendants](https://ytsaurus.tech/docs/commands.md#list). [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#list). ```c++ TNode::TListType List( const TYPath& path, const TListOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `path` | `TYPath` | - | Path to the node or attribute. | | `options` — optional settings: | | | | | `AttributeFilter` | `TMaybe` | none | List of attributes to be received with each node. | | `MaxSize` | `TMaybe` | none | Limit on the number of children (for composite nodes). | ### Copy [Copy the Cypress node to a new address](https://ytsaurus.tech/docs/commands.md#copy). [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#copy_move). {% note info "Note" %} Copying is always recursive. The `Recursive` parameter lets you specify whether to create intermediate directories for `destinationPath` or not. {% endnote %} ```c++ TNodeId Copy( const TYPath& sourcePath, const TYPath& destinationPath, const TCopyOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `sourcePath` | `TYPath` | - | Path to source node. | | `destinationPath` | `TYPath` | - | Path for creating the copy (must not exist without the `Force` option). | | `options` — optional settings: | | | | | `Recursive` | `bool` | `false` | Whether to create intermediate directories for `destinationPath`. | | `Force` | `bool` | `false` | If `destinationPath` exists, replace with new content. | | `PreserveAccount` | `bool` | `false` | Whether to save the accounts of the source nodes. | | `PreserveExpirationTime` | `bool` | `false` | Whether to copy the `expiration_time` attribute. | | `PreserveExpirationTimeout` | `bool` | `false` | Whether to copy the `expiration_timeout` attribute. | ### Move [Move the node to a new path](https://ytsaurus.tech/docs/commands.md#move). [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#copy_move). ```c++ TNodeId Move( const TYPath& sourcePath, const TYPath& destinationPath, const TMoveOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `sourcePath` | `TYPath` | - | Path to source node. | | `destinationPath` | `TYPath` | - | Path for creating the copy (must not exist without the `Force` option). | | `options` — optional settings: | | | | | `Recursive` | `bool` | `false` | Whether to create intermediate directories for `destinationPath`. | | `Force` | `bool` | `false` | If `destinationPath` exists, replace with new content. | | `PreserveAccount` | `bool` | `false` | Whether to save the accounts of the source nodes. | | `PreserveExpirationTime` | `bool` | `false` | Whether to copy the `expiration_time` attribute. | | `PreserveExpirationTimeout` | `bool` | `false` | Whether to copy the `expiration_timeout` attribute. | ### Link [Create](https://ytsaurus.tech/docs/commands.md#link) a [symbolic link](https://ytsaurus.tech/user-guide/storage/links.md) to the object at the new address. [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#link). ```c++ TNodeId Link( const TYPath& targetPath, const TYPath& linkPath, const TLinkOptions& options) ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------|--------------|------------|---------------------------------- | | `targetPath` | `TYPath` | - | Path to the source node where the link will direct. | | `linkPath` | `TYPath` | - | Path where the link will be created (must not exist without the `Force` option). | | `options` — optional settings | | | | | `Recursive` | `bool` | `false` | Whether to create intermediate directories. | | `IgnoreExisting` | `bool` | `false` | If `linkPath` exists and *constitutes a link*, do nothing and show no error | | `Force` | `bool` | `false` | If the `linkPath` node exists, create a link again in its place. | | `Attributes` | `TNode` | none | [Attributes](https://ytsaurus.tech/user-guide/storage/attributes.md) for `linkPath` in the form of `TNode` are written if created. | ### Concatenate [Concatenate](https://ytsaurus.tech/docs/commands.md#concatenate) a set of files or tables (in the order in which their paths are listed). [See the CLI equivalent](https://ytsaurus.tech/user-guide/storage/cypress-example.md#concatenate). The data merge takes place at the metadata level and exclusively on the [Cypress](https://ytsaurus.tech/user-guide/storage/cypress.md) master server. ```c++ void Concatenate( const TVector& sourcePaths, const TYPath& destinationPath, const TConcatenateOptions& options); ``` | **Parameter** | **Type** | **Default value** | **Description** | | ---------------------------|--------------|------------|---------------------------------- | | `sourcePaths` | `TVector` | - | Paths to input files or tables. | | `destinationPath` | `TYPath` | - | Path to the combined file or table, must exist. | | `options` — optional settings: | | | | | `Append` | `bool` | `false` | Whether to keep the contents of `destinationPath`, add new data at the end. Without this flag, the old `destinationPath` content will disappear. | ## File reading and writing To write a file to YT, you need to call the client/transaction method `CreateFileWriter(filePath)`. The returned `IFileWriter` interface is inherited from `IOutputStream`. If the file does not already exist, it will be created. ```c++ auto writer = client->CreateFileWriter("//tmp/file"); *writer << "something"; writer->Finish(); ``` Reading utilizes the `IFileReader` interface, which provides the `IInputStream` methods. ```c++ auto reader = client->CreateFileReader("//tmp/file"); TString anything; *reader >> anything; ``` Additional options are passed through the `TFileWriterOptions` and `TFileReaderOptions` structures. ## Structures for representing table rows There are several ways to represent individual table records in operations and during I/O. ### TNode To represent a record, TNode must have the [map](https://ytsaurus.tech/user-guide/storage/objects.md#primitive_types) type and constitute the display of column names as YSON values. ### Protobuf For more information about Protobuf, see [Protobuf representation of tables](https://ytsaurus.tech/docs/en/protobuf.md). Here, it is possible to define a custom protobuf type. To display protobuf fields in column names, use the following extensions: ```c++ import "yt/yt_proto/yt/formats/extension.proto"; message TSampleProto { optional int64 a = 1 [(NYT.column_name) = "column_a"]; optional double b = 2 [(NYT.column_name) = "column_b"]; optional string c = 3 [(NYT.column_name) = "column_c"]; } ``` There is support for nested structures, in which case binary protobuf serialization is used: a row with binary protobuf serialization of the [nested message](https://ytsaurus.tech/docs/en/protobuf.md#embedded) is stored in a table column. Nested structures can have `repeated` fields, which undergo binary serialization along with the rest of the nested message. {% note warning %} You should not use the `proto3` version. The [proto3 implementation](https://developers.google.com/protocol-buffers/docs/proto3#default) makes it so default field values are indistinguishable from missing fields. This results in value `0` of the `int` type, for example, not being written to the table. {% endnote %} ## Reading and writing tables See [yt/cpp/mapreduce/interface/io.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/interface/io.h). ```c++ auto writer = client->CreateTableWriter("//tmp/table"); writer->AddRow(TNode()("x", 1.)("y", 0.)); writer->AddRow(TNode()("x", 0.)("y", 1.)); writer->Finish(); ``` Block writing. When `AddRow()` is called, the data is stored in the internal buffer, and if this buffer is larger than 64 MB, a separate thread is triggered that transmits the accumulated data. The `Finish()` method is guaranteed to reset all the accumulated records or show an error. When destroying a writer without calling `Finish()`, a reset attempt will be made in the destructor without a guaranteed result. If the table did not exist before the creation of the writer, it will be created in the context of the same client or transaction with all the default attributes. If you need a different replication factor, you should create a table and give it the required attributes beforehand. For reading, the main methods are `GetRow()`, `IsValid()`, `Next()`: ```c++ auto reader = client->CreateTableReader("//tmp/ksv_table"); for (; reader->IsValid(); reader->Next()) { auto& row = reader->GetRow(); Cout << row.Key << "; " << row.SubKey << "; " << row.Value << Endl; } ``` The object returned from `GetRow()` can only be used before calling `Next()`. The `GetRowIndex()` method allows you to get the absolute index of the current row. Example with protobuf: ```c++ auto writer = client->CreateTableWriter("//tmp/table"); TSampleProto row; row.set_a(42); writer->AddRow(row); ``` ### TRichYPath { #trichypath } The `TRichYPath` structure allows you to pass various flags along with the path to the table. This applies to read and write functions as well as to input and output [operation](https://ytsaurus.tech/docs/en/#operations) tables. If you need to write in append mode: ```c++ client->CreateTableWriter(TRichYPath("//tmp/table").Append(true)); ``` If you want the write result to be sorted, use the `SortedBy` attribute: ```c++ auto path = TRichYPath("//tmp/table").SortedBy({"a", "b"}); ``` Based on the protobuf message, you can create a table schema: ```c++ auto path = WithSchema("//tmp/table"); ``` If you have a schema, you can no longer specify sorting with `SortedBy`. You can specify sorting when creating a schema: ```c++ auto path = WithSchema("//tmp/table", {"a", "b"}); ``` Or sort the schema later: `path.Schema_.SortBy({"a", "b"});` Additionally, during reading `TRichYPath` allows for setting horizontal and vertical selections: ```c++ auto path = TRichYPath("//tmp/table").AddRange(TReadRange() .LowerLimit(TReadLimit().RowIndex(10)) .UpperLimit(TReadLimit().RowIndex(20))); ``` Synonymously, you can write: ```c++ auto path = TRichYPath("//tmp/table").AddRange(TReadRange::FromRowIndices(10, 20)); ``` If the table is sorted, you can set the range by key. To do that, use the `TKey` class, which is a sequence of key column values or their prefix. In the example below, the `"//tmp/table"` table is implied to have two key columns of types `string` and `int64` (or more than two, in which case a lexicographic comparison also works). ```c++ auto path = TRichYPath("//tmp/table").AddRange(TReadRange() .LowerLimit(TReadLimit().Key({"foo", 100})) .UpperLimit(TReadLimit().Key({"foo", 200}))); ``` If you need string(s) with a specific key or index, you can use `TReadRange::Exact`: ```c++ auto path = TRichYPath("//tmp/table").AddRange(TReadRange() .Exact(TReadLimit().Key({"foo", 100}))); ``` Vertical selection: ```c++ auto path = TRichYPath("//tmp/table").Columns({"a", "b"}); ``` Renaming columns: ```c++ auto path = TRichYPath("//tmp/table").RenameColumns({{"a", "b"}, {"c", "d"}}); ``` The table column *a* will display under the name *b*, and column *c* — as *d*. The function resets the full list of renamed columns, so calling `RenameColumns` again will erase the results of the previous one. `Columns` is applied after `RenameColumns`, meaning in this case *b* and *d* will be suitable for `Columns`, while *a* and *c* — are not suitable. Reading from a remote cluster: ```c++ auto path = TRichYPath("//tmp/table").Cluster("my-remote-cluster"); ``` You can still use unnormalized paths, for example: ```c++ auto path = TRichYPath("//tmp/table[#0:#100500]"); ``` ### Format settings There is a [TFormatHints](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/interface/client_method_options.h) class for fine-tuning formats. You can pass it in the `TTableReaderOptions::FormatHints` and `TTableWriterOptions::FormatHints` fields when creating a reader/writer. If this setting works for the requested format, it is applied; otherwise, an exception is made. Available settings: - `SkipNullValuesForTNode` — do not create keys in the hash map for fields with the `#` value. - [Type conversions](https://ytsaurus.tech/user-guide/storage/data-types.md) when writing: `EnableStringToAllConversion`, `EnableAllToStringConversion`, `EnableIntegralTypeConversion%` (`uint64` <-> `int64`, enabled by default), `EnableIntegralToDoubleConversion`, `EnableTypeConversion` (all of the above simultaneously). ### Additional options If you need even more precise read/write options, you can use the `TTableWriterOptions` and `TTableReaderOptions` structures. They contain the `Config` field, where additional settings can be passed in `TNode` format that correspond to the [table_writer](https://ytsaurus.tech/user-guide/storage/io-configuration.md#table_writer) and [table_reader](https://ytsaurus.tech/user-guide/storage/io-configuration.md#table_reader) parameters. ```c++ auto writer = client->CreateTableWriter("//tmp/table", TTableWriterOptions().Config(TNode()("max_row_weight", 128 << 20))); ``` ### Parallel read { #parallel_read } There is a [library](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/library/parallel_io/parallel_reader.h) for parallel reading of tables. ## Running operations { #operations } See [link](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/interface/operation.h). Running an operation that contains a custom code requires: - Inheriting from one of the interfaces (`IMapper`, `IReducer`). - Defining the `Do()` function that should contain the record handler. - Using one of the `REGISTER_*` macros. - Calling the client/transaction method corresponding to the operation type in the client code. The`IMapper` and`IReducer` interfaces accept the `TTableReader` and `TTableWriter` types as template parameters. The `Do()` function takes pointers to these types. ```c++ #include class TExtractKeyMapper : public IMapper, TTableWriter> { public: void Do(TTableReader* input, TTableWriter* output) override { for (; input->IsValid(); input->Next()) { output->AddRow(TNode()("key", input->GetRow().Key)); } } }; ``` Next, you need to create a macro that registers the custom class: ```c++ REGISTER_MAPPER(TExtractKeyMapper). ``` There is no need to generate unique IDs — the type_info mechanism is used for identification. The job type will be explicitly displayed within the operation web interface in the custom job start string: ```c++ ./cppbinary --yt-map "TExtractKeyMapper" 1 0 ``` If the user wants to ID their jobs some other way, they need to ensure the names are unique within the same application and use the `REGISTER_NAMED_*` macros: ```c++ REGISTER_NAMED_MAPPER("The best extract key mapper in the world", TExtractKeyMapper); ``` The functions for running specific operation types are as follows: Map: - ```c++ IOperationPtr Map( const TMapOperationSpec& spec, ::TIntrusivePtr mapper, const TOperationOptions& options = TOperationOptions()) ``` Reduce: - ```c++ IOperationPtr Reduce( const TReduceOperationSpec& spec, ::TIntrusivePtr reducer, const TOperationOptions& options = TOperationOptions()) ``` Join-Reduce: - ```c++ IOperationPtr JoinReduce( const TJoinReduceOperationSpec& spec, ::TIntrusivePtr reducer, const TOperationOptions& options = TOperationOptions()) ```  MapReduce: - ```c++ IOperationPtr MapReduce( const TMapReduceOperationSpec& spec, ::TIntrusivePtr mapper, ::TIntrusivePtr reducer, const TOperationOptions& options = TOperationOptions()) ``` ```c++ IOperationPtr MapReduce( const TMapReduceOperationSpec& spec, ::TIntrusivePtr mapper, ::TIntrusivePtr reduceCombiner, ::TIntrusivePtr reducer, const TOperationOptions& options = TOperationOptions()) ``` You can specify a null pointer as `mapper`, which will make the operation skip the Map stage when running, and the function's second version will additionally start the [reduce_combiner](https://ytsaurus.tech/user-guide/data-processing/operations/reduce.md#reduce_combiner) stage. The operation start call parameters usually include: - Structure with the required specification parameters. - Pointers to instances of job classes. - Additional options (`TOperationOptions`). Be sure to add paths ([TRichYPath](https://ytsaurus.tech/docs/en/#trichypath)) to the input and output tables. Seeing how in general (in particular, in the protobuf backend) the types of table records can be different, the `AddInput()` and `AddOutput()` functions of the specification are templates: ```c++ TMapOperationSpec spec; spec.AddInput("//tmp/table") .AddInput("//tmp/other_table") .AddOutput("//tmp/output_table"); ``` Whenever possible, the discrepancy between the specification types and the types specified in IMapper/IReducer will be detected at runtime. A call without additional parameters looks like this: ```c++ client->Map(spec, new TExtractKeyMapper); ``` This is safe because `TIntrusivePtr` is created internally for the job object. ### Transferring user files The `TUserJobSpec` structure, which is part of the main specification, is used to transfer user files to the job's sandbox. File sets can vary for different job types in one operation: ```c++ TMapReduceOperationSpec spec; spec.MapperSpec(TUserJobSpec().AddLocalFile("./file_for_mapper")) .ReducerSpec(TUserJobSpec().AddLocalFile("./file_for_reducer")); ``` To use files that are already in the system, call the `TUserJobSpec::AddFile(fileName)` method. An important additional parameter is `TOperationOptions::Spec`. It is `TNode`, which can contain any specification parameters combined with the main specification generated by the client. Many of these options are described in the [Operation types](https://ytsaurus.tech/user-guide/data-processing/operations/overview.md) section. ### Multiple input and output tables If the operation writes to several output tables, the `AddRow()` method is used with two parameters: ```c++ writer->AddRow(row, tableIndex); ``` If the operation reads from multiple tables, the table index can be retrieved from `TTableReader` using the `GetTableIndex()` method. ### Protobuf backend When using protobuf, it is possible to specify different record types for input and output tables. In this case, the declaration of the job's basic class uses `TTableReader` or `TTableWriter` not from a specific custom protobuf class, but from the basic `Message`: ```c++ class TProtoReducer : public IReducer, TTableWriter> { ... } ``` You should not write `AddInput/AddOutput()` in the operation's specification. You must provide a specific type. In this case, the `GetRow()` and `AddRow()` methods become templates and must use specific user-defined types. For reading, you can select the function to call by using `GetTableIndex()` of the current entry before calling `GetRow()`. For writing, `AddRow(row, tableIndex)` is called. Types are controlled to be in accord with table indexes. ### Initializing and serializing jobs The `Start()` and `Finish()` methods are similar to their general interface counterparts, taking as a parameter the pointer to the corresponding `TTableWriter`. ```c++ virtual void Save(IOutputStream& stream) const; virtual void Load(IInputStream& stream); ``` Serialization is not tied to any frameworks. The content of a serialized job is stored in the sandbox in the `jobstate` file. If not a single byte was written during `Save()`, the file will be missing. That said, `Load()` in the job will be called from an empty stream. There is a `Y_SAVELOAD_JOB` helper that wraps the serialization from `util/ysaveload.h`. The order of calling methods in a job is as follows: `default ctor; Load(); Start(); Do(); Finish()`. ### Preparing an operation from a job class { #prepare_operation } The `IJob` class (the basic class for all jobs) has the virtual method `void PrepareOperation(const IOperationPreparationContext& context, TJobOperationPreparer& preparer) const` added, which can be overloaded in the user's jobs: - `context` allows you to get information about input and output tables and, in particular, their schemas. - `preparer` serves to control specification parameters. You can set: - Output table schema: `preparer.OutputSchema(tableIndex, schema)`. - Message types for the protobuf input and output formats: `preparer.InputDescription(tableIndex)` and `preparer.OutputDescription(tableIndex)`. - Column filtering for input tables: `preparer.InputColumnFilter(tableIndex, {"foo", "bar"})`. - Ability to rename input table columns: `preparer.InputColumnRenaming(tableIndex, {{"foo", "not_foo"}})`. The renaming is applied *after* filtering. All methods return `*this`, which enables chaining. Usually, when using protobuf, specifying only `.OutputDescription` is sufficient, and the output table schema is inferred automatically. If this is not the right behavior, you can set the second parameter (`inferSchema`) to `false`, such as `preparer.OutputDescription(tableIndex, false)`. For jobs marking `OutputDescription`, you can use short variants to start operations, such as `client->Map(new TMapper, input, output)`. If there are several similar tables, their description can be combined into groups using the `.(Begin|End)(Input|Output)Group` methods. `.Begin(Input|Output)Group` accepts the description of the table indexes that this group combines. It can be either a `[begin, end)` pair or a container with `int`, for example: ```c++ preparer .BeginInputGroup({0,1,2,3}) .Description() .ColumnFilter({"foo", "bar"}) .ColumnRenaming({{"foo", "not_foo"}}) .EndInputGroup() .BeginOutputGroup(0, 7) .Description() .EndOutputGroup(); ``` ### Notes - The `IMapper::Do()` function takes all the records from the job as input. This is different from the general interface logic. - The `IReducer::Do()` function accepts a range of records by one key, as before. In the case of the Reduce operation, this is determined by the `ReduceBy` specification parameter; in the case of JoinReduce, it is determined by `JoinBy`. - If the job class is a template based on user-defined parameters, `REGISTER_*` must be called for each instance that needs to be run on the cluster. ## Getting information about operations and jobs ### GetOperation Get [information about an operation](https://ytsaurus.tech/docs/commands.md#get_operation). ```c++ TOperationAttributes GetOperation( const TOperationId& operationId, const TGetOperationOptions& options) ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------------------------- | ----------------------------------- | ------- | ---------------------- | | `operationId` | | | Operation ID. | | `options` — optional settings: | | | | | `AttributeFilter` | `TMaybe` | none | Which attributes to return. | ### ListOperations Get a list of operations [that match the filters](https://ytsaurus.tech/docs/commands.md#list_operation). ```c++ TListOperationsResult ListOperations( const TListOperationsOptions& options) ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------------------------- | -------------------------- | ------- | ------------------------------------------------------------ | | `options` — optional settings: | | | | | `FromTime` | `TMaybe` | none | Start of the search ti*m*e interval. | | `ToTime` | `TMaybe` | none | End of the search ti*m*e interval. | | `CursorTime` | `TMaybe` | none | Cursor position (for pagination). | | `CursorDirection` | `TMaybe` | none | Which end of the ti*m*e interval to start listing operations from. | | `Filter` | `TMaybe` | none | "Text factors" (in particular, title and paths to input/output tables) of the operation include `Filter` as a substring. | | `Pool` | `TMaybe` | none | The operation was started in the `Pool` pool. | | `User` | `TMaybe` | none | The operation was started by `User`. | | `State` | `TMaybe` | none | The operation is in the `State` state. | | `Type` | `TMaybe` | none | The operation has the `Type` type. | | `WithFailedJobs` | `TMaybe` | none | The operation has (`WithFailedJobs == true`) or does not have (`WithFailedJobs == false`) failed jobs | | `IncludeArchive` | `TMaybe` | none | Whether to search for operations in the operations archive. | | `IncludeCounters` | `TMaybe` | none | Whether to include statistics on the total number of operations (not just for the specified interval) in the response. | | `Limit` | `TMaybe` | none | Return no more than `Limit` operations (the current maximum value of this parameter is `100`, which is also the default value). | ### UpdateOperationParameters Update the runtime parameters of the [running operation](https://ytsaurus.tech/docs/commands.md#update_operation_parameters). ```c++ void UpdateOperationParameters( const TOperationId& operationId, const TUpdateOperationParametersOptions& options) ``` | **Parameter** | **Type** | Default value | **Description** | | --------------------------------- | --------------------------------------- | ------- | ------------------------------------------------------------ | | `operationId` | Operation ID | | | | `options` — optional settings: | | | | | `Owner` | `TVector` | none | New operation owners. | | `Pool` | `TMaybe` | none | Move the operation to the `Pool` pool (applies to all the pool trees the operation is running for). | | `Weight` | `TMaybe` | none | New operation weight (applies to all the pool trees the operation is running for). | | `SchedulingOptionsPerPoolTree` | `TMaybe` | none | Scheduler options for each pool tree. | ### GetJob Get [information about the job](https://ytsaurus.tech/docs/commands.md#get_job). ```c++ TJobAttributes GetJob( const TOperationId& operationId, const TJobId& jobId, const TGetJobOptions& options) ``` | **Parameter** | **Description** | | ---------------------------------- | ----------- | | `operationId` | Operation ID | | `jobId` | Job ID | | `options` — optional settings: | | ### ListJobs Get jobs that [match the specified filters](https://ytsaurus.tech/docs/commands.md#list_jobs). ```c++ TListJobsResult ListJobs( const TOperationId& operationId, const TListJobsOptions& options) ``` | **Parameter** | **Type** | **Default value** | **Description** | | --------------------------------- | ----------------------------- | ------- | ------------------------------------------------------------ | | `operationId` | | | Operation ID. | | `options` — optional settings: | | | | | `Type` | `TMaybe` | none | The job has the `Type` type. | | `State` | `TMaybe` | none | The job is in the `State` state. | | `Address` | `TMaybe` | none | The job is running (or was run) on the `Address` node. | | `WithStderr` | `TMaybe` | none | Whether the job wrote anything to stderr. | | `WithSpec` | `TMaybe` | none | Whether the job specification was saved. | | `WithFailContext` | `TMaybe` | none | Whether the job's fail context was saved. | | `WithMonitoringDescriptor` | `TMaybe` | none | Whether the job was assigned a monitoring descriptor. | | `WithInterruptionInfo` | `TMaybe` | none | Whether the job has interruption info | | `SortField` | `TMaybe` | none | The field jobs are sorted by in the response. | | `SortOrder` | `TMaybe` | none | Ascending or descending job sorting order. | | `DataSource` | `TMaybe` | none | Where to look for jobs: in the controller agent and Cypress (`Runtime`), in the archive of jobs (`Archive`), automatically depending on whether the operation is in Cypress (`Auto`), or other (`Manual`). | | `IncludeCypress` | `TMaybe` | none | Whether to search for jobs in Cypress (the option factors into `DataSource == Manual`). | | `IncludeControllerAgent` | `TMaybe` | none | Search for jobs in the agent controller (the option factors into `DataSource == Manual`). | | `IncludeArchive` | `TMaybe` | none | Whether to search for jobs in the job archive (the option factors into `DataSource == Manual`). | | `Limit` | `TMaybe` | none | Return no more than `Limit` of jobs. | | `Offset` | `TMaybe` | none | Skip the first `Offset` jobs (in the sort order). | ### GetJobInput Get the input of a [running or failed job](https://ytsaurus.tech/docs/commands.md#get_job_input). ```c++ IFileReaderPtr GetJobInput( const TJobId& jobId, const TGetJobInputOptions& options) ``` | **Parameter** | **Description** | | ------------------------------------------- | -------- | | `jobId` | Job ID. | | `options` — optional settings (none yet) | | ### GetJobFailContext Get fail context of a [failed job](https://ytsaurus.tech/docs/commands.md#get_job_fail_context). ```c++ IFileReaderPtr GetJobFailContext( const TOperationId& operationId, const TJobId& jobId, const TGetJobFailContextOptions& options) ``` | **Parameter** | **Description** | | -------------------------------------------- | ----------- | | `operationId` | Operation ID. | | `jobId` | Job ID. | | `options` — optional settings (none yet) | | ### GetJobStderr Get stderr of a [running or failed job](https://ytsaurus.tech/docs/commands.md#get_job_stderr). ```c++ IFileReaderPtr GetJobStderr( const TOperationId& operationId, const TJobId& jobId, const TGetJobStderrOptions& options) ``` | **Parameter** | **Description** | | ------------------------------------------- | ----------- | | `operationId` | Operation ID. | | `jobId` | Job ID. | | `options` — optional settings (none yet) | | ## Working with file cache Clusters have a common file cache that stores files by key, which is their MD5 hash. It is currently at `//tmp/yt_wrapper/file_storage/new_cache`. You can use the `PutFileToCache` and `GetFileFromCache` methods to work with it. The same methods can be used to work with your cache that is located at a different path (but you will also need to clean it yourself). {% note warning %} All commands for working with the file cache are non-transactional, so they encompass the `IClient` methods, but not `IClientBase` or `ITransaction`. {% endnote %} ### PutFileToCache [Copy the file to the cache](https://ytsaurus.tech/docs/commands.md#put_file_to_cache). Returns a path to the cached file. ```c++ TYPath PutFileToCache( const TYPath& filePath, const TString& md5Signature, const TYPath& cachePath, const TPutFileToCacheOptions& options) ``` | **Parameter** | **Description** | | ------------------------------------------- | ------------------------------------------------------------ | | `filePath` | The path to the file in Cypress (the file must have the `md5` attribute; for this, it must be written with the `TFileWriterOptions::ComputeMD5` option). | | `md5Signature` | The file's MD5 hash. | | `cachePath` | The Cypress path to the file cache root. | | `options` — optional settings (none yet) | | ### GetFileFromCache [Get a path to the cached file](https://ytsaurus.tech/docs/commands.md#get_file_from_cache). Returns the path (or `Nothing`, if there is no file with this MD5). ```c++ TMaybe GetFileFromCache( const TString& md5Signature, const TYPath& cachePath, const TGetFileFromCacheOptions& options); ``` | **Parameter** | **Description** | | ------------------------------------------- | -------------------------------------- | | `md5Signature` | The file's MD5 hash. | | `cachePath` | The Cypress path to the file cache root. | | `options` — optional settings (none yet) | | ## Environment variables Currently these settings are available only from environment variables and are the same for all `IClient` instances. However, we are going to redesign this functionality so that almost everything can be configured directly from the code and changed at the individual `IClient` level. `YT_USER` is not listed because the `yt/cpp/mapreduce` client described on this page does not use this variable: the token determines the user. `GetClientOptionsFromEnv` belongs to C++ NApi and handles `YT_USER` according to [different rules](https://ytsaurus.tech/user-guide/storage/auth.md#user-selection). | *Variable* | *Type* | *Value* | | ------------------------------------- | ------- | ------------------------------------------------------------ | | YT_TOKEN | string | Value of the custom token. | | YT_TOKEN_PATH | string | Path to a file with the custom token. | | YT_PREFIX | string | A path that is prefixed to all paths in Cypress. | | YT_TRANSACTION | guid | External transaction ID. This parameter has `CreateClient()` create an `IClient` instance that runs in the context of the transaction. | | YT_POOL | string | Pool for running user operations. | | YT_FORCE_IPV4 | bool | Use only IPv4. | | YT_FORCE_IPV6 | bool | Use only IPv6. | | YT_CONTENT_ENCODING | string | HTTP header Content-Encoding: `identity`, `gzip`, `y-lzo`, `y-lzf` are supported. | | YT_ACCEPT_ENCODING | string | HTTP header Accept-Encoding. | | YT_USE_HOSTS | bool | Whether to use heavy proxies. | | YT_HOSTS | string | Path in the heavy proxy list request. | | YT_RETRY_COUNT | int | Number of attempts per HTTP request. | | YT_START_OPERATION_RETRY_COUNT | int | The number of attempts to start the operation when the limit of simultaneously running operations is exceeded. | | YT_VERSION | string | HTTP API version. | | YT_SPEC | json | Specification added to each operation specification. Order: main specification, `YT_SPEC`, `TOperationOptions::Spec`. | | YT_CONNECT_TIMEOUT | seconds | Socket connection timeout. | | YT_SOCKET_TIMEOUT | seconds | Timeout for working the socket. | | YT_TX_TIMEOUT | seconds | Transaction timeout. | | YT_PING_INTERVAL | seconds | Interval between consecutive transaction pings. | | YT_RETRY_INTERVAL | seconds | Interval between HTTP request attempts. | | YT_RATE_LIMIT_EXCEEDED_RETRY_INTERVAL | seconds | The interval between attempts if the request rate limit is exceeded. | | YT_START_OPERATION_RETRY_INTERVAL | seconds | Interval between attempts to start the operation. | ## Thread safety `IClient` and `ITransaction` have no variable state. Thread-safe. # Protobuf representation of tables This section describes the use of the [protobuf](https://en.wikipedia.org/wiki/Protocol_Buffers) structured data transfer protocol for working with tables in the C++ API. ## Introduction { #introduction } The C++ API enables you to use protobuf classes (messages) to read and write tables both by the client and inside the job. We recommend using the [proto2](https://protobuf.dev/programming-guides/proto/) version. If you use [proto3](https://protobuf.dev/programming-guides/proto3/), errors may occur: for example, when trying to write `0` in the `required=%true` field. ### How it works The user describes the proto structure in the `.proto` file. The proto structure can be marked with different [flags](https://ytsaurus.tech/docs/en/#flags). Flags affect how YTsaurus will generate or interpret the fields specified within messages. ## Elementary types { #primitive } To work with the [YTsaurus data type](https://ytsaurus.tech/user-guide/storage/data-types.md) from the first column, you can use the corresponding protobuf type from the second column in the table below. | **YTsaurus** | **Protobuf** | |---------------------------------|----------------------------------------------| | `string`, `utf8` | `string`, `bytes` | | `int{8,16,32,64}` | `int{32,64}`, `sint{32,64}`, `sfixed{32,64}` | | `uint{8,16,32,64}` | `uint{32,64}`, `fixed{32,64}` | | `double` | `double`, `float` | | `bool` | `bool` | | `date`, `datetime`, `timestamp` | `uint{32,64}`, `fixed{32,64}` | | `interval` | `int{32,64}`, `sint{32,64}`, `sfixed{32,64}` | If the range of the integer YTsaurus type does not correspond to the range of the protobuf type, a check is performed at the time of encoding or decoding. ## Optional/required properties { #optional_required } The `optional`/`required` properties in protobuf do not have to correspond to the options of columns in YT. The system always checks when protobuf messages are encoded or decoded. For example, if the `foo` column in YTsaurus has the `int64` type (`required=%true`), the following field can be used for its representation: ```protobuf optional int64 foo = 42; ``` There will be no errors as long as all protobuf messages that are written to the table have the filled in `foo` field. ## Embedded messages { #embedded } Historically, all embedded protobuf structures are written in YTsaurus as a byte string storing the [serialized](https://ytsaurus.tech/docs/en/#serialization) representation of the embedded structure. Embedded messages are quite efficient, but do not enable you to conveniently represent values in the web interface or work with them using other methods (without the help of protobuf). This method can also be specified explicitly by setting a special [flag](https://ytsaurus.tech/docs/en/#flags) in the field: ``` optional TEmbeddedMessage UrlRow_1 = 1 [(NYT.flags) = SERIALIZATION_PROTOBUF]; ``` You can specify an alternative flag, then YTsaurus will refer the field to the `struct` type: ```protobuf optional TEmbeddedMessage ColumnName = 1 [(NYT.flags) = SERIALIZATION_YT]; ``` To use embedded messages: - The table must have a [schema](https://ytsaurus.tech/user-guide/storage/static-schema.md). - The corresponding column (in the example: `ColumnName`) must have the [struct](https://ytsaurus.tech/user-guide/storage/data-types.md#schema_struct) YTsaurus type. - The `struct` type fields must correspond to the fields of the embedded message (in the example: `TEmbeddedMessage`). {% note warning %} The flag in embedded messages is not inherited by default. If the `SERIALIZATION_YT` flag is set for the field with the `T` type, the default behavior for structures embedded in `T` will still correspond to the `SERIALIZATION_PROTOBUF` flag. {% endnote %} ## Repeated fields { #repeated } To work with repeated fields, you must explicitly specify the `SERIALIZATION_YT` flag: ```protobuf repeated TType ListColumn = 1 [(NYT.flags) = SERIALIZATION_YT]; ``` In YT, such a field will have the `list` type. To use repeated fields: - The table must have a [schema](https://ytsaurus.tech/user-guide/storage/static-schema.md). - The corresponding column (in the example: `ListColumn`) must have the [list](https://ytsaurus.tech/user-guide/storage/data-types.md#schema_list) YTsaurus type. - The element of the `list` YTsaurus type must correspond to the protobuf column type (in the example: `TType`). This can be an [elementary](https://ytsaurus.tech/docs/en/#primitive) type or an [embedded message](https://ytsaurus.tech/docs/en/#embedded). {% note warning %} The `SERIALIZATION_PROTOBUF` flag for repeated fields is not supported. {% endnote %} ## Oneof fields { #oneof } By default, fields within a `oneof` group correspond to the [variant](https://ytsaurus.tech/user-guide/storage/data-types.md#schema_variant) YTsaurus type. For example, the message below will correspond to the structure with the `x` field of the `int64` type and the `my_oneof` field of the `variant` type: ```protobuf message TMessage { optional int64 x = 1; oneof my_oneof { string y = 2; bool z = 3; } } ``` If you infer the schema using `CreateTableSchema()`, a similar type will be inferred. To make `oneof` groups correspond to the fields of the structure where they are described, use the `(NYT.oneof_flags) = SEPARATE_FIELDS` flag: ```protobuf message TMessage { optional int64 x = 1; oneof my_oneof { option (NYT.oneof_flags) = SEPARATE_FIELDS; string y = 2; bool z = 3; } } ``` This message will correspond to the structure with optional fields `x`, `y`, and `z`. ## Map fields { #map } There are 4 options for displaying such a field in a column in a table. See the example below: ```protobuf message TValue { optional int64 x = 1; } message TMessage { map map_field = 1 [(NYT.flags) = SERIALIZATION_YT]; } ``` Depending on its flags, the `map_field` field can correspond to: - The list of structures with the `key` field of the ` string` type and the `value` field of the `string` type, which will contain the serialized protobuf `TValue` (as if the `SERIALIZATION_PROTOBUF` flag is set for the `value` field). In this case, the default flag is `MAP_AS_LIST_OF_STRUCTS_LEGACY`. - The list of structures with the `key` field of the `string` type and the `value` field of the `Struct` type (as if the `SERIALIZATION_PROTOBUF` flag is set for the `value` field). In this case, the default flag is `MAP_AS_LIST_OF_STRUCTS`. - The `Dict>>` dict: the `MAP_AS_DICT` flag. - The optional `Optional>>>` dict: the `MAP_AS_OPTIONAL_DICT` flag. ## Flags { #flags } You can use flags to customize the protobuf behavior. To do this, connect the library `.proto` file. ```protobuf import "mapreduce/yt/interface/protos/extension.proto"; ``` Flags can correspond to messages, `oneof` groups, and message fields. You can specify flags at the level of the `.proto` file, message, `oneof` group, and message field. ### SERIALIZATION_YT, SERIALIZATION_PROTOBUF { #serialization } Behavior of these flags is described [above](https://ytsaurus.tech/docs/en/#embedded). By default, `SERIALIZATION_PROTOBUF` is implied where relevant. You can change the flag for a single message: ```protobuf message TMyMessage { option (NYT.default_field_flags) = SERIALIZATION_YT; ... } ``` ### OTHER_COLUMNS { #other_columns } You can mark the field of the `bytes` type with the `OTHER_COLUMNS` flag. This field can contain a YSON map that contains representations of all fields not described by other fields of this protobuf structure. ```protobuf message TMyMessage { ... optional bytes OtherColumns = 1 [(NYT.flags) = OTHER_COLUMNS]; ... } ``` ### ENUM_STRING / ENUM_INT { #enum_int } You can mark the fields of the `enum` type with the `ENUM_STRING`/`ENUM_INT` flags: - If the field is marked `ENUM_INT`, it will be saved to the column as an integer. - If the field is marked `ENUM_STRING`, it will be saved to the column as a string. By default, `ENUM_STRING` is implied. ```protobuf enum Color { WHITE = 0; BLUE = 1; RED = -1; } ... optional Color ColorField = 1 [(NYT.flags) = ENUM_INT]; ... ``` ### ANY { #any } You can mark the fields of the `bytes` type with the `ANY` flag. Such fields contain a YSON representation of a column of any simple type. For example, you can write the following code for the `string` type column: ```protobuf // message.proto message TMyMessage { ... optional bytes AnyColumn = 1 [(NYT.flags) = ANY]; ... } ``` ```c++ // main.cpp TNode node = "Linnaeus"; TMyMessage m; m.SetAnyColumn(NodeToYsonString(node)); ``` # Lambda expressions In some YTsaurus operations, calculations are so simple that creating a class for each of them using the [C++ API](https://ytsaurus.tech/docs/en/description.md) feels excessive. In this case, you can use [lambda expressions](https://en.wikipedia.org/wiki/Anonymous_function) to describe operations. {% note warning %} Such lambda expressions currently can't capture variables, meaning they must have an empty capture block. This makes them equivalent to C functions. This is because generic lambda are problematic to serialize correctly in modern C++. To pass parameters to a lambda, you can sometimes use `static const` and [TSaveable](https://ytsaurus.tech/docs/en/#saveable) global variables. {% endnote %} {% note warning %} This only works from programs built statically for Linux, so you can't use settings like `JobBinaryLocalPath`. {% endnote %} ## Using the yt_lambda library Connect the [yt_lambda](https://github.com/ytsaurus/ytsaurus/blob/main/yt/cpp/mapreduce/library/lambda/yt_lambda.h)library and include the following header in the program: ``` #include ``` [Initialize YTsaurus](https://ytsaurus.tech/docs/en/description.md#init) and get the `client`. Pass the `client` or [transaction](https://ytsaurus.tech/docs/en/description.md#transactions) as the first parameter to the functions described in this document. This library's functions have good compatibility with the use of records in [protobuf](https://ytsaurus.tech/docs/en/protobuf.md) format. ## Operations overview All of the operations in this library take as input lambda-expressed operators on a single record, which are then repeatedly applied. In general, an operation can also be viewed as a chain of conversions of types: ![](https://ytsaurus.tech/_images/lambda-normal-op.png){ .center} ### Additive operations A variant of ordinary operations where [Reduce](https://ytsaurus.tech/user-guide/data-processing/operations/reduce.md) has the same input and output type and is associative. In this case, you can further simplify the implementation of the Reduce transformation and use it as a combiner for free. ![](https://ytsaurus.tech/_images/lambda-additive-op.png){ .center} ## CopyIf This function filters records in the table without modifying them. ```c++ template void CopyIf(const IClientBasePtr& client, const TRichYPath& from, const TRichYPath& to, bool (*p)(const T&)); ``` | **Parameter** | **Description** | | -------- | ------------------------------------------------------------ | | `T` | Record type (TNode or a specific [protobuf message](https://ytsaurus.tech/docs/en/protobuf.md) type) | | `from` | Input table name | | `to` | Output table name | | `p` | Filtering predicate | Notes: - The order of records in the table isn't changed, and the `Ordered` flag that may slightly slow down the operation is set. - A known defect: if there is only one input table, it has a schema, and is sorted, the sorting information doesn't reach the output table schema. ### Application ```c++ NYT::CopyIf(client, "//home/project/inTable", "//home/project/outTable", [](https://ytsaurus.tech/docs/en/auto&%20rec) { return rec.GetSomeField() > 10000; }); ``` ## TransformCopyIf, or Map This function performs a classic [Map](https://ytsaurus.tech/user-guide/data-processing/operations/map.md): it reads each record from the table of one format and writes them to the table of another format. Some records aren't included in the final table. ```c++ template void TransformCopyIf(const IClientBasePtr& client, const TRichYPath& from, const TRichYPath& to, bool (*mapper)(const R&, W&)); ``` | **Parameter** | **Description** | | -------- | ------------------------------------------------------------ | | `R` | Input record type (TNode or a specific protobuf message type) | | `W` | Output record type (TNode or a specific protobuf message type) | | `from` | Input table name | | `to` | Output table name | | `mapper` | A conversion function that takes an `src` record of the `R` type as input and fills the `dst` record of the `W` type. For `dst` to be written, return `true`. For performance reasons, the library passes as `dst` the same buffer variable that isn't even cleared between `mapper` calls. This is generally not a problem, because the `mapper` usually fills in the same `dst` fields. | Note: - The order of records in the table isn't changed, and the `Ordered` flag that may slightly slow down the operation is set. ### Application ```c++ NYT::TransformCopyIf( client, "//home/project/inTable", "//home/project/outTable", [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { dst.SetField(src.GetField()); return true; }); ``` ## AdditiveReduce ```c++ template void AdditiveReduce( const IClientBasePtr& client, const TKeyBase& from, const TRichYPath& to, const TKeyColumns& reduceFields, void (*reducer)(const T&, T&), bool (*finalizer)(const T&, TFin&)); ``` | **Parameter** | **Description** | | -------------- | ------------------------------------------------------------ | | `T` | Input and output record type for reducer (TNode or a specific protobuf message type) | | `TFin`Opt | **Optional parameter.** Output record type (TNode or a specific protobuf message type). **Specified only in the version with a finalizer** | | `from` | Name of an input table (input tables) | | `to` | Output table name | | `reduceFields` | Which keys' identical values will be used for sorting and folding (Reduce) | | `reducer` | A conversion function that takes a new `src` record of the `T` type as input and modifies the `dst` record of the same `T` type. The first record of the current range of records with the same `reduceFields` keys is initially included in `dst`. If there is one record in the current range, the `reducer` won't be called. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | | `finalizer`Opt | **Optional parameter.** A conversion function that takes a new `src` record of the `T` type as input and fills the `dst` record of the `TFin` type. Called once after Reduce has processed a range of input records to generate the final version of the output record. `dst` is initialized by default. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | ## Reduce ```c++ template void Reduce( const IClientBasePtr& client, const TKeyBase& from, const TRichYPath& to, const TKeyColumns& reduceFields, void (*reducer)(const R&, W&), bool (*finalizer)(const W&, TFin&)); ``` | **Parameter** | **Description** | | -------------- | ------------------------------------------------------------ | | `R` | Input record type (TNode or a specific protobuf message type) | | `W` | Reducer's output record type (TNode or protobuf) | | `TFin`Opt | **Optional parameter.** Output record type (TNode or a specific protobuf message type). **Specified only in the version with a finalizer** | | `from` | Name of an input table (input tables) | | `to` | Output table name | | `reduceFields` | Which keys' identical values will be used for sorting and folding (Reduce) | | `reducer` | A conversion function that takes a new `src` record of the `R` type as input and modifies the `dst` record of the `W` type. `dst` is initialized by default (in case of TNode, there will be an empty TNode, that is, `Undefined`; protobuf is usually initialized with zeros and empty strings, which makes it more practical). You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | | `finalizer`Opt | **Optional parameter.** A conversion function that takes a new `src` record of the `W` type as input and fills the `dst` record of the `TFin` type. Called once after Reduce has processed a range of input records to generate the final version of the output record. `dst` is initialized by default. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | ## AdditiveMapReduce[Sorted] { #additive_mapreduce } The `AdditiveMapReduce` function generates a merged [MapReduce operation](https://ytsaurus.tech/user-guide/data-processing/operations/mapreduce.md), that is, it converts records of the input table (or tables), sorts the result, and performs Reduce. The reducer must be an [associative binary operation](https://en.wikipedia.org/wiki/Associative_property) on same-type records — most often, this will be an addition of numbers. As a bonus, YT can use such a reducer as a combiner if needed. Unfortunately, merged MapReduce operations in YT always generate an unsorted table. There's a workaround in the form of the `AdditiveMapReduceSorted` function, which invokes `AdditiveMapReduce` and then sorts the output table. ```c++ template void AdditiveMapReduce( const IClientBasePtr& client, const TKeyBase& from, const TRichYPath& to, const TKeyColumns& reduceFields, bool (*mapper)(const R&, W&), void (*reducer)(const W&, W&), bool (*finalizer)(const W&, TFin&)); ``` | **Parameter** | **Description** | | -------------- | ------------------------------------------------------------ | | `R` | Input record type (TNode or a specific protobuf message type) | | `W` | Output record type for reducer (TNode or a specific protobuf message type) | | `TFin`Opt | **Optional parameter.** Output record type (TNode or a specific protobuf message type). **Specified only in the version with a finalizer** | | `from` | Name of an input table (input tables) | | `to` | Output table name | | `reduceFields` | Which keys' identical values will be used for sorting and folding (Reduce) | | `mapper` | A conversion function that takes an `src` record of the `R` type as input and fills the `dst` record of the `W` type. For `dst` to be written, return `true`. `mapper` may be `nullptr`. For performance reasons, the library passes as `dst` the same buffer variable that isn't even cleared between `mapper` calls. This is generally not a problem, because the `mapper` usually fills in the same `dst` fields. | | `reducer` | A conversion function that takes a new `src` record of the `W` type and modifies the `dst` record of the same `W` type. The first record of the current range of records with the same `reduceFields` keys is initially included in `dst`. If there is one record in the current range, the `reducer` won't be called. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | | `finalizer`Opt | **Optional parameter.** A conversion function that takes a new `src` record of the `W` type as input and fills the `dst` record of the `TFin` type. Called once after Reduce has processed a range of input records to generate the final version of the output record. `dst` is initialized by default. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | ### Application ```c++ NYT::AdditiveMapReduceSorted( client, inTable, // or inTables outTable, "name", // sort key, matches TStatsMsg::Name [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // dst is a buffer that may contain garbage. // We don't need to clear it because we set all fields. dst.SetName(src.GetSomeField()); dst.SetCount(src.GetSomeCountField()); return true; }, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // dst is initialized by the first record of equal key range. // This lambda function is called starting from the 2nd record. // dst.Name is filled with correct value and must not be modified. dst.SetCount(src.GetCount() + dst.GetCount()); }); ``` ## MapReduce[Sorted] { #mapreduce } The MapReduce function generates a merged MapReduce operation, that is, it converts records of the input table (or tables), sorts the result, and performs Reduce. Unlike [AdditiveMapReduce](https://ytsaurus.tech/docs/en/#additive_mapreduce), the reducer function doesn't have to be associative, so it can have different input and output record types (and thus can't be used as a combiner). To make the output clearer, you can use an additional lambda function — finalizer, — which will be called on the reducer output to generate the final version of the record. Unfortunately, merged MapReduce operations in YT always generate an unsorted table. There's a workaround in the form of the `MapReduceSorted` function, which invokes `MapReduce` and then sorts the output table. ```c++ template void MapReduce( const IClientBasePtr& client, const TKeyBase& from, const TRichYPath& to, const TKeyColumns& reduceFields, bool (*mapper)(const R& src, TMapped& dst), void (*reducer)(const TMapped& src, TReducerData& dst), bool (*finalizer)(const TReducerData& src, W& dst)); ``` | **Parameter** | **Description** | | ----------------- | ------------------------------------------------------------ | | `R` | Input record type (TNode or a specific protobuf message type) | | `TMapped` | Output record type for mapper and input record type for reducer (TNode or protobuf) | | `TReducerData`Opt | **Optional parameter.** Output record type for the reducer and input record type for the finalizer (any type, including C++ struct). **Specified only in the MapReduce version with a finalizer** | | `W` | Output record type (TNode or a specific protobuf message type) | | `from` | Name of an input table (input tables) | | `to` | Output table name | | `reduceFields` | Which keys' identical values will be used for sorting and folding (Reduce) | | `mapper` | A conversion function that takes an `src` record of the `R` type as input and fills the `dst` record of the `TMapped` type. For `dst` to be written, return `true`. `mapper` may be `nullptr`. For performance reasons, the library passes as `dst` the same buffer variable that isn't even cleared between `mapper` calls. This is generally not a problem, because the `mapper` usually fills in the same `dst` fields. | | `reducer` | A conversion function that takes a new `src` record of the `TMapped` type as input and modifies the `dst` record of the `W` type (or `TReducerData` when using a finalizer). `dst` is initialized by default (in case of TNode, there will be an empty TNode, that is, `Undefined`; protobuf is usually initialized with zeros and empty strings, which makes it more practical). You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | | `finalizer`Opt | **Optional parameter.** A conversion function that takes a new `src` record of the `TReducerData` type as input and fills the `dst` record of the `W` type. Called once after the reducer has processed a range of input records to generate the final version of the output record. `dst` is initialized by default. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | ### Application { #mapreduce_example } ```c++ message TSimpleKeyValue { required string Key = 1 [(NYT.column_name) = "key"]; required uint64 Value = 2 [(NYT.column_name) = "value"]; } message TKeyStat { required string Key = 1 [(NYT.column_name) = "key"]; required double Mean = 2 [(NYT.column_name) = "mean"]; required double Sigma = 3 [(NYT.column_name) = "sigma"]; }message TSimpleKeyValue { required string Key = 1 [(NYT.column_name) = "key"]; required uint64 Value = 2 [(NYT.column_name) = "value"]; } message TKeyStat { required string Key = 1 [(NYT.column_name) = "key"]; required double Mean = 2 [(NYT.column_name) = "mean"]; required double Sigma = 3 [(NYT.column_name) = "sigma"]; } ``` ```c++ struct TDispersionData { ui64 Count = 0; long double Sum = 0.; long double SumSquared = 0.; }; void CalculateDispersion(IClientPtr& client, TString input, TString output) { NYT::MapReduceSorted( client, input, output, "key", [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // mapper dst.SetName(src.GetSomeField()); dst.SetCount(src.GetSomeCountField()); return true; }, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // reducer double value = src.GetValue(); dst.Count++; dst.Sum += value; dst.SumSquared += value * value; }, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // finalizer double mean = (double)src.Sum / src.Count; double dispersion = (double)src.SumSquared / src.Count - mean * mean; dst.SetMean(mean); dst.SetSigma(std::sqrt(dispersion)); }); } ``` ## MapReduceCombined[Sorted] { #mapreduce_combined } The MapReduceCombined function generates a merged MapReduce operation, that is, it converts records of the input table (or tables), sorts the result, and performs Reduce similarly to [MapReduce](https://ytsaurus.tech/docs/en/#mapreduce), but with combiner mandatorily enabled. To make the output clearer, you can use an additional lambda function — finalizer, — which will be called on the reducer output to generate the final version of the record. Unfortunately, merged MapReduce operations in YT always generate an unsorted table. There's a workaround in the form of the `MapReduceCombinedSorted` function, which invokes `MapReduceCombined` and then sorts the output table. ```c++ template void MapReduceCombined( const IClientBasePtr& client, const TKeyBase& from, const TRichYPath& to, const TKeyColumns& reduceFields, bool (*mapper)(const R&, TMapped&), void (*combiner)(const TMapped&, W&), void (*reducer)(const W&, W&), bool (*finalizer)(const W&, TFin&)); ``` | **Parameter** | **Description** | | -------------- | ------------------------------------------------------------ | | `R` | Input record type (TNode or a specific protobuf message type) | | `TMapped` | Output record type for mapper and input record type for reducer (TNode or protobuf) | | `W` | Output record type for the combiner, input record type for the reducer and finalizer (TNode or a specific protobuf message type) | | `TFin`Opt | **Optional parameter.** Output entry type for finalizer (TNode or a specific protobuf message type). **Specified only in the version with a finalizer** | | `from` | Name of an input table (input tables) | | `to` | Output table name | | `reduceFields` | Which keys' identical values will be used for sorting and folding (Reduce) | | `mapper` | A conversion function that takes an `src` record of the `R` type as input and fills the `dst` record of the `TMapped` type. For `dst` to be written, return `true`. `mapper` may be `nullptr`. For performance reasons, the library passes as `dst` the same buffer variable that isn't even cleared between `mapper` calls. This is generally not a problem, because the `mapper` usually fills in the same `dst` fields. | | `combiner` | A conversion function that takes a new `src` record of the `TMapped` type as input and modifies the `dst` record of the `W` type. Essentially, it does the same thing that the reducer does in [MapReduce](https://ytsaurus.tech/docs/en/#mapreduce). `dst` is initialized by default (in case of TNode, there will be an empty TNode, that is, `Undefined`; protobuf is usually initialized with zeros and empty strings, which makes it more practical). You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | | `reducer` | An associative conversion function that takes a new `src` record as input and modifies the `dst` record of the same `W` type. The first record of the current range of records with the same `reduceFields` keys is initially included in `dst`. If there is one record in the current range, the `reducer` won't be called. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | | `finalizer`Opt | **Optional parameter.** A conversion function that takes a new `src` record of the `W` type as input and fills the `dst` record of the `TFin` type. Called once after Reduce has processed a range of input records to generate the final version of the output record. `dst` is initialized by default. You don't need to write anything in the `dst` fields that correspond to the keys — everything is already there | ### Application In addition to the protobuf messages defined in the [example for MapReduce](https://ytsaurus.tech/docs/en/#mapreduce_example): ```c++ message TDispersionDataMsg { required string Key = 1 [(NYT.column_name) = "key"]; required uint64 Count = 2 [(NYT.column_name) = "count"]; required double Sum = 3 [(NYT.column_name) = "sum"]; required double SumSquared = 4 [(NYT.column_name) = "sum_squared"]; }; ``` The function itself: ```c++ void CalculateDispersionWithCombiner(IClientPtr& client, TString input, TString output) { NYT::MapReduceCombinedSorted( client, input, output, "key", [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // mapper dst.SetName(src.GetSomeField()); dst.SetCount(src.GetSomeCountField()); return true; }, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // combiner double value = src.GetValue(); dst.SetCount(dst.GetCount() + 1); dst.SetSum(dst.GetSum() + value); dst.SetSumSquared(dst.GetSumSquared() + value * value); }, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // reducer dst.SetCount(src.GetCount() + dst.GetCount()); dst.SetSum(src.GetSum() + dst.GetSum()); dst.SetSumSquared(src.GetSumSquared() + dst.GetSumSquared()); }, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // finalizer double mean = src.GetSum() / src.GetCount(); double dispersion = src.GetSumSquared() / src.GetCount() - mean * mean; dst.SetMean(mean); dst.SetSigma(std::sqrt(dispersion)); }); } ``` ### Choosing between MapReduce and MapReduceCombined The `AdditiveMapReduce` function, which also provides the combiner, leaves it up to YT to choose whether to use the combiner or not. The `MapReduceCombined` function always forces the system to use the combiner even if omitting it would be more efficient. This is because writing lambdas in this function would be problematic, as this would allow inputting different types of records (from map and from combine) to the reducer. In general, the observed effect is as follows: the `MapReduceCombined` operations consume a few percent more CPU resources on the cluster than fully identical MapReduce operations, but are completed much faster. Since using [MapReduceCombined](https://ytsaurus.tech/docs/en/#mapreduce_combined) is as simple as using [MapReduce](https://ytsaurus.tech/docs/en/#mapreduce), it may be worth trying both during the development stage and leave the one that best suits your needs. ## Combining lambdas with the regular API You may want to use a lambda when calling operations in a standard way via `IClientBase` methods. For example, you wrote the mapper as a large class, and the reducer remained simple. Then it makes sense to use named wrapper classes (`TCopyIfMapper`, `TTransformMapper`, `TAdditiveReducer`, etc.). ### Example { #lambdas_mixed_example } ```c++ auto createReducer = []() { return new TAdditiveReducer([](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { dst.SetCount(src.GetCount() + dst.GetCount()); }); }; client->MapReduce( TMapReduceOperationSpec() .AddInput(inTable) .AddOutput(outTable) .ReduceBy(fields), new TMyComplicatedMapper(), createReducer(), // combiner createReducer(), // reducer TOperationOptions().Spec(TNode()("map_selectivity_factor", 0.005))); ``` ## TSaveable — global variables accessible from lambda operations { #saveable } If you create a global variable of the `NYT::TSaveable` type, where `TYourStruct` has access to the `ysaveload` variant serialization, then all the above functions described in the `yt_lambda` library will save the variable in the job state at the beginning of their work and restore it on the cluster before executing the job. Example: ```c++ struct TGlobalSettings { TString MailSuffix; Y_SAVELOAD_DEFINE(MailSuffix); }; NYT::TSaveable GlobalSettings; int main() { GlobalSettings.MailSuffix = "@domain-name"; ... /// use GlobalSettings.MailSuffix in lambda } ``` {% note warning "Attention" %} - Naturally, the contents of `TSaveable` aren't saved In ordinary (non-lambda) operations. In the [example above](https://ytsaurus.tech/docs/en/#lambdas_mixed_example), you can't use the `TSaveable` variable from the `TMyComplicatedMapper` class, but you can use it from the lambda created inside `createReducer()`. If necessary, you can manually add calls to `TSaveablesRegistry::Get()->SaveAll(stream)` and `LoadAll(stream) ` in the `Save` and `Load` functions of your mapper or reducer. - Don't use `TSaveable` if modification of these global variables and start of operations can be initiated from different threads to avoid data races and crashes. - You can create only one `TSaveable` variable for each user-defined type. This is intended and won't be changed in the near future. {% endnote %} # Usage examples You need to get a [YTsaurus token](https://ytsaurus.tech/user-guide/storage/auth.md) before running the examples. Run the examples on `Linux`, with no arguments. We recommend setting the log level to `INFO` or `DEBUG` to make programs output links to the web interface where you can monitor the execution. The log level is set using an environment variable with this command (more info is available in the [documentation](https://ytsaurus.tech/docs/en/description.md#logging)): ```bash export YT_LOG_LEVEL=INFO ``` For production processes, we recommend saving the most detailed `DEBUG` logs. If any problem occurs, these logs will help you find out what was happening. ## Basic level ### Simple Map (TNode version) Suppose there is a table with logins. You want to make a table with email addresses, deriving them from the login: `email = login + "@domain"`. To do this, you can use a simple mapper. The example is located at [yt/cpp/mapreduce/examples/tutorial/simple_map_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/simple_map_tnode). ```c++ 'yt/cpp/mapreduce/tutorial/simple_map_tnode/main.cpp' ``` ### Simple Map (Protobuf version) If you know the structure of the table, you may use the [Protobuf](https://ytsaurus.tech/docs/en/protobuf.md) format, which: - Is a bit faster. - Prevents such errors as typos in column names or usage of wrong type for column. The example is located at [yt/cpp/mapreduce/examples/tutorial/simple_map_protobuf](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/simple_map_protobuf). ```protobuf import "yt/yt_proto/yt/formats/extension.proto"; message TLoginRecord { optional string Name = 1 [(NYT.column_name) = "name"]; optional string Login = 2 [(NYT.column_name) = "login"]; } message TEmailRecord { optional string Name = 1 [(NYT.column_name) = "name"]; optional string Email = 2 [(NYT.column_name) = "email"]; } ``` ```c++ #include #include #include #include #include using namespace NYT; class TComputeEmailsMapper : public IMapper, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { for (auto& cursor : *reader) { const auto& loginRecord = cursor.GetRow(); TEmailRecord emailRecord; emailRecord.set_name(loginRecord.name()); emailRecord.set_email(loginRecord.login() + "@yandex-team.ru"); writer->AddRow(emailRecord); } } }; REGISTER_MAPPER(TComputeEmailsMapper) int main() { NYT::Initialize(); auto client = CreateClient("freud"); // Выходная табличка у нас будет лежать в tmp и содержать имя текущего пользователя. const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-emails-protobuf"; client->Map( TMapOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .AddOutput(outputTable), new TComputeEmailsMapper); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Simple Map with Protobuf using lambda expressions in C++ Some simple operations can be represented as C++ lambda expressions with an empty capture-list (not capturing variables) using the [yt/cpp/mapreduce/library/lambda](https://ytsaurus.tech/docs/en/lambda.md) library. The previous example can be rewritten as follows (`data.proto` is the same): ([yt/cpp/mapreduce/examples/tutorial/simple_map_lambda](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/simple_map_lambda)) ```c++ #include #include #include #include #include struct TGlobalSettings { TString MailSuffix; Y_SAVELOAD_DEFINE(MailSuffix); }; NYT::TSaveable GlobalSettings; int main() { NYT::Initialize(); auto client = NYT::CreateClient("freud"); // Выходная табличка у нас будет лежать в tmp и содержать имя текущего пользователя. auto outputTable = NYT::TRichYPath("//tmp/" + GetUsername() + "-tutorial-emails-protobuf") .CompressionCodec("zlib_9"); // TransformCopyIf запоминает содержимое GlobalSettings на момент своего запуска // и восстанавливает его в job'ах, в которых вызывается лямбда. // Этот способ требует аккуратности, если операции запускать из разных потоков. GlobalSettings.MailSuffix = "@yandex-team.ru"; NYT::TransformCopyIf( client, "//home/tutorial/staff_unsorted", outputTable, [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { dst.set_name(src.name()); dst.set_email(src.login() + GlobalSettings.MailSuffix); return true; }); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable.Path_ << Endl; return 0; } ``` ### Sorting a table and a simple Reduce operation Given the same input table, we can count how many times each name occurs. To do this we need Reduce operation. Reduce operation requires input table to be sorted, so we run the Sort operation before launching Reduce. The example is located at [yt/cpp/mapreduce/examples/tutorial/simple_reduce_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/simple_reduce_tnode). ```c++ #include #include #include using namespace NYT; class TCountNamesReduce : public IReducer, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { // В Do приходят все записи с общим reduce ключом, т.е. в нашем случае с общим полем `name'. const auto& row = reader->GetRow(); const auto name = row["name"]; ui32 count = 0; for ([[maybe_unused]] auto& cursor : *reader) { ++count; } TNode result; result["name"] = name; result["count"] = count; writer->AddRow(result); } }; REGISTER_REDUCER(TCountNamesReduce) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString sortedTmpTable = "//tmp/" + GetUsername() + "-tutorial-tmp"; const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-emails"; client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .Output(sortedTmpTable) .SortBy({"name"})); client->Reduce( TReduceOperationSpec() .ReduceBy({"name"}) .AddInput(sortedTmpTable) // Входная таблица должна быть отсортирована по тем колонкам, // по которым мы осуществляем reduce. .AddOutput(outputTable), new TCountNamesReduce); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Composite types and Protobuf format (using Reduce as an example) Let's say we have a table with a set of records on links occurring in web documents. This table has the following columns: - `DocTitle`: is a string containing the document name. - `Link` is a struct containing following fields: - `Host`: host part of the link url. - `Port`: port part of the link url. - `Path`: path part of the link url. - `OccurrenceCount`: how many times link appears in the document. Our input table is sorted by `DocTitle` column. We want to aggregate information about each document into single row of output table. Let's describe protobuf messages of the following form: ```c++ import "yt/yt_proto/yt/formats/extension.proto"; message TUrl { optional string host = 1 [(NYT.column_name) = "Host"]; optional int32 port = 2 [(NYT.column_name) = "Port"]; optional string path = 3 [(NYT.column_name) = "Path"]; } message TExtraInfo { optional uint32 total_occurrence_count = 1; } message TDoc { // Помечаем, что поля данного сообщения будут сериализоваться // в YT-режиме. Это означает, что им сопоставлены колонки таблицы // с соответствующими (возможно, сложными) типами. option (NYT.default_field_flags) = SERIALIZATION_YT; optional string title = 1 [(NYT.key_column_name) = "Title"]; repeated TUrl links = 2 [(NYT.column_name) = "Links"]; repeated uint32 occurrence_count = 3 [(NYT.column_name) = "UpdateTimes"]; // Данное поле специально помечено как сериализуемое в бинарном (PROTOBUF) режиме. // В таблице эта колонка будет иметь тип "string". optional TExtraInfo extra_info = 4 [(NYT.column_name) = "ExtraInfo", (NYT.flags) = SERIALIZATION_PROTOBUF]; } message TLinkEntry { option (NYT.default_field_flags) = SERIALIZATION_YT; optional string doc_title = 1 [(NYT.column_name) = "DocTitle"]; optional TUrl link = 2 [(NYT.column_name) = "Link"]; optional uint32 occurrence_count = 3 [(NYT.column_name) = "OccurrenceCount"]; } ``` `TLinkEntry` corresponds to the input table row, and `TDoc` corresponds to the output table row. The `TDoc` fields have the following semantics: - `Title`: Title. - `Links`: List of links occurring in the document. - `OccurrenceCounts`: List with the number of times the given links occur (this list has same length as the `Links` list). - `ExtraInfo`: Additional information, which in this case includes the total number of times the links occurs. {% note warning "Attention" %} `(NYT.field_serialization_mode)` in `TDoc` and `TLinkEntry` messages. By default, this option equals `PROTOBUF`, which means "serialize the field as a sequence of bytes". The `YT` value we set means that the corresponding nested message will be related to the composite type in the table schema. In `TDoc`, as an example, we marked the `ExtraInfo` field with the `(NYT.serialization_mode) = PROTOBUF` option (note the type of the respective field in the output table). {% endnote %} The [reducer](https://ytsaurus.tech/user-guide/data-processing/operations/reduce.md) is straightforward. ```c++ #include #include #include #include #include using namespace NYT; // Редьюсер агрегирует информацию о ссылках на документ с данным заголовком. class AggregateLinksReducer : public IReducer, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { TDoc doc; for (auto& cursor : *reader) { auto entry = cursor.MoveRow(); if (!doc.has_title()) { doc.set_title(entry.doc_title()); } doc.add_links()->Swap(entry.mutable_link()); doc.add_occurrence_count(entry.occurrence_count()); auto newCount = doc.extra_info().total_occurrence_count() + entry.occurrence_count(); doc.mutable_extra_info()->set_total_occurrence_count(newCount); } writer->AddRow(doc); } }; REGISTER_REDUCER(AggregateLinksReducer) int main() { NYT::Initialize(); TString cluster = "freud"; auto client = CreateClient(cluster); const TString sortedLinksTable = "//home/tutorial/links_sorted_schematized"; Cout << "Sorted links table: https://yt.yandex-team.ru/" << cluster << "/#page=navigation&offsetMode=row&path=" << sortedLinksTable << Endl; const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-docs-protobuf"; // Обратите внимание на опцию `InferOutputSchema`, // она заставляет навешивать на выходную таблицу схему. client->Reduce( TReduceOperationSpec() .AddInput(sortedLinksTable) .AddOutput(outputTable) .ReduceBy({"DocTitle"}), new AggregateLinksReducer, TOperationOptions() .InferOutputSchema(true)); Cout << "Output table: https://yt.yandex-team.ru/" << cluster << "/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Reduce with multiple input tables { #multiple-input-reduce-tnode } Imagine that besides the table with users from our previous examples, there is also a second table with robots. The following program creates a table where non-robot users are filtered out. The example is located at [yt/cpp/mapreduce/examples/tutorial/multiple_input_reduce_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/multiple_input_reduce_tnode). ```c++ #include #include #include using namespace NYT; class TFilterRobotsReduce : public IReducer, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { TNode loginRow; bool isRobot = false; for (auto& cursor : *reader) { const auto& curRow = cursor.GetRow(); // У нас есть информация о том из какой таблицы пришла запись. auto tableIndex = cursor.GetTableIndex(); if (tableIndex == 0) { // Таблица с логинами. loginRow = curRow; } else if (tableIndex == 1) { // Таблица про роботов. isRobot = curRow["is_robot"].AsBool(); } else { // Какая-то фигня, такого индекса быть не может. Y_ABORT(); } } if (isRobot) { writer->AddRow(loginRow); } } }; REGISTER_REDUCER(TFilterRobotsReduce) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString sortedLoginTable = "//tmp/" + GetUsername() + "-tutorial-login-sorted"; const TString sortedIsRobotTable = "//tmp/" + GetUsername() + "-tutorial-is_robot-sorted"; const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-robots"; client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .Output(sortedLoginTable) .SortBy({"uid"})); client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/is_robot_unsorted") .Output(sortedIsRobotTable) .SortBy({"uid"})); client->Reduce( TReduceOperationSpec() .ReduceBy({"uid"}) .AddInput(sortedLoginTable) // Таблицу с логинами мы добавляем первой, поэтому её TableIndex == 0 .AddInput(sortedIsRobotTable) // Таблицу про роботов мы добавляем второй, поэтому её TableIndex == 1 .AddOutput(outputTable), new TFilterRobotsReduce); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Reading files from operations { #map-tnode-with-file } The previous task can be solved in a different way: upload the table with robots directly to the machines running the operations and read it there as a whole in `yhash_set`. In this case, you don't have to sort the table and can use Map instead of Reduce. This method is used for small tables up to several GB in size. Input tables are the same as in the previous example: [with users](https://yt.yandex-team.ru/freud/#page=navigation&path=//home/dev/tutorial/staff_unsorted&offsetMode=row) and [with robots](https://yt.yandex-team.ru/freud/#page=navigation&path=//home/dev/tutorial/is_robot_unsorted&offsetMode=row). The following program creates a table where non-robot users are filtered out. The example is located at [yt/cpp/mapreduce/examples/tutorial/map_tnode_with_file](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/map_tnode_with_file). ```c++ #include #include #include #include #include using namespace NYT; class TFilterRobotsMap : public IMapper, TTableWriter> { public: void Do(TReader* loginReader, TWriter* writer) override { // Если мы прикрепляли табличку //path/to/table, то в операции мы будем видеть её под именем table TIFStream stream("is_robot_unsorted"); auto isRobotReader = CreateTableReader(&stream); THashSet robotIds; for (auto& cursor : *isRobotReader) { const auto& curRow = cursor.GetRow(); if (curRow["is_robot"].AsBool()) { robotIds.insert(curRow["uid"].AsInt64()); } } for (auto& cursor : *loginReader) { const auto& curRow = cursor.GetRow(); if (robotIds.contains(curRow["uid"].AsInt64())) { writer->AddRow(curRow); } } } }; REGISTER_MAPPER(TFilterRobotsMap) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString loginTable = "//home/tutorial/staff_unsorted"; const TString isRobotTable = "//home/tutorial/is_robot_unsorted"; const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-robots"; client->Map( TMapOperationSpec() .AddInput(loginTable) .MapperSpec(TUserJobSpec() .AddFile(TRichYPath(isRobotTable).Format("yson"))) // Таблицу с роботами добавляем в виде файла .AddOutput(outputTable), new TFilterRobotsMap); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Reduce with multiple input and output tables Here we have a similar task as in the previous example, except this time we'll write two output tables at once: with human users and with robots. The example is located at [yt/cpp/mapreduce/examples/tutorial/multiple_input_multiple_output_reduce_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/multiple_input_multiple_output_reduce_tnode). ```c++ #include #include #include using namespace NYT; class TSplitHumanRobotsReduce : public IReducer, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { TNode loginRow; bool isRobot = false; for (auto& cursor : *reader) { const auto& curRow = cursor.GetRow(); auto tableIndex = cursor.GetTableIndex(); if (tableIndex == 0) { loginRow = curRow; } else if (tableIndex == 1) { isRobot = curRow["is_robot"].AsBool(); } else { Y_ABORT(); } } // Второй аргумент метода `AddRow' указывает, в какую таблицу будет записано значение. if (isRobot) { writer->AddRow(loginRow, 0); } else { writer->AddRow(loginRow, 1); } } }; REGISTER_REDUCER(TSplitHumanRobotsReduce) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString sortedLoginTable = "//tmp/" + GetUsername() + "-tutorial-login-sorted"; const TString sortedIsRobotTable = "//tmp/" + GetUsername() + "-tutorial-is_robot-sorted"; const TString humanTable = "//tmp/" + GetUsername() + "-tutorial-humans"; const TString robotTable = "//tmp/" + GetUsername() + "-tutorial-robots"; client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .Output(sortedLoginTable) .SortBy({"uid"})); client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/is_robot_unsorted") .Output(sortedIsRobotTable) .SortBy({"uid"})); client->Reduce( TReduceOperationSpec() .ReduceBy({"uid"}) .AddInput(sortedLoginTable) .AddInput(sortedIsRobotTable) .AddOutput(robotTable) // выходная таблица номер 0 .AddOutput(humanTable), // выходная таблица номер 1 new TSplitHumanRobotsReduce); Cout << "Robot table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << robotTable << Endl; Cout << "Human table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << humanTable << Endl; return 0; } ``` ### Reduce with multiple input and output tables (Protobuf version) To create separate tables for human users and robots, we need to rewrite the same reducer to protobuf. The table with human users will contain the `login`, `email`, and `name` fields. The table with robots will contain the `login` and `uid` fields. We need to create separate protobuf message types for these tables. The example is located at [yt/cpp/mapreduce/examples/tutorial/multiple_input_multiple_output_reduce_protobuf](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/multiple_input_multiple_output_reduce_protobuf). ```c++ import "yt/yt_proto/yt/formats/extension.proto"; message TUserRecord { optional int64 uid = 1 [(NYT.column_name) = "uid"]; optional string name = 2 [(NYT.column_name) = "name"]; optional string login = 3 [(NYT.column_name) = "login"]; } message TIsRobotRecord { optional int64 uid = 1 [(NYT.column_name) = "uid"]; optional bool is_robot = 2 [(NYT.column_name) = "is_robot"]; } message THumanRecord { optional string name = 1 [(NYT.column_name) = "name"]; optional string email = 2 [(NYT.column_name) = "email"]; optional string login = 3 [(NYT.column_name) = "login"]; } message TRobotRecord { optional int64 uid = 1 [(NYT.column_name) = "uid"]; optional string login = 2 [(NYT.column_name) = "login"]; } ``` ```c++ #include #include #include #include #include using namespace NYT; class TSplitHumanRobotsReduce // Обратите внимание наш редьюс работает с несколькими типами записей // как на вход так и на выход, поэтому мы используем ::google::protobuf::Message : public IReducer< TTableReader<::google::protobuf::Message>, TTableWriter<::google::protobuf::Message>> { public: void Do(TReader* reader, TWriter* writer) override { TUserRecord userRecord; bool isRobot = false; for (auto& cursor : *reader) { auto tableIndex = cursor.GetTableIndex(); // Мы знаем номер таблицы и поэтому мы можем запросить конкретный тип protobuf'а в этом месте. // Тип protobuf сообщения передаётся шаблонным аргументом к методу `GetRow()'. if (tableIndex == 0) { userRecord = cursor.GetRow(); } else if (tableIndex == 1) { const auto& isRobotRecord = cursor.GetRow(); isRobot = isRobotRecord.is_robot(); } else { Y_ABORT(); } } // В AddRow мы можем передавать как TRobotRecord так и THumanRecord. if (isRobot) { TRobotRecord robotRecord; robotRecord.set_uid(userRecord.uid()); robotRecord.set_login(userRecord.login()); writer->AddRow(robotRecord, 0); } else { THumanRecord humanRecord; humanRecord.set_name(userRecord.name()); humanRecord.set_login(userRecord.login()); humanRecord.set_email(userRecord.login() + "@yandex-team.ru"); writer->AddRow(humanRecord, 1); } } }; REGISTER_REDUCER(TSplitHumanRobotsReduce) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString sortedUserTable = "//tmp/" + GetUsername() + "-tutorial-user-sorted"; const TString sortedIsRobotTable = "//tmp/" + GetUsername() + "-tutorial-is_robot-sorted"; const TString humanTable = "//tmp/" + GetUsername() + "-tutorial-humans"; const TString robotTable = "//tmp/" + GetUsername() + "-tutorial-robots"; client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .Output(sortedUserTable) .SortBy({"uid"})); client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/is_robot_unsorted") .Output(sortedIsRobotTable) .SortBy({"uid"})); client->Reduce( TReduceOperationSpec() .ReduceBy({"uid"}) .AddInput(sortedUserTable) .AddInput(sortedIsRobotTable) .AddOutput(robotTable) .AddOutput(humanTable), new TSplitHumanRobotsReduce); Cout << "Robot table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << robotTable << Endl; Cout << "Human table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << humanTable << Endl; return 0; } ``` ### Reading and writing tables When writing a table you can overwrite existing data or append your data to the table. When reading a table you can read the entire table or ranges by row number or key. Learn more about [reading and writing data](https://ytsaurus.tech/user-guide/storage/formats.md#table_data) and [tables](https://ytsaurus.tech/user-guide/storage/objects.md#tables). The example is located at [yt/cpp/mapreduce/examples/tutorial/table_read_write_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/table_read_write_tnode). ```c++ #include #include using namespace NYT; int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString table = "//tmp/" + GetUsername() + "-read-write"; { // Просто пишем данные в таблицу, если таблица существует, её перезапишут. auto writer = client->CreateTableWriter(table); TNode row; row["english"] = "one"; row["russian"] = "один"; writer->AddRow(row); row["english"] = "two"; row["russian"] = "два"; writer->AddRow(row); // Лучше не забывать звать метод Finish явно. // Он конечно позовётся в деструкторе writer'а, но если случатся какие-нибудь ошибки, // то у вас не будет возможности их поймать, потому что деструктор в случае ошибки абортит программу. writer->Finish(); } { // Дописываем данные в конец таблицы, // Для этого необходимо выставить опцию Append. auto writer = client->CreateTableWriter(TRichYPath(table).Append(true)); TNode row; row["english"] = "three"; row["russian"] = "три"; writer->AddRow(row); writer->Finish(); } { // Читаем всю таблицу auto reader = client->CreateTableReader(table); Cout << "*** ALL TABLE ***" << Endl; for (auto& cursor : *reader) { // reader имеет тот же самый интерфейс, что и reader в джобах auto& row = cursor.GetRow(); Cout << "russian: " << row["russian"].AsString() << "; " << "english: " << row["english"].AsString() << Endl; } Cout << Endl; } { // Читаем первые 2 строки. auto reader = client->CreateTableReader(TRichYPath(table).AddRange( TReadRange() .LowerLimit(TReadLimit().RowIndex(0)) .UpperLimit(TReadLimit().RowIndex(2)))); // читаем с 0й по 2ю строки, 2я невключительно Cout << "*** FIRST TWO ROWS ***" << Endl; for (auto& cursor : *reader) { auto& row = cursor.GetRow(); Cout << "russian: " << row["russian"].AsString() << "; " << "english: " << row["english"].AsString() << Endl; } Cout << Endl; } { // Мы можем отсортировать таблицу и читать записи по ключам. client->Sort(TSortOperationSpec() .SortBy({"english"}) .AddInput(table) .Output(table)); auto reader = client->CreateTableReader(TRichYPath(table).AddRange( TReadRange() .Exact(TReadLimit().Key({"three"})))); // если нужен один ключ а не диапазон, можно использовать // Exact вместо LowerLimit / UpperLimit, // Key указывает, что мы ищем запись по ключу и работает // только для сортированных таблиц. Cout << "*** EXACT KEY ***" << Endl; for (auto& cursor : *reader) { auto& row = cursor.GetRow(); Cout << "russian: " << row["russian"].AsString() << "; " << "english: " << row["english"].AsString() << Endl; } Cout << Endl; } return 0; } ``` ### Transferring states to jobs Sometimes you may need to write an operation that takes certain arguments. For example, write a program that filters the source table by user name. The example is located at [yt/cpp/mapreduce/examples/tutorial/stateful_map_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/stateful_map_tnode). ```c++ #include #include #include #include using namespace NYT; class TFilterMapper : public IMapper, TTableWriter> { public: Y_SAVELOAD_JOB(Pattern_, MaxDistance_); // Заклинание, которое говорит, какие переменные нужно передавать на сервер. TFilterMapper() = default; // У джобы обязательно должен быть конструктор по умолчанию TFilterMapper(TString pattern, double maxDistance) : Pattern_(std::move(pattern)) , MaxDistance_(maxDistance) { } void Do(TReader* reader, TWriter* writer) override { for (auto& cursor : *reader) { const auto& row = cursor.GetRow(); const auto& name = row["name"].AsString(); if (NLevenshtein::Distance(name, Pattern_) <= MaxDistance_) { writer->AddRow(row); } } } private: TString Pattern_; size_t MaxDistance_ = 0; }; REGISTER_MAPPER(TFilterMapper) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-output-statefull-map"; client->Map( TMapOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .AddOutput(outputTable), new TFilterMapper("Arkady", 2)); // Мы создаём объект TFilterMapper, и конструктор заполняет поля Pattern_ и MaxDistance_. // Библиотека сериализует поля, указанные в Y_SAVELOAD_JOB, и загружает их на сервер. // На сервере вызывается конструктор по умолчанию и сериализованные поля загружаются. Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### MapReduce operation (Protobuf version) YTsaurus has the [MapReduce](https://ytsaurus.tech/user-guide/data-processing/operations/mapreduce.md) merged operation, which works a bit faster than [Map](https://ytsaurus.tech/user-guide/data-processing/operations/map.md) + [Sort](https://ytsaurus.tech/user-guide/data-processing/operations/sort.md) + [Reduce](https://ytsaurus.tech/user-guide/data-processing/operations/reduce.md). Let's return to one of our first examples and once again get the statistics of how many times a name occurs in our table with users. Before we do that, we first need to standardize the names by converting all of them to lowercase so that people with the names `ARCHIE` and `Archie` are counted as one. The example is located at [yt/cpp/mapreduce/examples/tutorial/mapreduce_protobuf](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/mapreduce_protobuf). ```c++ #include #include #include #include #include using namespace NYT; // // Для того чтобы запустить операцию mapreduce, нам нужны обычные классы Mapper'а и Reducer'а // (эти классы даже можно использовать в других местах в отдельных операциях Map/Reduce). // class TNormalizeNameMapper : public IMapper< TTableReader, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { for (auto& cursor : *reader) { auto row = cursor.GetRow(); row.set_name(ToLowerUTF8(TString(row.name()))); writer->AddRow(row); } } }; REGISTER_MAPPER(TNormalizeNameMapper) class TCountNameReducer : public IReducer< TTableReader, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { TNameStatistics result; ui64 count = 0; for (auto& cursor : *reader) { const auto& row = cursor.GetRow(); if (!result.has_name()) { result.set_name(row.name()); } ++count; } result.set_count(count); writer->AddRow(result); } }; REGISTER_REDUCER(TCountNameReducer) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-output"; // Запуск операции MapReduce несильно отличается от запуска других операций. // Нам надо указать список ключей, по которым мы будем редьюсить // и два класса -- один Mapper и один Reducer. client->MapReduce( TMapReduceOperationSpec() .ReduceBy({"name"}) .AddInput("//home/tutorial/staff_unsorted") .AddOutput(outputTable), new TNormalizeNameMapper, new TCountNameReducer); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### MapReduce operation (version with lambda expressions) The task is the same as in the previous example. The example is located at [yt/cpp/mapreduce/examples/tutorial/mapreduce_lambda](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/mapreduce_lambda). ```c++ #include #include #include #include #include using namespace NYT; int main() { NYT::Initialize(); auto client = CreateClient("freud"); // Эта таблица будет с некоторой вероятностью не отсортирована. // Если вместо MapReduce() использовать MapReduceSorted(), тогда // она будет дополнительно досортирована по "name" и это будет отражено в схеме const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-output"; MapReduce( client, "//home/tutorial/staff_unsorted", outputTable, {"name"}, // список ключей, по которым мы будем редьюсить [](https://ytsaurus.tech/docs/en/auto&%20src,%20auto&%20dst) { // mapper dst.set_name(ToLowerUTF8(TString(src.name()))); return true; }, [](https://ytsaurus.tech/docs/en/auto&%20/*src*/,%20auto&%20dst) { // reducer // dst.set_count() не вызываем, т.к. когда по // "name" редьюсим, это поле уже заполнено. // так можно делать, т.к. конструктор протобуфа dst // проинициализирует .Count в 0 (по умолчанию): dst.set_count(dst.count() + 1); }); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Preparing an operation in a job class { #prepare_operation } You can set up the operation parameters by overloading the `IJob::PrepareOperation` method. For more information, see [Description](https://ytsaurus.tech/docs/en/description.md#prepare_operation). ```protobuf import "yt/yt_proto/yt/formats/extension.proto"; message TGrepperRecord { optional string key = 1; optional bytes other = 2 [(NYT.flags) = OTHER_COLUMNS]; }; ``` ```c++ #include #include #include #include #include using namespace NYT; // Этот маппер отдаёт все строки, значение в колонке `column` которых равно `pattern`. class TGrepper : public IMapper, TTableWriter> { public: TGrepper() = default; TGrepper(TString column, TString pattern) : Column_(column) , Pattern_(pattern) { } void Do(TReader* reader, TWriter* writer) override { for (const auto& cursor : *reader) { auto row = cursor.GetRow(); if (row.key() == Pattern_) { writer->AddRow(row); } } } void PrepareOperation(const IOperationPreparationContext& context, TJobOperationPreparer& preparer) const override { auto schema = context.GetInputSchema(/* tableIndex */ 0); // Выходная схема совпадает со входной во всём, кроме названия колонки, по которой ищем. for (auto& column : schema.MutableColumns()) { if (column.Name() == Column_) { column.Name("Key"); } } preparer .InputColumnRenaming(/* tableIndex */ 0, {{Column_, "Key"}}) .InputDescription(/* tableIndex */ 0) // Выключаем автовывод схемы, т.к. он ничего не даст (у нас есть поле, помеченное OTHER_COLUMNS). .OutputDescription(/* tableIndex */ 0, /* inferSchema */ false) .OutputSchema(/* tableIndex */ 0, schema); } Y_SAVELOAD_JOB(Column_, Pattern_); private: TString Column_; TString Pattern_; }; REGISTER_MAPPER(TGrepper) int main() { NYT::Initialize(); auto client = CreateClient("freud"); // Выходная табличка у нас будет лежать в tmp и содержать имя текущего пользователя. const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-grepped-emails"; client->Map( new TGrepper("login", "lev"), "//home/tutorial/staff_unsorted_schematized", outputTable); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ## Advanced level ### Running multiple operations in parallel To run multiple operations in parallel, use the [TOperationTracker](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/library/operation_tracker/operation_tracker.h) class. The example is located at [yt/cpp/mapreduce/examples/tutorial/operation_tracker](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/operation_tracker). ```c++ #include #include // OperationTracker живёт в этой библиотеке #include #include using namespace NYT; int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString sortedLoginTable = "//tmp/" + GetUsername() + "-tutorial-login-sorted"; const TString sortedIsRobotTable = "//tmp/" + GetUsername() + "-tutorial-is_robot-sorted"; const TString humanTable = "//tmp/" + GetUsername() + "-tutorial-humans"; const TString robotTable = "//tmp/" + GetUsername() + "-tutorial-robots"; TOperationTracker tracker; tracker.AddOperation( client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .Output(sortedLoginTable) .SortBy({"uid"}), TOperationOptions().Wait(false))); // Wait(false) говорит клиенту не ждать завершения операции tracker.AddOperation( client->Sort( TSortOperationSpec() .AddInput("//home/tutorial/is_robot_unsorted") .Output(sortedIsRobotTable) .SortBy({"uid"}), TOperationOptions().Wait(false))); tracker.WaitAllCompleted(); // Ждёт когда обе операции завершатся, бросит исключение в случае ошибок. return 0; } ``` ### Reduce with enable_key_guarantee=false Suppose you want to filter a table with URLs by regular expressions that are located in a separate table with hosts (each host has its own regular expression). And while you could run Reduce on the `host` column, real data is often arranged in such a way that some hosts have a disproportionately large number of URLs. In our example, URLs from the host `https://www.youtube.com` take up more than half of the table. The job responsible for processing such a host will take a disproportionately large amount of time (keys like `https://www.youtube.com` here are also called *monster keys*). However, you don't need all the records with the same host to go to the same machine to solve the task. You can distribute the table with URLs among several machines: note that the host must be received along with the URL. ![](https://ytsaurus.tech/_images/reduce.svg){ .center} ![](https://ytsaurus.tech/_images/join_reduce.svg){ .center} The example is located at [yt/cpp/mapreduce/examples/tutorial/join_reduce_tnode](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/join_reduce_tnode). ```c++ #include #include #include using namespace NYT; class TFilterVideoRegexp : public IReducer, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { // Так же как и с обычным Reducer'ом в каждый вызов метода Do // придут записи с общим JoinBy ключом. TMaybe regex; for (auto& cursor : *reader) { auto row = cursor.GetRow(); if (cursor.GetTableIndex() == 0) { // таблица с хостами const auto videoRegexp = row["video_regexp"].AsString(); // Дебажная печать, stderr можно будет посмотреть в web интерфейсе Cerr << "Processing host: " << row["host"].AsString() << Endl; if (!videoRegexp.empty()) { regex = TRegExMatch(videoRegexp); } } else { // таблица с урлами if (regex && regex->Match(row["path"].AsString().c_str())) { writer->AddRow(row); } } } } }; REGISTER_REDUCER(TFilterVideoRegexp) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-join-reduce"; const TString outputTmpVideoTable = "//tmp/" + GetUsername() + "-tutorial-join-reduce-video-sorted"; const TString outputTmpDocTable = "//tmp/" + GetUsername() + "-tutorial-join-reduce-doc-sorted"; client->Sort(TSortOperationSpec() .SortBy({"host"}) .AddInput("//home/tutorial/host_video_regexp") .Output(outputTmpVideoTable)); client->Sort(TSortOperationSpec() .SortBy({"host"}) .AddInput("//home/tutorial/doc_title") .Output(outputTmpDocTable)); client->Reduce( TReduceOperationSpec() .JoinBy({"host"}) .AddInput( TRichYPath(outputTmpVideoTable) .Foreign(true)) // важно отметить хостовую таблицу как foreign .AddInput(outputTmpDocTable) .AddOutput(outputTable) .EnableKeyGuarantee(false), new TFilterVideoRegexp); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Batch requests You can execute light requests (create/delete a table, check its existence, etc.) in batches. Use this method if you need to run a large amount of uniform operations. Batch requests can save a lot of time. The example is located at [yt/cpp/mapreduce/examples/tutorial/batch_request](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/batch_request). ```c++ #include #include using namespace NYT; int main() { NYT::Initialize(); auto client = CreateClient("freud"); // Создаём batch запрос (это можно делать и из транзакции). auto request = client->CreateBatchRequest(); // Добавляем запросы в batch NThreading::TFuture docTitleExists = request->Exists("//home/tutorial/doc_title"); NThreading::TFuture unexistingTableExists = request->Exists("//home/tutorial/unexisting_table"); const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-test-batch"; NThreading::TFuture createResult = request->Create(outputTable, NT_TABLE); // Выполняем batch запрос. request->ExecuteBatch(); // Проверяем результаты. Cout << "Table //home/tutorial/doc_title exists: " << docTitleExists.GetValue() << Endl; Cout << "Table //home/tutorial/unexisting_table exists: " << unexistingTableExists.GetValue() << Endl; try { // Следует проверять все результаты с помощью GetValue(), // т.к. отдельные запросы могут пофейлиться и тогда соответствующая TFuture будет содержать ошибку. // // Если запускать эту программу второй раз то Create пофейлится, потому что таблица уже существует. createResult.GetValue(); } catch (const std::exception& ex) { Cerr << "Create " << outputTable << " failed: " << ex.what() << Endl; } return 0; } ``` ### Delivering a table dump to a job as a file In YTsaurus, you can deliver files to jobs (for example, as was described [earlier](https://ytsaurus.tech/docs/en/#map-tnode-with-file)). A less popular method is delivering a table dump to a job. This can prove useful when a job requires a map stored in a small (a few gigabytes) table. However, don't use this approach to load large tables: if you have a table that is dozens of gigabytes in size, it won't fit on a single node of the cluster where the jobs are running. The example is located at [yt/cpp/mapreduce/examples/tutorial/pass_table_as_file](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/pass_table_as_file). ```c++ #include #include #include #include using namespace NYT; class TFilterRobotsMap : public IMapper, TTableWriter> { public: // Мы читаем дамп таблицы в методе Start, перед тем как обработать первую запись в Map-таблице. // Настройка доставки таблицы в джобу происходит в функции main. void Start(TWriter*) override { // Нам в джобу доставили файлик "robot_table" с дампом таблицы. TFileInput tableDump("robot_table"); // Есть функция CreateTableReader<>, которая умеет создавать читателя из любого IInputStream'а. // Созданный читатель имеет интерфейс аналогичный другим читателям. auto reader = CreateTableReader(&tableDump); for (auto& cursor : *reader) { const auto& curRow = cursor.GetRow(); if (curRow["is_robot"].AsBool()) { RobotUids.insert(curRow["uid"].AsInt64()); } } } void Do(TReader* reader, TWriter* writer) override { for (auto& cursor : *reader) { const auto& curRow = cursor.GetRow(); if (RobotUids.contains(curRow["uid"].AsInt64())) { writer->AddRow(curRow); } } } private: THashSet RobotUids; }; REGISTER_MAPPER(TFilterRobotsMap) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-robots"; client->Map( TMapOperationSpec() .MapperSpec( TUserJobSpec() // Самое интересное -- мы просим YT доставить нам табличку в виде файла. .AddFile( TRichYPath("//home/tutorial/is_robot_unsorted") // Тут указываем таблицу, которую нам надо доставить. .Format("yson") // Это формат, в котором таблица будет прочитана, нам нужен yson, чтобы TNode-читатель в джобе смог прочитать файл. .FileName("robot_table") // Это имя файла, с дампом таблицы. // Тут же можно было бы указать фильтрацию по колонкам или фильтрацию по номерам строк таблицы // с помощью соответсвтующих методов TRichYPath, но нам ничего этого не надо. )) .AddInput("//home/tutorial/staff_unsorted") .AddOutput(outputTable), new TFilterRobotsMap); Cout << "Output table: https://yt.yandex-team.ru/freud/#page=navigation&offsetMode=row&path=" << outputTable << Endl; return 0; } ``` ### Writing and retrieving job statistics A running operation gathers a lot of statistics (see the [documentation](https://ytsaurus.tech/user-guide/problems/jobstatistics.md)). Example of a simple Map with statistics collection: [yt/cpp/mapreduce/examples/tutorial/job_statistics](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/job_statistics): ```c++ #include #include using namespace NYT; class TComputeEmailsMapper : public IMapper, TTableWriter> { public: void Do(TReader* reader, TWriter* writer) override { // Соберём несколько интересных статистик по сотрудникам i64 fortyTwoCount = 0; i64 shortLoginCount = 0; for (const auto& cursor : *reader) { const auto& row = cursor.GetRow(); auto login = row["login"].AsString(); TNode outRow; outRow["name"] = row["name"]; outRow["email"] = login + "@yandex-team.ru"; writer->AddRow(outRow); if (login.find("42") != TString::npos) { ++fortyTwoCount; } if (login.size() <= 4) { ++shortLoginCount; } } // Пользовательскую статистику можно писать по пути (имени) WriteCustomStatistics("names/with_42", fortyTwoCount); WriteCustomStatistics(TNode() // А можно в виде мапы ("names", TNode() ("short", shortLoginCount))); } }; REGISTER_MAPPER(TComputeEmailsMapper) int main() { NYT::Initialize(); auto client = CreateClient("freud"); const TString outputTable = "//tmp/" + GetUsername() + "-tutorial-emails"; auto operation = client->Map( TMapOperationSpec() .AddInput("//home/tutorial/staff_unsorted") .AddOutput(outputTable), new TComputeEmailsMapper); auto jobStatistics = operation->GetJobStatistics(); // Выведем системные статистики Cout << "time/total: " << jobStatistics.GetStatisticsAs("time/total").Sum() << Endl; Cout << "data/input/row_count: " << jobStatistics.GetStatistics("data/input/row_count").Sum() << Endl; // NB: для получения пользовательских статистик используется другой метод Cout << "names/with_42: " << jobStatistics.GetCustomStatistics("names/with_42").Sum() << Endl; Cout << "names/short: " << jobStatistics.GetCustomStatistics("names/short").Sum() << Endl; return 0; } ``` ### Basic work with dynamic tables YTsaurus supports [dynamic tables](https://ytsaurus.tech/user-guide/dynamic-tables/overview.md). You can perform basic operations with such tables by using the C++ wrapper. {% note warning "Attention" %} Unlike the other examples, you will have to take additional steps to run this one: - By default, users don't have the permissions to create dynamic tables, so you first need to get permissions to create and mount dynamic tables on some cluster. - When running the example, pass the cluster name and the path to the test table (it must not exist). For more information, see the comments in the program text. {% endnote %} The example is located at [yt/cpp/mapreduce/examples/tutorial/dyntable_get_insert](https://github.com/ytsaurus/ytsaurus/tree/main/yt/cpp/mapreduce/examples/tutorial/dyntable_get_insert). ```c++ #include #include // for WaitForTabletsState #include using namespace NYT; int main(int argc, const char** argv) { NYT::Initialize(); // // Программа принимает аргументами имя кластера и путь к таблице, с которой она будет работать (таблица не должна существовать). // // По умолчанию у пользователей нет прав монтировать динамические таблицы, и перед запуском программы необходимо получить // такие права (права на монтирование таблиц) на каком-нибудь кластере YT. if (argc != 3) { Cerr << "Usage:\n"; Cerr << '\t' << argv[0] << " \n"; Cerr << '\n'; Cerr << "For example:\n"; Cerr << '\t' << argv[0] << " freud //home/ermolovd/test-dyntable" << Endl; return 1; } auto client = CreateClient(argv[1]); const TString dynamicTablePath = argv[2]; auto schema = TTableSchema() .AddColumn("key", EValueType::VT_INT64, ESortOrder::SO_ASCENDING) .AddColumn("value", EValueType::VT_STRING); // ВАЖНО: при создании динамической таблицы нам нужно // - указать атрибут dynamic: true // - указать схему // Это нужно сделать обязательно в вызове Create. Т.е. не получится сначала создать таблицу, // а потом проставить эти атрибуты. client->Create( dynamicTablePath, NT_TABLE, TCreateOptions() .Force(true) .Attributes( TNode() ("dynamic", true) ("schema", schema.ToNode()))); // Для того чтобы начать работу с динамической таблицей, её необходимо "подмонтировать". // // Часто создание / монтирование / размонтирование таблиц делается отдельными административными скриптами, // а приложение просто расчитывает, что таблицы существуют и уже подмонтированы. // // Мы для полноты примера подмонтируем таблицу, а в конце её размонтируем. client->MountTable(dynamicTablePath); Cout << "Waiting tablets are mounted..." << Endl; // Функция MountTable (и UnmountTable) запускает асинхронный процесс монтирования, // который может занять значительное время (десятки секунд) для больших таблиц. // Нам необходимо дождаться завершения этого процесса. WaitForTabletsState(client, dynamicTablePath, TS_MOUNTED); // Вставлять значения в динтаблицу можно с помощью InsertRows client->InsertRows(dynamicTablePath, { TNode()("key", 1)("value", "один"), TNode()("key", 42)("value", "сорок два"), TNode()("key", 100500)("value", "стопятьсот"), }); // Получать значения из динтаблицы можно с помощью LookupRows, // возвращает список найденных строчек. auto result = client->LookupRows(dynamicTablePath, { TNode()("key", 100500), TNode()("key", 42), }); Cout << "====== LOOKUP RESULT ======" << Endl; for (const auto& row : result) { Cout << "key: " << row["key"].AsInt64() << " value: " << row["value"].AsString() << Endl; } Cout << "====== END LOOKUP RESULT ======" << Endl; client->UnmountTable(dynamicTablePath); Cout << "Waiting tablets are unmounted..." << Endl; WaitForTabletsState(client, dynamicTablePath, TS_UNMOUNTED); return 0; } ``` # Reference A detailed description of the Go API is located at [https://pkg.go.dev/go.ytsaurus.tech/yt/go](https://pkg.go.dev/go.ytsaurus.tech/yt/go). # Java interface with examples (ytsaurus-client) We have Java SDK for YTsaurus which is published in [maven](https://central.sonatype.com/artifact/tech.ytsaurus/ytsaurus-client). A detailed description of the Java SDK is located [here](https://java.ytsaurus.tech). The examples are located at [yt/java/ytsaurus-client-examples](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/ytsaurus-client-examples). Before using the examples, read the [instructions for obtaining the token](https://ytsaurus.tech/user-guide/storage/auth.md). If `setAuth` is not passed to the client builder, `ytsaurus-client` loads the username from `YT_USER`, or uses the value of the Java system property `user.name` (`System.getProperty("user.name")`) if the variable is not set. If the Java process in a container runs as `root` and `user.name` is not overridden at JVM startup, its value is usually `root`. The token is loaded independently from `YT_TOKEN` or `~/.yt/token`. If the local username differs from the YTsaurus username, set `YT_USER` explicitly; the username must match the user associated with the token. For details, see [Selecting a username](https://ytsaurus.tech/user-guide/storage/auth.md#user-selection). Also you need to have tutorial tables on your YTsaurus cluster. It can be generated using this script: ```bash ./yt/yt/scripts/upload_tutorial_data/upload_tutorial_data.py --yt-directory //home/dev/tutorial --proxy ``` Every example can be run from its directory using gradle: ```bash cd yt/java/ytsaurus-client-examples/ YT_PROXY= gradle run ``` ### Working with Cypress The example is located at [yt/java/ytsaurus-client-examples/cypress-operations-example/src/main/java/tech/ytsaurus/example/ExampleCypressOperations.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/cypress-operations-example/src/main/java/tech/ytsaurus/example/ExampleCypressOperations.java). ```java package tech.ytsaurus.example; import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.request.GetNode; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.ysontree.YTreeNode; public class ExampleCypressOperations { private ExampleCypressOperations() { } public static void main(String[] args) { // *** Create the client *** // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } // The most convenient way to create a client is through builder // The only required parameter to specify is the cluster. // // The YT token will be picked up from `~/.yt/token`, and the username from the operating system. YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); // It is necessary to call close() on the client to shut down properly // The most reliable and convenient way to do this is with the try-with-resources statement. try (client) { // *** Simple requests *** // Most of the client methods return CompletableFuture // The request will be sent in the background. CompletableFuture listResult = client.listNode("/"); // You can use the join method to wait for the result of the request. // It is recommended to read the CompletableFuture documentation: // https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html System.out.println("listResult: " + listResult.join()); // *** Using optional parameters in a request *** // There is a full version of the method for all requests. // You can pass a request object in it, specifying advanced options there. CompletableFuture getResult = client.getNode( GetNode.builder() .setPath(YPath.simple("//home/dev/tutorial")) .setAttributes(List.of("account", "row_count")) .setTimeout(Duration.ofSeconds(10)) .build() ); System.out.println("getResult: " + getResult.join()); // *** Errors *** // Method calls will not throw exceptions // (maybe except when there is some kind of software error or misconfiguration). CompletableFuture badListResult = client.listNode("//some/directory/that/does/not/exist"); try { // If YT returns an error or a network error occurs as a result of executing a request, // then the corresponding exception will be stored in CompletableFuture. badListResult.join(); } catch (CompletionException ex) { System.out.println("ERROR: " + ex); } } } } ``` ### Reading and writing static tables (Entity version) The example is located at [yt/java/ytsaurus-client-examples/read-write-entity-example/src/main/java/tech/ytsaurus/example/ExampleReadWriteEntity.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/read-write-entity-example/src/main/java/tech/ytsaurus/example/ExampleReadWriteEntity.java). ```java package tech.ytsaurus.example; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import javax.persistence.Entity; import tech.ytsaurus.client.AsyncReader; import tech.ytsaurus.client.AsyncWriter; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.request.ReadTable; import tech.ytsaurus.client.request.WriteTable; import tech.ytsaurus.core.cypress.YPath; public class ExampleReadWriteEntity { private ExampleReadWriteEntity() { } @Entity static class TableRow { private String english; private String russian; TableRow() { } TableRow(String english, String russian) { this.english = english; this.russian = russian; } @Override public String toString() { return String.format("TableRow(\"%s, %s\")", english, russian); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); try (client) { // The table is located in `//tmp` and contains the name of the current user. // The username is necessary in case two people run this example at the same time // so that they use different output tables. YPath table = YPath.simple("//tmp/" + System.getProperty("user.name") + "-read-write"); // Write a table. // Create the writer. // The table with the inferred from TableRow schema will be created by writeTableV2. AsyncWriter writer = client.writeTableV2(new WriteTable<>(table, TableRow.class)).join(); writer.write(List.of( new TableRow("one", "один"), new TableRow("two", "два")) ).join(); writer.finish().join(); // Read a table. // Create the reader. try (AsyncReader reader = client.readTableV2(new ReadTable<>(table, TableRow.class)).join()) { List rows = new ArrayList<>(); var executor = Executors.newSingleThreadExecutor(); // Read all rows asynchronously. reader.acceptAllAsync(rows::add, executor).join(); for (TableRow row : rows) { System.out.println("russian: " + row.russian + "; english: " + row.english); } executor.shutdown(); } catch (IOException e) { throw new RuntimeException(e); } } } } ``` ### Reading and writing static tables (YTreeMapNode version) The example is located at [yt/java/ytsaurus-client-examples/read-write-ytree-example/src/main/java/tech/ytsaurus/example/ExampleReadWriteYTree.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/read-write-ytree-example/src/main/java/tech/ytsaurus/example/ExampleReadWriteYTree.java). ```java package tech.ytsaurus.example; import java.util.ArrayList; import java.util.List; import java.util.Map; import tech.ytsaurus.client.TableReader; import tech.ytsaurus.client.TableWriter; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.request.CreateNode; import tech.ytsaurus.client.request.ReadTable; import tech.ytsaurus.client.request.WriteTable; import tech.ytsaurus.core.cypress.CypressNodeType; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.tables.ColumnValueType; import tech.ytsaurus.core.tables.TableSchema; import tech.ytsaurus.ysontree.YTree; import tech.ytsaurus.ysontree.YTreeMapNode; public class ExampleReadWriteYTree { private ExampleReadWriteYTree() { } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); try (client) { // The table is located in `//tmp` and contains the name of the current user. // The username is necessary in case two people run this example at the same time // so that they use different output tables. YPath table = YPath.simple("//tmp/" + System.getProperty("user.name") + "-read-write"); // Create table TableSchema tableSchema = TableSchema.builder() .addValue("english", ColumnValueType.STRING) .addValue("russian", ColumnValueType.STRING) .build().toWrite(); client.createNode(CreateNode.builder() .setPath(table) .setType(CypressNodeType.TABLE) .setAttributes(Map.of("schema", tableSchema.toYTree())) .setIgnoreExisting(true) .build() ).join(); // Write a table. // Create the writer. TableWriter writer = client.writeTable( new WriteTable<>(table, YTreeMapNode.class) ).join(); try { while (true) { // It is necessary to wait for readyEvent before trying to write. writer.readyEvent().join(); // If false is returned, then readyEvent must be waited for before trying again. boolean accepted = writer.write(List.of( YTree.mapBuilder() .key("english").value("one") .key("russian").value("один") .buildMap(), YTree.mapBuilder() .key("english").value("two") .key("russian").value("два") .buildMap() ), tableSchema); if (accepted) { break; } } } catch (Exception ex) { throw new RuntimeException(ex); } finally { // Waiting for completion of writing. An exception might be thrown if something goes wrong. writer.close().join(); } // Read a table. // Create the reader. TableReader reader = client.readTable( new ReadTable<>(table, YTreeMapNode.class) ).join(); List rows = new ArrayList<>(); try { // We will read while we can. while (reader.canRead()) { // We wait until we can continue reading. reader.readyEvent().join(); List currentRows; while ((currentRows = reader.read()) != null) { rows.addAll(currentRows); } } } catch (Exception ex) { throw new RuntimeException("Failed to read"); } finally { reader.close().join(); } for (YTreeMapNode row : rows) { System.out.println("russian: " + row.getString("russian") + "; english: " + row.getString("english")); } } } } ``` ### Working with dynamic tables The example is located at [yt/java/ytsaurus-client-examples/dynamic-table-example/src/main/java/tech/ytsaurus/example/ExampleDynamicTable.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/dynamic-table-example/src/main/java/tech/ytsaurus/example/ExampleDynamicTable.java). ```java package tech.ytsaurus.example; import java.util.List; import java.util.Map; import tech.ytsaurus.client.ApiServiceTransaction; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.request.CreateNode; import tech.ytsaurus.client.request.LookupRowsRequest; import tech.ytsaurus.client.request.ModifyRowsRequest; import tech.ytsaurus.client.request.StartTransaction; import tech.ytsaurus.client.request.TransactionType; import tech.ytsaurus.client.rows.UnversionedRow; import tech.ytsaurus.client.rows.UnversionedRowset; import tech.ytsaurus.core.cypress.CypressNodeType; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.tables.ColumnSchema; import tech.ytsaurus.core.tables.ColumnSortOrder; import tech.ytsaurus.core.tables.TableSchema; import tech.ytsaurus.typeinfo.TiType; import tech.ytsaurus.ysontree.YTree; public class ExampleDynamicTable { private ExampleDynamicTable() { } public static void main(String[] args) { // // The program takes as arguments the name of the cluster and the path to the table it will work with // (the table must not exist). // // By default, users do not have permissions to mount dynamic tables, and you must obtain such permissions // (permissions to mount tables) on some YT cluster before starting the program. if (args.length != 2) { throw new IllegalArgumentException("Incorrect arguments count"); } String cluster = args[0]; String path = args[1]; TableSchema schema = TableSchema.builder() .setUniqueKeys(true) .add( ColumnSchema.builder("key", TiType.int64()) .setSortOrder(ColumnSortOrder.ASCENDING) .build() ) .add( ColumnSchema.builder("value", TiType.string()) .build() ) .build(); YTsaurusClient client = YTsaurusClient.builder() .setCluster(cluster) .build(); try (client) { // ATTENTION: when creating a dynamic table, we need to // - specify attribute dynamic: true // - specify scheme // This must be done in the createNode call. // That is, it will not work to create a table first, and then put down these attributes. CreateNode createNode = CreateNode.builder() .setPath(YPath.simple(path)) .setType(CypressNodeType.TABLE) .setAttributes(Map.of( "dynamic", YTree.booleanNode(true), "schema", schema.toYTree() )) .build(); client.createNode(createNode).join(); // To start working with a dynamic table, you need to "mount" it. // // Creating/mounting/unmounting tables is usually done by dedicated administrative scripts, // and the application simply assumes that the tables exist and are already mounted. // // We will mount the table and unmount it at the end for completeness of the example. client.mountTable(path).join(); // Fill the table. try (ApiServiceTransaction transaction = client.startTransaction(new StartTransaction(TransactionType.Tablet)).join()) { // Inserting values into a dynamic table can be done using modifyRows. transaction.modifyRows( ModifyRowsRequest.builder() .setPath(path) .setSchema(schema) .addInsert(List.of(1, "a")) .addInsert(List.of(2, "b")) .addInsert(List.of(3, "c")) .build()).join(); transaction.commit().join(); } // Read the table. try (ApiServiceTransaction transaction = client.startTransaction(new StartTransaction(TransactionType.Tablet)).join()) { // Get values from a dynamic table can be done using lookupRows. // It returns UnversionedRows consists of the rows it finds. UnversionedRowset rowset = transaction.lookupRows( LookupRowsRequest.builder() .setPath(path) .setSchema(schema.toLookup()) .addFilter(1) .build()).join(); System.out.println("====== LOOKUP RESULT ======"); for (UnversionedRow row : rowset.getRows()) { System.out.println(row.toYTreeMap(schema, true)); } System.out.println("====== END LOOKUP RESULT ======"); transaction.commit().join(); } // Unmount the table. client.unmountTable(path).join(); } } } ``` ### Map operation (UserClass version) The example is located at [yt/java/ytsaurus-client-examples/map-entity-example/src/main/java/tech/ytsaurus/example/ExampleMapEntity.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/map-entity-example/src/main/java/tech/ytsaurus/example/ExampleMapEntity.java). ```java package tech.ytsaurus.example; import javax.persistence.Entity; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.operations.MapSpec; import tech.ytsaurus.client.operations.Mapper; import tech.ytsaurus.client.operations.MapperSpec; import tech.ytsaurus.client.operations.Operation; import tech.ytsaurus.client.operations.Statistics; import tech.ytsaurus.client.request.MapOperation; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.operations.OperationContext; import tech.ytsaurus.core.operations.Yield; import tech.ytsaurus.lang.NonNullApi; public class ExampleMapEntity { private ExampleMapEntity() { } @Entity static class InputStaffInfo { private String login; private String name; private long uid; public String getLogin() { return login; } public String getName() { return name; } public long getUid() { return uid; } } @Entity static class OutputStaffInfo { private String email; private String name; OutputStaffInfo() { } OutputStaffInfo(String email, String name) { this.email = email; this.name = name; } } // The mapper class must implement the appropriate interface. // Generic type arguments are the classes to represent the input and output objects. @NonNullApi public static class SimpleMapper implements Mapper { @Override public void map(InputStaffInfo entry, Yield yield, Statistics statistics, OperationContext context) { String name = entry.getName(); String email = entry.getLogin().concat("@ytsaurus.tech"); yield.yield(new OutputStaffInfo(name, email)); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); // The output table is located in `//tmp` and contains the name of the current user. // The username is necessary in case two people run this example at the same time // so that they use different output tables. YPath outputTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-emails"); try (client) { Operation op = client.map( MapOperation.builder() .setSpec(MapSpec.builder() .setInputTables(YPath.simple("//home/tutorial/staff_unsorted").withRange(0, 2)) .setOutputTables(outputTable) .setMapperSpec(new MapperSpec(new SimpleMapper())) .build()) .build() ).join(); System.err.println("Operation was finished (OperationId: " + op.getId() + ")"); System.err.println("Status: " + op.getStatus().join()); } System.err.println( "Output table: " + outputTable ); } } ``` ### Map operation (YTreeMapNode version) The example is located at [yt/java/ytsaurus-client-examples/map-entity-example/src/main/java/tech/ytsaurus/example/ExampleMapEntity.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/map-entity-example/src/main/java/tech/ytsaurus/example/ExampleMapEntity.java). ```java package tech.ytsaurus.example; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.operations.MapSpec; import tech.ytsaurus.client.operations.Mapper; import tech.ytsaurus.client.operations.MapperSpec; import tech.ytsaurus.client.operations.Operation; import tech.ytsaurus.client.operations.Statistics; import tech.ytsaurus.client.request.MapOperation; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.operations.OperationContext; import tech.ytsaurus.core.operations.Yield; import tech.ytsaurus.lang.NonNullApi; import tech.ytsaurus.ysontree.YTree; import tech.ytsaurus.ysontree.YTreeMapNode; public class ExampleMapYTree { private ExampleMapYTree() { } // The mapper class must implement the appropriate interface. // Generic type arguments are the classes to represent the input and output objects. // In this case, it's a universal YTreeMapNode that allows you to work with an arbitrary table. @NonNullApi public static class SimpleMapper implements Mapper { @Override public void map(YTreeMapNode entry, Yield yield, Statistics statistics, OperationContext context) { String name = entry.getString("name"); String email = entry.getString("login").concat("@ytsaurus.tech"); YTreeMapNode outputRow = YTree.builder().beginMap() .key("name").value(name) .key("email").value(email) .buildMap(); yield.yield(outputRow); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); // The output table is located in `//tmp` and contains the name of the current user. // The username is necessary in case two people run this example at the same time // so that they use different output tables. YPath outputTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-emails"); try (client) { Operation op = client.map( MapOperation.builder() .setSpec(MapSpec.builder() .setInputTables(YPath.simple("//home/tutorial/staff_unsorted").withRange(0, 2)) .setOutputTables(outputTable) .setMapperSpec(new MapperSpec(new SimpleMapper())) .build()) .build() ).join(); System.err.println("Operation was finished (OperationId: " + op.getId() + ")"); System.err.println("Status: " + op.getStatus().join()); } System.err.println( "Output table: " + outputTable ); } } ``` ### Reduce operation (UserClass version) The example is located at [yt/java/ytsaurus-client-examples/reduce-entity-example/src/main/java/tech/ytsaurus/example/ExampleReduceEntity.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/reduce-entity-example/src/main/java/tech/ytsaurus/example/ExampleReduceEntity.java). ```java package tech.ytsaurus.example; import java.util.Iterator; import javax.persistence.Entity; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.operations.Operation; import tech.ytsaurus.client.operations.ReduceSpec; import tech.ytsaurus.client.operations.ReducerSpec; import tech.ytsaurus.client.operations.ReducerWithKey; import tech.ytsaurus.client.operations.SortSpec; import tech.ytsaurus.client.operations.Statistics; import tech.ytsaurus.client.request.ReduceOperation; import tech.ytsaurus.client.request.SortOperation; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.operations.Yield; import tech.ytsaurus.lang.NonNullApi; public class ExampleReduceEntity { private ExampleReduceEntity() { } @Entity static class InputStaffInfo { private String login; private String name; private long uid; public String getLogin() { return login; } public String getName() { return name; } public long getUid() { return uid; } } @Entity static class OutputStaffInfo { private String name; private int count; OutputStaffInfo() { } OutputStaffInfo(String name, int count) { this.name = name; this.count = count; } } // The reducer class must implement the appropriate interface. // Generic type arguments are the classes to represent the input and output objects. // The third generic type argument in the ReducerWithKey interface specifies the type of the key. @NonNullApi public static class SimpleReducer implements ReducerWithKey { @Override public String key(InputStaffInfo entry) { return entry.getName(); } @Override public void reduce(String key, Iterator input, Yield yield, Statistics statistics) { // All rows with a common reduce key come to reduce, i.e. in this case with a common field `name`. int count = 0; while (input.hasNext()) { input.next(); ++count; } yield.yield(new OutputStaffInfo(key, count)); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); YPath outputTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-emails"); YPath sortedTmpTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-tmp"); try (client) { // Sort the input table by `name`. client.sort(SortOperation.builder() .setSpec(SortSpec.builder() .setInputTables(YPath.simple("//home/tutorial/staff_unsorted")) .setOutputTable(sortedTmpTable) .setSortBy("name") .build()) .build()).join(); Operation op = client.reduce(ReduceOperation.builder() .setSpec(ReduceSpec.builder() .setInputTables(sortedTmpTable) .setOutputTables(outputTable) .setReduceBy("name") .setReducerSpec(new ReducerSpec(new SimpleReducer())) .build()) .build() ).join(); System.err.println("Operation was finished (OperationId: " + op.getId() + ")"); System.err.println("Status: " + op.getStatus().join()); } System.err.println( "Output table: " + outputTable ); } } ``` ### Reduce operation (YTreeMapNode version) The example is located at [yt/java/ytsaurus-client-examples/reduce-ytree-example/src/main/java/tech/ytsaurus/example/ExampleReduceYTree.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/reduce-ytree-example/src/main/java/tech/ytsaurus/example/ExampleReduceYTree.java). ```java package tech.ytsaurus.example; import java.util.Iterator; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.operations.Operation; import tech.ytsaurus.client.operations.ReduceSpec; import tech.ytsaurus.client.operations.ReducerSpec; import tech.ytsaurus.client.operations.ReducerWithKey; import tech.ytsaurus.client.operations.SortSpec; import tech.ytsaurus.client.operations.Statistics; import tech.ytsaurus.client.request.ReduceOperation; import tech.ytsaurus.client.request.SortOperation; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.operations.Yield; import tech.ytsaurus.lang.NonNullApi; import tech.ytsaurus.ysontree.YTree; import tech.ytsaurus.ysontree.YTreeMapNode; public class ExampleReduceYTree { private ExampleReduceYTree() { } // The reducer class must implement the appropriate interface. // Generic type arguments are the classes to represent the input and output objects. // In this case, it's a universal YTreeMapNode that allows you to work with an arbitrary table. // The third generic type argument in the ReducerWithKey interface specifies the type of the key. @NonNullApi public static class SimpleReducer implements ReducerWithKey { @Override public String key(YTreeMapNode entry) { return entry.getString("name"); } @Override public void reduce(String key, Iterator input, Yield yield, Statistics statistics) { // All rows with a common reduce key come to reduce, i.e. in this case with a common field `name`. int count = 0; while (input.hasNext()) { input.next(); ++count; } YTreeMapNode outputRow = YTree.builder().beginMap() .key("name").value(key) .key("count").value(count) .buildMap(); yield.yield(outputRow); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); YPath outputTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-emails"); YPath sortedTmpTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-tmp"); try (client) { // Sort the input table by `name`. client.sort(SortOperation.builder() .setSpec(SortSpec.builder() .setInputTables(YPath.simple("//home/tutorial/staff_unsorted")) .setOutputTable(sortedTmpTable) .setSortBy("name") .build()) .build()).join(); Operation op = client.reduce(ReduceOperation.builder() .setSpec(ReduceSpec.builder() .setInputTables(sortedTmpTable) .setOutputTables(outputTable) .setReduceBy("name") .setReducerSpec(new ReducerSpec(new SimpleReducer())) .build()) .build() ).join(); System.err.println("Operation was finished (OperationId: " + op.getId() + ")"); System.err.println("Status: " + op.getStatus().join()); } System.err.println( "Output table: " + outputTable ); } } ``` ### MapReduce operation (UserClass version) The example is located at [yt/java/ytsaurus-client-examples/map-reduce-entity-example/src/main/java/tech/ytsaurus/example/ExampleMapReduceEntity.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/map-reduce-entity-example/src/main/java/tech/ytsaurus/example/ExampleMapReduceEntity.java). ```java package tech.ytsaurus.example; import java.util.Iterator; import javax.persistence.Column; import javax.persistence.Entity; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.operations.MapReduceSpec; import tech.ytsaurus.client.operations.Mapper; import tech.ytsaurus.client.operations.MapperSpec; import tech.ytsaurus.client.operations.Operation; import tech.ytsaurus.client.operations.ReducerSpec; import tech.ytsaurus.client.operations.ReducerWithKey; import tech.ytsaurus.client.operations.Statistics; import tech.ytsaurus.client.request.MapReduceOperation; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.operations.OperationContext; import tech.ytsaurus.core.operations.Yield; import tech.ytsaurus.lang.NonNullApi; public class ExampleMapReduceEntity { private ExampleMapReduceEntity() { } @Entity static class InputStaffInfo { private String login; private String name; private long uid; public String getLogin() { return login; } public String getName() { return name; } public long getUid() { return uid; } } @Entity static class StaffNameLength { private String name; @Column(name = "name_length") private int nameLength; StaffNameLength() { } StaffNameLength(String name, int nameLength) { this.name = name; this.nameLength = nameLength; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getNameLength() { return nameLength; } public void setNameLength(int nameLength) { this.nameLength = nameLength; } } @Entity static class OutputStaffInfo { private String name; @Column(name = "sum_name_length") private int sumNameLength; OutputStaffInfo() { } OutputStaffInfo(String name, int sumNameLength) { this.name = name; this.sumNameLength = sumNameLength; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getSumNameLength() { return sumNameLength; } public void setSumNameLength(int sumNameLength) { this.sumNameLength = sumNameLength; } } @NonNullApi public static class SimpleMapper implements Mapper { @Override public void map(InputStaffInfo entry, Yield yield, Statistics statistics, OperationContext context) { String name = entry.getName(); yield.yield(new StaffNameLength(name, name.length())); } } @NonNullApi public static class SimpleReducer implements ReducerWithKey { @Override public String key(StaffNameLength entry) { return entry.getName(); } @Override public void reduce(String key, Iterator input, Yield yield, Statistics statistics) { // All rows with a common reduce key come to reduce, i.e. in this case with a common field `name`. int sumNameLength = 0; while (input.hasNext()) { StaffNameLength staffNameLength = input.next(); sumNameLength += staffNameLength.getNameLength(); } yield.yield(new OutputStaffInfo(key, sumNameLength)); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); YPath inputTable = YPath.simple("//home/tutorial/staff_unsorted"); YPath outputTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-emails"); try (client) { Operation op = client.mapReduce(MapReduceOperation.builder() .setSpec(MapReduceSpec.builder() .setInputTables(inputTable) .setOutputTables(outputTable) .setReduceBy("name") .setSortBy("name") .setReducerSpec(new ReducerSpec(new SimpleReducer())) .setMapperSpec(new MapperSpec(new SimpleMapper())) .build()) .build() ).join(); System.err.println("Operation was finished (OperationId: " + op.getId() + ")"); System.err.println("Status: " + op.getStatus().join()); } System.err.println( "Output table: " + outputTable ); } } ``` ### MapReduce operation (YTreeMapNode version) The example is located at [yt/java/ytsaurus-client-examples/map-reduce-ytree-example/src/main/java/tech/ytsaurus/example/ExampleMapReduceYTree.java](https://github.com/ytsaurus/ytsaurus/blob/main/yt/java/ytsaurus-client-examples/map-reduce-ytree-example/src/main/java/tech/ytsaurus/example/ExampleMapReduceYTree.java). ```java package tech.ytsaurus.example; import java.util.Iterator; import tech.ytsaurus.client.YTsaurusClient; import tech.ytsaurus.client.operations.MapReduceSpec; import tech.ytsaurus.client.operations.Mapper; import tech.ytsaurus.client.operations.MapperSpec; import tech.ytsaurus.client.operations.Operation; import tech.ytsaurus.client.operations.ReducerSpec; import tech.ytsaurus.client.operations.ReducerWithKey; import tech.ytsaurus.client.operations.Statistics; import tech.ytsaurus.client.request.MapReduceOperation; import tech.ytsaurus.core.cypress.YPath; import tech.ytsaurus.core.operations.OperationContext; import tech.ytsaurus.core.operations.Yield; import tech.ytsaurus.lang.NonNullApi; import tech.ytsaurus.ysontree.YTree; import tech.ytsaurus.ysontree.YTreeMapNode; public class ExampleMapReduceYTree { private ExampleMapReduceYTree() { } @NonNullApi public static class SimpleMapper implements Mapper { @Override public void map(YTreeMapNode entry, Yield yield, Statistics statistics, OperationContext context) { String name = entry.getString("name"); YTreeMapNode outputRow = YTree.builder().beginMap() .key("name").value(name) .key("name_length").value(name.length()) .buildMap(); yield.yield(outputRow); } } @NonNullApi public static class SimpleReducer implements ReducerWithKey { @Override public String key(YTreeMapNode entry) { return entry.getString("name"); } @Override public void reduce(String key, Iterator input, Yield yield, Statistics statistics) { // All rows with a common reduce key come to reduce, i.e. in this case with a common field `name`. int sumNameLength = 0; while (input.hasNext()) { YTreeMapNode node = input.next(); sumNameLength += node.getInt("name_length"); } YTreeMapNode outputRow = YTree.builder().beginMap() .key("name").value(key) .key("sum_name_length").value(sumNameLength) .buildMap(); yield.yield(outputRow); } } public static void main(String[] args) { // You need to set up cluster address in YT_PROXY environment variable. var clusterAddress = System.getenv("YT_PROXY"); if (clusterAddress == null || clusterAddress.isEmpty()) { throw new IllegalArgumentException("Environment variable YT_PROXY is empty"); } YTsaurusClient client = YTsaurusClient.builder() .setCluster(clusterAddress) .build(); YPath inputTable = YPath.simple("//home/tutorial/staff_unsorted"); YPath outputTable = YPath.simple("//tmp/" + System.getProperty("user.name") + "-tutorial-emails"); try (client) { Operation op = client.mapReduce(MapReduceOperation.builder() .setSpec(MapReduceSpec.builder() .setInputTables(inputTable) .setOutputTables(outputTable) .setReduceBy("name") .setSortBy("name") .setReducerSpec(new ReducerSpec(new SimpleReducer())) .setMapperSpec(new MapperSpec(new SimpleMapper())) .build()) .build() ).join(); System.err.println("Operation was finished (OperationId: " + op.getId() + ")"); System.err.println("Status: " + op.getStatus().join()); } System.err.println( "Output table: " + outputTable ); } } ``` # YTsaurus JavaScript Wrapper (Browser + Node.js) A wrapper for queries to YTsaurus clusters that uses the YTsaurus HTTP API with the closest interface possible. ## Installation `npm install @ytsaurus/javascript-wrapper` ## Connection and use The library is written in CommonJS style: you can use it in a Node.js app or bundle dependencies with Webpack. ```javascript const yt = require('@ytsaurus/javascript-wrapper')() // Enables you to set default settings for all commands. // If you need to work with multiple clusters simultaneously, the proxy and token are sent to each command separately. yt.setup.setGlobalOption('proxy', 'cluster-name.yt.my-domain.com'); yt.setup.setGlobalOption('secure', true); yt.setup.setGlobalOption('authentication', { type: 'domain' }); yt.setup.setGlobalOption('timeout', 15000); // Example 1 yt.v3.get({ path: '//sys/users/user/@' }) yt.v3.get({ setup: { proxy: 'cluster-name.yt.my-domain.com', authentication: { type: 'none' } }, parameters: { path: '//sys/users/user/@' } }) .done(function (userAttributes) { // ... }); // Example 2 yt.v3.set({ path: '//sys/users/user/@banned' }, true); yt.v3.set({ setup: { proxy: 'cluster-name.yt.my-domain.com', authentication: { type: 'oauth', token: 'abcdefghijklmnopqrstuvwxyz' } }, parameters: { path: '//sys/users/user/@banned' }, data: true }) .done(function () { // ... }); ``` The library can also be used as a global variable. ## Settings The wrapper enables you to specify global and local settings for each query. | **Name** | **Type** | **Default value** | | --- | --- | --- | | `secure` | boolean | `true` | | `useHeavyProxy` | boolean | - | | `proxy` | string | - | | `heavyProxy` | string | `true` | | `timeout` | Number | `100,000` | | `useEncodedParameters` | boolean | `true` | | `authentication` | object | `{ type: 'none' }` | | `dataType` | string | - | | `encodedParametersSettings` | object | ``` { maxSize: 64 * 1024, maxCount: 2, encoder(string) { return window.btoa(utf8.encode(string)); } } ``` ## Events You can subscribe to `requestStart`, `requestEnd`, and `error` events: yt.subscribe('requestStart', function () { console.log('requestStart'); }); This can be useful for displaying the loader (you can count how many active queries there are) and for logging errors occurring on API requests. ## Commands For information about the available commands, see the [Commands](https://ytsaurus.tech/docs/en/commands.md) section. The only difference is that the command names are written in the usual style for the JavaScript language, namely CamelCase. Not all commands have been implemented, but they can be added if needed. If there is no command, the wrapper usually displays a corresponding error. Generic command format: ```javascript // yt..([, ]) yt.v3.get({ path: "//home/user/@account" }); yt.v3.set({ path: "//home/user/@account" }, "default"); ``` Generic command format supporting the configuration of local settings: ```javascript /** * yt..({ * parameters: , * data: , * setup: * }) */ const setup = { timeout: 3000 }; yt.v3.get({ setup, parameters: { path: "//home/user/@account" } }); yt.v3.set({ setup, parameters: { path: "//home/user/@account" } }, "default"); ``` ## Constants for error codes | **Constant** | **Code** | | --- | --- | | `yt.codes.GENERAL_ERROR` | 1 | | `yt.codes.NODE_DOES_NOT_EXIST` | 500 | | `yt.codes.NODE_ALREADY_EXISTS` | 501 | | `yt.codes.PERMISSION_DENIED` | 901 | | `yt.codes.USER_IS_BANNED` | 903 | | `yt.codes.USER_EXCEEDED_RPS` | 904 | ## Useful links [Registering commands by version on the backend](https://github.com/YTsaurus/YTsaurus/blob/main/yt/yt/client/driver/driver.cpp). # Commands This section lists all the commands available in the YTsaurus system API and includes a full description of their options. ## Structure { #structure } To put it simply, each command has the following structure: - Command name (string). - Description of the input and output formats (a [YSON string](https://ytsaurus.tech/docs/user-guide/storage/data-types.md#yson) with attributes). - Parameters of executed action (a YSON structure). - Input and output data streams. Each command also defines the type of its input and output data. Below all the possible options for the input and output data: - No data (`null`). - The data is a `binary` stream. For example, [file](https://ytsaurus.tech/docs/user-guide/storage/files.md) contents. - The data is [structured](https://ytsaurus.tech/docs/user-guide/storage/formats.md#structured_data) (`structured`). For example, regular [Cypress](https://ytsaurus.tech/docs/user-guide/storage/cypress.md) nodes. - The data is a `tabular` stream. For example, [table](https://ytsaurus.tech/docs/user-guide/storage/objects.md#tables) contents. In addition to that, each command can be: - Mutating or not (whether it changes anything in the metastate or not). - Light or heavy (light commands only transmit command parameters within a query, but heavy commands write or read the data stream). ### Formats { #formats } If your command works with structured or tabular data, you need to specify a [format](https://ytsaurus.tech/docs/user-guide/storage/formats.md) for it. For the input stream, use the `input_format` parameter, and for the output stream, use the `output_format` parameter, respectively. For structured data formats, `YSON` (default) and `JSON` formats are supported. For tabular data, there exist many [formats](https://ytsaurus.tech/docs/user-guide/storage/formats.md). ### Retries { #retry } A command retry is an option to repeat the query in the event of transient (intermittent) errors. It is expected that the response to a query retry would be indistinguishable from the response to the original query, as if there were no transient errors. It doesn't mean, however, that the response to a retry will be totally identical to the original query response (that is, only standard isolation is guaranteed). Availability of retry options and the mechanism of retries depends on the command properties. #### Light commands { #light } For non-mutating light commands, you can repeat the original query. The mutating light commands change the system status. That's why you need to hint the system that such a query has already been made. To do this, before executing the command, generate the `mutation_id` for the command. This is a standard GUID that consists of four 32-bit numbers in the HEX format separated by a dash (`-`). Specify the `mutation_id` generated both in the parameters of both the original query and in the retries. In addition, you need to add, to the original query, the `retry` parameter with the value of `false`. To the retried queries, add the `retry` parameter with the value of `true`. Some light mutating commands do not support retries (an example is `concatenate`). If you need to retry such commands, you can use transactions. {% note info "Note" %} `mutation_id` are usually kept for 5-10 minutes. {% endnote %} #### Heavy commands { #hard } You can't retry heavy commands However, you can implement a retry mechanism by using transactions. ## Transactions { #transactions } [Transactions](https://ytsaurus.tech/docs/user-guide/storage/transactions.md) are an integral Cypress property. Many commands that interact with Cypress this way or another, are transactional. Each command or group of commands has a separate indication of whether it is transactional or not. Transactional commands support the following parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | --------------------- | ------------------------------------------------------------ | | `transaction_id` | No | *null-transaction-id* | Current transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | ## Other query parameters You can also specify prerequisites for your queries. Moreover, for light non-mutating queries, you can also specify where to read the data from. For more information, see [Query parameters](https://ytsaurus.tech/docs/en/query-parameters.md). ## Working with transactions For more information about transactions, see the [Transactions](https://ytsaurus.tech/docs/user-guide/storage/transactions.md) article. ### start_tx Command properties: **Mutating**, **Light**. Semantics: - Begin a new transaction in the context of the current transaction. - The new transaction is a nested (internal) transaction for the given one. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | No | *null-transaction-id* | ID of the current transaction (it will become a parent transaction for the transaction created by the command). | | `ping_ancestor_transactions` | No | `false` | Whether to ping, when running the operation, all the parent transactions (to extend their TTL). | | `timeout` | No | `15000` | Transaction TTL since the last extension (in ms). | | `deadline` | No | `missing` | Transaction execution deadline (in UTC). | | `attributes` | No | `missing` | Enables you to set attributes for the created transaction. | | `type` | No | `master` | Enables you to set the transaction type: `master` or `tablet`. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the new transaction. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } OUTPUT "0-54c-1-bb49086d" ``` ### ping_tx Command properties: **Mutating**, **Light**. Semantics: - Update the transaction. Detailed description. - `ping_tx` pings the transaction on the server (including all the parent transactions if `ping_ancestor_transactions` is specified). This way you can extend the TTL for the transaction. - If the transaction started at the time `s` with the timeout (TTL) of `t`, then the transaction will complete at `s+t` by default. - If you ping the transaction at the time `r` (`s < r < s + t`), it will be extended until `r + t`. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | Yes | | Parent transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } ``` ### commit_tx Command properties: **Mutating**, **Light**. Semantics: - Complete the transaction successfully. - While there are some incompleted internal transactions, the outer transaction can't complete. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | Yes | | Transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } ``` ### abort_tx Command properties: **Mutating**, **Light**. Semantics: - Abort the transaction. - All the active internal transactions are aborted as well. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | Yes | | Transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `force` | No | `false` | Forcibly aborts the transaction even if this may result in consistency loss (in case of a tablet transaction and a [two-phase commit](https://en.wikipedia.org/wiki/Two-phase_commit_protocol)). | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } ``` ### start_transaction {% note info %} The HTTP API supports this command from version 4 onward. Earlier versions use the `start_tx` command. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Begin a new transaction in the context of the current transaction. - The new transaction is a nested (internal) transaction for the given one. - This is the API v4 equivalent of `start_tx` with structured output. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | No | *null-transaction-id* | ID of the current transaction (it will become a parent transaction for the transaction created by the command). | | `ping_ancestor_transactions` | No | `false` | Whether to ping, when running the operation, all the parent transactions (to extend their TTL). | | `timeout` | No | `15000` | Transaction TTL since the last extension (in ms). | | `deadline` | No | `missing` | Transaction execution deadline (in UTC). | | `attributes` | No | `missing` | Enables you to set attributes for the created transaction. | | `type` | No | `master` | Enables you to set the transaction type: `master` or `tablet`. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Map with `transaction_id` key containing the new transaction ID. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } OUTPUT { "transaction_id" = "0-54c-1-bb49086d"; } ``` ### ping_transaction {% note info %} The HTTP API supports this command from version 4 onward. Earlier versions use the `ping_tx` command. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Update the transaction by pinging it to extend its TTL. - This is the API v4 equivalent of `ping_tx` with structured output. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | Yes | | Transaction ID to ping. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | Input data: - Type: `null`. Output data: - Type: `structured`. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } OUTPUT { } ``` ### commit_transaction {% note info %} The HTTP API supports this command from version 4 onward. Earlier versions use the `commit_tx` command. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Complete the transaction successfully. - While there are some incomplete internal transactions, the outer transaction can't complete. - This is the API v4 equivalent of `commit_tx` with structured output. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | Yes | | Transaction ID to commit. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | Input data: - Type: `null`. Output data: - Type: `structured`. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } OUTPUT { } ``` ### abort_transaction {% note info %} The HTTP API supports this command from version 4 onward. Earlier versions use the `abort_tx` command. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Abort the transaction. - All the active internal transactions are aborted as well. - This is the API v4 equivalent of `abort_tx` with structured output. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | Yes | | Transaction ID to abort. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `force` | No | `false` | Forcibly aborts the transaction even if this may result in consistency loss (in case of a tablet transaction and a two-phase commit). | Input data: - Type: `null`. Output data: - Type: `structured`. Example: ```bash PARAMETERS { "transaction_id" = "0-54b-1-36223d20" } OUTPUT { } ``` ## Working with Cypress For more information about the metainformation tree, see [Cypress](https://ytsaurus.tech/docs/user-guide/storage/cypress.md). {% note info "Note" %} All the commands used to work with Cypress are transactional. {% endnote %} ### create { #create } Command properties: **Mutating**, **Light**. Semantics: - Create a node of a specified type in Cypress. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ----------------- | ---------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. At default settings, the *path* should not exist. | | `type` | Yes | | [Node type](https://ytsaurus.tech/docs/user-guide/storage/objects.md). | | `recursive` | No | `false` | Whether to create intermediate nodes recursively. | | `attributes` | No | `{}` | Enables you to set attributes for the created node. | | `ignore_existing` | No | `false` | If the created node exists already, it is not recreated. In particular, the transmitted attributes are ignored. Moreover, both the existing and created nodes must have the same type, otherwise the query will return an error. | | `lock_existing` | No | `false` | Set an [exclusive lock](https://ytsaurus.tech/docs/user-guide/storage/transactions.md#locks) on the specified node even if it already exists. This parameter is only used together with `ignore_existing`. If the lock couldn't be set, the command fails. | | `ignore_type_mismatch` | No | `false` | Do not return an error if the existing node has a type different from the requested one. The node is not recreated, and the transmitted attributes are ignored. This parameter is only used together with `ignore_existing`. | | `force` | No | `false` | If the specified node already exists, it is deleted and replaced with a new one. In this situation, the existing node can be of any type. When the node is recreated, its ID changes. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the created node. Examples: ```bash PARAMETERS { "path" = "//tmp/table" ; "type" = "table" } OUTPUT "0-4-191-6c07cd58" ``` ```bash PARAMETERS { "type" = "user" ; attributes = { name = 'kulichek-robot' } } OUTPUT "7727-417b1-1f5-d4f116ca" ``` ```bash PARAMETERS { "path" = "//tmp/document" ; "type" = "document" ; attributes = { value = {} } } OUTPUT "7727-417b1-1f5-d4f116ca" ``` ### remove { #remove } Command properties: **Mutating**, **Light**. Semantics: - Delete the Cypress node. - Deletes the node successfully, even if a non-empty subtree is growing from it. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. The path *must* exist. | | `recursive` | No | `false` | Enables you to delete the entire subtree in the case when the deleted node is a composite type. | | `force` | No | `false` | Enables you to treat the command as successfully executed if the deleted node is missing. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//tmp/table" } ``` ### set { #set } Command properties: **Mutating**, **Light**. Semantics: - Write new content into the Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. If `recursive == false`, then the path *must* exist (except, probably, the last token). | | `recursive` | No | `false` | Create all non-existent intermediate nodes on the path. | | `force` | No | `false` | Enables you to modify any Cypress node instead of only attributes and documents. | Input data: - Type: `structured`. - Value: Node content. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//tmp/node" } INPUT { "my_integer" = 4 ; "my_double" = 2.718281828 ; "map" = { "a" = 1 ; "b" = 2 } ; "list" = [ 1, 2, 3 ] } ``` ### multiset_attributes Command properties: **Mutating**, **Light**. Semantics: - Set several attributes at the specified path (if the attributes exist already, they are overwritten). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to attributes of a node in Cypress. The path *must* exist. | Input data: - Type: `structured`. - Value: A map node that contains new attribute values. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//tmp/node/@" } INPUT { "attribute1" = 4 ; "attribute2" = "attribute2 value"; } ``` ### get { #get } Command properties: **Non-mutating**, **Light**. Semantics: - Get the content of the Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. The path *must* exist. | | `attributes` | No | `[]` | A list of attributes that need to be obtained along with each node. | | `max_size` | No | `missing` | Sets a limit on the number of children that will be issued in the case of virtual composite nodes (for regular map nodes, this option doesn't make sense). | | `ignore_opaque` | No | `false` | Ignore the `opaque` attribute when executing a query (never use this option without explicit advice from YTsaurus developers). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Node content. Example: ```bash PARAMETERS { "path" = "//tmp/node" } OUTPUT { "my_integer" = 4 ; "my_double" = 2.718281828 ; "map" = { "a" = 1 ; "b" = 2 } ; "list" = [ 1, 2, 3 ] } ``` ### list { #list } Command properties: **Non-mutating**, **Light**. Semantics: - Get a list of descendants for the Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. The path *must* exist. | | `attributes` | No | `[]` | A list of attributes that need to be obtained along with each node. | | `max_size` | No | `missing` | Limits the number of descendants output. | Input data: - Type: `null`. Output data: - Type: `structured` - Value: A list of the node's descendants. Example: ```bash PARAMETERS { "path" = "//tmp/node" } OUTPUT [ "home" ; "sys" ; "statbox" ; "tmp" ] ``` ### lock Command properties: **Mutating**, **Light**. Semantics: - Set a lock on the Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. The path *must* exist. | | `mode` | No | `exclusive` | Lock type (snapshot, shared, exclusive). | | `waitable` | No | `false` | In case of a conflict, it's doesn't fail but is enqueued. You can find out whether the lock was actually set using the `state` attribute of the lock object. See [Transactions](https://ytsaurus.tech/docs/user-guide/storage/transactions.md#locking_queue). | | `child_key` | No | | The key in the dictionary on which the lock is taken (for the `shared` type only). | | `attribute_key` | No | | The name of the attribute on which the lock is taken (for the `shared` type only). | {% note info "Note" %} Be sure to specify the transaction ID for this command. {% endnote %} Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the created lock and ID of the branched node. Example: ```bash PARAMETERS { "path" = "//tmp/node" } OUTPUT { OUTPUT "lock_id" = "0-1-3fe00c8-353e9ba4"; OUTPUT "node_id" = "0-1-3fe012f-9ad48d90"; OUTPUT } ``` ### unlock {% note warning "Attention" %} Locks are automatically removed at the end of the transaction. Do not use the `unlock` command unless absolutely needed. See [Transactions](https://ytsaurus.tech/docs/user-guide/storage/transactions.md#lock_operations). {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Release all the locks set for the selected transaction on the Cypress node. - You can only release explicit locks. - This command releases both already taken and enqueued locks. - If the node is not locked, the command is considered successful. - If the locked node version includes any changes compared to the original version, the unlock fails. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ---------------------------------------------------- | | `path` | Yes | | Path to the node in Cypress. The path *must* exist. | {% note info "Note" %} Be sure to specify the transaction ID for this command. {% endnote %} Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//tmp/node" } ``` ### copy { #copy } Command properties: **Mutating**, **Light**. Semantics: - Copy the Cypress node to the new address. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `source_path` | Yes | | Path to the source node in Cypress. The path *must* exist. | | `destination_path` | Yes | | Path where the copy will be created. The path *must not* exist. | | `recursive` | No | `false` | Whether to create the skipped levels (map nodes) at the destination path. | | `ignore_existing` | No | `false` | If the node already exists at `destination_path`, do nothing (you can't use this parameter together with `force = %true`). | | `lock_existing` | No | `false` | Set an [exclusive lock](https://ytsaurus.tech/docs/user-guide/storage/transactions.md#locks) on the node at `destination_path`, even if it already exists. This parameter is only used together with `ignore_existing`. If the command fails to set a lock, it returns an error. | | `force` | No | `false` | Allows you to specify an existing node to be replaced, as the destination path. | | `preserve_account` | No | `false` | Whether to keep the accounts of the source nodes or use the account at the destination path. | | `preserve_expiration_time` | No | `false` | Whether to copy the [`expiration_time`](https://ytsaurus.tech/docs/user-guide/storage/cypress.md#TTL) attribute or leave it empty. | | `preserve_expiration_timeout` | No | `false` | Whether to copy the [`expiration_timeout`](https://ytsaurus.tech/docs/user-guide/storage/cypress.md#TTL) attribute or leave it empty. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the created node. Example: ```bash PARAMETERS { "source_path" = "//tmp/from" ; "destination_path" = "//tmp/to" ; } OUTPUT "0-4-191-6c07cd58" ``` ### move { #move} Command properties: **Mutating**, **Light**. Semantics: - Move the Cypress node to a new path. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `source_path` | Yes | | Path to the source node in Cypress. The path *must* exist. | | `destination_path` | Yes | | A new path in Cypress. The path *must not* exist. | | `recursive` | No | `false` | Whether to create the skipped levels (map nodes) at the destination path. | | `force` | No | `false` | Allows you to specify an existing node to be replaced, as the destination path. | | `preserve_account` | No | `false` | Whether to keep the accounts of the source nodes or use the account at the destination path. | | `preserve_expiration_time` | No | `false` | Whether to copy the [`expiration_time`](https://ytsaurus.tech/docs/user-guide/storage/cypress.md#TTL) attribute or leave it empty. | | `preserve_expiration_timeout` | No | `false` | Whether to copy the [`expiration_timeout`](https://ytsaurus.tech/docs/user-guide/storage/cypress.md#TTL) attribute or leave it empty. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "source_path" = "//tmp/from" ; "destination_path" = "//tmp/to" ; } ``` ### link { #link } Command properties: **Mutating**, **Light**. Semantics: - Create a symbolic link to the object at the new path. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | --------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `link_path` | Yes | | Path where the link will be created. The path *must not* exist. | | `target_path` | Yes | | Path to the source node in Cypress. The path *must* exist. | | `attributes` | No | `missing` | Attributes of the node created as a result of the command. | | `recursive` | No | `false` | Whether to create intermediate nodes recursively. | | `ignore_existing` | No | `false` | Enables you to avoids operation failure if the node already exists and is a link. The call will return an error if the node exists but is not a link. | | `lock_existing` | No | `false` | Set an exclusive lock on the specified node even if it already exists. It's only used together with the `ignore_existing` parameter. | | `force` | No | `false` | Recreate the link if the path where the link is created already exists. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the created node. Example: ```bash PARAMETERS { "target_path" = "//tmp/from" ; "link_path" = "//tmp/to" ; } OUTPUT "0-4-191-6c07cd58" ``` ### exists { #exists } Command properties: **Non-mutating**, **Light**. Semantics: - Checks what are the node exists. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ---------------------------------- | | `path` | Yes | | Path to the source node in Cypress. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: (API v4) dictionary with a key `value` and `true` or `false` string as a value. (API v3) `true` or `false` string. Example: ```bash PARAMETERS { "path" = "//tmp/my_table/@_format" ; } OUTPUT(API v4) { "value" = "true" } OUTPUT(API v3) true ``` ### concatenate { #concatenate } Semantics: - Merge the set of files or tables (in the order in which their paths are listed). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | No | *null-transaction-id* | Current transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `source_paths` | Yes | | The list of paths to input files or tables in Cypress. The paths *must* exist and all items at the paths should either be files or tables. | | `destination_path` | Yes | | Path to a file or table in Cypress. The path *must* exist. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "source_paths" = ["//tmp/file1"; "//tmp/file2"]; "destination_path" = "//tmp/file"; } ``` ## Access control { #access-control } For more information about access permissions, see [Access control](https://ytsaurus.tech/docs/user-guide/storage/access-control.md). {% note info "Note" %} This set of commands is not transactional. {% endnote %} ### add_member Command properties: **Mutating**, **Light**. Semantics: - Add to a group a user or another group. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `member` | Yes | | The name of the user or group that needs to be added to the group. | | `group` | Yes | | The name of the group into which you are adding a user or another group. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "group" = "admins"; "member" = "devs"; } ``` ### remove_member Command properties: **Mutating**, **Light**. Semantics: - Add to a group a user or another group. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `member` | Yes | | The name of the user or group that needs to be deleted from the group. | | `group` | Yes | | The name of the group that you are deleting a user or another group from. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "group" = "admins"; "member" = "devs"; } ``` ### check_permission Command properties: **Non-mutating**, **Light**. Semantics: - Check if a user has a certain permission to access a certain Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the node in Cypress. | | `user` | Yes | | The name of the user that you are checking the permission for. | | `permission` | Yes | | The name of the permission checked. | | `columns` | No | | The list of columns the access to which should be checked. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//sys/accounts/statbox"; "permission" = "use"; "user" = "bob"; } ``` ## Working with files { #files } For more information about files, see the [Files](https://ytsaurus.tech/docs/user-guide/storage/files.md) article. {% note info "Note" %} All the commands used to work with files are transactional. {% endnote %} ### write_file Command properties: **Mutating**, **Heavy**. Synonyms (supported, but not recommended). - upload. Semantics: - Upload the content to the file. - The file must exist. - If the file `path` includes the `append=%true` attribute, the data is appended to the file; otherwise, the file is overwritten. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | No | *null-transaction-id* | Current transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `path` | Yes | | Path to a file in Cypress. The path *must* exist. | | `compute_md5` | No | `false` | Whether to calculate the MD5 sum for the file written; if both `compute_md5=%true` and `append=%true`, but the file does not have the `md5` attribute (that is, previously it was written without `compute_md5=%true`), an error will occur. | Input data: - Type: `binary`. - Value: File content. Output data: - Type: `null`. Example: ```bash PARAMETERS {"path" = "//tmp/file"} INPUT this is sample file content ``` ### read_file Command properties: **Non-mutating**, **Heavy**. Synonyms (supported, but not recommended). - download. Semantics: - Get the content of the Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `transaction_id` | No | *null-transaction-id* | Current transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `path` | Yes | | Path to a file in Cypress. The path *must* exist. | | `offset` | No | 0 | The position starting from which the data should be read. | | `length` | No | | The length of the data to be read; by default, the file is read to the end. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: File content. Example: ```bash PARAMETERS { "path" = "//tmp/file" } OUTPUT this is sample file content ``` ### start_distributed_write_file_session { #start_distributed_write_file_session } Part of the distributed API. See [start_distributed_write_session](https://ytsaurus.tech/docs/en/#start_distributed_write_session) Command properties: **Mutating**, **Light**. Semantics: - Initialize a session of distributed writing to a file. - Request cookies for distributed session participants. - Not all requested cookies have to be used. - The same cookie object can be reused to write a new fragment. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to a file in Cypress. | | `cookie_count` | Yes | | Number of distributed session participants. Positive number. | | `transaction_id` | No | *null-transaction-id* | Current transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `timeout` | No | | Session TTL since the last ping (in ms). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: A signed distributed session object along with a list of signed cookies of participants. Example: ```bash PARAMETERS { "path" = "//tmp/file"; "cookie_count" = "2"; } OUTPUT { "session" = "sample_signed_session"; "cookies" = ["sample_signed_cookie_1"; "sample_signed_cookie_2"]; } ``` ### ping_distributed_write_file_session { #ping_distributed_write_file_session } Part of the distributed API. Command properties: **Mutating**, **Light**. Semantics: - Pings the distributed session transaction on the server (including all parent transactions if `ping_ancestor_transactions` is specified), thereby extending the transaction's TTL. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `session` | Yes | | Object of the signed distributed session. | Input data: - Type: `null`. Output data: - Type: `null`. ### finish_distributed_write_file_session { #finish_distributed_write_file_session } Part of the distributed API. Command properties: **Mutating**, **Light**. Semantics: - Merges write fragments into a file. - The merging order corresponds to the order of transmitted write results. - The merging order does not depend on the order of issued cookies during [start_distributed_write_file_session] (#start_distributed_write_file_session). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `session` | Yes | | Object of the signed distributed session. | | `results` | Yes | | List of write fragments (see [write_file_fragment](https://ytsaurus.tech/docs/en/#write_file_fragment)). | Input data: - Type: `null`. Output data: - Type: `null`. ### write_file_fragment { #write_file_fragment } Part of the distributed API. Command properties: **Mutating**, **Heavy**. Semantics: - Write a data fragment to a file. - The same cookie object can be reused to write a new fragment. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `cookie` | Yes | | Signed cookie of a distributed session's participant. | Input data: - Type: `binary`. - Value: Contents of the file write fragment. Output data: - Type: `structured`. - Value: Signed result of writing the fragment to a file. Example: ```bash PARAMETERS {"cookie" = "sample_signed_cookie_1"} INPUT this is sample file fragment content OUTPUT "sample_signed_result" ``` ## Working with file cache { #file_cache } To learn more about the file cache, see the [File cache](https://ytsaurus.tech/docs/user-guide/storage/file-cache.md) section. ### put_file_to_cache Command properties: **Mutating**, **Light**. Semantics: - Add the file to the cache. - Checks that, at the provided path, there's a file with the MD5 hash given in the command options. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------ | | `path` | Yes | | Path to the file in Cypress. | | `md5` | Yes | | Expected MD5 hash for the file. | | `cache_path` | Yes | | Path to the file cache. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Path to the cached file. Example: ```bash PARAMETERS { "path" = "//tmp/file"; "md5" = "a3dcb4d229de6fde0db5686dee47145d"; "cache_path" = "//tmp/yt_wrapper/file_storage/new_cache"; } OUTPUT "//tmp/yt_wrapper/file_storage/new_cache/5d/a3dcb4d229de6fde0db5686dee47145d" ``` ### get_file_from_cache { #get_file_from_cache } Command properties: **Non-mutating**, **Light**. Semantics: - Get a path to the cached file. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ---------------------- | | `md5` | Yes | | MD5 hash for the file. | | `cache_path` | Yes | | Path to the file cache. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Path to the cached file. Example: ```bash PARAMETERS { "md5" = "a3dcb4d229de6fde0db5686dee47145d"; "cache_path" = "//tmp/yt_wrapper/file_storage/new_cache"; } OUTPUT "//tmp/yt_wrapper/file_storage/new_cache/5d/a3dcb4d229de6fde0db5686dee47145d" ``` ## Working with tables { #tables } To learn more about static tables, see the [Static tables](https://ytsaurus.tech/docs/user-guide/storage/static-tables.md) section. To learn more about dynamic tables, see the [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md) section. ### write_table { #write_table } Command properties: **Mutating**, **Heavy**. Scope: [Static tables](https://ytsaurus.tech/docs/user-guide/storage/static-tables.md). Synonyms (supported, but not recommended). - write. Semantics: - Add new entries to a static table. - The table must exist. - If the table `path` includes the `append=%true` attribute, the entries are appended to the table; otherwise, the table is overwritten. - If the table `path` includes the `sorted_by` attribute, the system checks that the data is sorted by the specified set of keys and the resulting table is labeled as sorted. You can write sorted data (when the `sorted_by` attribute is specified) only if any new key is greater than or equal to any old key. - You can write unsorted data (when the `sorted_by` attribute is not specified) to any table, but the table's sort flag will be removed. - The command can be nested in a [transaction](https://ytsaurus.tech/docs/en/#transactions). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to a table in Cypress. The path *must* exist. | | `table_writer` | No | From the driver configuration | [Table write options](https://ytsaurus.tech/docs/user-guide/storage/io-configuration.md). | Input data: - Type: `tabular`. - Value: Table content. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//tmp/node" ; "table_writer" = { "codec_id" = "gzip" }; } INPUT { "id" = 1; "value" = 1.125; }; INPUT { "id" = 2; "value" = 2.000; }; INPUT { "id" = 3; "value" = 3.850; }; ``` ### read_table { #read_table } Command properties: **Non-mutating**, **Heavy**. Scope: [Static](https://ytsaurus.tech/docs/user-guide/storage/static-tables.md) and [dynamic](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md) tables. Synonyms (supported, but not recommended). - read. Semantics: - Retrieve entries from a Cypress table. - The command can be nested in a [transaction](https://ytsaurus.tech/docs/en/#transactions). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to a table in Cypress. The path *must* exist. | | `table_reader` | No | From the driver configuration | [Table's read options](https://ytsaurus.tech/docs/user-guide/storage/io-configuration.md#table_reader). | | `control_attributes` | No | From the driver configuration | [Configuration of the control read attributes](https://ytsaurus.tech/docs/user-guide/storage/io-configuration.md#control_attributes). | | `unordered` | No | `false` | Whether to read data in parallel, regardless of the entry order. | | `omit_inaccessible_columns` | No | `false` | Skip columns the user has no access to instead of returning an error. | | `omit_inaccessible_rows` | No | `false` | Skip rows the user has no access to instead of returning an error. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Table content. Example: ```bash PARAMETERS { "path" = "//tmp/node" } OUTPUT { "id" = 1; "value" = 1.125; }; OUTPUT { "id" = 2; "value" = 2.000; }; OUTPUT { "id" = 3; "value" = 3.850; }; ``` ### read_blob_table Command properties: **Non-mutating**, **Heavy**. Scope: Static [tables with binary data](https://ytsaurus.tech/docs/user-guide/storage/blobtables.md). Semantics: - Get a binary data stream from a specific column in a Cypress table. - The command can be nested in a transaction. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ---------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to a table in Cypress. The path *must* exist. | | `table_reader` | No | From the driver config file | [Table's read options](https://ytsaurus.tech/docs/user-guide/storage/io-configuration.md). | | `part_index_column_name` | No | `part_index` | Name of the column that stores BLOB indexes. | | `data_column_name` | No | `data` | Name of the column that stores data. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: Contents of a data column in the table. Example: ```bash PARAMETERS { "path" = "//tmp/node" } OUTPUT "Hello world" ``` ### partition_tables Command properties: **Non-mutating**, **Heavy**. Semantics: - Split a set of tables into ranges of a specified size (similar to how tables are split in operations for distribution across jobs). - The command can be executed within a [transaction](https://ytsaurus.tech/docs/en/#transactions). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ---------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `paths` | Yes | | List of paths to tables in Cypress. | | `data_weight_per_partition` | Yes | | Desired size of one partition. | | `partition_mode` | No | `unordered` | Partitioning mode (`ordered`, `unordered`), corresponds to the mode of the map operation. | | `max_partition_count` | No | | Maximum number of partitions; this option takes precedence over `data_weight_per_partition`. | | `enable_cookies` | No | | The response will include a `cookie` field that can be used with the [read_table_partition](https://ytsaurus.tech/docs/en/#read_table_partition) call. | Input data: - Type: `null`. Output data: - Type: `structured`; - Value: a list of table partitions. Example: ``` PARAMETERS { "path" = "//tmp/node" ; "data_weight_per_partition"=200000000 ; } OUTPUT { "partitions" = [ { "table_ranges" = [ < "ranges" = [ { "lower_limit" = { "row_index" = 0; }; "upper_limit" = { "row_index" = 24569; }; }; ]; > "//home/dev/ermolovd/YT-11914"; ]; "aggregate_statistics" = { "chunk_count" = 24569; "data_weight" = 605020; "row_count" = 24569; "value_count" = 24569; "compressed_data_size" = 491277; }; }; { "table_ranges" = [ < "ranges" = [ { "lower_limit" = { "row_index" = 24569; }; "upper_limit" = { "row_index" = 24570; }; }; ]; > "//home/dev/ermolovd/YT-11914"; ]; "aggregate_statistics" = { "chunk_count" = 1; "data_weight" = 25; "row_count" = 1; "value_count" = 1; "compressed_data_size" = 20; }; }; ]; } ``` ### read_table_partition Command properties: **Non-mutating**, **Heavy**. Semantics: - Retrieve records within a table range. - The range must be obtained in advance using the `[partition_tables](https://ytsaurus.tech/docs/en/#partition_tables)` command with the `enable_cookies=%true` option. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ---------------- | ------------------------- | ------------ | | `cookie` | Yes | | Cookie corresponding to the table range, obtained from the [partition_tables](https://ytsaurus.tech/docs/en/#partition_tables) call with the `enable_cookies=%true` option. | Input data: - Type: `null`. Output data: - Type: `tabular`; - Value: contents of the table range. ### select_rows Command properties: **Non-mutating**, **Heavy**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Execute an SQL-like query for a dynamic table in accordance with the [supported features](https://ytsaurus.tech/docs/user-guide/dynamic-tables/dyn-query-language.md). - The transaction can be executed against a data snapshot with a specified timestamp. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `query` | Yes | | Query string. | | `timestamp` | No | `sync last committed` | Which timestamp the query should run against. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Set of rows with the result. Example: ```bash PARAMETERS { "query" = "key, value from [//tmp/sometable]" } OUTPUT { "key" = 1; "value" = "hello"; }; OUTPUT { "key" = 2; "value" = "world; }; ``` ### insert_rows Command properties: **Mutating**, **Heavy**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Write rows to a dynamic table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the dynamic table. | | `update` | No | `false` | If set to `false`, the columns missing in the input data will be written with the value of `Null` (overwriting the current value in the table). If set to `true`, such columns will preserve their previous value in the table. | | `aggregate` | No | `false` | If set to `false`, the [aggregating columns](https://ytsaurus.tech/docs/user-guide/dynamic-tables/sorted-dynamic-tables.md#aggr_columns) will be overwritten by the new value. If set to `true`, such columns will apply the delta from the source data. | | `atomicity` | No | `full` | Supported values: `none` and `full`. If set to `none`, writing will occur on each tablet, independent of the others. If set to `full`, either all of the passed rows will be written, or nothing. [Read more](https://ytsaurus.tech/docs/user-guide/dynamic-tables/sorted-dynamic-tables.md). | | `require-sync-replica` | No | `true` | The option makes sense only in case of replication. If set to `true`, inserting will occur only if the table has a synchronous replica. If set to `false`, a synchronous replica is not required. | Input data: - Type: `tabular`. - Value: Table content. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table" } INPUT { "id" = 1; "value" = 1.125; }; INPUT { "id" = 2; "value" = 2.000; }; INPUT { "id" = 3; "value" = 3.850; }; ``` ### delete_rows Command properties: **Mutating**, **Heavy**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Delete all rows with the specified keys from the dynamic table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------------- | | `path` | Yes | | Path to the dynamic table. | Input data: - Type: `tabular`. - Value: Set of rows with the keys. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//tmp/sometable" } INPUT { "id" = 1; }; INPUT { "id" = 2; }; INPUT { "id" = 3; }; ``` ### lock_rows Command properties: **Mutating**, **Heavy**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Lock writing to the rows in the dynamic table while the current transaction is running. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------------- | | `path` | Yes | | Path to the dynamic table. | | `locks` | Yes | | List of the involved `lock groups` from the table schema. | | `lock_type` | No | `shared strong` | Lock types: `shared weak`, `shared strong`, `exclusive`. | A shared lock can be taken from multiple different transactions at the same time. An exclusive lock can be taken from a single transaction that usually updates this row. When using read locks (in contrast to write locks), one row can be locked by multiple transactions. That's why, when there is a continuous stream of transactions that take a shared lock on a certain row, the row becomes locked permanently, and you won't be able to update it. This effect is referred to as `write starvation`. However this is never the case for write-write conflicts because they have an `exclusive lock` that is released every time a write transaction is complete. To alleviate the effect of `write starvation`, you can decrease isolation of shared locks. For this, you can specify the `weak` or `strong` lock mode. The distinction of the `weak` mode is that when the transaction is complete, the timestamps until which the rows were locked by shared locks, aren't saved. The practical implication is that a write operation isn't locked if nested in a transaction that overlaps with the current transaction in time but completes later. However, if the write transaction completes earlier, the transaction with a shared lock isn't applied. In the `weak` mode, the shared lock becomes asymmetric. Input data: - Type: `tabular`. - Value: Set of rows with the keys. ### lookup_rows Command properties: **Non-mutating**, **Heavy**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Select the rows with the specified keys from the table. - The command can be executed against a data snapshot with a specified timestamp. - It is guaranteed that the relative order of rows retrieved in the response will be the same as the order of keys in the query. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ----------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the dynamic table. The path should be simple (without columns, ranges, etc.) | | `column_names` | No | | Which columns to include in the response. | | `keep_missing_rows` | No | `false` | Whether to include the rows corresponding to the non-found keys. If set to `true`, the corresponding positions will include `#` (entity). | | `timestamp` | No | `sync last committed` | Which timestamp the query should run against. | Input data: - Type: `tabular`. - Value: Set of rows with the keys. Output data: - Type: `tabular`. - Value: A set of rows with the specified keys and queried columns. Example: ```bash PARAMETERS { "path" = "//tmp/sometable" } INPUT { "id" = 1; }; INPUT { "id" = 2; }; INPUT { "id" = 3; }; OUTPUT { "id" = 1; "value" = 1.125; }; OUTPUT { "id" = 2; "value" = 2.000; }; OUTPUT { "id" = 3; "value" = 3.850; }; ``` ### trim_rows Command properties: **Mutating**, **Heavy**. Scope: [Ordered](https://ytsaurus.tech/docs/user-guide/dynamic-tables/ordered-dynamic-tables.md) dynamic tables. Semantics: - Remove rows from the beginning of the tablet of an ordered dynamic table . After that, the deleted data can no longer be read by the `select_rows` command. Numbering of other rows is unchanged in this case. - The command is executed outside of transactions. Parameters: | **parameter** | **Required** | **Default value** | **Description** | | ------------------- | ------------- | ------------------------- | ----------------------------- | | `path` | Yes | | Path to the dynamic table. | | `tablet_index` | Yes | | Index of the truncated tablet. | | `trimmed_row_count` | Yes | | Number of deleted rows. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table"; tablet_index = 10; trimmed_row_count = 43 } ``` ### mount_table Command properties: **Mutating**, **Light**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Mounts the tablets of the dynamic table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to the dynamic table. | | `first_tablet_index` | No | `0` | Index of the first mounted tablet. | | `last_tablet_index` | No | `tablet_count - 1` | Index of the last mounted tablet. | | `cell_id` | No | | If specified, the tablets are mounted to the specified cell. Otherwise, the system selects suitable cells (in most cases, you should delegate this choice to the system). | | `freeze` | No | `false` | If set to `true`, the tablets are mounted to a frozen state. See also the description of the [freeze_table](https://ytsaurus.tech/docs/en/#freeze_table) command. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table" } ``` ### unmount_table Command properties: **Mutating**, **Light**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Unmounts the tablets of the dynamic table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------- | ---------- | ------------------------- | ---------------------------------------- | | `path` | Yes | | Path to the dynamic table. | | `first_tablet_index` | No | `0` | Index of the first unmounted tablet. | | `last_tablet_index` | No | `tablet_count - 1` | Index of the last unmounted tablet. | | `force` | No | `false` | Forcibly unmount the tablets. Using this flag poses a risk of data loss, that's why you need YTsaurus admin rights to use it. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table"; "first_tablet_index" = 10; "last_tablet_index" = 20; } ``` ### remount_table Command properties: **Mutating**, **Light**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Enables you to update certain settings of dynamic tables without unmounting them. - When remounted like that, the table remains accessible for reading and writing. - The settings are taken from the table attributes and eventually reach the node where the tablets are processed. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------- | ------------- | ------------------------- | ------------------------------------------ | | `path` | Yes | | Path to the dynamic table. | | `first_tablet_index` | No | `0` | Index of the first remounted tablet. | | `last_tablet_index` | No | `tablet_count - 1` | Index of the last remounted tablet. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table" } ``` ### freeze_table Command properties: **Mutating**, **Light**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Changes the tablets of the dynamic table to a frozen state, flushing all the data to the disk. You can read data in this state, but you can't write new entries. However, all the other written data exist in chunks and are available to map-reduce operations, for example (even without using the `enable_dynamic_store_read` option). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------- | ------------- | ------------------------- | -------------------------------------- | | `path` | Yes | | Path to the dynamic table. | | `first_tablet_index` | No | `0` | Index of the first frozen tablet. | | `last_tablet_index` | No | `tablet_count - 1` | Index of the last frozen tablet. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table" } ``` ### unfreeze_table Command properties: **Mutating**, **Light**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Unfreezes a table that was frozed (for example, by a `freeze_table` command), making the table write-accessible. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------- | ------------- | ------------------------- | -------------------------------------- | | `path` | Yes | | Path to the dynamic table. | | `first_tablet_index` | No | `0` | Index of the first unfrozen tablet. | | `last_tablet_index` | No | `tablet_count - 1` | Index of the last unfrozen tablet. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table" } ``` ### reshard_table Command properties: **Mutating**, **Light**. Scope: [Dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Reshards the dynamic table (that is, changes the set of its tablets). - The resharded tablets must be unmounted. - Be sure to specify `tablet_count` for an ordered table. For a sorted table, you can specify both `tablet_count` and `pivot_keys`. The resharded tablets are replaced by a set of new tablets. - In the case of a sorted table: - When passing `pivot_keys`, the first key in `pivot_keys` must match the first key of the first resharded tablet. The number of `pivot_keys` is equal to the number of new tablets that the resharded tablets are split into. - When passing `tablet_count`, the system will select boundary keys based on the data available in the table as evenly as possible. If the table isn't large enough, you might get less tablets then requested as a result. At default settings, your resulting tablets can't be smaller than about 200 MB each. For smaller slicing, use the option`enable_slicing`. - If the first key column of the table has an integer type, then along with `tablet_count`, you can use `uniform=True`. In this case, uniform values from the range of the appropriate type will be selected as boundary keys. `0, 2^64/n, 2^64\*2/n, ...` for an unsigned 64-bit type and `-2^63, -2^63 + 2^64/n, -2^63 + 2^64\*2/n, ...` for a signed 64-bit type. - For an ordered table, `table_count` specifies the number of new tablets that the sharded tablets are split into. In this case, if the resulting tablets are higher in numbers than the old ones, new empty tablets are created. If the resulting tablets are smaller in numbers, the corresponding number of source trailing tablets are merged into a single tablet in their natural order. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Table path. | | `first_tablet_index` | No | `0` | Index of the first resharded tablet. | | `last_tablet_index` | No | `tablet_count - 1` | Index of the last resharded tablet. | | `pivot_keys` | No | | Boundary keys for the new tablets (for a sorted table). | | `tablet_count` | No | | Number of new tablets. | | `uniform` | No | `false` | Uniformly reshard tablets based on an integer key column. | | `enable_slicing` | No | `false` | Use sampling to increase granularity (for a more precise splitting into tablets) when boundary keys are selected automatically. This might help if you have many entries on one key and few entries on another key. | | `slicing_accuracy` | No | `0.05` | Tolerance acceptable for uniform regarding into a given number of tablets. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "path" = "//home/user/table"; "schema" = [{"name" = "key"; type = "int64"; "sort_order" = "ascending"}; {"name" = "value"; type = "string"}]; } ``` ### reshard_table_automatic Command properties: **Mutating**, **Light**. Scope: [Sorted](https://ytsaurus.tech/docs/user-guide/dynamic-tables/sorted-dynamic-tables.md) dynamic tables. Semantics: - Forces an iteration of a [background tablet balancer](https://ytsaurus.tech/docs/user-guide/dynamic-tables/resharding.md), that is, reshards the tablets that go beyond the `min_tablet_size`–`max_tablet_size` interval. - In contrast to `reshard_table`, it can work with mounted tables. In the process of its operation, it can amount tablets. - It's started asynchronously by default, but if you specify `keep_actions`, it returns a list of IDs that you can use to track the progress. Most APIs do it automatically when passed the `sync=True` flag. - The command does not support replicated tables (you can use it independently on different replicas, however). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Table path. | | `keep_actions` | No | `false` | If set to `true`, respond with a list of IDs to track the progress. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: A list of `tablet_action_id` if `keep_actions=True`. Otherwise, it returns an empty list. Example: ```bash PARAMETERS { "path" = "//home/user/table"; } OUTPUT [ "11-22-33-44"; ] ``` ### alter_table Command properties: **Mutating**, **Light**. Scope: [Static](https://ytsaurus.tech/docs/user-guide/storage/static-tables.md) and [dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md). Semantics: - Changes the scheme and other table settings (both static and dynamic). - You can usually use `alter_table` to change the settings that require complex joint validation. The attributes are used to change the recommended settings that only affect new data. - Changing the schema involves [various validation checks](https://ytsaurus.tech/docs/user-guide/storage/static-schema.md) because the system has to make sure that the existing data matches the schema. - You can change a static table to a dynamic table, but not vice versa. - You can change the schema of a dynamic table, as well as `upstream_replica_id`, only if the table is unmounted. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | --------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Table path. | | `schema` | No | | If specified, it sets a new schema for the table. | | `dynamic` | No | | If specified, it changes a static table to a dynamic table. This setting can only be changed outside a transaction. | | `upstream_replica_id` | No | | If specified, it changes the ID of the replica object on the metacluster. For more information, see [Replicated dynamic tables](https://ytsaurus.tech/docs/user-guide/dynamic-tables/replicated-dynamic-tables.md). | Input data: - Type: `null`. Output data: - Type: `null` (API v3), `structured` (API v4). Example: ```bash PARAMETERS { "path" = "//home/user/table"; "schema" = [{"name" = "key"; type = "int64"; "sort_order" = "ascending"}; {"name" = "value"; type = "string"}]; } ``` ### alter_table_replica Command properties: **Mutating**, **Light**. Scope: [Replicated](https://ytsaurus.tech/docs/user-guide/dynamic-tables/replicated-dynamic-tables.md) dynamic tables. Semantics: - Changes the replica's properties: enables/disables it or changes its mode. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | -------------------------------- | ----------------------------------------------------- | | `replica_id` | Yes | | Replica ID. | | `enabled` | No | Doesn't change the enabled/disabled status of the replica | If set to `true`, enables the table. If set to `false`, disables the table. | | `mode` | No | Doesn't change the replica's mode | Changes the replica mode `sync`/`async`. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "replica_id" = "730e-8611b-3ff02c5-f647333f"; "enabled" = %true; } ``` ### get_table_columnar_statistics Command properties: **Non-mutating**, **Light**. Scope: [Static](https://ytsaurus.tech/docs/user-guide/storage/static-tables.md) and [dynamic](https://ytsaurus.tech/docs/user-guide/dynamic-tables/overview.md) tables. Semantics: - Get statistics on the set of columns in the given set of tables (taken completely or partially, by ranges). - The statistics includes: - The total `data_weight` for each of the requested columns. - The total `data_weight` for all the old chunks (that the metainformation about each column hasn't been saved to because the chunk has been generated before column-by-column statistics were supported). - The total weight of all the timestamps of rows in a dynamic table. - The paths should always include the `column selectors`. - The command can be nested in a transaction. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `paths` | Yes | | A list of paths to tables in Cypress. The tables *must* exist. The paths *must* include `column selectors`. | Input data: - Type: `null`. Output data: - Type: `structured`. - Values: Column-by-column statistics for the requested columns. Example: ```bash PARAMETERS { "paths" = ["//tmp/table1{a,b,c}"; "//tmp/table2{a,b,c}"]; "transaction_id" = "1234-abcd-abcd-7890" } OUTPUT { "column_data_weights" = { "a" = 8124; "b" = 124241241; "c" = 3121414; }; "legacy_chunks_data_weight" = 100242; "timestamp_total_weight" = 50056; } ``` ### start_distributed_write_session { #start_distributed_write_session } YT provides a mechanism for distributed file and table writes. In a scenario when multiple participants write to a single table or file, this mechanism is less taxing on the master server resources compared to using the [write_table](https://ytsaurus.tech/docs/en/#write_table)/[write_file](https://ytsaurus.tech/docs/en/#write_file) method (with append=true). The workflow for using distributed table write methods is as follows (same applies to distributed file writes, see [start_distributed_write_file_session](https://ytsaurus.tech/docs/en/#start_distributed_write_file_session)): 1. First, one participant (the distributed session host) initiates the distributed write mechanism using the [start_distributed_write_session](https://ytsaurus.tech/docs/en/#start_distributed_write_session) method. The method returns a session and a cookie array for write participants. 2. The host distributes cookies among participants. Each session participant writes data via [write_fragment](https://ytsaurus.tech/docs/en/#write_fragment) using their respective cookie. This method returns a write result that the participant passes to the distributed session host. 3. Finally, the host calls the [finish_distributed_write_session](https://ytsaurus.tech/docs/en/#finish_distributed_write_session) method, passing the array of write results to it. All write fragments will be combined into the final table. 4. Between [start_distributed_write_session](https://ytsaurus.tech/docs/en/#start_distributed_write_session) and [finish_distributed_write_session](https://ytsaurus.tech/docs/en/#finish_distributed_write_session), the session must be pinged with the [ping_distributed_write_session](https://ytsaurus.tech/docs/en/#ping_distributed_write_session) method to prevent timeout termination. Command properties: **Mutating**, **Light**. Semantics: - Initialize a session of distributed writing to a table. - Request cookies for distributed session participants. - Not all requested cookies have to be used. - The same cookie object can be reused to write a new fragment. - If the table `path` includes the `append=%true` attribute, the entries are appended to the table; otherwise, the table is overwritten. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `path` | Yes | | Path to a table in Cypress. | | `cookie_count` | Yes | | Number of distributed session participants. Positive number. | | `transaction_id` | No | *null-transaction-id* | Current transaction ID. | | `ping_ancestor_transactions` | No | `false` | Whether to ping all the parent transactions while running the operation. | | `timeout` | No | | Session TTL since the last ping (in ms). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: A signed distributed session object along with a list of signed cookies of participants. Example: ```bash PARAMETERS { "path" = "//tmp/table"; "cookie_count" = "2"; } OUTPUT { "session" = "sample_signed_session"; "cookies" = ["sample_signed_cookie_1"; "sample_signed_cookie_2"]; } ``` ### ping_distributed_write_session { #ping_distributed_write_session } Part of the distributed tabular API. Command properties: **Mutating**, **Light**. Semantics: - Pings the distributed session transaction on the server (including all parent transactions if `ping_ancestor_transactions` is specified), thereby extending the transaction's TTL. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `session` | Yes | | Object of the signed distributed session. | Input data: - Type: `null`. Output data: - Type: `null`. ### finish_distributed_write_session { #finish_distributed_write_session } Part of the distributed tabular API. Command properties: **Mutating**, **Light**. Semantics: - Merges write fragments into a table. - For sorted tables, passed write results are sorted by boundary keys. Key intervals must not overlap, and the keys themselves can't be duplicate. With all invariants met, the order of passed results does not matter. - For unsorted tables, fragments are combined in the order in which the results were passed. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `session` | Yes | | Object of the signed distributed session. | | `results` | Yes | | List of write fragments (see [write_fragment](https://ytsaurus.tech/docs/en/#write_fragment)). | Input data: - Type: `null`. Output data: - Type: `null`. ### write_fragment { #write_fragment } Part of the distributed tabular API. Command properties: **Mutating**, **Heavy**. Semantics: - Write a data fragment to a table. - The same cookie object can be reused to write a new fragment. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `cookie` | Yes | | Signed cookie of a distributed session's participant. | | `max_row_buffer_size` | No | 1_MB | Size of the string buffer for the internal writer | Input data: - Type: `tabular`. - Value: Table fragment content. Output data: - Type: `structured`. - Value: Signed result of writing the table fragment. Example: ```bash PARAMETERS {"cookie" = "sample_signed_cookie_1"} INPUT { "id" = 1; "value" = 1.125; }; INPUT { "id" = 2; "value" = 2.000; }; OUTPUT "sample_signed_result" ``` ### enable_table_replica Command properties: **Mutating**, **Light**. Semantics: - Enable a table replica. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `replica_id` | Yes | | Replica ID to enable. | Input data: - Type: `null`. Output data: - Type: `structured`. ### disable_table_replica Command properties: **Mutating**, **Light**. Semantics: - Disable a table replica. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `replica_id` | Yes | | Replica ID to disable. | Input data: - Type: `null`. Output data: - Type: `structured`. ### get_in_sync_replicas Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the list of replicas that are in sync with a table at a given timestamp. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the table. | | `timestamp` | Yes | | Timestamp to check synchronization at. | | `all_keys` | No | `false` | Whether to check all keys. | Input data: - Type: `tabular`. Output data: - Type: `structured`. - Value: List of in-sync replica IDs. ### pull_rows Command properties: **Non-mutating**, **Heavy**. Semantics: - Pull rows from a table for replication purposes. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the table. | | `upstream_replica_id` | Yes | | Upstream replica ID. | | `replication_progress` | Yes | | Current replication progress. | | `upper_timestamp` | No | | Upper timestamp limit for pulling. | | `order_rows_by_timestamp` | No | `false` | Whether to order rows by timestamp. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Rows pulled from the table. ### explain_query Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the execution plan for a query without actually running it. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `query` | Yes | | Query string to explain. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Query execution plan. ### get_table_pivot_keys {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get the pivot keys that define tablet boundaries for a table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the table. | | `represent_key_as_list` | No | `false` | Whether to represent keys as lists instead of tuples. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of pivot keys. ### get_tablet_infos {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get information about specific tablets of a table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the table. | | `tablet_indexes` | Yes | | List of tablet indexes to get information for. | | `request_errors` | No | `false` | Whether to include error information. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Information about the requested tablets. ### get_tablet_errors {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get error information for tablets of a table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the table. | | `limit` | No | | Maximum number of errors to return. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of tablet errors. ### get_table_mount_info {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get mount information for a dynamic table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the table. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Table mount information including cell IDs and tablet states. ### balance_tablet_cells Command properties: **Mutating**, **Light**. Semantics: - Balance tablets across tablet cells for optimal resource distribution. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `bundle` | Yes | | Tablet cell bundle name. | | `tables` | No | | Specific tables to balance (if omitted, balance all tables in bundle). | | `keep_actions` | No | `false` | Whether to keep the actions for inspection. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Balancing actions taken or planned. ### cancel_tablet_transition {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Cancel an ongoing tablet state transition (e.g., mounting, unmounting). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `tablet_id` | Yes | | ID of the tablet. | | `cell_id` | Yes | | ID of the tablet cell. | Input data: - Type: `null`. Output data: - Type: `structured`. ### create_table_backup Command properties: **Mutating**, **Light**. Semantics: - Create a backup of one or more tables with consistent snapshot semantics. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `manifest` | Yes | | Backup manifest describing tables and their destinations. | | `checkpoint_timestamp_delay` | No | `5s` | Delay for checkpoint timestamp selection. | | `force` | No | `false` | Force backup even if destination tables exist. | | `preserve_account` | No | `false` | Preserve the source account in the backup. | Input data: - Type: `null`. Output data: - Type: `structured`. ### restore_table_backup Command properties: **Mutating**, **Light**. Semantics: - Restore tables from a previously created backup. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `manifest` | Yes | | Restore manifest describing source backups and target locations. | | `force` | No | `false` | Force restore even if target tables exist. | | `mount` | No | `false` | Whether to mount tables after restoration. | | `enable_replicas` | No | `false` | Whether to enable table replicas after restoration. | | `preserve_account` | No | `false` | Preserve the backup account in restored tables. | Input data: - Type: `null`. Output data: - Type: `structured`. ### locate_skynet_share Command properties: **Non-mutating**, **Heavy**. Semantics: - Locate data for Skynet share integration. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to locate. | Input data: - Type: `null`. Output data: - Type: `structured`. ## Running operations For more information about running data processing operations, see [Data processing](https://ytsaurus.tech/docs/user-guide/data-processing/operations/overview.md). All the operations are run asynchronously, the specified commands only launch them. To find out whether the operation is complete or not, request the [operation status](https://ytsaurus.tech/docs/user-guide/data-processing/operations/overview.md#status) using the `get_operation` command. All the commands used to work with operations are also transactional. It means that everything you do with tables in an operation will be executed within the specified transaction when you run the operation. The node responsible for the operation (`//sys/operations/`) is updated by the [scheduler](https://ytsaurus.tech/docs/user-guide/data-processing/scheduler/scheduler-and-pools.md)outside of any transactions. ### start_operation {% note info %} The HTTP API supports this operation from version v4 onward. Earlier versions of the API use commands with the same names to run operations, such as `map`, `reduce`, `sort`, and so on. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Run the operation of the specified type. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `operation_type` | Yes | | Operation type (can be one of these: `map`, `reduce`, `map_reduce`, `remote_copy`, `erase`, `sort`, `merge`, `vanilla`). | | `spec` | Yes | | Operation specification. For more information, see [Setting up operations](https://ytsaurus.tech/docs/user-guide/data-processing/operations/operations-options.md). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "operation_type" = "map_reduce"; "spec" = { "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_path" = "//tmp/table_out" } } OUTPUT "37878b-ba919c15-cdc97f3a-8a983ece" ``` ### merge Command properties: **Mutating**, **Light**. Semantics: - Start merging the source tables. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/merge.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ----------------------- | -------------- | ------------------------- | ------------------------------------------- | | `spec` | Yes | | Operation specification. | | *spec[input_table_paths]* | Yes | | List of input tables. | | *spec[output_table_path]* | Yes | | Output table. | | *spec[mode]* | No | `unordered` | Merging mode ( `unordered`, `ordered`, `sorted`). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_path" = "//tmp/table_out" } } OUTPUT "37878b-ba919c15-cdc97f3a-8a983ece" ``` ### erase Command properties: **Mutating**, **Light**. Semantics: - Start erasing data from the source table. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/erase.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ---------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `spec` | Yes | | Operation specification. | | *spec[table_path]* | Yes | | An input table with the specified row selector. The same table will be used for output. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "table_path" = "//tmp/table[#0:#500]" ; } } OUTPUT "3f9e62-ce8d2965-6350842b-3e4628d2" ``` ### map Command properties: **Mutating**, **Light**. Semantics: - Run the map operation on the source tables, writing the output to the output tables. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/map.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------------ | ------------- | ------------------------- | -------------------------------------------------- | | `spec` | Yes | | Operation specification (see the relevant fields below). | | *spec[input_table_paths]* | Yes | | List of input tables. | | *spec[output_table_paths]* | Yes | | List of output tables. | | *spec[mapper]\[command]* | Yes | | The command that runs the mapper. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "mapper" = { "command" = "cat" } ; "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_paths" = [ "//tmp/table_out" ] } } OUTPUT "33ab3f-bf1df917-b35fe9ed-c70a4bf4" ``` ### reduce Command properties: **Mutating**, **Light**. Semantics: - Run the reduce operation on the source tables, writing the output to the output tables. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/reduce.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------------ | ------------- | ------------------------- | -------------------------------------------------- | | `spec` | Yes | | Operation specification (see the relevant fields below). | | *spec[input_table_paths]* | Yes | | List of input tables. | | *spec[output_table_paths]* | Yes | | List of output tables. | | *spec[reduce_by]* | Yes | | Columns that `reduce` runs against. | | *spec[reducer]\[command]* | Yes | | Command that runs the reducer. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "reducer" = { "command" = "cat" ; } ; "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_paths" = [ "//tmp/table_out" ] ; "reduce_by" = [ "my_key" ] ; } } OUTPUT "33ab3f-bf1df917-b35fe9ed-c70a4bf4" ``` ### sort Command properties: **Mutating**, **Light**. Semantics: - Start sorting the source tables. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/sort.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ----------------------- | ------------- | ------------------------- | -------------------------------------------------------- | | `spec` | Yes | | Operation specification (see the relevant fields below). | | *spec[input_table_paths]* | Yes | | List of input tables. | | *spec[output_table_path]* | Yes | | Output table. | | *spec[sort_by]* | Yes | | And non-empty set of column names that make up the sort key. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_path" = "//tmp/table_out" ; "sort_by" = [ "mykey" ]; } } OUTPUT "37878b-ba919c15-cdc97f3a-8a983ece" ``` ### map_reduce Command properties: **Mutating**, **Light**. Semantics: - Run Map-Reduce on the source tables. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/mapreduce.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `spec` | Yes | | Operation specification (see the relevant fields below). | | *spec[input_table_paths]* | Yes | | List of input tables. | | *spec[output_table_paths]* | Yes | | List of output tables. | | *spec[mapper]\[command]* | No | | The command that runs the mapper. | | *spec[sort_by]* | No | | A non-empty set of column names by which the data for reduces will be sorted. | | *spec[reduce_by]* | Yes | | Columns that reduce runs against. | | *spec[reducer]\[command]* | Yes | | Command that runs the reducer. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "mapper" = { "command" = "cat" } ; "reducer" = { "command" = "cat" } ; "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_path" = "//tmp/table_out" ; "reduce_by" = [ "my_key" ] ; } } OUTPUT "37878b-ba919c15-cdc97f3a-8a983ece" ``` ### remote_copy Command properties: **Mutating**, **Light**. Semantics: - Copy the source tables from the specified cluster. - Detailed description of all [specification parameters](https://ytsaurus.tech/docs/user-guide/data-processing/operations/remote-copy.md). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------------ | ------------- | ------------------------- | -------------------------------------------------- | | `spec` | Yes | | Operation specification (see the relevant fields below). | | *spec[input_table_paths]* | Yes | | List of input tables. | | *spec[output_table_paths]* | Yes | | List of output tables. | | *spec[cluster_name]* | Yes | | Cluster with the source tables. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: ID of the running operation. Example: ```bash PARAMETERS { "spec" = { "input_table_paths" = [ "//tmp/table_in", "//tmp/table_in" ] ; "output_table_path" = "//tmp/table_out" ; "cluster_name" = ; } } OUTPUT "37878b-ba919c15-cdc97f3a-8a983ece" ``` ## Working with operations {% note info "Note" %} All the commands used to work with the operations are non-[transactional](https://ytsaurus.tech/docs/en/#transactions). {% endnote %} The table describes the `TOperation` type, which contains information about the operation returned by the `get_operation` and `list_operations` commands: | **Field** | **Type** | **Description** | |--------------------------|-----------------|---------------------------------------------------------------------------------------------------| | `id` | `string` | String representation of the operation ID (for example, `d840bb39-3d893e5b-3fe03e8-f009b1fb`). | | `type` | `string` | Operation type (for example, `map`, `reduce`, `map_reduce`). | | `state` | `string` | Current operation state (for example, `completed`, `running`,`failed`). | | `authenticated_user` | `string` | User that ran the operation. | | `start_time` | `ISO 8601 string` | Start time. | | `finish_time` | `ISO 8601 string` | End time (if the operation completed). | | `brief_progress` | `map` | Summarized statistics on jobs. | | `progress` | `map` | Full statistics on jobs. | | `brief_spec` | `map` | Part of the specification with light fields. | | `provided_spec` | `map` | Specification given by the user when starting the operation. | | `full_spec` | `map` | Specification where all the fields omitted by the user are populated by defaults. | | `unrecognized_spec` | `map` | Specification fields that were entered by the user but not recognized by the scheduler. | | `experiment_assignment_names` | `list` | Names of the experiments applied to the operation. | | `experiment_assignments` | `list` | Descriptions of the experiments applied to the operation. | | `spec` | `map` | Specification given by the user when starting the operation, with the experiments applied. | | `controller_agent_address` | `string` | Address of the controller agent (host:port) responsible for the operation. | | `events` | `list` | List of state change events that occurred during the operation. | | `alerts` | `map` | Alerts set up for the operation at the moment. | | `alert_events` | `list` | List of alert start and end events that occurred during the operation. | | `result` | `map` | A map with an `error` field that can include an error if the operation failed. | | `committed` | `bool` | Whether the operation results were committed. | | `suspended` | `bool` | Whether the operation is currently suspended. | | `scheduling_attributes_per_pool_tree` | `map` | Map with information about the operation's execution across different trees. | | `task_names` | `list` | Names of the tasks comprising the operation. | | `controller_features` | `map` | Set of quantitative characteristics that can be used to analyze experiments applied to operations. | ### list_operations { #list_operations } {% note warning "Attention" %} This command can create a significant load against the cluster. Do not use it in your workflow without a prior approval by the administrator. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get a list of operations that match the filters. Parameters: | **Parameter** | **Type** | **Required** | **Default value** | **Description** | | ---------------- | -------------- | ----------------- | ------------------------- | ------------------------------------------------------------ | | `attributes` | `list` | No | `Null` | List of operation attributes that need to be returned in the response. | | `from_time` | `ISO 8601 string` | No | `Null` | Bottom limit for the time interval for operation selection (by the time when the operation began). | | `to_time` | `ISO 8601 string` | No | `Null` | Top limit for the time interval for operation selection (by the time when the operation began). | | `cursor_time` | `ISO 8601 string` | No | `Null` | Time beginning from which a list of operation should be returned. | | `cursor_direction` | `{past, future}` | No | `past` | Time direction used to list the operations. | | `user` | `string` | No | `Null` | Username to filter for. | | `state` | `string` | No | `Null` | Operation state used to filter data. | | `type` | `string` | No | `Null` | Type of filtering operation. | | `filter` | `string` | No | `Null` | Substring that `filter_factors` should include. `filter_factors` is a concatenation of various operation attributes, such as `id`, `type`, `authenticated_user`, `state`, `input_table_paths`, `output_table_paths`, `experiments`, `annotations`, `runtime_parameters`, `pool`, and `title`. | | `pool` | `string` | No | `Null` | Pool used for filtering. | | `pool_tree` | `string` | No | `Null` | Pool tree used for filtering. | | `with_failed_jobs` | `bool` | No | `Null` | Return only the operations that have jobs with the `failed` status. | | `access` | `map` | No | `Null` | Dictionary with the mandatory fields `subject` (a string) and `permissions` (a list of strings) that set a filter by access rights. If specified, only the operations for which a `subject` has every right in the `permissions` list, are returned. | | `include_archive` | `bool` | No | `false` | Whether to request operations from the archive. | | `include_counters` | `bool` | No | `true` | Whether to return statistics for the requested operations. | | `limit` | `int` | No | `100` | List of operations that need to be returned in the response. | Input data: - Type: `null`. Output data: - Type: `structured`. | **Parameter** | **Type** | **Description** | | ------------ | ------- | ------------ | | `operations` | `list` | List of operations. | | `incomplete` | `bool` | Whether the list of operations is complete. That is, whether all the operations in the range `from_time` — `to_time` are listed. | | `type_counts` | `map` | Map indicating the number of operations of various types that match all specified filters (except the filter by type). | | `state_counts` | `map` | Map indicating the number of operations with various states that match all specified filters (except the filter by state). | | `pool_counts` | `map` | Map indicating the number of operations in various pools that match all specified filters (except the filter by pool). | | `pool_tree_counts` | `map` | Map indicating the number of operations in various pool trees that match all specified filters (except the filter by pool tree). | | `failed_jobs_count` | `int` | Number of unsuccessful jobs with the `failed` state. | Example: ```bash PARAMETERS { } OUTPUT { "operations" = [ { "id" = "7001208d-fef089b3-3fe03e8-453d99a1"; "type" = "remote_copy"; "state" = "initializing"; "authenticated_user" = "user-name"; "brief_progress" = {}; "brief_spec" = { ... }; "start_time" = "2018-02-06T11:06:34.200591Z"; "suspended" = %false; "weight" = 1.; }; ]; "incomplete" = %true; "pool_counts" = { "pool-counts-example" = 2; "user-name-1" = 2; ... }; "user_counts" = { "yql" = 52; "user-name-1" = 2; }; "state_counts" = { "materializing" = 10; "pending" = 763; "running" = 1848; "completed" = 6654; "aborted" = 37; "failed" = 98; }; "type_counts" = { "map" = 4294; "merge" = 1337; "erase" = 97; "sort" = 1126; "reduce" = 886; "map_reduce" = 1609; "remote_copy" = 24; }; "failed_jobs_count" = 109; } ``` ### get_operation { #get_operation } Command properties: **Non-mutating**, **Light**. Semantics: - Get information about an operation. Parameters: | **Parameter** | **Type** | **Required** | **Default value** | **Description** | | ------------ | ------- | ----------------- | ------------------------- | ----------------------- | | `operation_id (operation_alias)` | `GUID (string)` | Yes | | Operation ID. | | `attributes` | `list` | No | `[]` | Operation attributes. | {% note info %} An operation can be accessed either via `operation_id` or `operation_alias`. For more information about operation aliases, see the section [Operation options](https://ytsaurus.tech/docs/user-guide/data-processing/operations/operations-options.md#common_options). {% endnote %} Input data: - Type: `null`. Output data: - Type: `structured`. - Operation description (`TOperation`). Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4"; attributes = [ "state" ] } OUTPUT { "state" = "running"; } ``` ### list_operation_events { #list_operation_events } Command properties: **Non-mutating**, **Light**. Semantics: - Get operation events. Parameters: | **Parameter** | **Type** | **Required** | **Default value** | **Description** | | -------------- | --------- | ---------------- | ------------------------- | -------------------------------------------------------------------- | | `operation_id (operation_alias)` | `GUID (string)` | Yes | | Operation ID. | | `event_type` | `string` | No | `Null` | Event type. If the value is empty, it returns events of all types. | Input data: - Type: `null`. Output data: - Type: `list`. - The list of the `TOperationEvent` operation events. `TOperationEvent` is a structure that contains information about an operation event. The following event types are supported: - `incarnation_started` — start of a new incarnation of a gang operation. All events, irrespective of their type, have the following fields: | **Field** | **Type** | **Description** | |--------------------------|-------------------|---------------------------------------------------------------------------------------------------| | `timestamp` | `ISO 8601 string` | Event time. | | `type` | `string` | Event type. | Other fields depend on the event type. - `incarnation_started`: | **Parameter** | **Type** | **Required** | **Default value** | **Description** | | --------------------------- | -------- | ----------------- |--------------------------- | --------------------------------------------------------------------------------------- | | `incarnation` | `string` | Yes | | Incarnation identifier. | | `incarnation_switch_reason` | `string` | No | `Null` | Incarnation switch reason. The empty value refers to an incarnation at the operation start. | | `incarnation_switch_info` | `map` | No | `{}` | Additional information about the incarnation switch reason. | Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4" } OUTPUT { [ { "timestamp" = "2025-07-03T08:01:41.497318Z"; "event_type" = "incarnation_started"; "incarnation" = "d31999cc-7ee0f616-ecb005cb-b0ec2d45"; "incarnation_switch_info" = {}; }; ] } ``` ### abort_operation Command properties: **Mutating**, **Light**. Semantics: - Abort the operation. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `operation_id (operation_alias)` | Yes | | Operation ID. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4" } ``` ### complete_operation Command properties: **Mutating**, **Light**. Semantics: - Instantly complete the operation, saving the currently calculated result. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `operation_id (operation_alias)` | Yes | | Operation ID. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4" } ``` ### suspend_operation Command properties: **Mutating**, **Light**. Semantics: - Suspend the operation. - This command is executed instantly. After that, no more jobs are started for the operation, and the running jobs are optionally aborted. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------------ | ------------- | ------------------------- | ----------------------------------------------- | | `operation_id (operation_alias)` | Yes | | Operation ID. | | `abort_running_jobs` | No | `false` | Whether to abort the running operation jobs. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4" } ``` ### resume_operation Command properties: **Mutating**, **Light**. Semantics: - Resume the suspended operation. - This command is executed instantly. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `operation_id (operation_alias)` | Yes | | Operation ID. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4" } ``` ### update_operation_parameters { #update_operation_parameters } Command properties: **Mutating**, **Light**. Semantics: - Update the parameters of a running operation. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | -------------------------------------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `operation_id (operation_alias)` | Yes | | Operation ID. | | `parameters` | Yes | | Dictionary with the operation parameters. | | *parameters[owners]* | No | | (deprecated, will be removed) List of new owners of the operation. | | *parameters[acl]* | No | | New ACL of the operation (it overlaps the base ACL). | | *parameters[pool]* | No | | Name of the pool to which the operation has to be switched over in all its trees. | | *parameters[weight]* | No | | New weight of the operation in all the trees. | | *parameters[scheduling_options_per_pool_tree]* | No | | Dictionary `{tree name: scheduler settings for this tree}`. The settings are described below. To learn more about the scheduler, see [Scheduler and pools](https://ytsaurus.tech/docs/user-guide/data-processing/scheduler/scheduler-and-pools.md). | | *parameters[options_per_job_shell]* | No | | Dictionary `{Job Shell name: settings for this Job Shell}`. The settings are described below. | Scheduler settings for the tree: | **Parameter** | **Required** | **Default value** | **Description** | | --------------- | ------------- | ------------------------- | ------------------------------------------------------- | | `weight` | No | | Weight of the operation in the tree. | | `pool` | No | | Pool of the operation in the tree. | | `resource_limits` | No | | Dictionary `{ resource name: limit}`. Resource limits in the tree. | Job Shell settings: | **Parameter** | **Required** | **Default value** | **Description** | | --------------- | ------------- | ------------------------- | ------------------------------------------------------- | | `owners` | No | | Subjects (users or groups) who will have access to the Job Shell. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS {"operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4"; "parameters" = {"pool" = "username"; "scheduling_options_per_pool_tree" = {"default" = {"weight" = 2; "resource_limits" = { "user_slots" = 1; "cpu" = 0.5; "network" = 10; "memory" = 1000000000}}}}} ``` ### patch_operation_spec { #patch_operation_spec } Command properties: **Mutating**, **Lightweight**. Semantics: - Update the specification of a running operation. Parameters: | **Parameter** | **Required** | **Description** | | -------------------------------- | ------------ | ------------------------------------------------------------ | | `operation_id (operation_alias)` | Yes | Operation ID. | | `patches` | Yes | List of patches to apply to the operation specification. | Patch format: | **Parameter** | **Required** | **Description** | | ------------- | ------------ | ------------------------------------------------------------ | | `path` | Yes | Path to the parameter in the operation specification in [YPath](https://ytsaurus.tech/docs/user-guide/storage/ypath.md) format. | | `value` | Yes | New value for the parameter. | Input data: - Type: `null`. Output data: - Type: `null`. Modifiable parameters: | **Parameter** | **Description** | **Comment** | | -------------------------------- | ------------------------------------------------------------ | ----------- | | `/max_failed_job_count` | Number of failed jobs after which the operation is considered failed. | | | `/tasks//job_count` | Number of jobs for task `` in Vanilla operations. | Cannot be modified for gang operations or operations with `fail_on_job_restart`. | Notes: - This command allows you to modify individual parameters of an operation specification on the fly, without needing to restart the operation. - Patches are applied atomically: either all patches from the list are applied, or none. - Changing specification parameters may cause running jobs to be aborted if the desired job count is less than the current count. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4"; "patches" = [ { "path" = "/max_failed_job_count"; "value" = 15; }; { "path" = "/tasks/worker/job_count"; "value" = 10; }; { "path" = "/tasks/coordinator/job_count"; "value" = 5; }; ] } ``` ### check_operation_permission { #check_operation_permission } Command properties: **Idempotent**, **Lightweight**. Semantics: - Check if a user has a specific permission for an operation. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `operation_id` | Yes | | Operation identifier. | | `user` | Yes | | Username for which permission needs to be checked. | | `permission` | Yes | | Permission to check. Possible values: `read`, `manage`, `administer`. | Input data: - Type: `null`. Output data: - Type: `structured`. | **Parameter** | **Type** | **Description** | | ------------ | -------- | ------------ | | `action` | `string` | Permission check result. | {% note info "Note" %} Having `manage` permission for a pool allows you to perform some [actions](https://ytsaurus.tech/docs/user-guide/data-processing/scheduler/manage-pools.md#allowed_pool_actions) on operations running in the pool without formal permissions for the operation. In such cases, the command will indicate that the user does not have permission, as it only checks the permissions for the operation itself. {% endnote %} Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4"; "user" = "vasya"; "permission" = "manage"; } OUTPUT { "action" = "allow" } ``` ## Working with jobs {% note info "Note" %} All the commands used to work with jobs are non-[transactional](https://ytsaurus.tech/docs/en/#transactions). {% endnote %} The table describes the `TJob` type, which contains information about the job returned by the `get_job` and `list_jobs` commands: | Field | Type | Description | |------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| | `job_id`/`id` | `string` | String representation of the job ID (for example, `d6bf44e17-8c200aed-41103e8-4958aff3`). The field name depends on the command: `job_id` for `get_job` and `id` for `list_jobs`. | | `operation_id` | `string` | String representation of the operation ID (for example, `2cb654e4-9ab04944-4110384-100d7ed`). | | `allocation_id` | `string` | String representation of the ID of the allocation used to execute the job (for example, `2cb654e4-9ab04944-4110384-d7ed`). | | `type` | `string` | Job type (for example, `vanilla`, `map`, `partition_reduce`). | | `state` | `string` | Current job state (for example, `running`, `failed`, `completed`). | | `controller_state` | `string` | Job state according to the controller. | | `archive_state` | `string` | Job state according to the node. | | `address` | `string` | Address of the exec node (host:port) that ran the job. If the node has multiple addresses, the value matches the default address. | | `addresses` | `map` | Map containing the addresses of the exec node that ran the job. Used when the exec node can have multiple addresses. | | `task_name` | `string` | Name of the task the job belonged to. | | `start_time` | `ISO 8601 string` | Job start time. | | `finish_time` | `ISO 8601 string` | Job end time. | | `has_spec` | `bool` | Flag indicating whether the job specification was archived. | | `has_competitors` | `bool` | Flag indicating whether the job is speculative or has speculative competitors. | | `has_probing_competitors` | `bool` | Flag indicating whether the job is a probe or has probe competitors. | | `job_competition_id` | `string` | For speculative jobs, matches the ID of the competitor job. | | `probing_job_competition_id` | `string` | For probes, matches the ID of the competitor job. | | `pool` | `string` | Name of the [pool](https://ytsaurus.tech/docs/user-guide/data-processing/scheduler/scheduler-and-pools.md) where the job was run. | | `pool_tree` | `string` | Name of the [pool tree](https://ytsaurus.tech/docs/user-guide/data-processing/scheduler/scheduler-and-pools.md) where the job was run. | | `progress` | `float in [0,1]` | Evaluation of the share of work executed by the job by the current moment. | | `stderr_size` | `int` | Size of the saved stderr of the job (you can get stderr using the [`get_job_stderr`](https://ytsaurus.tech/docs/en/#get_job_stderr) command). | | `fail_context_size` | `int` | Size of the saved fail_context of the job (you can get fail_context using the [`get_fail_context`](https://ytsaurus.tech/docs/en/#get_fail_context) command). | | `error` | `map` | Error that interrupted the job. | | `statistics` | `map` | Job statistics. | | `brief_statistics` | `map` | Summarized job statistics. | | `exec_attributes` | `map` | Metainformation about the job's initiation on the exec node. | | `monitoring_descriptor` | `string` | ID used by the monitoring system to store information about the job. | | `job_cookie` | `int` | Unique job index within the operation task that serves as an implementation detail of the operation controller. In vanilla operations, it can be used to group workers: if a job is interrupted, the new job launched to replace it will have the same `job_cookie`. | | `operation_incarnation` | `string` | ID of the gang operation incarnation. | | `interruption_info` | `map` | Information about the reasons for the job's interruption. | | `archive_features` | `map` | Information about the job's archived attributes. | | `core_infos` | `map` | List of metainformation about the job's saved core dumps. | | `events` | `map` | List of job state transition events. | | `is_stale` | `bool` | Whether the information about the job is outdated (if `%true`, some fields might need update). Information about the job is considered outdated if it hasn't been updated for a long time. The information in the job archive is updated by the node running the job and the operation controller. The update process is asynchronous. If the node and the controller restart at the same time for some reason (for example, as a result of an update), the information about the final job state (`completed`, `failed`, or `aborted`) might not end up in the archive, resulting in this job always returning as stale. Despite the `running` status, such jobs likely haven't been running for a long time and should be ignored. | ### get_job { #get_job } Command properties: **Non-mutating**, **Light**. Semantics: - Getting information about a job. The command can run both for running and completed jobs (if information about the jobs was saved). Parameters: | **Parameter** | **Type** | **Required** | **Default value** | **Description** | | -------------- | ------------- | ---------------- | ------------------------- | ------------ | | `operation_id (operation_alias)` | `GUID (string)` | Yes | | Operation ID. | | `job_id` | `GUID` | Yes | | Job ID. | | `attributes` | `list` | No | `Null` | List of job attributes that need to be returned in the response. | The `attributes` parameter is a subset of `TJob` objects. If it's `Null`, all job attributes are returned; otherwise, the user receives only the attributes specified in `attributes`. Input data: - Type: `null`. Output data: - Type: `structured`. - Job description (`TJob`). Example: ```bash PARAMETERS { "operation_id" = "e13c5406-e5dd6f5d-3fe03e8-fe05f0d3"; "job_id" = "f11ae559-a0375703-3fe0384-8f1"} OUTPUT { "operation_id" = "e13c5406-e5dd6f5d-3fe03e8-fe05f0d3"; "job_id" = "f11ae559-a0375703-3fe0384-8f1"; "state" = "completed"; "start_time" = "2018-02-06T09:37:02.858492Z"; "finish_time" = "2018-02-06T09:42:19.185525Z"; "address" = "hostname.net:9012"; "statistics" = { "data" = { ... }; }; "events" = [ ... ]; } ``` ### list_jobs { #list_jobs } {% note warning "Attention" %} This command can create significant load on the cluster. Do not use it in your workflow without a prior approval by the administrator. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get all the jobs that match the filters for a given operation. Parameters: | **Parameter** | **Type** | **Required** | **Default value** | **Description** | | -------------------- | ------------------------------------------------------------ | ---------------- | ------------------------- | ------------------------------------------------------------ | | `operation_id (operation_alias)` | `GUID (string)` | Yes | | Operation ID. | | `type (job_type)` | `EJobType` | No | `Null` | When you specify the parameter, the response will only include the jobs with the specified `job_type`. | | `state (job_state)` | `EJobState` | No | `Null` | When you specify the parameter, the response will only include the jobs with the specified `job_state`. | | `address` | `string` | No | `Null` | If this parameter is specified, the response will only include the jobs with an address that starts with `address`. | | `with_stderr` | `bool` | No | `Null` | At `Null`, all the jobs are returned. At `True`, only the jobs with a non-null `stderr` are returned. At `False`, only the jobs with a null `stderr` are returned. | | `with_fail_context` | `bool` | No | `Null` | At `Null`, all the jobs are returned. At `True`, only the jobs that have a saved `fail_context` are returned. At `False`, only the jobs that do not have `fail_context` are returned. | | `with_spec` | `bool` | No | `Null` | At `Null`, all the jobs are returned. At `True`, only the jobs that have a saved specification are returned. At `False`, only the jobs that do not have a specification are returned. | | `with_competitors` | `bool` | No | `Null` | At `Null`, all the jobs are returned. At `True`, only the jobs for which speculative copies were run along with those copies are returned. At `False`, only the jobs that do not have speculative copies are returned. | | `job_competition_id` | `GUID` | No | `Null` | When you specify the parameter, the response will include the job with the `job_competition_id` and all of its speculative copies (if any). | | `with_monitoring_descriptor` | `bool` | No | `Null` | At `Null`, all the jobs are returned. At `True`, only the jobs that have `monitoring_descriptor` are returned. At `False`, only the jobs that do not have `monitoring_descriptor` are returned. | | `with_interruption_info` | `bool` | No | `Null` | At `Null`, all the jobs are returned. At `True`, only the jobs that have `interruption_info` are returned. At `False`, only the jobs that do not have `interruption_info` are returned. | | `task_name` | `string` | No | `Null` | When you specify the parameter, the response will only include the jobs with the specified `task_name`. | | `operation_incarnation` | `string` | No | `Null` | When you specify the parameter, the response will only include the jobs belonging to the requested incarnation. | | `from_time` | `ISO 8601 string` | No | `Null` | Bottom limit for the time interval for operation selection (based on the time when the operation began). | | `to_time` | `ISO 8601 string` | No | `Null` | Upper limit for the time interval for operation selection (based on the time when the operation began for running jobs and based on the time the operation ended for completed jobs). | | `continuation_token` | `string` | No | `Null` | String used for pagination (see below). | | `sort_field` | `{none,type,state,start_time,finish_time,address,duration,progress,id}` | No | `none` | Sort fields. | | `sort_order` | `{ascending,descending}` | No | `ascending` | Sorting order. | | `limit` | `int` | No | `1000` | Limit on the number of returned jobs. | | `offset` | `int` | No | `0` | Offset by the given number of jobs. | | `attributes` | `list` | No | `Null` | List of job attributes that need to be returned in the response. | The `job_type`, `job_state`, `address`, `with_stderr`, `with_fail_context`, `with_competitors`, `with_spec`, `with_monitoring_descriptor`, and `with_interruption_info` parameters define the job *filter*. The response will only include the jobs that meet the filtering criteria. The `sort_field` and`sort_order` define the order of jobs in the response. In this case, the `limit` and `offset` parameters define the slice (subset) of jobs in the response: the first `offset` jobs are skipped, and then `limit` of the remaining jobs is selected. The `attributes` parameter is a list of `TJob` objects. If it's `Null`, default attributes are returned; otherwise, the user receives only the attributes specified in `attributes`. Input data: - Type: `null`. Output data: - Type: `structured`. | **Parameter** | **Type** | **Description** | | ------------ | ------- | ------------ | | `jobs` | `list` | List of jobs. | | `controller_agent_job_count` | `int` | Number of jobs in the response that the operation controller stores information about. | | `archive_job_count` | `int` | Number of jobs in the response that the archive stores information about. | | `type_counts` | `map` | Map indicating the number of jobs of various types that match all specified filters (except the filter by type). | | `state_counts` | `map` | Map indicating the number of jobs with various states that match all specified filters (except the filter by state). | | `errors` | `list` | List of errors. | | `continuation_token` | `string` | String used for pagination. It encodes the request parameters along with the index from which the job list should continue. You can pass it in the `continuation_token` parameter. | Example: ```bash PARAMETERS { "operation_id" = "4505e8eb-28fa88e2-3fe03e8-c6fcd8fa"; } OUTPUT { "jobs" = [ { "id" = "55aff293-7ef14284-3fe0384-3e07"; "type" = "map"; "state" = "failed"; "address" = "hostname.net:9012"; "start_time" = "2018-05-05T00:41:27.433832Z"; "finish_time" = "2018-05-05T00:49:04.288196Z"; "fail_context_size" = 973230u; "error" = { "code" = 1205; "message" = "User job failed"; ... }; ... }; ... { "id" = "69ae20a7-887b25ab-3fe0384-3cff"; "type" = "map"; "state" = "running"; "address" = "hostname.net:9012"; "start_time" = "2018-05-07T13:04:03.339873Z"; "progress" = 0.; "brief_statistics" = < "timestamp" = "2018-05-07T13:04:08.431740Z"; > { "processed_input_compressed_data_size" = 0; "processed_input_data_weight" = 0; "processed_output_uncompressed_data_size" = 0; "processed_output_compressed_data_size" = 0; "processed_input_uncompressed_data_size" = 0; "processed_input_row_count" = 0; }; }; ]; "cypress_job_count" = 200; "scheduler_job_count" = 208; "archive_job_count" = #; "type_counts" = { "map" = 408; }; "state_counts" = { "running" = 208; "failed" = 200; }; } ``` ### abandon_job Command properties: **Mutating**, **Light**. Semantics: - Abort the job and consider that its input data was processed successfully. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | -------------------- | | `job_id` | Yes | | Job ID. | Input data: - Type: `null` Output data: - Type: `null`. Example: ```bash PARAMETERS { "job_id" = "1225d-1f2fb8c4-f1075d39-5fb7cdff" } ``` ### abort_job Command properties: **Mutating**, **Light**. Semantics: - Abort the job, and let it later be restarted by the scheduler (as any aborted operation job). Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ----------------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `job_id` | Yes | | Job ID. | | `interrupt_timeout` | No | | A timeout for successful completion of the job after it stops receiving input data. | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "job_id" = "1225d-1f2fb8c4-f1075d39-5fb7cdff" } ``` ### dump_job_context Command properties: **Non-mutating**, **Light**. Semantics: - Get the `input context` of the job. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `job_id` | Yes | | Job ID. | | `path` | Yes | | A path where to save the set of input parameters received by the job. The path's components should exist (except for the file itself). | Input data: - Type: `null`. Output data: - Type: `null`. Example: ```bash PARAMETERS { "job_id" = "1225d-1f2fb8c4-f1075d39-5fb7cdff"; "path" = "//tmp/input_context" } ``` ### get_job_input { #get_job_input } Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the full input for the job. The command is used both with running and failed jobs with a saved specification and all the input data available. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | -------------------- | | `job_id` | Yes | | Job ID. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: Job input. Example: ```bash PARAMETERS { "job_id" = "1225d-1f2fb8c4-f1075d39-5fb7cdff"} ``` ### get_job_fail_context { #get_job_fail_context } Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the `fail context` of the job. The command is used with failed jobs. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `operation_id (operation_alias)` | Yes | | Operation ID. | | `job_id` | Yes | | Job ID. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: `Fail context` of the job. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4"; "job_id" = "1225d-1f2fb8c4-f1075d39-5fb7cdff"} ``` ### get_job_stderr { #get_job_stderr } Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the stderr of the job. The command is used with failed jobs. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `operation_id (operation_alias)` | Yes | | Operation ID. | | `job_id` | Yes | | Job ID. | | `type` | No | | Type of stderr to retrieve: `user_job_stderr` or `gpu_check_stderr`. | | `offset` | No | | Offset from the beginning in bytes. | | `limit` | No | | Maximum size in bytes. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: The stderr of the job. Example: ```bash PARAMETERS { "operation_id" = "33ab3f-bf1df917-b35fe9ed-c70a4bf4"; "job_id" = "1225d-1f2fb8c4-f1075d39-5fb7cdff"; "offset" = 500; "limit" = 100 } # Response parameters: # total_size = 1000 # end_offset = 600 # Binary output: (stderr content) ``` ### get_job_spec Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the specification of a job. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | | `omit_node_directory` | No | `true` | Whether to omit node directory information. | | `omit_input_table_specs` | No | `false` | Whether to omit input table specifications. | | `omit_output_table_specs` | No | `false` | Whether to omit output table specifications. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Job specification. ### get_job_input_paths Command properties: **Non-mutating**, **Heavy**. Semantics: - Get the input paths for a job. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of input paths. ### dump_job_proxy_log {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Dump the job proxy log for a specific job. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | | `operation_id` | Yes | | Operation ID containing the job. | | `path` | Yes | | Path where to dump the log. | Input data: - Type: `null`. Output data: - Type: `structured`. ### get_job_trace Command properties: **Non-mutating**, **Heavy**. Semantics: - Get trace information for a job. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | | `trace_id` | No | | Specific trace ID to retrieve. | | `from_time` | No | | Start time for trace filtering. | | `to_time` | No | | End time for trace filtering. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: Job trace data. ### list_job_traces Command properties: **Non-mutating**, **Light**. Semantics: - List available traces for a job. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | | `limit` | No | | Maximum number of traces to return. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of job traces. ### poll_job_shell Command properties: **Mutating**, **Light**. Semantics: - Poll for output from a job shell command. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | | `parameters` | Yes | | Poll parameters including shell descriptor. | | `shell_name` | No | | Name of the shell. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Shell output. ### run_job_shell_command Command properties: **Mutating**, **Heavy**. Semantics: - Run a shell command in a job's environment. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `job_id` | Yes | | Job ID. | | `command` | Yes | | Shell command to execute. | | `shell_name` | No | | Name of the shell. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: Command output. ## Working with queries { #queries } The Query Tracker system enables executing SQL-like queries across YTsaurus data. ### start_query {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Start a new query in the Query Tracker system. - The query will be executed asynchronously by the specified query engine. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `engine` | Yes | | Query engine to use (e.g., `yql`, `chyt`, `spyt`). | | `query` | Yes | | Query text to execute. | | `settings` | No | | Engine-specific query settings. | | `files` | No | | List of files to attach to the query. | | `stage` | No | | Query Tracker stage to use (e.g., `production`, `testing`). | | `draft` | No | `false` | If `true`, the query is created as a draft and not executed immediately. | | `annotations` | No | | User-defined annotations for the query. | | `access_control_object` | No | | Access control object name. | | `access_control_objects` | No | | List of access control object names. | | `secrets` | No | | Map of secret values for the query. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Map with `query_id` key containing the query ID. Example: ```bash PARAMETERS { "engine" = "yql"; "query" = "SELECT * FROM `//tmp/table`" } OUTPUT { "query_id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p"; } ``` ### abort_query {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Abort a running query. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `query_id` | Yes | | ID of the query to abort. | | `stage` | No | | Query Tracker stage to use (e.g., `production`, `testing`). | Input data: - Type: `null`. Output data: - Type: `structured`. Example: ```bash PARAMETERS { "query_id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p" } OUTPUT { } ``` ### get_query {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get information about a query. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `query_id` | Yes | | Query ID. | | `stage` | No | | Query Tracker stage to use (e.g., `production`, `testing`). | | `attributes` | No | | List of attributes to fetch. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Query information including state, progress, and results. Example: ```bash PARAMETERS { "query_id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p" } OUTPUT { "id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p"; "state" = "completed"; "engine" = "yql"; "query" = "SELECT * FROM `//tmp/table`"; "result_count" = 1; } ``` ### list_queries {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - List queries matching the specified filters. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `stage` | No | `production` | Query Tracker stage to use (e.g., `production`, `testing`). | | `from_time` | No | | Start time for query filtering. | | `to_time` | No | | End time for query filtering. | | `cursor_time` | No | | Cursor time for pagination. | | `cursor_direction` | No | `past` | Cursor direction: `past` or `future`. | | `user` | No | | Filter by user. | | `state` | No | | Filter by query state. | | `engine` | No | | Filter by query engine. | | `filter` | No | | Additional filter string. | | `limit` | No | `100` | Maximum number of queries to return. | | `attributes` | No | | List of attributes to fetch for each query. | | `search_by_token_prefix` | No | | Enable search by token prefix. | | `use_full_text_search` | No | | Enable full-text search. | | `tutorial_filter` | No | | Filter tutorial queries. | | `sort_order` | No | | Sort order for the results. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of queries with pagination information. Example: ```bash PARAMETERS { "user" = "root"; "limit" = 10 } OUTPUT { "queries" = [ ... ]; "incomplete" = %false; "timestamp" = 1234567890u; } ``` ### get_query_result {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get the result schema and metadata for a completed query. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `query_id` | Yes | | Query ID. | | `result_index` | No | `0` | Index of the result (queries can produce multiple results). | | `stage` | No | | Query Tracker stage to use (e.g., `production`, `testing`). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Result metadata including schema and row count. Example: ```bash PARAMETERS { "query_id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p"; "result_index" = 0 } OUTPUT { "schema" = [ ... ]; "data_statistics" = { "row_count" = 1000; }; } ``` ### read_query_result {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Heavy**. Semantics: - Read the actual data from a query result. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `query_id` | Yes | | Query ID. | | `result_index` | No | `0` | Index of the result to read. | | `stage` | No | | Query Tracker stage to use (e.g., `production`, `testing`). | | `columns` | No | | List of columns to read. | | `lower_row_index` | No | `0` | Lower row index for range reading. | | `upper_row_index` | No | | Upper row index for range reading. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Rows from the query result. Example: ```bash PARAMETERS { "query_id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p"; "lower_row_index" = 0; "upper_row_index" = 100 } ``` ### alter_query {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Modify query metadata such as annotations or access control settings. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `query_id` | Yes | | Query ID. | | `stage` | No | `production` | Query Tracker stage to use (e.g., `production`, `testing`). | | `annotations` | No | | New annotations for the query. | | `access_control_object` | No | | New access control object name. | | `access_control_objects` | No | | New list of access control object names. | Input data: - Type: `null`. Output data: - Type: `structured`. Example: ```bash PARAMETERS { "query_id" = "1a2b3c4d-5e6f7g8h-9i0j1k2l-3m4n5o6p"; "annotations" = { "project" = "analytics" } } OUTPUT { } ``` ### get_query_tracker_info {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get information about the Query Tracker system itself. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `stage` | No | `production` | Query Tracker stage to use (e.g., `production`, `testing`). | | `attributes` | No | | List of attributes to fetch. | | `settings` | No | | Query Tracker settings. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Query Tracker system information. Example: ```bash PARAMETERS { } OUTPUT { "query_tracker_stage" = "production"; "cluster_name" = "primary"; "supported_features" = { ... }; "access_control_objects" = [ ... ]; "clusters" = [ ... ]; "engines_info" = { ... }; } ``` ### get_query_declared_parameters_info {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get information about parameters declared in a query text. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `stage` | No | `production` | Query Tracker stage to use (e.g., `production`, `testing`). | | `query` | Yes | | Query text to analyze. | | `engine` | Yes | | Query engine. | | `settings` | No | | Engine-specific settings. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Information about declared parameters. Example: ```bash PARAMETERS { "engine" = "yql"; "query" = "SELECT * FROM table WHERE id = $user_id" } OUTPUT { "parameters" = [ { "name" = "user_id"; "type" = "int64" }; ]; } ``` ## Working with queues { #queues } The Queue system provides durable message queue functionality in YTsaurus. ### register_queue_consumer {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Register a consumer for a queue to track consumption progress. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `queue_path` | Yes | | Path to the queue. | | `consumer_path` | Yes | | Path to the consumer. | | `vital` | Yes | | Whether the consumer is vital for the queue. | | `partitions` | No | | List of partition indexes to register for. | Input data: - Type: `null`. Output data: - Type: `structured`. ### unregister_queue_consumer {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Unregister a previously registered queue consumer. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `queue_path` | Yes | | Path to the queue. | | `consumer_path` | Yes | | Path to the consumer. | Input data: - Type: `null`. Output data: - Type: `structured`. ### list_queue_consumer_registrations {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - List all consumer registrations for a queue. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `queue_path` | No | | Path to the queue (if omitted, list for all queues). | | `consumer_path` | No | | Path to the consumer (if omitted, list for all consumers). | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of consumer registrations. ### pull_queue {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Heavy**. Semantics: - Pull data from a queue partition starting from a specified offset. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `queue_path` | Yes | | Path to the queue. | | `partition_index` | Yes | | Partition index to pull from. | | `offset` | No | `0` | Offset to start pulling from. | | `max_row_count` | No | | Maximum number of rows to pull. | | `max_data_weight` | No | | Maximum data weight to pull. | | `data_weight_per_row_hint` | No | | Hint for average data weight per row. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Rows from the queue. ### pull_queue_consumer {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Heavy**. Semantics: - Pull data for a registered consumer from a queue partition. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `consumer_path` | Yes | | Path to the consumer. | | `queue_path` | Yes | | Path to the queue. | | `partition_index` | Yes | | Partition index to pull from. | | `offset` | Yes | | Offset to start pulling from. | | `max_row_count` | No | | Maximum number of rows to pull. | | `max_data_weight` | No | | Maximum data weight to pull. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Rows from the queue. ### advance_queue_consumer {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Advance a consumer's read position in a queue partition. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `consumer_path` | Yes | | Path to the consumer. | | `queue_path` | Yes | | Path to the queue. | | `partition_index` | Yes | | Partition index. | | `new_offset` | Yes | | New offset for the consumer. | | `old_offset` | No | | Expected old offset (for optimistic locking). | Input data: - Type: `null`. Output data: - Type: `structured`. ### create_queue_producer_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Create a producer session for writing to a queue. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `producer_path` | Yes | | Path to the producer. | | `queue_path` | Yes | | Path to the queue. | | `session_id` | Yes | | Unique session identifier. | | `user_meta` | No | | User-defined metadata for the session. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Session information including epoch and sequence number. ### remove_queue_producer_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Remove a producer session. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `producer_path` | Yes | | Path to the producer. | | `queue_path` | Yes | | Path to the queue. | | `session_id` | Yes | | Session identifier to remove. | Input data: - Type: `null`. Output data: - Type: `structured`. ### push_queue_producer {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Push rows to a queue through a producer session. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `producer_path` | Yes | | Path to the producer. | | `queue_path` | Yes | | Path to the queue. | | `session_id` | Yes | | Producer session identifier. | | `epoch` | Yes | | Session epoch number. | | `sequence_number` | No | | Starting sequence number for the rows. | Input data: - Type: `tabular`. - Value: Rows to push to the queue. Output data: - Type: `structured`. - Value: Last sequence number and skipped row count. ## Working with pipelines and flows { #flows } The Flow system provides pipeline management and execution capabilities. ### get_pipeline_spec {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get the specification of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `spec_path` | No | | Path within the spec to retrieve. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Pipeline specification and version. ### set_pipeline_spec {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Set or update the specification of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `spec_path` | No | | Path within the spec to set. | | `force` | No | `false` | Force update even if validation fails. | | `expected_version` | No | | Expected current version (for optimistic locking). | Input data: - Type: `structured`. - Value: Pipeline specification. Output data: - Type: `structured`. - Value: New version. ### remove_pipeline_spec {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Remove a pipeline specification or a part of it. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `spec_path` | Yes | | Path within the spec to remove. | | `force` | No | `false` | Force removal even if validation fails. | | `expected_version` | No | | Expected current version. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: New version. ### get_pipeline_dynamic_spec {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get the dynamic specification of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `spec_path` | No | | Path within the dynamic spec to retrieve. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Dynamic pipeline specification and version. ### set_pipeline_dynamic_spec {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Set or update the dynamic specification of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `spec_path` | No | | Path within the dynamic spec to set. | | `expected_version` | No | | Expected current version. | Input data: - Type: `structured`. - Value: Dynamic pipeline specification. Output data: - Type: `structured`. - Value: New version. ### remove_pipeline_dynamic_spec {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Remove a pipeline dynamic specification or a part of it. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `spec_path` | Yes | | Path within the dynamic spec to remove. | | `expected_version` | No | | Expected current version. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: New version. ### start_pipeline {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Start execution of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | Input data: - Type: `null`. Output data: - Type: `structured`. ### stop_pipeline {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Stop execution of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | Input data: - Type: `null`. Output data: - Type: `structured`. ### pause_pipeline {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Pause execution of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | Input data: - Type: `null`. Output data: - Type: `structured`. ### get_pipeline_state {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get the current state of a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Pipeline state. ### get_flow_view {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Get a view of the flow for a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | | `view_path` | No | | Path within the flow view. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Flow view data. ### flow_execute {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Heavy**. Semantics: - Execute a flow command on a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | Input data: - Type: `structured`. - Value: Flow command to execute. Output data: - Type: `structured`. - Value: Execution result. ### flow_execute_plaintext {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Heavy**. Semantics: - Execute a plaintext flow command on a pipeline. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `pipeline_path` | Yes | | Path to the pipeline. | Input data: - Type: `null`. Output data: - Type: `binary`. - Value: Plaintext execution result. ## Authentication { #authentication } ### set_user_password Command properties: **Mutating**, **Light**. Semantics: - Set or update a user's password. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `user` | Yes | | Username. | | `current_password_sha256` | No | | SHA256 hash of the current password (required for password updates). | | `new_password_sha256` | Yes | | SHA256 hash of the new password. | | `password_is_temporary` | No | `false` | Whether the password is temporary and must be changed on first use. | Input data: - Type: `null`. Output data: - Type: `structured`. ### issue_token Command properties: **Mutating**, **Light**. Semantics: - Issue an authentication token for a user. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `user` | Yes | | Username. | | `password_sha256` | No | | SHA256 hash of the user's password. | | `description` | No | | Description for the token. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: The issued token. ### revoke_token Command properties: **Mutating**, **Light**. Semantics: - Revoke an authentication token. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `user` | Yes | | Username. | | `password_sha256` | No | | SHA256 hash of the user's password. | | `token_sha256` | Yes | | SHA256 hash of the token to revoke. | Input data: - Type: `null`. Output data: - Type: `structured`. ### list_user_tokens Command properties: **Non-mutating**, **Light**. Semantics: - List authentication tokens for a user. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `user` | Yes | | Username. | | `password_sha256` | No | | SHA256 hash of the user's password. | | `with_metadata` | No | `false` | Whether to include token metadata. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of tokens. ## Journal operations { #journals } ### read_journal Command properties: **Non-mutating**, **Heavy**. Semantics: - Read rows from a journal. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the journal. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Rows from the journal. ### write_journal Command properties: **Mutating**, **Heavy**. Semantics: - Write rows to a journal. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the journal. | | `enable_chunk_preallocation` | No | `false` | Whether to enable chunk preallocation for better performance. | Input data: - Type: `tabular`. - Value: Rows to write to the journal. Output data: - Type: `structured`. ### truncate_journal {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Truncate a journal to a specified row count. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the journal. | | `row_count` | Yes | | Number of rows to keep from the beginning. | Input data: - Type: `null`. Output data: - Type: `structured`. ## Chaos and Replication { #chaos } ### alter_replication_card {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Modify a replication card's configuration. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `replication_card_id` | Yes | | Replication card ID. | | `replicated_table_options` | No | | New options for replicated tables. | | `enable_replicated_table_tracker` | No | | Whether to enable the replicated table tracker. | Input data: - Type: `null`. Output data: - Type: `structured`. ### update_replication_progress {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Update replication progress for a chaos table replica. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `replica_id` | Yes | | Replica ID. | | `progress` | Yes | | New replication progress. | | `force` | Yes | | Whether to force the update. | Input data: - Type: `null`. Output data: - Type: `structured`. ### ping_chaos_lease {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Ping a chaos lease to extend its lifetime. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `chaos_lease_id` | Yes | | Chaos lease ID. | | `ping_ancestors` | No | `true` | Whether to ping ancestor leases. | Input data: - Type: `null`. Output data: - Type: `structured`. ## Shuffle operations { #shuffle } ### start_shuffle {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Start a shuffle operation for distributed data exchange. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `account` | Yes | | Account to use for the shuffle. | | `partition_count` | Yes | | Number of partitions. | | `parent_transaction_id` | Yes | | Parent transaction ID. | | `medium` | No | | Storage medium to use. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Signed shuffle handle. ### read_shuffle_data {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Heavy**. Semantics: - Read data from a shuffle partition. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `signed_shuffle_handle` | Yes | | Signed handle from start_shuffle. | | `partition_index` | Yes | | Partition index to read from. | Input data: - Type: `null`. Output data: - Type: `tabular`. - Value: Rows from the shuffle partition. ### write_shuffle_data {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Heavy**. Semantics: - Write data to a shuffle operation. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `signed_shuffle_handle` | Yes | | Signed handle from start_shuffle. | | `partition_column` | Yes | | Column name to use for partitioning. | Input data: - Type: `tabular`. - Value: Rows to write to the shuffle. Output data: - Type: `structured`. ## Distributed write sessions { #distributed_write } ### start_distributed_write_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Start a distributed write session for parallel writing to a table. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the target table. | | `cookie_count` | No | | Number of write cookies to generate. | | `session_timeout` | No | | Session timeout duration. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Session information and write cookies. ### ping_distributed_write_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Ping a distributed write session to keep it alive. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `session` | Yes | | Session identifier. | Input data: - Type: `null`. Output data: - Type: `null`. ### finish_distributed_write_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Finish a distributed write session and commit all writes. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `session` | Yes | | Session identifier. | | `results` | Yes | | Write results from all participants. | Input data: - Type: `null`. Output data: - Type: `null`. ### write_table_fragment {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Heavy**. Semantics: - Write a fragment of data to a table as part of a distributed write session. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `cookie` | Yes | | Write cookie from start_distributed_write_session. | Input data: - Type: `tabular`. - Value: Rows to write. Output data: - Type: `structured`. - Value: Signed write result. ### start_distributed_write_file_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Start a distributed write session for parallel writing to a file. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the target file. | | `cookie_count` | No | | Number of write cookies to generate. | | `session_timeout` | No | | Session timeout duration. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Session information and write cookies. ### ping_distributed_write_file_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Ping a distributed file write session to keep it alive. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `session` | Yes | | Session identifier. | Input data: - Type: `null`. Output data: - Type: `null`. ### finish_distributed_write_file_session {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Finish a distributed file write session and commit all writes. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `session` | Yes | | Session identifier. | | `results` | Yes | | Write results from all participants. | Input data: - Type: `null`. Output data: - Type: `null`. ### write_file_fragment {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Heavy**. Semantics: - Write a fragment of binary data to a file as part of a distributed write session. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `cookie` | Yes | | Write cookie from start_distributed_write_file_session. | Input data: - Type: `binary`. - Value: Binary data to write. Output data: - Type: `structured`. - Value: Signed write result. ## Other ### parse_ypath Command properties: **Non-mutating**, **Light**. Semantics: - Perform parsing of the passed [YPath](https://ytsaurus.tech/docs/user-guide/storage/ypath.md) by putting all IDs of the complex YPath into attributes. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------ | | `path` | Yes | | The path that needs to be parsed. | Input data: - Type: `null`. Output data: - Type: `structured`. Example: ```bash PARAMETERS { "path" = "//tmp/table[#1:#2]" } OUTPUT { "path" = "//tmp/table" } ``` ### execute_batch { #execute_batch } Command properties: **Mutating if the set includes mutating commands**, **Light**. Semantics: - Use a single query to execute the set of commands passed in the parameters. - The command can (and will be) executed in parallel. It means that if a set includes both writing to and reading from the node, the reading result can either be the older value or the updated one. - The set can only include light commands. - The set can only include commands with the input type of `null` or `structured`. - The set can only include commands with the output type of `null` or `structured`. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ------------------------------------------------------------ | | `requests` | Yes | | Description of executed queries. | | `concurrency` | No | `50` | A numeric parameter that sets the maximum number of commands running on the cluster in parallel. Use this parameter to avoid exhausting your request rate limit. | Input data: - Type: `structured`. - The queries executed are listed in the `requests` parameter, which is a list. - Each element in this list is a dictionary including the following fields: | **Parameter** | **Required** | **Default value** | **Description** | | ---------- | ------------- | ------------------------- | ------------------------------------------------------------ | | `command` | Yes | | Command name. | | `parameters` | Yes | | Dictionary with the command parameters. | | `input` | No | | Input for the query (for the commands with the `structured` input type, for example, `set`). | Output data: - Type: `structured`. - The output produces a list of the same length as at the input. - Each list item describes the result of a single query execution. This is a dictionary of the following format: | **Parameter** | **Required** | **Description** | | -------- | ------------- | ------------------------------------------------------------ | | `error` | No | Error that arose during the query execution (if any). | | `output` | No | Output for the query (for the successful commands with the `structured` input type, for example, `get`). | Example: ```bash PARAMETERS { "requests" = [ { "command" = "set"; "parameters" = {"path" = "//tmp/a"}; "input" = "value_a"; }; { "command" = "get"; "parameters" = {"path" = "//tmp/b"}; }; { "command" = "get"; "parameters" = {"path" = "//nonexisting"}; }; ]; } OUTPUT [ { }; { output = "value_b"; }; { error = {...} }; ] ``` ### get_supported_features Command properties: **Non-mutating**, **Light**. Semantics: - Returns a dictionary with the `features` field describing elementary data types, compression codecs, erasure codecs, and other features supported by the cluster. Parameters: - No Input data: - Type: `null`. Output data: - Type: `structured`. The `features` key in the response includes a dictionary with the following fields. | **Key** | **Value type** | **Description** | | ------------------ | ---------------- | ------------------------------------------------------------ | | primitive\_types | list of rows | [Primitive types](https://ytsaurus.tech/docs/user-guide/storage/data-types.md#schema_primitive). | | erasure\_codecs | list of rows | [Erasure codecs](https://ytsaurus.tech/docs/user-guide/storage/replication.md). | | compression\_codecs | list of rows | [Compression codecs](https://ytsaurus.tech/docs/user-guide/storage/compression.md). | Example: ```bash PARAMETERS { } OUTPUT { "features" = { "primitive_types" = ["int8"; "int16"; ... ]; "erasure_codecs" = ["none", "isa_lrc_12_2_2"; "isa_reed_solomon_6_3"; ... ]; "compression_codecs" = ["none"; "snappy"; "brotli_1"; ... ]; }; } ``` ### generate_timestamp Command properties: **Non-mutating**, **Light**. Semantics: - Generates a monotonous timestamp. Parameters: - No Input data: - Type: `null`. Output data: - Type: `structured`. The `timestamp` key in the response stores a `uint64` value. Example: ```bash PARAMETERS { } OUTPUT { "timestamp" = 1723665447133469427u; } ``` ### get_version Command properties: **Non-mutating**, **Light**. Semantics: - Get version information about the YTsaurus cluster. Parameters: - No Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Version information including build version and features. ### get_current_user Command properties: **Non-mutating**, **Light**. Semantics: - Get the current authenticated user. Parameters: - No Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Current user name. ### discover_proxies {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Non-mutating**, **Light**. Semantics: - Discover available proxy addresses in the cluster. Parameters: - No Input data: - Type: `null`. Output data: - Type: `structured`. - Value: List of proxy addresses. ### get_bundle_config Command properties: **Non-mutating**, **Light**. Semantics: - Get configuration for a tablet cell bundle. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `bundle_name` | Yes | | Name of the bundle. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Bundle configuration. ### set_bundle_config Command properties: **Mutating**, **Light**. Semantics: - Set configuration for a tablet cell bundle. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `bundle_name` | Yes | | Name of the bundle. | | `bundle_config` | No | | New bundle configuration. | Input data: - Type: `structured`. - Value: Bundle configuration. Output data: - Type: `null`. ### externalize {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Externalize a Cypress node to another cell. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the node. | | `cell_tag` | Yes | | Target cell tag. | Input data: - Type: `null`. Output data: - Type: `null`. ### internalize {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Internalize an externalized Cypress node. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `path` | Yes | | Path to the node. | Input data: - Type: `null`. Output data: - Type: `null`. ### check_permission_by_acl Command properties: **Non-mutating**, **Light**. Semantics: - Check if a user has specific permission according to an ACL. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `user` | Yes | | Username to check. | | `permission` | Yes | | Permission to check (e.g., `read`, `write`). | | `acl` | Yes | | Access control list to check against. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Permission check result. ### transfer_account_resources {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Transfer resource quota between accounts. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `source_account` | Yes | | Source account name. | | `destination_account` | Yes | | Destination account name. | | `resource_delta` | Yes | | Resources to transfer. | Input data: - Type: `null`. Output data: - Type: `structured`. ### transfer_pool_resources {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Transfer resource quota between scheduler pools. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `source_pool` | Yes | | Source pool name. | | `destination_pool` | Yes | | Destination pool name. | | `pool_tree` | Yes | | Pool tree name. | | `resource_delta` | Yes | | Resources to transfer. | Input data: - Type: `null`. Output data: - Type: `structured`. ### transfer_bundle_resources {% note info %} This command is only available from API version 4 onward. {% endnote %} Command properties: **Mutating**, **Light**. Semantics: - Transfer resource quota between tablet cell bundles. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `source_bundle` | Yes | | Source bundle name. | | `destination_bundle` | Yes | | Destination bundle name. | | `resource_delta` | Yes | | Resources to transfer. | Input data: - Type: `null`. Output data: - Type: `structured`. ### join_reduce Command properties: **Mutating**, **Light**. Semantics: - Start a join-reduce operation to join multiple tables and reduce the result. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `spec` | Yes | | Operation specification. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Operation ID. ### remote_copy Command properties: **Mutating**, **Light**. Semantics: - Copy data from another YTsaurus cluster. Parameters: | **Parameter** | **Required** | **Default value** | **Description** | | ------------ | ------------- | ------------------------- | ----------------------- | | `spec` | Yes | | Operation specification including source cluster and paths. | Input data: - Type: `null`. Output data: - Type: `structured`. - Value: Operation ID. # Query parameters This section contains information about additional query parameters. For more information about commands, see the [Commands](https://ytsaurus.tech/docs/en/commands.md) section. ## Prerequisites { #conditions } Here is a list of the commands for which you can specify success conditions: - `set`; - `remove`; - `create`; - `lock`; - `copy`; - `move`; - `link`; - `create`. There are two types of conditions: - `prerequisite_revisions`: A set of hints including `path`, `transaction_id`, `revision`. The query will be executed only if all the paths provided in the hint exist (in the context of the given transactions) and the revisions are as hinted. - `prerequisite_transactions` includes a list of [transactions](https://ytsaurus.tech/docs/user-guide/storage/transactions.md) that must exist when the query is executed. Example: ```python revision = client.get("//tmp/my_table/@revision") #... some calculations ... revision_parameter = yt.create_revision_parameter(path="//tmp/my_table", revision=revision) revision_client = yt.create_client_with_command_params(prerequisite_revisions=[revision_parameter]) revision_client.move("//tmp/transformed_table", "//tmp/my_table") ``` ## Data source for reading { #data-source } In [non-mutating](https://ytsaurus.tech/docs/en/commands.md#concepts) queries to [Cypress](https://ytsaurus.tech/docs/user-guide/storage/cypress.md), you can specify a data source for reading, using the `read_from` parameter. A popular value for the `read_from` parameter is `cache`. In this case, the response is read from the cache (usually hosted on dedicated instances in the cluster). This allows decreasing the load on the master server. This is important when running numerous heavy queries against Cypress: for example, if you need to traverse a large subtree. Example: ```python for obj in client.search("//tmp/my_table", read_from="cache"): if "abc" in str(obj): ... ``` # Error codes This section presents a collection of error codes that you may encounter when working with YTsaurus. The error code is a non-negative integer: - `0` means a successful execution `NYT::EErrorCode::OK` - `1` means generic errors `NYT::EErrorCode::Generic` Other values are split between subsystems. You can view registered error codes by [searching](https://github.com/search?q=repo%3Aytsaurus%2Fytsaurus+path%3A%2F%5Eyt%5C%2F%2F+YT_DEFINE_ERROR_ENUM&type=code) for the `YT_DEINE_ERROR_ENUM` construct in the code base. To exactly construe certain errors, you need quite a deep knowledge of the system architecture. The list of errors is continuously expanded (mostly in a backward-compatible way). # What is YTsaurus Flow? YTsaurus Flow YTsaurus Flow is a framework for streaming cross-DC event processing with exactly-once guarantees within the YTsaurus ecosystem. It offers APIs for [C++](https://ytsaurus.tech/docs/en/cpp/getting-started.md), [Java and Kotlin](https://ytsaurus.tech/docs/en/java/getting-started.md), [Python](https://ytsaurus.tech/docs/en/python/getting-started.md), and [Go](https://ytsaurus.tech/docs/en/go/getting-started.md), and supports declarative pipeline descriptions in [YQL](https://ytsaurus.tech/docs/en/yql/getting-started.md). Its closest external counterparts are [Google Cloud Dataflow](https://cloud.google.com/products/dataflow?skip_cache=true%22%22) and [Apache Flink](https://flink.apache.org/). The system is under active development, but more than ten teams have already built their production processes on it. The system can reliably: - Handle loads exceeding 100 GB/s or 1 million events per second. - Support 150+ logical pipeline nodes. ## Contacts {#contact} For any issues with YTsaurus Flow, open a [GitHub Issue](https://github.com/ytsaurus/ytsaurus/issues). ## System properties {#properties} - Native support for multi-stage [pipelines](https://ytsaurus.tech/docs/en/concepts/glossary.md#pipeline). As a result, you get simpler deployment and system management. - Support for [watermarks](https://ytsaurus.tech/docs/en/concepts/watermarks.md) and [timers](https://ytsaurus.tech/docs/en/concepts/timers.md). - [Exactly-once semantics](https://ytsaurus.tech/docs/en/concepts/guarantees.md) for event processing by default. - Typical event processing latency under stable operation: 1s–10s. - Automatic balancing of [partitions](https://ytsaurus.tech/docs/en/concepts/glossary.md#partition) across machines. - Fault tolerance: the pipeline survives the failure of individual machines and data centers. - Ability to implement business logic in [C++](https://ytsaurus.tech/docs/en/cpp/getting-started.md), [Java and Kotlin](https://ytsaurus.tech/docs/en/java/getting-started.md), [Python](https://ytsaurus.tech/docs/en/python/getting-started.md), [Go](https://ytsaurus.tech/docs/en/go/getting-started.md), and [YQL](https://ytsaurus.tech/docs/en/yql/getting-started.md). - Support for [stateful processing](https://ytsaurus.tech/docs/en/concepts/stateful.md) with persistent state in YTsaurus dynamic tables. - Support for running in YTsaurus. ## Choose a language {#choose-language} Flow supports several languages for implementing business logic: - **[C++](https://ytsaurus.tech/docs/en/cpp/getting-started.md)** — native implementation, maximum performance, full control. Use this for high-load pipelines. - **[Java and Kotlin](https://ytsaurus.tech/docs/en/java/getting-started.md)** — run via the [companion](https://ytsaurus.tech/docs/en/concepts/companion.md) mechanism. They support Spring Boot. These are suitable for teams with a JVM stack. - **[Python](https://ytsaurus.tech/docs/en/python/getting-started.md)** — runs via the [companion](https://ytsaurus.tech/docs/en/concepts/companion.md) mechanism. This is the easiest way to prototype a pipeline or process a small data stream. - **[Go](https://ytsaurus.tech/docs/en/go/getting-started.md)** — runs via the [companion](https://ytsaurus.tech/docs/en/concepts/companion.md) mechanism. A single binary runs the pipeline and acts as a companion in the job. Suitable for teams with a Go stack. - **[YQL](https://ytsaurus.tech/docs/en/yql/getting-started.md)** — declarative pipeline description as an SQL query. It has a low entry barrier and doesn’t require writing code in C++, Java, Kotlin, Go, or Python. It’s under active development, and not all planned features are available yet. ## Target system properties {#target-properties} - Smart planning of the entire pipeline, taking into account CPU/RAM consumption of individual pipeline nodes and shared resources (common caches, databases, etc.) used by multiple nodes. - Ability to run pipelines on clusters with thousands of nodes or more. - Minimal downtime when nodes, DCs, or clusters fail, as well as during updates. ## See also {#see-also} - [Getting started](https://ytsaurus.tech/docs/en/start.md) - [Quick start](https://ytsaurus.tech/docs/en/quickstart.md) - [Basic concepts](https://ytsaurus.tech/docs/en/concepts/glossary.md) - [Task examples](https://ytsaurus.tech/docs/en/tasks.md) - [Flow releases](https://github.com/ytsaurus/ytsaurus/releases?q=tag%3Aflow%2F0&expanded=true) # Getting started with YTsaurus Flow This section walks you through the steps to implement and run your own pipeline in Flow. ## Choose a language {#choose-language} Flow supports several languages for implementing business logic: - **[C++](https://ytsaurus.tech/docs/en/cpp/getting-started.md)** — native implementation, maximum performance, full control. Use this for high-load pipelines. - **[Java and Kotlin](https://ytsaurus.tech/docs/en/java/getting-started.md)** — run via the [companion](https://ytsaurus.tech/docs/en/concepts/companion.md) mechanism. They support Spring Boot. These are suitable for teams with a JVM stack. - **[Python](https://ytsaurus.tech/docs/en/python/getting-started.md)** — runs via the [companion](https://ytsaurus.tech/docs/en/concepts/companion.md) mechanism. This is the easiest way to prototype a pipeline or process a small data stream. - **[Go](https://ytsaurus.tech/docs/en/go/getting-started.md)** — runs via the [companion](https://ytsaurus.tech/docs/en/concepts/companion.md) mechanism. A single binary runs the pipeline and acts as a companion in the job. Suitable for teams with a Go stack. - **[YQL](https://ytsaurus.tech/docs/en/yql/getting-started.md)** — declarative pipeline description as an SQL query. It has a low entry barrier and doesn’t require writing code in C++, Java, Kotlin, Go, or Python. It’s under active development, and not all planned features are available yet. ## General plan No matter which language you choose, creating a pipeline involves the following steps: 1. **Try the [Quick start](https://ytsaurus.tech/docs/en/quickstart.md)** — run the C++ Word Count pipeline to get familiar with the Flow infrastructure. 2. **Review the basic concepts**. Read the [glossary](https://ytsaurus.tech/docs/en/concepts/glossary.md) to understand the Flow model: pipelines, streams, computations, and messages. 3. **Study the concepts**. Get to know [Computation](https://ytsaurus.tech/docs/en/concepts/computation.md), [Watermarks and Timers](https://ytsaurus.tech/docs/en/concepts/watermarks.md), and [Stateful processing](https://ytsaurus.tech/docs/en/concepts/stateful.md), as well as the [guarantees](https://ytsaurus.tech/docs/en/concepts/guarantees.md) provided by the system. 4. **Explore examples** in your chosen language: - C++: [WordCount](https://ytsaurus.tech/docs/en/cpp/examples/word_count.md), [Shuffle](https://ytsaurus.tech/docs/en/cpp/examples/shuffle.md), [WaitClickJoin](https://ytsaurus.tech/docs/en/cpp/examples/wait_click_join.md) - Java: [WordCount](https://ytsaurus.tech/docs/en/java/examples/wordcount.md), [Shuffle](https://ytsaurus.tech/docs/en/java/examples/shuffle.md), [WaitClickJoin](https://ytsaurus.tech/docs/en/java/examples/wait_click_join.md) - Python: [WordCount](https://ytsaurus.tech/docs/en/python/examples/wordcount.md), [Shuffle](https://ytsaurus.tech/docs/en/python/examples/shuffle.md), [WaitClickJoin](https://ytsaurus.tech/docs/en/python/examples/wait_click_join.md) - Go: [WordCount](https://ytsaurus.tech/docs/en/go/examples/wordcount.md), [Shuffle](https://ytsaurus.tech/docs/en/go/examples/shuffle.md), [WaitClickJoin](https://ytsaurus.tech/docs/en/go/examples/wait_click_join.md) - YQL: [Quick start](https://ytsaurus.tech/docs/en/yql/getting-started.md) 5. **Check out the available [connectors](https://ytsaurus.tech/docs/en/connectors/about.md)** — queues, static tables, and others. 6. **Describe the pipeline spec** in YSON format. In addition to the examples, the [Spec & DynamicSpec](https://ytsaurus.tech/docs/en/concepts/spec.md) section will help you. 7. **Implement your business logic** in the language you’ve chosen, following the relevant quick start guide. 8. **Create the necessary objects in YTsaurus** — tables, queues, and the pipeline. 9. **Write tests**. Follow the instructions for your programming language: - [C++](https://ytsaurus.tech/docs/en/cpp/testing.md) - [Java](https://ytsaurus.tech/docs/en/java/testing.md) - [Python](https://ytsaurus.tech/docs/en/python/testing.md) - [Go](https://ytsaurus.tech/docs/en/go/testing.md) 10. **Run the pipeline** and inspect its state with `yt flow get-pipeline-state `. For release rules, read [Release and table change rules](https://ytsaurus.tech/docs/en/devops/vanilla/releases.md#release-and-configure-basic-rules). ## See also - [About Flow](https://ytsaurus.tech/docs/en/about.md) - [Quick start](https://ytsaurus.tech/docs/en/quickstart.md) - [Basic concepts](https://ytsaurus.tech/docs/en/concepts/glossary.md) - [Connectors](https://ytsaurus.tech/docs/en/connectors/about.md) # Quick start with YTsaurus Flow Run the C++ Word Count example as a first pipeline. It reads text rows from a queue, splits them into words, and stores per-word counts in a dynamic table. You need access to a YTsaurus cluster and a pool in which you can start a [Vanilla operation](https://ytsaurus.tech/docs/en/devops/vanilla/initial-deploy.md#prerequisites). ## Build the example {#build} From the [source repository](https://github.com/ytsaurus/ytsaurus/tree/main), build the example binary: ```bash ya make yt/yt/flow/examples/cpp/word_count ``` The binary is `yt/yt/flow/examples/cpp/word_count/word_count`. In the `reader` computation's `processing_function_parameters`, the example sets `min_word_length = 4`, so it ignores words shorter than four bytes; set it to `0` to count every word. The [C++ walkthrough](https://ytsaurus.tech/docs/en/cpp/getting-started.md#define-messages) explains its source, computations, and state. ## Prepare the pipeline {#prepare} Choose an existing writable Cypress directory. The commands below use `//home/flow/word-count` as a sample base path: replace it everywhere with a directory you own, and replace `` with your cluster proxy name. From the [Word Count example's schema](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/word_count/test/yt_sync.py), create the input queue, its consumer, and the sorted external state table: ```bash yt --proxy create table //home/flow/word-count/input_queue --attributes '{dynamic=%true;schema=[{name=text;type=string};{name="$timestamp";type=uint64};{name="$cumulative_data_weight";type=int64}]}' yt --proxy create queue_consumer //home/flow/word-count/consumer yt --proxy register-queue-consumer //home/flow/word-count/input_queue //home/flow/word-count/consumer --vital yt --proxy create table //home/flow/word-count/word_counts --attributes '{dynamic=%true;schema=[{name=hash;type=uint64;sort_order=ascending;expression="farm_hash(word)"};{name=word;type=string;sort_order=ascending};{name=count;type=int64}]}' yt --proxy mount-table //home/flow/word-count/input_queue yt --proxy mount-table //home/flow/word-count/word_counts ``` The `hash` and `word` key columns match the counter's `group_by_schema`; `count` stores the value. The queue has the `text` input column and the two system columns used by the [Queue API](https://ytsaurus.tech/docs/user-guide/dynamic-tables/queues.md#api). The `create queue_consumer` command mounts the consumer's table automatically. The vital registration prevents automatic queue trimming from deleting rows before this consumer reads them. Before continuing, check that all three objects exist: ```bash yt --proxy exists //home/flow/word-count/input_queue yt --proxy exists //home/flow/word-count/consumer yt --proxy exists //home/flow/word-count/word_counts ``` Each command should print `true`. This C++ runner creates the pipeline object and its internal tables on first launch, then mounts those tables. For a separate preparation step, see [creating a pipeline object](https://ytsaurus.tech/docs/en/concepts/pipeline-object.md#create). Open `yt/yt/flow/examples/cpp/word_count/pipeline.yson`. Add `cluster_url` and the pipeline `path` for your cluster. Replace `queue_path` and `consumer_path` with `//home/flow/word-count/input_queue` and `//home/flow/word-count/consumer`, substituting your actual cluster name for `cluster_name`. Replace the external state manager's `//path/to/word_counts` with the state-table path above. Use the same base path you chose for every object. Add a `vanilla` block at the top level of the config with `enable = %true`, your pool name, and a worker count. The [initial deployment guide](https://ytsaurus.tech/docs/en/devops/vanilla/initial-deploy.md#enable) gives the block and its resource settings. ```yson "cluster_url" = ""; "path" = "//home/flow/word-count/pipeline"; "vanilla" = { "enable" = %true; "pool" = ""; "worker" = {"count" = 1}; }; ``` Put these fields inside the existing outer braces of `pipeline.yson` and replace the placeholders with your cluster and pool. Replace the sample pipeline path if you chose another base directory. If your cluster runs jobs in Docker/CRI on Kubernetes, check [cluster-name resolution](https://ytsaurus.tech/docs/en/devops/docker-environment.md#cluster-name) from inside the jobs and [external proxy access](https://ytsaurus.tech/docs/en/devops/docker-environment.md#external-access) from the runner before launching. Add these network settings only when your installation requires them. ## Validate and launch {#launch} For this C++ runner, `--validate-only` checks the config locally without starting or updating a pipeline: ```bash ./yt/yt/flow/examples/cpp/word_count/word_count --config yt/yt/flow/examples/cpp/word_count/pipeline.yson --validate-only ``` After validation, launch the pipeline: ```bash YT_FLOW_WAIT=0 ./yt/yt/flow/examples/cpp/word_count/word_count --config yt/yt/flow/examples/cpp/word_count/pipeline.yson ``` `YT_FLOW_WAIT=0` returns after launch. Without it, the default `YT_FLOW_WAIT=1` keeps the runner in the foreground streaming controller log records while this ongoing pipeline runs. Interrupting the runner does not stop the Vanilla operation. Check that the pipeline was created and is working: ```bash yt --proxy exists //home/flow/word-count/pipeline yt --proxy flow get-pipeline-state //home/flow/word-count/pipeline ``` The first command should print `true`, and the second `working`. Send one input row and inspect the counts after the next processing epoch: ```bash echo '{text="hello world hello"}' | yt --proxy insert-rows //home/flow/word-count/input_queue --format yson yt --proxy select-rows '* from [//home/flow/word-count/word_counts]' --format json ``` Look for `hello` with `count` 2 and `world` with `count` 1. If the table is still empty, wait for another processing epoch and repeat the query; inspect [diagnostics](https://ytsaurus.tech/docs/en/devops/diagnostics.md) if the jobs fail or make no progress. You can also inspect running jobs in the YTsaurus UI. When finished, follow [pipeline removal](https://ytsaurus.tech/docs/en/devops/vanilla/pipeline-operations.md#remove) for the Flow node and decide separately whether to remove the example's queue, consumer, and external state table. For another language, follow its guide: [Go](https://ytsaurus.tech/docs/en/go/getting-started.md), [Java](https://ytsaurus.tech/docs/en/java/getting-started.md), [Python](https://ytsaurus.tech/docs/en/python/getting-started.md), or [YQL](https://ytsaurus.tech/docs/en/yql/getting-started.md). # YTsaurus Flow concepts Before designing a pipeline, learn about the [Pipeline object](https://ytsaurus.tech/docs/en/pipeline-object.md), [computations](https://ytsaurus.tech/docs/en/computation.md), [state](https://ytsaurus.tech/docs/en/stateful.md), and [processing guarantees](https://ytsaurus.tech/docs/en/guarantees.md). Refer to the [glossary](https://ytsaurus.tech/docs/en/glossary.md) for terminology. # Glossary of YTsaurus Flow ## Introduction {#introduction} This article collects all the key concepts of YTsaurus Flow: from the basic data processing model to the architecture and mechanisms for ensuring correctness. The material is structured from simple to complex: first, stream processing is covered, followed by parallelism, external connections, system design, and pipeline management. When you’re getting familiar with the system, read the article from start to finish, following the links for details. Later, this page will serve as a reference: other documentation sections often link here to remind you of term definitions. ## Basic model {#basic-model} To understand Flow as a stream processing system, you can use an analogy with a factory assembly line: - [Pipeline](https://ytsaurus.tech/docs/en/#pipeline) — the entire factory that solves a specific business task. - [Stream](https://ytsaurus.tech/docs/en/#stream) — the conveyor belt that carries data. - [Message](https://ytsaurus.tech/docs/en/#message) — an individual item on the belt. - [Computation](https://ytsaurus.tech/docs/en/#computation) — a workstation that takes items from one or several belts, processes them, and places the results on other belts. ### Pipeline {#pipeline} This is a specific business task that is being executed. It can contain many different data grouping and processing nodes. From the YTsaurus perspective, a pipeline is represented as a Cypress object of type `pipeline`. For details about the object structure, creation methods, and management, see the [Pipeline Object](https://ytsaurus.tech/docs/en/pipeline-object.md) section. ### Stream {#stream} {#stream-and-computation} A stream is a schematized, named data flow that connects [computations](https://ytsaurus.tech/docs/en/#computation) in the pipeline graph. Each stream has a fixed schema (a set of typed fields) and is identified by `stream_id`. A stream consists of [messages](https://ytsaurus.tech/docs/en/#message). Each stream has exactly one writer — a [computation](https://ytsaurus.tech/docs/en/#computation) that outputs data to it — and an arbitrary number of readers. The order of messages in a stream is not guaranteed in general. However, for derived messages with matching [keys](https://ytsaurus.tech/docs/en/#key) along the entire [lineage](https://ytsaurus.tech/docs/en/#lineage) chain, there are guarantees of relative order. For more details, see the [Message Processing Order](https://ytsaurus.tech/docs/en/ordering.md) section. ### Computation {#computation} A computation is a node in the pipeline graph that performs a specific data transformation. It reads [messages](https://ytsaurus.tech/docs/en/#message) from input [streams](https://ytsaurus.tech/docs/en/#stream), processes them, and writes the results to output [streams](https://ytsaurus.tech/docs/en/#stream). It can have multiple inputs and multiple outputs. The input stream is split into [partitions](https://ytsaurus.tech/docs/en/#partition) for parallel processing. For more details, see the [Computation](https://ytsaurus.tech/docs/en/computation.md) section. ### Passthrough Computation {#passthrough} This is a type of [computation](https://ytsaurus.tech/docs/en/#computation) that doesn’t contain any user business logic: incoming messages are converted to the output [stream](https://ytsaurus.tech/docs/en/#stream) schema and passed on unchanged. It’s used for simple schema alignment between streams. For more details, see [Computation](https://ytsaurus.tech/docs/en/computation.md#passthrough). ### Swift {#swift} This is a data processing principle in Flow where the result of a [computation](https://ytsaurus.tech/docs/en/#computation) isn’t stored in YTsaurus. Instead, the transformation function must be strictly deterministic — if needed, the result is recomputed. This reduces the load on YTsaurus while maintaining [exactly-once](https://ytsaurus.tech/docs/en/#exactly-once) guarantees. For more details, see the [Swift](https://ytsaurus.tech/docs/en/swift.md) section. ### Message {#message} A single message within a stream. In the context of a single [computation](https://ytsaurus.tech/docs/en/#computation), there are several types of messages: - `input` — messages from other [computations](https://ytsaurus.tech/docs/en/#computation); essentially, they’re the `output` of another [computation](https://ytsaurus.tech/docs/en/#computation), but with a grouped schema. - `source` — messages from internal [sources](https://ytsaurus.tech/docs/en/#source). - `timer` — special internal [timer](https://ytsaurus.tech/docs/en/#timer) messages. - `output` — the result of a [computation](https://ytsaurus.tech/docs/en/#computation)’s work, which becomes public and can go to an internal [sink](https://ytsaurus.tech/docs/en/#sink) and to other [computations](https://ytsaurus.tech/docs/en/#computation). ### Key {#key} A set of [message](https://ytsaurus.tech/docs/en/#message) field values defined via `group_by_schema` in the [spec](https://ytsaurus.tech/docs/en/spec.md) of a [computation](https://ytsaurus.tech/docs/en/#computation). All messages with the same key are routed to a single [partition](https://ytsaurus.tech/docs/en/#partition) — each partition is assigned a key range `[LowerKey; UpperKey)`. The key is the isolation unit for [state](https://ytsaurus.tech/docs/en/#state), [timers](https://ytsaurus.tech/docs/en/#timer), and [ordering guarantees](https://ytsaurus.tech/docs/en/ordering.md#ordering-guarantees). The concept of a key depends on the message type: - For `input` messages, the key is defined via `group_by_schema`. - For `source` messages, the key is the source’s partition key. - For `output` messages, there is no key. ### Lineage {#lineage} The collection of input [messages](https://ytsaurus.tech/docs/en/#message) and [timers](https://ytsaurus.tech/docs/en/#timer) from which specific output results of a [computation](https://ytsaurus.tech/docs/en/#computation) were derived. The framework uses lineage to compute system output messages and to ensure [ordering guarantees](https://ytsaurus.tech/docs/en/ordering.md#ordering-guarantees) for derived messages. For more details, see the [Lineage](https://ytsaurus.tech/docs/en/lineage.md) section. ## Timer {#timer} A delayed-call mechanism tied to a specific [grouping key](https://ytsaurus.tech/docs/en/#key) within `TTransformComputation`. A timer lets a [computation](https://ytsaurus.tech/docs/en/#computation) say, “Wake me up when time X arrives.” Each timer contains two timestamp fields: - `TriggerTimestamp` — the trigger moment. When the [EventWatermark](https://ytsaurus.tech/docs/en/glossary.md#timestamps-and-watermarks) (or another configured watermark) exceeds this value, the timer is passed to the computation for processing. - `EventTimestamp` — the business time of the original event, “held” in the timer. Timers are reliably stored in YTsaurus. The result of timer processing is recorded in the same transaction in which the timer is removed — this ensures [exactly-once](https://ytsaurus.tech/docs/en/#exactly-once) guarantees. {% note warning %} In the current implementation, all active timers are additionally stored in the process memory. With a large number of timers, this can lead to `Out of Memory` errors when jobs start. {% endnote %} For more details, see the [Timers](https://ytsaurus.tech/docs/en/timers.md) section. ## Resource {#resource} Resources exist to describe data shared across multiple [jobs](https://ytsaurus.tech/docs/en/#job). They can be clients to YTsaurus and other systems, caches for calls to external systems, machine learning models, and so on. Currently, resources can only be static (that is, unchangeable without a restart). ## Distributed Throttler {#distributed-throttler} In Flow, you can create named distributed throttlers that are shared across all [jobs](https://ytsaurus.tech/docs/en/#job) in a pipeline. They’re used to limit the load on external APIs, balance throughput between [partitions](https://ytsaurus.tech/docs/en/#partition) of a single [computation](https://ytsaurus.tech/docs/en/#computation), or slow down reading from sources. They’re implemented as a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) on the controller; jobs request a quota before processing messages or manually from user code. For more details, see the [Distributed Throttler](https://ytsaurus.tech/docs/en/distributed_throttler.md) section. ## State {#state} Persistent data associated with a specific [grouping key](https://ytsaurus.tech/docs/en/#key) within a [computation](https://ytsaurus.tech/docs/en/#computation). It’s stored in YTsaurus dynamic tables and updated atomically when an [epoch](https://ytsaurus.tech/docs/en/#epoch) is committed. An empty state value corresponds to the absence of a row in the table. For more details, see the [Stateful Processing](https://ytsaurus.tech/docs/en/stateful.md) section. ## Parallelism and execution {#parallelism-and-execution} To ensure scalability of the data processing workflow, Flow splits each [computation](https://ytsaurus.tech/docs/en/#computation) into multiple [partitions](https://ytsaurus.tech/docs/en/#partition), which are processed in parallel within [jobs](https://ytsaurus.tech/docs/en/#job). ### Partition {#partition} The input stream to a specific [computation](https://ytsaurus.tech/docs/en/#computation) can be quite large, so for parallel data processing, the [computation](https://ytsaurus.tech/docs/en/#computation) is split into multiple partitions. Approximate stream limits per partition: no more than 1 MB/s and no more than 1000 messages per second. Thanks to the `group_by_schema` field in the [computation](https://ytsaurus.tech/docs/en/#computation) [spec](https://ytsaurus.tech/docs/en/spec.md), the input stream is grouped, and data with the same [key](https://ytsaurus.tech/docs/en/#key) will always go to a specific partition. For this, each partition is assigned a key range `[LowerKey; UpperKey)`. ### Job {#job} A specific run of a handler for a specific [partition](https://ytsaurus.tech/docs/en/#partition) on a [worker](https://ytsaurus.tech/docs/en/#worker) selected by the [controller](https://ytsaurus.tech/docs/en/#controller). ### Epoch {#epoch} A job processes data in epochs to regularly and reliably save the processing progress. The epoch mechanism helps maintain [exactly-once](https://ytsaurus.tech/docs/en/#exactly-once) guarantees with a reasonable load on YTsaurus, and also to minimize downtime when recovering from failures. Some epochs may not contain commits to YTsaurus. Some [computation](https://ytsaurus.tech/docs/en/#computation) implementations may not make any commits at all. ### Layout {#layout} A list of all [jobs](https://ytsaurus.tech/docs/en/#job) and [partitions](https://ytsaurus.tech/docs/en/#partition) in the system. In other words, it’s a description of what’s running where right now. ## External connections {#external-connections} Flow interacts with external systems (queues, tables, etc.) via [connectors](https://ytsaurus.tech/docs/en/#connector). Each connector provides a [source](https://ytsaurus.tech/docs/en/#source) for reading and/or a [sink](https://ytsaurus.tech/docs/en/#sink) for writing. ### Connector {#connector} A component that connects the pipeline to an external system by reading messages from the connector’s [source](https://ytsaurus.tech/docs/en/#source) and/or writing messages to the connector’s [sink](https://ytsaurus.tech/docs/en/#sink). A connector can include both a [source](https://ytsaurus.tech/docs/en/#source) and a [sink](https://ytsaurus.tech/docs/en/#sink) (for example, in the case of [QYT](https://ytsaurus.tech/docs/connectors/queue.md)) or only a [source](https://ytsaurus.tech/docs/en/#source) (for example, in the case of [ServiceLog](https://ytsaurus.tech/docs/connectors/servicelog.md)). In code and specs, sources and sinks are described directly. The connector itself doesn’t exist as an entity; it’s more of a logical grouping element for sources and sinks by the external system that’s being interacted with. For more details about connectors, see the [Connectors Documentation](https://ytsaurus.tech/docs/connectors/about.md). ### Source {#source} A component that lets a [computation](https://ytsaurus.tech/docs/en/#computation) read messages from an external system. It’s implemented as a separate class and configured in the [computation](https://ytsaurus.tech/docs/en/#computation) spec. ### Sink {#sink} A component that lets a [computation](https://ytsaurus.tech/docs/en/#computation) write messages to an external system. It’s implemented as a separate class and configured in the [computation](https://ytsaurus.tech/docs/en/#computation) spec. ## System architecture {#system-architecture} A running pipeline consists of a set of processes with two roles — [controller](https://ytsaurus.tech/docs/en/#controller) and [worker](https://ytsaurus.tech/docs/en/#worker). The controller manages work distribution, and the workers directly process the data. ### Controller {#controller} Manages the entire pipeline. It assigns [jobs](https://ytsaurus.tech/docs/en/#job) to [workers](https://ytsaurus.tech/docs/en/#worker), provides an API for pipeline management, is responsible for synchronizing all settings, monitors the status of all [workers](https://ytsaurus.tech/docs/en/#worker), and collects their statuses. It can be run in multiple instances (recommended: 2–3); then, one instance will be the leader, and the others will wait in stand-by mode to ensure fault tolerance. Specs for execution are provided to the controller via a special API. #### Computation Controller {#computation-controller} The controller for a specific [computation](https://ytsaurus.tech/docs/en/#computation): it’s responsible for creating the required number of [partitions](https://ytsaurus.tech/docs/en/#partition) with the necessary settings. Don’t confuse it with the plain [controller](https://ytsaurus.tech/docs/en/#controller). Computation Controller is a class, while the controller is a program that manages the cluster operation and creates the necessary Computation Controllers. #### JobManager {#job-manager} A module responsible for creating and distributing [jobs](https://ytsaurus.tech/docs/en/#job) across [workers](https://ytsaurus.tech/docs/en/#worker). #### LeaseManager {#lease-manager} A module responsible for creating and pinging `Lease` — master transactions that jobs use during commits as `prerequisite_transaction_ids`. They help ensure [exactly-once](https://ytsaurus.tech/docs/en/#exactly-once) guarantees. ### Worker {#worker} One of the two main types of processes in the system, performing the actual computations within the [jobs](https://ytsaurus.tech/docs/en/#job) assigned to it. It regularly sends [heartbeats](https://ytsaurus.tech/docs/en/#heartbeat) to the [controller](https://ytsaurus.tech/docs/en/#controller), reporting the statuses of all jobs. In return, it receives the current system settings, in particular the [Layout](https://ytsaurus.tech/docs/en/#layout). #### Heartbeat {#heartbeat} A periodic message a [worker](https://ytsaurus.tech/docs/en/#worker) sends to the [controller](https://ytsaurus.tech/docs/en/#controller) reporting the statuses of all its [jobs](https://ytsaurus.tech/docs/en/#job). The controller's response carries the current system settings, in particular the [Layout](https://ytsaurus.tech/docs/en/#layout); a worker that stops sending heartbeats is treated as unavailable. #### Message Distributor {#message-distributor} A [worker](https://ytsaurus.tech/docs/en/#worker) module responsible for distributing messages between different [jobs](https://ytsaurus.tech/docs/en/#job). It runs based on the system’s current [layout](https://ytsaurus.tech/docs/en/#layout). It sends a message until it receives confirmation from the recipient about processing, including the fact of a reliable commit of the processing result (referred to in the codebase as `MarkPersisted`). #### Resource Manager {#resource-manager} A [worker](https://ytsaurus.tech/docs/en/#worker) component that creates [resources](https://ytsaurus.tech/docs/en/#resource) for shared use. For example, several [computations](https://ytsaurus.tech/docs/en/#computation) can use the same binary database or the same YTsaurus client. #### BufferStateManager {#buffer-state-manager} A component responsible for creating and maintaining the current sizes of all (or almost all) buffers in the system. Each job has buffers for incoming and outgoing messages. The task of `BufferStateManager` is to ensure the buffer sizes necessary for uninterrupted operation. ### Companion {#companion} A separate process launched on the same host as the [worker](https://ytsaurus.tech/docs/en/#worker) and executing user code in [Python](https://ytsaurus.tech/docs/python/getting-started.md), [Go](https://ytsaurus.tech/docs/go/getting-started.md), [Java or Kotlin](https://ytsaurus.tech/docs/java/getting-started.md), or C++. It interacts with the [worker](https://ytsaurus.tech/docs/en/#worker) via gRPC. It runs [computations](https://ytsaurus.tech/docs/en/#computation) outside the worker process, including non-C++ implementations and C++ code through the [C++ companion](https://ytsaurus.tech/docs/en/companion.md#cpp-companion). For more details, see the [Companion](https://ytsaurus.tech/docs/en/companion.md) section. ## Timestamps and watermarks {#timestamps-and-watermarks} Correct time-based event processing is one of the key tasks of a streaming system. Flow tracks three types of timestamps for each message and maintains a watermark mechanism to determine processing progress. ### EventTimestamp, SystemTimestamp, AlignmentTimestamp, StabilizedEventTimestamp, and Watermarks For each message in Flow, the following timestamps can be used: - [EventTimestamp](https://ytsaurus.tech/docs/en/watermarks#eventtimestamp) — the true time of the event associated with the message. - [SystemTimestamp](https://ytsaurus.tech/docs/en/watermarks.md#systemtimestamp) — the time when the specific message was created. For messages generated inside Flow, `SystemTimestamp` takes the “YTsaurus time”. For `source` streams, it’s the time when the message appeared in the remote system. - [AlignmentTimestamp](https://ytsaurus.tech/docs/en/ordering.md#alignment-timestamp) — a timestamp used to align the processing progress of partitions. It’s computed automatically. - [StabilizedEventTimestamp](https://ytsaurus.tech/docs/en/ordering.md#stabilized-event-timestamp) — a timestamp computed based on `AlignmentTimestamp`; it’s used as a non-decreasing approximation of `EventTimestamp` across the message stream (with a matching set of [keys](https://ytsaurus.tech/docs/en/#key) in the [lineage](https://ytsaurus.tech/docs/en/#lineage)). The first three timestamps are stored in the message itself, while `StabilizedEventTimestamp` is computed on the fly when needed. [Watermark](https://ytsaurus.tech/docs/en/watermarks.md) is a timestamp indicating that the system no longer expects older events to arrive. Each stream in the system maintains current `SystemWatermark` and `EventWatermark` values. {% note info %} Unfortunately, typical data-writing protocols for queues can’t guarantee that a yesterday’s event won’t be written to the queue. Therefore, for `SystemWatermark` on internal streams, you can say it’s absolute, while `EventWatermark` is only heuristic and approximate. {% endnote %} ## Ensuring Exactly Once {#exactly-once} By default, Flow ensures exactly-once semantics for event processing. The mechanism is based on barrier Lease transactions, deduplication of input messages by `message_id` (for internal streams) and by offsets (for sources), and the atomicity of output data via [epoch](https://ytsaurus.tech/docs/en/glossary.md#epoch) transactions. If needed, you can relax the semantics to at-least-once or at-most-once. For more details, see the [Processing Guarantees](https://ytsaurus.tech/docs/en/guarantees.md) section. ## Message deduplication and message processing order {#deduplication} The main way to deduplicate messages is by `message_id`, not by offset. The offset-based mechanism is used only when working with queues. Also, Flow doesn’t provide any guarantee about the order of event processing. ## Manage the pipeline {#manage-pipeline} This section describes the pipeline lifecycle operations: from configuring it via specs to release, migration, and running in different environments. ### Spec and DynamicSpec {#spec-and-dynamic-spec} Any pipeline running on Flow is a combination of two YSON configs called `Spec` and `DynamicSpec`. - `Spec` — defines the pipeline’s static properties, its topology, node properties, connections between them, object types, etc. You can change the static spec only if the pipeline is stopped. - `DynamicSpec` — defines the pipeline’s dynamic properties. Accordingly, the specs contain the properties of all system components, both from the user layer (for example, the list of [computations](https://ytsaurus.tech/docs/en/#computation) and the [stream](https://ytsaurus.tech/docs/en/#stream) schemas between them) and from the system-level settings (compression codecs in system tables, buffer sizes, etc.). ### Pipeline release {#release-pipeline} Updating the pipeline, which includes changing its executable files. ### Update pipeline specs {#update-pipeline-specs} Update the [static](https://ytsaurus.tech/docs/en/*static_spec_upd) and/or dynamic [spec](https://ytsaurus.tech/docs/en/spec.md) of the pipeline. ### Start, stop, and pause the pipeline {#start-stop-pause-pipeline} Change the pipeline’s target state and transition the pipeline to that state. The existing states are: - `unknown` — the pipeline hasn’t started yet. - `working` — the pipeline is running, and messages are being processed. - `stopped` — the pipeline is stopped, and all messages have been fully processed. - `paused` — the pipeline is paused (jobs are stopped, and intermediate messages might not be fully processed). - `draining` — a transitional state: the pipeline is in the process of stopping. - `pausing` — a transitional state: the pipeline is in the process of pausing. - `completed` — a final state: all pipeline sources were [finite](https://ytsaurus.tech/docs/python/testing.md) (`finite = true`), and all messages from them have been processed. You can’t exit this state — you’ll need to recreate the pipeline. This state most often appears in [integration tests](https://ytsaurus.tech/docs/python/testing.md), and it can also occur in production pipelines if the deployment steps are executed in the wrong order (for example, if a source was mistakenly marked as finite) or if an incorrect spec is set. ### Internal pipeline tables {#inner-pipeline-tables} Service tables that are placed in the pipeline directory in YTsaurus. They are necessary for the pipeline to run (for example, to deduplicate incoming messages, store partition information, etc.). The full list of internal tables and their purposes are provided in the section [Pipeline Object → Internal tables](https://ytsaurus.tech/docs/en/pipeline-object.md#internal_tables). ### User tables {#user-tables} Tables that users work with. Typically, their content has a product-related meaning, for example: - Ordered tables — queues from which the user pipeline reads input events or writes output events. - Sorted tables with entity states (banner/offer/user profiles, etc.) that the user builds while the pipeline is running. ### Migration {#migration} The process of changing the structure of objects in YTsaurus — for example, adding new tables, changing the schema of existing tables, or deleting them. The main reasons to run migrations are: - Deploying a new pipeline version that isn’t compatible with the current state of objects in YTsaurus. Migration lets you update the objects to a state that the new pipeline version can work with, without losing data. - Changing the table type (replicated/standalone/chaos) for performance or availability reasons. Examples of changes: #| || **Require migration** - Changing multiple tables (adding new ones or deleting old ones). - Changing table schemas. - Changing table types (replicated/standalone/chaos). | **Do not require migration** - Changing most table attributes (for example, changing compaction settings, which only affect performance). - Changing the list of replicas. || |# ### Environment {#environment} The installation where the pipeline runs — testing, pre-production, or production. It’s a synonym for the term “stage”. ## See also - [Computation](https://ytsaurus.tech/docs/en/computation.md) - [Spec and DynamicSpec](https://ytsaurus.tech/docs/en/spec.md) - [Watermarks and Timers](https://ytsaurus.tech/docs/en/watermarks.md) - [Stateful processing](https://ytsaurus.tech/docs/en/stateful.md) - [Connectors](https://ytsaurus.tech/docs/connectors/about.md) [*static_spec_upd]: You can change it only if the pipeline is stopped. # Pipeline Object in YTsaurus This section describes the Cypress object `pipeline` — the deployment unit of YTsaurus Flow. It covers the data model, creation methods, internal tables, and management tools. Related sections: [Pipeline in the glossary](https://ytsaurus.tech/docs/en/glossary.md#pipeline), [Spec, DynamicSpec and Config](https://ytsaurus.tech/docs/en/spec.md), [Stateful processing](https://ytsaurus.tech/docs/en/stateful.md), [Pipeline internal tables](https://ytsaurus.tech/docs/en/glossary.md#inner-pipeline-tables). ## Data Model { #data_model } In YTsaurus, a *pipeline* is a special Cypress object of type `pipeline`. Structurally, it’s a map node — a directory where a set of service [internal tables](https://ytsaurus.tech/docs/en/#internal_tables) required for Flow operation are automatically created. ## Internal Tables { #internal_tables } Table 1 — Pipeline internal tables | Table name | Purpose | | --- | --- | | `input_messages` | Index of input messages for [transform computations](https://ytsaurus.tech/docs/en/computation.md#ttransformcomputation), used for deduplication. | | `compact_input_messages` | Compact index of input messages. It’s used by default for all computations, except when `experimental_enable_non_uint_key` is enabled; the behavior is overridden by the `use_compact_input_messages` parameter in [TComputationSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs.md#NYT_NFlow_TComputationSpec). | | `compact_output_messages` | Not used. | | `compact_partition_output_messages` | Output messages of transform computations, physically grouped by partitions and chunked by `stream_id` / `chunk_id` for optimal reading. | | `states` | User and service [states](https://ytsaurus.tech/docs/en/stateful.md) stored by [key](https://ytsaurus.tech/docs/en/glossary.md#key). | | `partition_states` | States stored by partition. | | `timers` | [Timers](https://ytsaurus.tech/docs/en/glossary.md#timer) of the user code. | | `controller_logs` | Event logs of the [Controller](https://ytsaurus.tech/docs/en/glossary.md#controller) in the PublicFlowController category. | | `flow_state` | Current Flow state. | | `flow_state_obsolete` | Flow KV storage for named objects (spec, dynamic_spec, etc.). | | `flow_control` | Published controller address; also the leader lease with the Dyntable election backend. | | `key_visitor_states` | Durable key-range scan cursor and pass state. | | `partition_transactions` | Service table for safe retry of transactions. | | `leases` | Dyntable election backend: partition lease owners and the pipeline-wide lease deadline that fences stale workers. | | `leader_election_lock` | Controller election lock for the Chaos backend. | After you create a pipeline, these tables appear under the path `/` and are automatically mounted. The table set, schemas, and base attributes for `create pipeline` are defined in [`definitions.yson`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/pipeline_tables/definitions.yson). The `yt_sync_mini` library also applies [physical-attribute presets](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/pipeline_tables/presets.py). The `input_messages` and `compact_input_messages` tables hold temporary records used for exactly-once deduplication, not a history of input. The controller advances their [SystemWatermark](https://ytsaurus.tech/docs/en/watermarks.md), and dynamic table cleanup removes processed records, including after the pipeline finishes. An empty `input_messages` table is therefore normal and does not mean input was lost. {% note warning "Attention" %} The internal tables are service tables, and their structure may change between Flow releases. Don’t read from or write to them directly from your user code. To read debugging data (for example, controller logs), use `yt flow show-logs` and other commands from the `yt flow` family. {% endnote %} ## Creating a Pipeline { #create } ### Via yt_sync_mini { #yt-sync-mini } The recommended way to create a pipeline in open source is the Python library [`yt_sync_mini`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/yt_sync_mini). It creates a map node of type `pipeline`, all [internal tables](https://ytsaurus.tech/docs/en/#internal_tables) with correct schemas and physical attributes, and mounts them immediately. The operation is idempotent — running it again on an existing pipeline is a no-op. ```python import yt.wrapper as yt from yt.yt.flow.library.python.yt_sync_mini import create_pipeline client = yt.YtClient(proxy="") create_pipeline(client, "") ``` ### Low-level Creation of a Cypress Node { #low-level-create } To integrate with an existing deployment system, you can create the pipeline using the standard `create` mechanism, as with other Cypress object types (table, map_node, queue_consumer, etc.). By default, `create pipeline` creates and mounts the internal tables. Pass `initialize_tables=%false` when creating the object to disable initialization; you are then responsible for creating and mounting the tables with the correct schemas and attributes. #### Via YTsaurus CLI ```bash yt --proxy create pipeline ``` To create the internal tables yourself: ```bash yt --proxy create pipeline --attributes '{initialize_tables=%false}' ``` #### Via Python (YTsaurus wrapper) ```python import yt.wrapper as yt client = yt.YtClient(proxy="") client.create( "pipeline", "" ) ``` #### Via C++ (YTsaurus native client) ```cpp #include NYT::NApi::TCreateNodeOptions options; auto nodeId = NYT::NFlow::CreatePipelineNode(client, pipelinePath, options); ``` ## External State { #external-state } When the schema of the [internal tables](https://ytsaurus.tech/docs/en/#internal_tables) changes in a new Flow version, the format upgrade is performed via a separate migration — see [Pipeline internal tables](https://ytsaurus.tech/docs/en/glossary.md#inner-pipeline-tables) and [Basic rollout rules](https://ytsaurus.tech/docs/devops/vanilla/releases.md#release-and-configure-basic-rules). If the pipeline uses [External State](https://ytsaurus.tech/docs/en/stateful.md) (user tables outside the node), creating them and evolving their schemas is your responsibility. You perform the operations using standard commands like `yt create table ... --attributes '{dynamic=%true; schema=...}'` and `yt mount-table` — see examples in the [Create command](https://ytsaurus.tech/user-guide/storage/cypress-example.md#create) section. ## See also { #see_also } - [Glossary: Pipeline](https://ytsaurus.tech/docs/en/glossary.md#pipeline) - [Pipeline internal tables](https://ytsaurus.tech/docs/en/glossary.md#inner-pipeline-tables) - [Basic pipeline rollout rules](https://ytsaurus.tech/docs/devops/vanilla/releases.md#release-and-configure-basic-rules) # Computation in YTsaurus Flow Use a Computation as the main building block of a [pipeline](https://ytsaurus.tech/docs/en/glossary.md#pipeline). You get messages from input [streams](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation), process them, and send the results to output streams. ## Computation types {#computation-types} Flow implements four basic Computation types. You’ll find each type described in the sections below. Classes with `Swift` in their name follow the [Swift](https://ytsaurus.tech/docs/en/swift.md) principle. This is an approach to data processing without full materialization, while preserving [exactly-once](https://ytsaurus.tech/docs/en/glossary.md#exactly-once) guarantees and requiring deterministic transformations. In C++, these classes define execution modes and are used by built-in adapters. Implement new user logic only as a [process function](https://ytsaurus.tech/docs/cpp/process-functions.md), and select the appropriate `TProcessFunction*Computation` in the spec. Don’t create user subclasses of the Computation base classes. ### TTransformComputation {#ttransformcomputation} This mode handles arbitrary transformations of input data. The processing result is stored in YTsaurus, so the transformation needn’t be deterministic. It supports [timers](https://ytsaurus.tech/docs/en/glossary.md#timer), [states](https://ytsaurus.tech/docs/en/glossary.md#state), and [Sink](https://ytsaurus.tech/docs/en/glossary.md#sink). Run user process functions in this mode with `TProcessFunctionComputation`; for a passthrough variant without business logic, use [TPassthroughComputation](https://ytsaurus.tech/docs/en/#passthrough). ### TSwiftMapComputation {#tswiftmapcomputation} This mode implements a deterministic Map without materializing results in YTsaurus. It doesn’t support timers, [Source](https://ytsaurus.tech/docs/en/glossary.md#source), or Sink. The process function must be strictly deterministic — the system recomputes the result if needed. Use `TProcessFunctionSwiftMapComputation` for user logic and [TSwiftPassthroughComputation](https://ytsaurus.tech/docs/en/#passthrough) for passthrough. ### TSwiftOrderedSourceComputation {#tswiftorderedsourcecomputation} This mode reads data from external sources. It requires that the data stream from each instance is ordered. It supports `WatermarkStrategy` to estimate [watermarks](https://ytsaurus.tech/docs/en/glossary.md#timestamps-and-watermarks). Use `TProcessFunctionSourceComputation` for user logic and [TSwiftPassthroughOrderedSourceComputation](https://ytsaurus.tech/docs/en/#passthrough) for passthrough. ### TTransformOrderedSourceComputation {#ttransformorderedsourcecomputation} This mode processes `Source` data with arbitrary custom logic: parsing, filtering, or expanding one message into several. Run the user process function with `TProcessFunctionTransformOrderedSourceComputation` instead of chaining `TSwiftPassthroughOrderedSourceComputation` → `TProcessFunctionComputation`. The processing result is materialized in YTsaurus, as in `TTransformComputation`, so there are no determinism requirements: after a restart, Flow delivers the already materialized messages with the `MessageId` values previously assigned to them instead of recomputing them. The source offset, the materialized output messages, and the [states](https://ytsaurus.tech/docs/en/stateful.md) are committed in a single YTsaurus transaction — processing of each source message is applied exactly once, including state updates. A process function keeps its state in a `TMutableStateKeyClient` field, initializes it through `initContext->InitClient(...)` in `Init(const IRuntimeInitContextPtr&)`, and reads it through `GetState(message->Key)` during processing. For an example, see [Process functions (C++)](https://ytsaurus.tech/docs/cpp/process-functions.md). Supported: `source_streams` (exactly one ordered `Source`), several output streams, `watermark_strategy` (`watermark_generator` estimates the source watermarks, `watermark_alignment` aligns reading, `event_timestamp_assigner` assigns `event_timestamp`), `skip_if_expression`, and messages with `distribute = false`. A non-empty `group_by_schema`, `input` streams, [timers](https://ytsaurus.tech/docs/en/glossary.md#timer), and [key-visitor streams](https://ytsaurus.tech/docs/en/key_visitor.md) cause a spec validation error. ## Passthrough Computation {#passthrough} A [passthrough Computation](https://ytsaurus.tech/docs/en/glossary.md#passthrough) doesn’t include custom business logic. Incoming messages are converted to the output [stream](https://ytsaurus.tech/docs/en/glossary.md#stream) schema and passed on unchanged. Use it to simply align schemas between streams, for example, when reading a queue and moving data to another stream without any processing. Flow implements three C++ classes: | Class | Base class | Purpose | |-------|--------------|------------| | `TPassthroughComputation` | `TTransformComputation` | Converts `input` messages to the `output` stream schema | | `TSwiftPassthroughComputation` | `TSwiftMapComputation` | Does the same, without materialization ([Swift](https://ytsaurus.tech/docs/en/swift.md)) | | `TSwiftPassthroughOrderedSourceComputation` | `TSwiftOrderedSourceComputation` | Converts `source` messages to the `output` stream | Passthrough is natively implemented in Flow using C++ and doesn’t require a Java or Python companion. To enable it, specify the corresponding C++ class in the `computation_class_name` field in the Computation’s static spec: ```yson "passthrough" = { "computation_class_name" = "NYT::NFlow::TPassthroughComputation"; "group_by_schema" = [...]; "input_stream_ids" = [...]; "output_stream_ids" = [...]; }; ``` For more details, see [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md#tpassthroughcomputation). ## Common properties {#common-properties} - All execution within a single [partition](https://ytsaurus.tech/docs/en/glossary.md#partition) is strictly single-threaded. You achieve multithreading by increasing the number of partitions. - All Computations handle filling the message and timer metadata fields. - The `OutputCollector` object collects output messages and timers. - The `SetParents` method lets you manage the [lineage](https://ytsaurus.tech/docs/en/glossary.md#lineage) of messages to correctly calculate metadata fields. ## Implementation in different languages Each language offers its own set of interfaces for implementing a Computation: - **C++**: implement a process function (`IProcessFunction`, `IBatchProcessFunction`, or `IKeyedBatchProcessFunction`) and select its built-in mode adapter in the spec. [Learn more →](https://ytsaurus.tech/docs/cpp/process-functions.md) - **Java**: implement the `RowFunction` or `BatchFunction` interfaces with the `onMessage`/`onTimer` methods. [Learn more →](https://ytsaurus.tech/docs/java/computation.md) - **Python**: inherit from `RowFunction` or `BatchFunction` and use the `on_message`/`on_timer` methods. [Learn more →](https://ytsaurus.tech/docs/python/computation.md) - **Go**: implement `flow.RowFunction` (`OnMessage`) or `flow.BatchFunction` (`OnMessages`); timers use `flow.RowTimerFunction` or `flow.BatchTimerFunction`. [Learn more →](https://ytsaurus.tech/docs/go/computation.md) - **YQL**: Computations are generated automatically based on a declarative description. [Learn more →](https://ytsaurus.tech/docs/yql/getting-started.md) ## See also - [Stateful processing](https://ytsaurus.tech/docs/en/stateful.md) - [Watermarks](https://ytsaurus.tech/docs/en/watermarks.md) - [Timers](https://ytsaurus.tech/docs/en/timers.md) - [Specs](https://ytsaurus.tech/docs/en/spec.md) - [Process functions (C++)](https://ytsaurus.tech/docs/cpp/process-functions.md) - [Computation modes (C++)](https://ytsaurus.tech/docs/cpp/computation.md) - [Computation (Java)](https://ytsaurus.tech/docs/java/computation.md) - [Computation (Python)](https://ytsaurus.tech/docs/python/computation.md) - [Computation (Go)](https://ytsaurus.tech/docs/go/computation.md) - [Computation (YQL)](https://ytsaurus.tech/docs/yql/features.md) ## Input filtering {#input-filter} `skip_if_expression` filters input messages before deduplication and user processing. Skipped messages do not enter the deduplication store. Matching messages are acknowledged without being passed to the handler, including when the entire batch is filtered out. Input statistics, including key distribution and heavy hitters, describe the original stream before filtering. # Swift in YTsaurus Flow **Swift** is a data processing principle in YTsaurus Flow where the result of a [computation](https://ytsaurus.tech/docs/en/glossary.md#computation) **isn’t saved in YTsaurus**. Instead, the transformation function must be strictly deterministic: if you need the result again (for example, when restarting a job), it’s recomputed from the same input data. ## Why you need it {#motivation} In the classic approach (`TTransformComputation`), each [epoch](https://ytsaurus.tech/docs/en/glossary.md#epoch) ends with a transactional write of results to YTsaurus. This ensures exactly-once delivery but puts a load on the cluster: for each input message, the system performs a lookup and writes to the deduplication table, plus one write for each output message. Swift removes this limitation: if the function is deterministic, you don’t need to store its output — you can reproduce it when needed. This lets you: - Reduce the load on YTsaurus to zero or a minimum (only metadata). - Increase throughput for stateless transformations. ## How exactly-once guarantees are maintained {#exactly-once} Even though output data isn’t written to YTsaurus, [exactly-once](https://ytsaurus.tech/docs/en/glossary.md#exactly-once) delivery to another Flow computation is preserved thanks to determinism: - If a job fails before delivering the result, Flow restarts it and gets **the same output** from the same input data. - The [Message Distributor](https://ytsaurus.tech/docs/en/glossary.md#message-distributor) keeps delivering the message until it receives an acknowledgment (`MarkPersisted`) from the receiver, which prevents data loss. So, exactly-once is ensured not by storing the output but by the **idempotency** of the computation and deduplication at the recipient. `TSwiftMapComputation` does not support [Sink](https://ytsaurus.tech/docs/en/glossary.md#sink). `TSwiftOrderedSourceComputation` supports direct delivery to an external sink. Deferred publications provide output-epoch finalization but do not by themselves upgrade at-least-once input to exactly-once. ## Determinism requirement {#determinism} The transformation function in a Swift computation must be **deterministic**: for the same input data, it must return the same output, including the order of messages. The requirement also covers [lineage](https://ytsaurus.tech/docs/en/lineage.md) — the binding of output messages to their parents: on recomputation, each output must get the same parents in the same order. A typical mistake is iterating over an unordered structure (such as a hash table or a set) when splitting the batch into groups: the order of groups changes from run to run, and the recomputation produces a different output. By default every output message of a Swift computation has exactly one parent; multiple parents are allowed only with [`allow_batching_with_relaxed_guarantees`](https://ytsaurus.tech/docs/en/guarantees.md#swift-allow-batching-with-relaxed-guarantees). {% note warning %} Breaking determinism when updating a pipeline without a [drain](https://ytsaurus.tech/docs/en/glossary.md#start-stop-pause-pipeline) can lead to duplicates or loss of intermediate messages: different parts of the system might process different versions of the output. {% endnote %} There can be exceptions to this rule, driven by the pipeline’s business logic specifics. But the developer of the business logic must clearly understand why they’re implementing them and what mechanisms ensure the pipeline’s overall result stays correct. By default, Flow appends the derived message’s sequence number to its message ID. If two replays of a Swift computation emit different messages at the same position, those messages get the same message ID. This can cause data loss or duplicates; if the messages have different keys, it can also violate Flow’s internal invariants. In C++, Java, Python, and Go, output options can remove emission order from message identity by selecting a payload hash or user-defined suffix (see the [C++ API description](https://ytsaurus.tech/docs/cpp/process-functions.md#message-id-suffixes)). This prevents that particular ID collision only when the semantic suffix is stable; it doesn’t make a nondeterministic computation deterministic. ## Swift computation classes {#classes} Flow implements two base Swift classes: ### TSwiftMapComputation {#swift-map} A deterministic Map without materializing results in YTsaurus. - **Load on YTsaurus:** ~0 writes per epoch (only system background processes). - **Doesn’t support:** [Source](https://ytsaurus.tech/docs/en/glossary.md#source) or [Sink](https://ytsaurus.tech/docs/en/glossary.md#sink). - **Supports:** [timers](https://ytsaurus.tech/docs/en/glossary.md#timer) and [key-visitor streams](https://ytsaurus.tech/docs/en/key_visitor.md) — only for working with [state](https://ytsaurus.tech/docs/en/glossary.md#state), for example, for background cleanup (GC). Emitting output messages from timer or visit processing is **prohibited**: the output stream can’t depend on a timer or visit stream in `streams_dependency`. Since timer streams are added by default to the dependencies of each output, a spec with timers and outputs must explicitly define `streams_dependency`. - **Requires:** strict determinism and that each resulting message has exactly one parent — the input message. For more details, see the [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md#tswiftmapcomputation) section. ### TSwiftPassthroughComputation {#swift-passthrough-map} A [Passthrough computation](https://ytsaurus.tech/docs/en/glossary.md#passthrough) that inherits from `TSwiftMapComputation`. It converts `input` messages to the `output` stream schema without custom logic. For more details, see [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md#tswiftpassthroughcomputation). ### TSwiftOrderedSourceComputation {#swift-source} The main class for reading ordered data from external sources. - **Load on YTsaurus:** ~1–2 writes per epoch (metadata for recovery; the messages themselves aren’t stored). - **Supports:** [Sink](https://ytsaurus.tech/docs/en/glossary.md#sink) and `WatermarkStrategy` for evaluating [watermarks](https://ytsaurus.tech/docs/en/glossary.md#timestamps-and-watermarks). Direct delivery to an external sink without deduplication is at-least-once. - **Requires:** exactly one [Source](https://ytsaurus.tech/docs/en/glossary.md#source) that implements `IOrderedSource`. For more details, see the [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md#tswiftorderedsourcecomputation) section. ### TSwiftPassthroughOrderedSourceComputation {#swift-passthrough-source} A Passthrough computation that inherits from `TSwiftOrderedSourceComputation`. It converts `source` messages to the `output` stream by mapping them to a new schema. For more details, see [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md#tswiftpassthroughorderedsourcecomputation). ## Comparison with TTransformComputation {#comparison} | Type | Writes to YTsaurus per epoch | Timer support | State support | Determinism requirement | |------|--------------------------------------|---------------|---------------|-------------------------| | `TTransformComputation` | 2 per input message and 1 per output message | Yes | Yes | No | | `TSwiftOrderedSourceComputation` | ~1–2 (metadata) | No | No | Yes | | `TSwiftPassthroughOrderedSourceComputation` | ~1–2 (metadata) | No | No | Yes | | `TSwiftMapComputation` | ~0 | Yes (state only) | Yes | Yes | | `TSwiftPassthroughComputation` | ~0 | No | No | Yes | ## See also - [Computation](https://ytsaurus.tech/docs/en/computation.md) - [Processing guarantees](https://ytsaurus.tech/docs/en/guarantees.md) - [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md) - [Computation (Java)](https://ytsaurus.tech/docs/java/computation.md) - [Computation (Python)](https://ytsaurus.tech/docs/python/computation.md) - [Computation (Go)](https://ytsaurus.tech/docs/go/computation.md) # Stateful processing in YTsaurus Flow Use stateful processing to handle events with read-modify-write operations on states stored in YTsaurus. For example, you can count statistics for incoming events by key: you load the old value, update it, and write it back. ## State access model {#model} You access state inside a [computation](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation) through three components: - **State** — user data tied to a [key](https://ytsaurus.tech/docs/en/glossary.md#key) and stored in a YTsaurus dynamic table. - **State client** — a type-specific object (`Client`) that the process function creates for each named state and initializes in `Init`. You access state by key through the client. It can be **read-write** (works with both [internal](https://ytsaurus.tech/docs/en/#internal-state) and [external](https://ytsaurus.tech/docs/en/#external-state) states) or **read-only** ([joiner](https://ytsaurus.tech/docs/en/#external-state-joiner) for external state). - **State accessor** — what the client returns for a specific key (based on an input message, timer, or explicit key): a representation of the state of the same type `TState`. The accessor behaves like a smart pointer to the state: a read-write accessor lets you read, modify, and clear the state; a read-only accessor lets you only read it. {% note warning %} The accessor is valid only within the current [epoch](https://ytsaurus.tech/docs/en/glossary.md#epoch). Don't store it in process-function fields or reuse it across epochs — get the state through the client again in each epoch. {% endnote %} ## State types {#state-types} ### Internal State {#internal-state} This is the simplest way to work with state: you don't need to create tables — Flow manages them automatically. Data loads at the start of the [epoch](https://ytsaurus.tech/docs/en/glossary.md#epoch) and writes on commit. You get read-write access through the same client you use for external state. The state type can be arbitrary; the only requirement is that it's serializable to YSON (to persist between epochs). ### External State {#external-state} This is state in a user-managed dynamic table. You create and manage the tables. You get read-write access through the same client you use for internal state, but the backend is a **state manager** (`TSimpleExternalStateManager`). You declare it in the computation spec in the top-level `external_state_managers` section; the implementation resolves via `external_state_manager_class_name`. It supports caching. ### External State Joiner {#external-state-joiner} You get read-only access to external states via a key-based join — through a **joiner** (`TSimpleExternalStateJoiner`). You declare it in the computation spec in the top-level `external_state_joiners` section (at the same level as `external_state_managers`); the implementation resolves via `external_state_joiner_class_name`. It supports TTL-based caching: loaded states live in the shared [StateCache](https://ytsaurus.tech/docs/en/#state-cache) and reload from YT only after the TTL expires or the state is evicted from the cache. {% note warning %} Tables accessed by a read-write state manager must be modified only through it (or when the [pipeline](https://ytsaurus.tech/docs/en/glossary.md#pipeline) is [stopped](https://ytsaurus.tech/docs/en/glossary.md#start-stop-pause-pipeline)), because it may use caches. {% endnote %} {% note warning "One table, one writer" %} Only one computation should write to an external state table: writes from different [partitions](https://ytsaurus.tech/docs/en/glossary.md#partition) and transactions break state consistency. The state manager owns its table for writing: `TSimpleExternalStateManager` declares it as its own. To get read-only access to another computation's state, use an [external state joiner](https://ytsaurus.tech/docs/en/#external-state-joiner) (`TSimpleExternalStateJoiner`) or send messages to the writer computation — joiners don't lock the table. The spec validation checks write ownership: each state table must have exactly one owner writer, and a pipeline where two managers lock the same table for writing is rejected with the error `State table is claimed for writing by both ...`. Read-only consumers (joiners) don't lock the table for writing, so they can share it with the owner writer. {% endnote %} {% note info %} For any state, an empty value corresponds to the absence of a row in the table. If the state is empty after modification, the corresponding row is deleted. By default, emptiness and state clearing are determined automatically (by comparing to the default value); the state type can override this behavior. {% endnote %} ## State storage {#storage} States are stored in YTsaurus dynamic tables. Here's a simple schema example: #| || **name** | **type** | **sort_order** | **expression** || || `hash` | `uint64` | `ascending` | `farm_hash(my_key)` || || `my_key` | `string` | `ascending` | || || `my_value_1` | `string` | | || || `my_value_2` | `string` | | || |# ### group_by_schema consistency {#group-by-schema} For correctness and performance, we strongly recommend that you use, as the [group_by_schema](https://ytsaurus.tech/docs/en/spec.md#computation) for a [computation](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation), the schema of the first key columns of the dynamic table with states (strictly a prefix of the key columns). This ensures that: - Only one [partition](https://ytsaurus.tech/docs/en/glossary.md#partition) handles events for a single key (correctness). - One partition handles a limited number of tablets (performance). Here's an example of a `group_by_schema` consistent with the state table schema from the example above: #| || **name** | **type** | **expression** || || `hash` | `uint64` | `farm_hash(my_key)` || || `my_key` | `string` | || |# ## StateCache {#state-cache} Flow provides a shared two-level (uncompressed + compressed) LRU cache for states. Configure it at `/dynamic_spec/job_tracker/state_cache`. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `compressed_cache_weight` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `1Gi` Limit for `compressed`. || || `uncompressed_cache_weight` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `100Mi` Limit for `uncompressed`. || |# ## Implementation in different languages - **C++**: the client `TMutableStateKeyClient` (read-write) or `TJoinedStateKeyClient` (read-only) returns the accessor `TStateAccessor` / `TConstStateAccessor`; the same key client works with both internal and external states. [Learn more →](https://ytsaurus.tech/docs/cpp/state.md) - **Java**: YsonStateAccessor, ProtoStateAccessor, ExternalStateAccessor. [Learn more →](https://ytsaurus.tech/docs/java/state.md) - **Python**: ctx.state(), ctx.external_state(), ctx.proto_state(). [Learn more →](https://ytsaurus.tech/docs/python/state.md) - **Go**: `flow.OpenYSONState`, `flow.OpenProtoState`, `flow.OpenExternalState`, and `flow.OpenJoinedExternalState` open a keyed state accessor. [Learn more →](https://ytsaurus.tech/docs/go/state.md) ## See also - [Working with states (C++)](https://ytsaurus.tech/docs/cpp/state.md) - [Working with states (Java)](https://ytsaurus.tech/docs/java/state.md) - [Working with states (Python)](https://ytsaurus.tech/docs/python/state.md) - [Working with states (Go)](https://ytsaurus.tech/docs/go/state.md) # Watermarks in YTsaurus Flow ## Why time matters in stream processing {#why-time-matters} Imagine you’re sorting letters by their writing date, but they arrive out of order. One letter dated March 1 arrives today, and another dated March 5 arrived yesterday. How do you know that you’ve received all the March letters and can start summarizing the data? A watermark is exactly that signal: “all events older than moment X have been received and processed.” In real systems, the situation is even more interesting. Suppose an ad impression is recorded in the system at 10:05, but the user saw the ad at 10:02. Which time should you consider the “real” one? The time recorded in the system (10:05) or the time of the event itself (10:02)? The answer depends on your task, and each case requires its own time scale. Flow supports both time scales—`SystemTimestamp` (record time) and `EventTimestamp` (event time)—as well as the Watermarks mechanism to track processing progress. Below, you’ll find an overview of each of these elements. Currently, Flow doesn’t include the `Window` concept that exists in [The Dataflow Model](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43864.pdf) and [Apache Beam](https://beam.apache.org/). However, it does provide basic building blocks that let you define `EventTimestamp`, calculate `EventWatermark`, and set up `Timers`. ## Two time scales: SystemTimestamp and EventTimestamp {#two-timescales} ### SystemTimestamp All messages in Flow must include the `SystemTimestamp` field, which stores the event creation time. For `source` streams, this is the time when the event was created in the external system. For all other streams, it’s strictly the `YT` time. ### EventTimestamp All messages in `Flow` must have the `EventTimestamp` field populated. This is the time of the event itself in business logic terms: for example, the moment a user clicks or the time a log entry is written on the client side. You set `EventTimestamp` using `WatermarkStrategy` in `SourceComputation`. ## SystemWatermark All public messages (of the `input` and `output` types) have `SystemTimestamp`. `SystemWatermark` takes a value such that any possible message with `SystemTimestamp < SystemWatermark` can be considered processed. The exception is `source` streams. Since they come from an external system, `SystemWatermark` for them may be inaccurate. You use `SystemWatermark` to estimate the time lag across all streams. Because `Flow` assigns all `SystemTimestamp` values itself (and knows the current YTsaurus time) and also maintains information about the current `inflight` data, it can reliably keep the `SystemWatermark` value up to date. Currently, you can expect a delay of up to a few minutes. You can assume that `SystemWatermark` for `input` and `output` messages is completely accurate. That’s why you can use it to clean up the input message deduplication table (`input_messages` or `compact_input_messages`) containing already processed `message_id` values. ## EventWatermark {#event-watermark} You use the configured `WatermarkStrategy` in `SourceComputation` to estimate `EventWatermark` at different points in the system. `WatermarkStrategy` includes three main modules: - `EventTimestampAssigner` — to automatically populate `EventTimestamp` based on a specific column in `output` streams. - `WatermarkGenerator` — settings for the `EventWatermark` generator. It lets you configure `out_of_orderness_bound` and also handle `idle` and `unavailable` partitions. - `WatermarkAlignment` — a special module for aligning reads. For more details, see the [spec](https://ytsaurus.tech/docs/en/spec.md#watermark-strategy). ## Late Data Late Data is the term for messages that arrive in violation of `EventWatermark`. This concept was introduced in the article about [The Dataflow Model](https://static.googleusercontent.com/media/research.google.com/ru//pubs/archive/43864.pdf). You can read more about how to handle these events in the article. ## WatermarkGenerator This module for source [computations](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation) tries to provide a lower-bound estimate for the `EventTimestamp` of future events across all `output` streams. The algorithm works as follows: 1. You estimate `EventWatermark` for each [partition](https://ytsaurus.tech/docs/en/glossary.md#partition) of the input queue independently. 2. You take the minimum value across all partitions. 3. To estimate `EventWatermark` for a specific partition, you use `max(MaxTimestamp – OutOfOrdernessBound, min(MinAheadTimestamp, MaxAheadTimestamp - OutOfOrdernessBound))`, where: * `MaxTimestamp` — the maximum time encountered in that partition. * `OutOfOrdernessBound` — a parameter defined in the spec that characterizes the delays in writing data to the queue. * `MinAheadTimestamp` — the minimum time in the next unprocessed data batch. * `MaxAheadTimestamp` — the maximum time in the next unprocessed data batch. 4. Unavailable partitions, as well as partitions that aren’t receiving writes, can be ignored to a limited extent. You can find the settings for these heuristics in the [spec description](https://ytsaurus.tech/docs/en/spec.md#watermark-generator). This approach isn’t optimal. In the future, we plan to: - Calculate `MaxTimestamp` across multiple dimensions — to protect against cases like “impressions happen, but clicks don’t.” - Maintain the `MinTimestamp` value for the last N messages from a partition — to protect against cases where multiple providers write to a partition and one of them lags significantly more than `OutOfOrdernessBound`. {% note info %} We also plan to create a protocol for passing `Watermark` information between different [pipelines](https://ytsaurus.tech/docs/en/glossary.md#pipeline). {% endnote %} ## WatermarkState Flow maintains `EventWatermark` and `SystemWatermark` values for each stream and each `alignment` group. ## Timers {#timers} A timer lets `TransformComputation` say, “wake me up when `EventWatermark` reaches time X.” This is the main tool for delayed processing, joins with waiting, and windowed aggregations. Timers are closely tied to watermarks: by default, `EventWatermark` determines when a timer fires. For more details on how timers work, their configuration, and the API, see the [Timers](https://ytsaurus.tech/docs/en/timers.md) section. ## Configuration {#configuration} You configure `WatermarkStrategy` (`EventTimestampAssigner`, `WatermarkGenerator`, `WatermarkAlignment`) in the [Spec](https://ytsaurus.tech/docs/en/spec.md#watermark-strategy) section. ## See also - [Timers](https://ytsaurus.tech/docs/en/timers.md) - [Message processing order](https://ytsaurus.tech/docs/en/ordering.md) - [Spec and DynamicSpec](https://ytsaurus.tech/docs/en/spec.md) # Timers in YTsaurus Flow ## Why you need timers {#why-timers} Many stream processing tasks require waiting. For example, you might need to treat a conversion as failed if no click arrives within 30 minutes after an impression. The standard message exchange between computations isn’t suitable for this: a message is either present or not, and you can’t wait for an event’s “absence”. Timers solve this problem. A process function in transform mode can create a timer — tell the system, “Wake me up when time X arrives.” When the moment comes, the function receives a `ProcessTimer` / `on_timer` / `onTimer` call and can make a decision based on the accumulated [state](https://ytsaurus.tech/docs/en/glossary.md#state). Typical use cases: - **Join with wait**: correlate an ad impression with a click that might arrive with a delay. - **Timeout**: handle a situation where the expected event never arrives. - **Windowed aggregations**: close a time window and output the result when the accumulated data becomes stale. ## How timers work {#how-timers-work} ### Lifecycle {#lifecycle} 1. **Registration**. The computation calls `output.add_timer` / `output.addTimer` / `output->AddTimer`, passing `TriggerTimestamp` and `EventTimestamp`. 2. **Storage**. The timer is reliably stored in YTsaurus and additionally cached in the process memory. 3. **Triggering**. When the [EventWatermark](https://ytsaurus.tech/docs/en/watermarks.md#event-watermark) (or another configured watermark) exceeds `TriggerTimestamp`, the timer is delivered to the computation. 4. **Processing and deletion**. The processing result and the timer deletion are recorded in a single transaction — this ensures exactly-once guarantees (see [Deduplication](https://ytsaurus.tech/docs/en/glossary.md#deduplication)). ### Timer fields {#timer-fields} Each timer includes two timestamp fields: - `TriggerTimestamp` — the moment when the timer should trigger. The time scale (`event_time`, `system_time`, `real_time`) is set in the [configuration](https://ytsaurus.tech/docs/en/#configuration). - `EventTimestamp` — the business time of the original event, held in the timer. It lets you know when the triggering event occurred while processing the timer. The timer is also bound to the **[grouping key](https://ytsaurus.tech/docs/en/glossary.md#key)** (the same key that `Computation` uses), so it automatically lands in the correct partition and stays isolated from other keys. ## Which computations support timers {#supported-computations} | Process-function adapter | Timer support | |---|---| | `TProcessFunctionComputation` | ✓ | | `TProcessFunctionSwiftMapComputation` | ✗ | | `TProcessFunctionSourceComputation` | ✗ | | `TProcessFunctionTransformOrderedSourceComputation` | ✗ | ## Configuration {#configuration} To enable timers in a computation, you must fill the `timers` field in its spec. Each array element describes a single timer stream with the following parameters: Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `time_type` | **Type**: [NYT::NFlow::ETimeType](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_ETimeType) **Default value**: `event_time` The time type used by the timer: `event_time`, `system_time`, `real_time` || || `streams` | **Type**: `std::optional>>` List of streams that the timer monitors. || || `streams_with_delays` | **Type**: `std::optional, `[TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration)`>>` List of streams that the timer monitors with an individual delay specified || || `deduplicate_equal_timestamps` | **Type**: `bool` **Default value**: `true` Enables timer deduplication: if you try to create a timer with the same key and `trigger_timestamp`, only one timer remains — the one with the smaller `EventTimestamp` value || |# Explanations: - `time_type` defines the scale that the system uses to compare `TriggerTimestamp` with the current watermark: - `event_time` (default) — compared with `EventWatermark` across all `input` streams (or the streams listed in `streams`). - `system_time` — compared with `SystemWatermark`. - `real_time` — compared with real astronomical time. - `streams` / `streams_with_delays` — let you limit which input streams contribute to the watermark calculation for this timer. `streams_with_delays` also lets you set an individual delay for each stream. - `deduplicate_equal_timestamps` — merges timers in the same stream with the same key and `TriggerTimestamp`, keeping the smallest `EventTimestamp`. Deduplication is not guaranteed: duplicates are possible. Enabled by default. ## Processing order {#timer-ordering} Within one timer stream, timers for the same key are processed in `TriggerTimestamp` order. Processing order across timer streams is not guaranteed. To make timer A fire after timer B, add stream B to the `streams` field in timer A's spec. Timer A will then wait for stream B's watermark to advance. ## Blocking related timer streams {#timer-stream-blocking} When output limits block a timer stream, delivery of ready timers pauses for its entire group, even if the blocked stream has no registered timers. Other groups can continue processing. Timer streams belong to the same group if they use the same `time_type` (`event_time` or `system_time`) and their watermark input sets overlap. Grouping is transitive: streams that wait for `{a, b}`, `{b, c}`, and `{c, d}` form one group. Differences in `streams_with_delays` values do not split a group. All `real_time` timer streams form a separate group, regardless of their configured inputs and delays. Groups include all timer streams configured for the computation, not just those with registered timers. Grouping does not add ordering guarantees across streams. ## Timer structure {#timer-structure} Source: [yt/yt/flow/library/cpp/common/timer-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/timer-inl.h) #| || **Parameter** | **Description** || || `message_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** Unique timer ID. || || `system_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Timestamp of the timer creation. || || `event_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Timestamp of the real event associated with this timer. || || `stream_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** The stream this message belongs to. || || `key` | **Type**: `NYT::TStrongTypedef` **Required parameter** Key of the timer. || || `key_schema` | **Type**: `NYT::TIntrusivePtr` **Required parameter** Key schema, matches the `group_by_schema` of the corresponding `Computation`. || || `trigger_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Time for the timer to fire. || |# ## API by language {#api} ### C++ {#api-cpp} ```cpp void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& /*context*/) override { output->AddTimer( TSystemTimestamp(message->EventTimestamp.Underlying() + TDuration::Minutes(30).Seconds()), message->EventTimestamp); } void ProcessTimer( const TInputTimerConstPtr& timer, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override { // timer->Key, timer->EventTimestamp, timer->TriggerTimestamp auto builder = context->MakeOutputMessageBuilder(); // ... output->AddMessage(builder.Finish()); } ``` For more details, see [Process functions (C++)](https://ytsaurus.tech/docs/cpp/process-functions.md). ### Java {#api-java} ```java // Create a timer in onMessage: output.addTimer(message.getEventTimestamp() + 30 * 60_000_000_000L, message.getEventTimestamp()); // Process the triggered timer (RowFunction): @Override public void onTimer(Timer timer, OutputCollector output, RuntimeContext ctx) { // timer.getKey(), timer.getEventTimestamp(), timer.getTriggerTimestamp() } // For BatchFunction: @Override public void onTimers(List timers, OutputCollector output, RuntimeContext ctx) { ... } ``` For more details, see the [Computation (Java)](https://ytsaurus.tech/docs/java/computation.md) section. ### Python {#api-python} ```python # Create a timer in on_message: output.add_timer(trigger_timestamp=message.event_timestamp + 30 * 60_000_000_000, event_timestamp=message.event_timestamp) # Process the triggered timer (RowFunction): def on_timer(self, timer, output, ctx): # timer.key, timer.event_timestamp, timer.trigger_timestamp, timer.stream_id # For BatchFunction: def on_timers(self, timers, output, ctx): for timer in timers: ... ``` For more details, see the [Computation (Python)](https://ytsaurus.tech/docs/python/computation.md) section. ### Go {#api-go} ```go // Create a timer in OnMessage: out.AddTimer(flow.TimerRequest{ TriggerTimestamp: msg.EventTimestamp + 30*60_000_000_000, EventTimestamp: msg.EventTimestamp, }) // Process the triggered timer (RowFunction): func (f myFunction) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { // timer.Key, timer.EventTimestamp, timer.TriggerTimestamp, timer.StreamID return nil } // For BatchFunction: func (f myFunction) OnTimers(ctx context.Context, rt flow.Runtime, timers []flow.Timer, out flow.OutputCollector) error { ... } ``` For more details, see the [Computation (Go)](https://ytsaurus.tech/docs/go/computation.md) section. ## Examples {#examples} Example implementation of a join with wait (impression + click, 30-minute timeout): - [C++](https://ytsaurus.tech/docs/cpp/examples/wait_click_join.md) - [Java](https://ytsaurus.tech/docs/java/examples/wait_click_join.md) - [Python](https://ytsaurus.tech/docs/python/examples/wait_click_join.md) - [Go](https://ytsaurus.tech/docs/go/examples/wait_click_join.md) ## Limitations and known issues {#limitations} {% note warning %} **Process memory**. In the current implementation, all active timers are additionally stored in the process memory, on top of YTsaurus. With a large number of timers, this can lead to Out of Memory errors when jobs start. {% endnote %} - Swift-map mode doesn’t support timers. If you need timer functionality, run the process function under `TProcessFunctionComputation`. - Timestamps are transmitted in nanoseconds (uint64). ## See also - [Watermarks and Timers](https://ytsaurus.tech/docs/en/watermarks.md) - [Stateful processing](https://ytsaurus.tech/docs/en/stateful.md) - [Computation (C++)](https://ytsaurus.tech/docs/cpp/computation.md) - [Computation (Java)](https://ytsaurus.tech/docs/java/computation.md) - [Computation (Python)](https://ytsaurus.tech/docs/python/computation.md) - [Computation (Go)](https://ytsaurus.tech/docs/go/computation.md) # Key Visitor Streams in YTsaurus Flow ## Why you need key-visitor streams {#why-key-visitors} Stateful computations accumulate an [internal state](https://ytsaurus.tech/docs/en/glossary.md#state) per key while processing incoming messages. You often need to periodically scan this state—even without an incoming message for a specific key. Common tasks include: - **TTL / eviction**: iterate over all keys and delete outdated records. - **Periodic aggregates**: emit the current value for each key once a day. - **Forced re-evaluation**: trigger a recalculation for all keys in the background. Standard timers (see [Timers](https://ytsaurus.tech/docs/en/timers.md)) aren’t suitable here. You must register them for each key in advance, but the set of keys can grow without an explicit “new key appeared” event. For example, keys may arrive only via state stores, not through a message stream. A key-visitor stream solves this problem. A background task in the worker periodically scans the entire state of a partition and emits a `TVisit` message for each key into a special internal stream. The process function subscribes to this stream via `ProcessVisit` (C++) or `on_visit` / `OnVisit` / `onVisit` (Python / Go / Java) and decides how to handle its state, just like it would for a regular incoming message. ## How it works {#how-it-works} ### Pass lifecycle {#pass-lifecycle} 1. **Background fill**. Each partition runs a background loop. It reads the state page by page using `KeyStates::List` and passes the keys to an internal visit buffer. The speed is regulated by a throttler configured so that one full pass takes the specified `Period`. 2. **Emit**. Ready `TVisit` messages are delivered by the engine via `GetNextBatch` and reach the process function in `ProcessVisit`. 3. **Coverage**. After visits for a key range are delivered to the consumer, the range is marked as *Committed* in `TKeyVisitorStore`. The coverage is persisted to the system table `key_visitor_states`. That’s why a worker restart or partition rebalance doesn’t cause a re-scan. 4. **End of pass**. When the coverage is complete, the background loop immediately calls `StartNewPass`. The pace is set by the throttler, so the next pass still takes `Period`. Rotation is atomic: a single Sync-transaction deletes the previous pass’s rows and seeds the first interval of the new one. If a crash happens midway, it rolls back, and the coverage is preserved. 5. **Final pass**. With the default `finite=%true`, when every followed stream is Completed (by default, all input and source streams—see `upstream_streams`), the visitor finishes after a final pass and its partitions become `Completed`. With the default `full_final_pass=%true`, that pass starts after the inputs finish and guarantees at least one complete scan. Setting `full_final_pass=%false` marks the current pass final and can finish sooner without that guarantee. With `finite=%false`, there is no final pass and the visitor keeps scanning. ### Partitioning {#partitioning} The state is partitioned by the uint64 hash of the first column in `group_by_schema` (usually `farm_hash(key)`). Each partition uses its own `TKeyVisitor` to scan its hash range. Inside a partition, the range is split into a statically defined number of *buckets* (`bucket_count`). Buckets are scanned in round-robin order. This smooths the load and keeps the “unfinished coverage” evenly distributed across the partition. That way, if a worker crashes, it doesn’t discard progress in one narrow area. ## Guarantees {#guarantees} | Property | Guarantee | |---|---| | Each key per period | You get exactly one visit per key (no duplicates or omissions). | | Worker restart | Coverage is preserved; rotation is atomic, and a crash keeps the old pass. | | Partition rebalance | The new worker sees the committed coverage via `key_visitor_states`. | | Completion of followed streams | With default `finite=%true`, the visitor completes after the inputs and sources selected by `upstream_streams` finish. Default `full_final_pass=%true` guarantees a full pass started after completion; `%false` may finish during the current pass. | | `finite=%false` | No final pass: the visitor scans while the pipeline runs. | | `Period` | Best-effort. Under throttler load or slow `KeyStates` reads, the achieved period grows. See observability below. | | Scan order | Within a bucket, keys are sorted; between buckets, the order is round-robin. Not event-time. | ## Which computations support visit streams {#supported-computations} | Process-function adapter | Visit stream support | |---|---| | `TProcessFunctionComputation` | ✓ | | `TProcessFunctionSwiftMapComputation` | ✓ (only for state handling: emitting to output from `ProcessVisit` is forbidden, see [Swift](https://ytsaurus.tech/docs/en/swift.md#swift-map)) | | `TProcessFunctionSourceComputation` | ✗ | | `TProcessFunctionTransformOrderedSourceComputation` | ✗ | ## Configuration {#configuration} To make a computation accept a visit stream, you must fill the `key_visitor_streams` field in its static spec: ```yson "tester" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "NYT::NFlow::NMyProject::TMyFunction"; "group_by_schema" = [...]; "key_visitor_streams" = { "visit_iter" = {}; }; ... }; ``` ### Schema requirements {#schema-requirements} - `group_by_schema` must start with a `uint64` column. This column is used as the hash to split the partition into buckets. The check runs at spec submit time. ### Static spec parameters {#static-params} - `names`: a list of internal-state names to count keys from. If unspecified or empty, all keys from all internal states of the computation are used. - `external_names`: a list of [external-state managers](https://ytsaurus.tech/docs/en/glossary.md#state) and visitor-driven external-state joiners (see [Static table joiner](https://ytsaurus.tech/docs/en/#static-table-joiner)) for the computation, whose tables are used to count keys. This lets the visitor scan the external state (including that of companion computations) and evict outdated records via `clear()` in the visit handler. If only `external_names` is specified (without `names`), only the listed external state is scanned; internal states aren’t scanned. If neither `names` nor `external_names` is specified, all internal states are scanned, but external state and joiners aren’t; a joiner is scanned only if explicitly listed in `external_names`, and in no more than one visit stream of the computation (checked at spec submit time). - `upstream_streams`: which of the computation’s input and source streams the visitor follows, that is, whose completion it waits for before the final pass (see [Pass lifecycle](https://ytsaurus.tech/docs/en/#pass-lifecycle)). When unspecified, all input and source streams of the computation. Only meaningful with `finite=%true` (see [Dynamic spec parameters](https://ytsaurus.tech/docs/en/#dynamic-params)). For when the list needs narrowing, see [`streams_dependency`](https://ytsaurus.tech/docs/en/#deps). ### Dynamic spec parameters {#dynamic-params} In `dynamic_spec.computations..key_visitor_streams.`: - `period`: the target duration for one full pass. Default is 1 day. - `max_scan_rows_per_iteration`: the limit for a single `KeyStates::List` call (in rows, not keys). It must be strictly greater than the maximum number of internal-state names per key; otherwise, the scan stalls (see [Diagnostics](https://ytsaurus.tech/docs/en/#diagnostics)). - `buffer_row_limit`: the maximum size of the internal buffer of ready visits between the background fill and `GetNextBatch`. - `finite`: whether the visitor follows the computation inputs and sources to completion. Default `%true` performs a final pass after they finish, then completes the partitions. Set `%false` for a periodic scanner that must run throughout the pipeline lifetime, especially a computation with no input or source streams. This dynamic setting can be switched on a running pipeline to request completion. Switching back to `%false` resumes scanning only before a pass has been marked final; once partitions are `Completed`, they must be recreated by repartitioning. - `full_final_pass`: whether the final pass must be a complete scan begun after the inputs finish. Default `%true` guarantees at least one such scan. With `%false`, the current pass can be marked final and stop sooner, without that guarantee. - `background_fill_period`: the pause between iterations of the background loop in idle state. Each such iteration performs one read (`KeyStates::List`) per scanned source. So this parameter sets the base frequency of the visitor’s read requests—about `1 / background_fill_period` per second per source per partition (with the default of 500 ms, that’s about 2 reads/s), independent of `period` (which only affects the width of the hash slice for each read). Under load (cap-hit, bucket/pass change), iterations are rescheduled immediately, and the frequency can be higher. ### Computation with only a visit stream {#key-visitor-only} A key-visitor stream can be the **only** source of work for a computation: `input_stream_ids` is empty, there are no `source_streams`, and work comes only from scanning the external state (`external_names`). This is the auditor pattern: periodically re-evaluate keys in an external table without a message stream. Such a computation is partitioned by uint64 hash ranges in the same way as an input-driven one (see [Partitioning](https://ytsaurus.tech/docs/en/#partitioning)); the number of partitions is set by `min_partition_count` / `max_partition_count` / `desired_partition_count` in the dynamic spec. The requirement for `group_by_schema` (first column must be `uint64`, see [Schema requirements](https://ytsaurus.tech/docs/en/#schema-requirements)) is mandatory for such a computation—the partition ranges are built from it. Set `finite=%false` in the [dynamic parameters](https://ytsaurus.tech/docs/en/#dynamic-params) for this input-free scanner. With default `%true`, there are no inputs to wait for, so its first pass is final and the partitions complete after one full scan. {% note warning %} **One external table, one writer.** A visitor computation modifies the external state. Don’t modify the same external table from multiple computations: concurrent writes from different computations aren’t serialized and lead to races and lost updates. At spec validation time, YT path ownership is checked: if two computations declare writes (via their external-state managers) to the same `(cluster, path)`, the spec submit fails with the `claimed for writing` error. {% endnote %} ### Static table joiner {#static-table-joiner} The scan can read not only the computation’s state but also an external **static** table—via the external-state joiner `TStaticTableKeyVisitorJoiner`, listed in `external_names`. The table must be strictly sorted by the computation’s `group_by_schema`: the prefix of its key columns must match that schema in names and types. The joiner reads the table sequentially, in the same key order as the state scan, and passes the table row to `ProcessVisit` as a read-only state for the visit key. Table keys take part in the scan on an equal footing with state keys: a visit arrives even for a key that isn’t yet in the computation’s own state. This is the basis of the reconciliation pattern: a periodic scan aligns the computation’s own state with the external table—keys present in the table are updated, and keys missing from it are deleted. Requirements for the table, behavior when the source is unavailable, and a code example are in the [TStaticTableKeyVisitorJoiner](https://ytsaurus.tech/docs/cpp/state.md#static-table-key-visitor-joiner) section. ### `streams_dependency` {#deps} By default, the visit stream is **not** part of the computation’s `streams_dependency`: visitors are usually for internal cleanup and don’t produce output, and a stuck visitor shouldn’t block the completion of output streams. The “last pass” signal is delivered to the visitor locally by the worker (`SetUpstreamCompleted`)—this doesn’t require an edge in the graph. If the process function **emits messages to output from `ProcessVisit`**, you must explicitly list the visit stream as the parent of that output in `streams_dependency`. Example: ```yson "streams_dependency" = { "visits" = ["keys"; "visit_iter"]; }; ``` Here, `visits` is the output stream, `keys` is the input, and `visit_iter` is the key-visitor stream. For a computation with only a visit stream (no input), there’s a single parent: `"visits" = ["visit_iter"]`. If that output comes back to the same computation as an input stream, you get a **cyclic topology**: the cycle `visit_iter → requests → … → responses` closes on the computation’s own input stream. By default, the visitor follows that input, but it only completes once the visitor stops scanning the state. In a production pipeline this doesn’t matter: sources are infinite, input streams never complete, and no value of `upstream_streams` starts a final pass. It matters where all pipeline sources are [finite](https://ytsaurus.tech/docs/python/testing.md) and the pipeline must reach [`completed`](https://ytsaurus.tech/docs/en/glossary.md#start-stop-pause-pipeline), that is, in [integration tests](https://ytsaurus.tech/docs/python/testing.md). There, keep in `upstream_streams` (see [Static spec parameters](https://ytsaurus.tech/docs/en/#static-params)) only the input streams that don’t depend on the visitor: ```yson "input_stream_ids" = ["keys"; "responses"]; "output_stream_ids" = ["requests"]; "key_visitor_streams" = { "visit_iter" = { "upstream_streams" = ["keys"]; }; }; "streams_dependency" = { "requests" = ["keys"; "visit_iter"]; }; ``` Here, `responses` is produced by the visitor via `requests`, so the visitor follows `keys` only. ## Observability {#observability} ### Visit EventTimestamp {#event-timestamp} Each emitted `TVisit` carries an `EventTimestamp` equal to the expected scan time for that key. The formula is: ``` scheduleLag = max(0, elapsed − ScannedFraction · Period) EventTimestamp = now − scheduleLag ``` where `elapsed` is the time since the start of the current pass (taken as the minimum `PassStartedAt` among already scanned intervals; it’s persisted in `key_visitor_states`, so it survives restarts and rebalances). If the scan runs on schedule, `scheduleLag = 0`, and `EventTimestamp ≈ SystemTimestamp`. If you fall behind (for example, the throttler limits throughput due to a slow backend), `EventTimestamp` lies in the past, and the visit stream’s watermark lags by exactly the amount of the delay. This gives a direct signal to the downstream consumer and shows up as event-lag in standard flow metrics. ### Scheduled visit rate {#scheduled-visit-rate} In the visit stream's inflight metrics, `new_count_per_sec` and `offered_count_per_sec` both estimate the rate required to visit all matching keys within `period`: estimated distinct key count divided by the period in seconds. This is not the actual buffer fill rate. `ready_count` reports buffered visits plus an estimate of overdue visits in the unscanned part of the current pass, and `processed_count_per_sec` reports committed processing. The estimate uses the density of distinct matching keys in recently scanned hash ranges: smoothed key count divided by smoothed hash coverage, multiplied by the partition's hash span. Both counters use the same observation timestamps and a 30-second EMA window. Before the window matures, the ratio of cumulative counts and coverage provides an initial estimate. Empty reads contribute coverage with zero keys and can lower the estimated density; an estimated zero does not mean that the visitor is `Empty`. The counters are not reset between passes. With no new reads, including while the buffer is full, the estimated rate stays unchanged. No population or EMA state is persisted: after restart or repartitioning, the job estimates density from new local observations, independently of the restored scan cursor. Without positive hash coverage, the rate is absent; this includes ranges with identical hash values at both boundaries, even after a complete pass. Small samples, uneven hash distribution, and capped reads within one hash can cause transient bias. Changes in unscanned regions are not immediately visible. Once the finite visitor becomes `Empty`, its New and Offered rates are zero. ### Diagnostics {#diagnostics} - **`max_scan_rows_per_iteration` is too small**. If a single key has more internal-state names than `max_scan_rows_per_iteration`, the scan can’t progress: the limit is hit mid-key, all its rows are discarded, and no progress is made. The computation reports the error `/key_visitor//scan_cap_stall` via `StatusProfiler` (`Key visitor stalled: a single key has more than max_scan_rows_per_iteration = N rows`). The error clears automatically once the read progresses. The solution is to increase `max_scan_rows_per_iteration` via `Reconfigure`. - **State backend is unavailable**. If reading the state during a scan fails, the worker doesn’t crash: an error is reported to `/key_visitor//background_fill`, and the read is automatically retried. A transient YT/RPC error resolves on retry; a persistent issue (schema mismatch, missing table) remains visible in the status and doesn’t break the pipeline. # Processing guarantees in YTsaurus Flow ## Overview {#overview} Flow provides **exactly-once semantics** for event processing by default. This means: - Consumers get the result of processing each input message exactly once, with no losses or duplicates. - The [state](https://ytsaurus.tech/docs/en/stateful.md) is updated atomically along with message processing. If needed, you can relax the semantics to [**at-least-once**](https://ytsaurus.tech/docs/en/#at-least-once) or [**at-most-once**](https://ytsaurus.tech/docs/en/#at-most-once) — for example, to reduce latency or lower the pipeline’s resource consumption. ## Exactly-once semantics {#exactly-once} ### How it works {#how-it-works} Exactly-once is ensured by three mechanisms that work together. #### 1. Lease transaction {#lease} A `Lease` is a controller-owned master transaction that the controller creates for each [job](https://ytsaurus.tech/docs/en/glossary.md#job). Users do not create the Lease or control its lifetime. The controller keeps the Lease active with periodic pings and checks the state of all Leases on every scheduling cycle. Every ordinary transaction in which a job commits [epoch](https://ytsaurus.tech/docs/en/glossary.md#epoch) data lists that job’s Lease as a prerequisite in `prerequisite_transaction_ids`. The Lease must remain active for the commit to succeed. A Lease ends in one of two ways. The controller aborts it whenever it removes the job: during partition rebalancing, after a job failure or a lost worker, or when the pipeline stops, pauses, or completes. A Lease also expires on its own if the controller stops keeping it alive with pings. Both routes activate the same barrier. After the Lease is aborted or expires, commits guarded by that prerequisite are rejected, so a zombie job cannot write data after the controller no longer considers it the owner of its [partition](https://ytsaurus.tech/docs/en/glossary.md#partition). Even when it has no data, a job periodically commits a nearly empty transaction with the same prerequisite to detect that its Lease has been lost. When a Lease expires, the controller notices it on the next scheduling cycle and removes the job it belonged to. While the pipeline remains active and scheduling continues, after a Lease is aborted or expires the controller removes the old job if it is still present, schedules the necessary replacement jobs for the affected current partitions, and creates a new Lease for each replacement job. Each replacement restores state from the last committed epoch, resumes processing the affected current partitions, and may reprocess only uncommitted work. When the pipeline completes, stops, or pauses, the controller instead removes jobs, terminates their Leases, and schedules no replacements. Lease is a commit-time ownership barrier. By itself, it does not perform [input message deduplication](https://ytsaurus.tech/docs/en/#input-dedup), provide [output data atomicity](https://ytsaurus.tech/docs/en/#output-atomicity) or [state atomicity](https://ytsaurus.tech/docs/en/#automatic-guarantees), or cover [arbitrary external side effects](https://ytsaurus.tech/docs/en/#side-effects). #### 2. Input message deduplication {#input-dedup} - **Internal streams** (`input`): deduplication by `message_id` using the internal `input_messages` table or its compact variant `compact_input_messages`. The compact variant is selected automatically when `experimental_enable_non_uint_key` is disabled (partitioning is done exclusively by a `uint64` hash column); you can override this behavior with the `use_compact_input_messages` parameter. Older `message_id` values are removed from this table using [SystemWatermark](https://ytsaurus.tech/docs/en/glossary.md#timestamps-and-watermarks). - **Sources** (`source`): deduplication by offsets — consumer offsets advance only after a successful [epoch](https://ytsaurus.tech/docs/en/glossary.md#epoch) commit. - **[Timers](https://ytsaurus.tech/docs/en/glossary.md#timer)**: the result of timer processing is committed in the same transaction as the timer’s deletion. #### 3. Output data atomicity {#output-atomicity} The way output messages are delivered depends on the [sink](https://ytsaurus.tech/docs/en/glossary.md#sink) type: - **Synchronous sinks** (for example, `TSyncQueueSink`) write data to the target system in the same epoch transaction. The write and acknowledgment are atomic. - **Asynchronous sinks** (for example, `TAsyncQueueSink`) synchronously store messages in YTsaurus in the `output_messages` table, then send messages to the target system after the epoch is committed, using the `(producer_id, seqNo)` pair for deduplication on the receiver side (Queue API). If sending fails, messages from `output_messages` are sent again with the same `seqNo`. This essentially implements the [transactional outbox](https://en.wikipedia.org/wiki/Inbox_and_outbox_pattern#The_outbox_pattern) pattern with deduplication in the target system, if it supports it. ### What the developer must do {#developer-responsibilities} Flow’s exactly-once guarantees apply to the pipeline’s internal state and built-in delivery mechanisms. However, your custom code can break these guarantees if you don’t follow these rules. #### Computation determinism {#determinism} Determinism requirements depend on the computation type. - **For [Swift](https://ytsaurus.tech/docs/en/swift.md) computations** (`TSwiftMapComputation`, `TSwiftOrderedSourceComputation`) determinism is strictly required. In Swift computations, computation results aren’t materialized between epochs and can be recalculated on restart. If different attempts to compute the same message yield different results, you might mix results from different attempts, or lose or duplicate data. - **For regular computations** (`TTransformComputation`, `TTransformOrderedSourceComputation`) non-deterministic code is technically allowed: exactly one attempt (out of all attempts caused by job restarts) is applied atomically. No result mixing occurs. However, if your code produces different output on a rerun, it can lead to unexpected behavior — especially if it has side effects (like writing to external systems) or updates state based on non-deterministic values. {% note warning "What breaks determinism" %} - Using the current time (`Now()`, `time.time()`) in business logic. - Generating random numbers. - Calling external services whose results can change between calls. {% endnote %} #### Side effects {#side-effects} Any action outside Flow — an HTTP request, writing to a file, sending a notification — **isn’t covered** by exactly-once guarantees. If a job restarts, such an action might be executed again. If side effects are unavoidable, you must ensure their idempotency yourself (for example, by using the message’s unique ID as an idempotency key). #### External state {#external-state} Updates to Flow’s [internal state](https://ytsaurus.tech/docs/en/stateful.md) (YSON-state, External State via `IExternalStateManager`) are atomic and safe. However, if a computation modifies data in an external system (a database, cache), those changes aren’t part of the epoch transaction and might be duplicated. ### What Flow guarantees automatically {#automatic-guarantees} - Internal streams between [computations](https://ytsaurus.tech/docs/en/glossary.md#computation) — exactly-once. - [State](https://ytsaurus.tech/docs/en/stateful.md) updates — atomic with message processing. - Timer processing and deletion — in the same transaction. - Advancing [source](https://ytsaurus.tech/docs/en/glossary.md#source) offsets — only after a successful epoch commit. ## At-least-once semantics {#at-least-once} At-least-once guarantees that each message is processed **at least once**, but allows repeated processing. This can be useful when your business logic is naturally idempotent and the cost of deduplication is unreasonably high. ### Mode `processing_mode: at_least_once_consistent` {#processing-mode} You set the `processing_mode` parameter at the `TTransformComputation` level in the spec. The default value is `exactly_once`. ``` "computations" = { "MyComputation" = { "processing_mode" = "at_least_once_consistent"; ... }; }; ``` **What changes**: input message deduplication by `message_id` is disabled. Messages might be processed again after a job restart. **What doesn’t change**: state remains consistent (that’s why “consistent” is in the mode name). Epoch transactionality, the Lease mechanism, and the atomicity of state updates all keep working. **When to use it**: when deduplication overhead is significant and your business logic is idempotent (for example, `insert_or_assign` instead of `increment`). ### Batching in Swift computations: `allow_batching_with_relaxed_guarantees` {#swift-allow-batching-with-relaxed-guarantees} You set the `allow_batching_with_relaxed_guarantees` parameter in the `parameters` block of a `TSwiftMapComputation` computation. The default value is `%false`. ``` "computations" = { "MyBatcher" = { "parameters" = { "allow_batching_with_relaxed_guarantees" = %true; }; ... }; }; ``` **What changes**: a single output message can combine multiple input messages (batching) — for example, to collapse many small messages into one large message and reduce the load on the number of messages per partition downstream. The combined message gets a `MessageId` derived deterministically from the set of its parents' `MessageId`s, and an input message is considered processed only after all its children are delivered. **What this means for guarantees**: on restart, the batch boundaries might differ. A replay that reproduces a batch with the same set of parents yields the same `MessageId` and is deduplicated; but if a parent ends up in a batch with a different composition, that batch's `MessageId` is different — downstream computations must be ready to see each parent’s content more than once (at-least-once). Also, the `MessageId` order within a single key isn’t preserved on the downstream computations’ side: any logic that relies on `MessageId` ordering within a key must be rewritten to account for this. **When to use it**: when you need to aggregate the message stream (batching/collapsing), and the downstream logic is idempotent and doesn’t depend on `MessageId` order within a key. With the flag disabled (default), each output message has exactly one parent and the Swift computation keeps exactly-once guarantees. ## At-most-once semantics {#at-most-once} At-most-once guarantees that each message is processed **no more than once**, but allows message loss. You achieve this semantics by configuring `at_most_once_strategy` on asynchronous sinks that support it. ``` "sinks" = { "my_sink" = { "at_most_once_strategy" = { "enabled" = %true; "total_queue_bytes_limit" = 104857600; // 100 MB "suspend_destruction_duration" = 60000; // ms }; ... }; }; ``` **How it works**: messages are placed in an internal queue. If the queue is full (`total_queue_bytes_limit`), new messages **are dropped without an error**. Deduplication and delivery guarantees are absent. **When to use it**: for telemetry, metrics, best-effort notifications — scenarios where losing some messages is acceptable and throughput is more important than reliability. ## Ordering guarantees {#ordering} Flow **doesn’t guarantee** global message processing order. However, there are guarantees for derived messages with matching [keys](https://ytsaurus.tech/docs/en/glossary.md#key) within a single [lineage](https://ytsaurus.tech/docs/en/lineage.md) chain. For details, see the [Message processing order](https://ytsaurus.tech/docs/en/ordering.md) section. ## How connectors affect guarantees {#connectors} Each [connector](https://ytsaurus.tech/docs/connectors/about.md) has its own set of guarantees, which depend on the connection type (source/sink) and the sink variant (sync/async/at-least-once). ### Queue (QYT) {#queue-guarantees} - **Source**: exactly-once — consumer offsets advance only after epoch commit. - **Synchronous sink** (`TSyncQueueSink`): exactly-once — writing to the queue happens in the main epoch transaction. Works only on the main processing cluster. - **Asynchronous sink** (`TAsyncQueueSink`): exactly-once — messages are stored in `output_messages`, then delivered with `producer_id` + `seqNo`. Queue API deduplicates repeats. Works cross-cluster. For more details, see [Queue](https://ytsaurus.tech/docs/connectors/queue.md). ### Static Table {#static-table-guarantees} - **Source**: exactly-once — deduplication by read ranges. - **Sink** (`TArrivalOrderTableSink`): exactly-once — the output table and its progress commit in a single master transaction; a per-partition frontier deduplicates replay, so a partially covered replay writes only the uncovered tail without restarting the job; the delivery callback fires only after that external commit and the following Flow commit. For more details, see [Static Table](https://ytsaurus.tech/docs/connectors/static-table.md). ### HTTP {#http-guarantees} - **Source**: none. - **Asynchronous sink** (`TAsyncHttpSink`): at-least-once — POST requests run outside the epoch transaction, so the receiver must make the operation idempotent or deduplicate the stable message ID sent in `Idempotency-Key` by default. Enabling `at_most_once_strategy` makes spec loading fail. For more details, see [HTTP extension](https://ytsaurus.tech/docs/extensions/http.md). ### Kafka {#kafka-guarantees} - **Source**: exactly-once — offsets are stored in the pipeline state and advance only after epoch commit. - **Asynchronous sink** (`TKafkaSink`): exactly-once by default — messages are stored in `output_messages` and written after epoch commit in Kafka transactions together with a progress marker, and a restarted sink skips the messages the marker covers. Consumers must read with `isolation.level = read_committed`. If the marker is lost while messages may already be committed, the sink stops instead of writing possible duplicates. With `delivery_guarantee = at_least_once`, the sink writes with an idempotent producer without transactions, and a restart may write again messages that are already in Kafka. - **Synchronous sink** (`TAtLeastOnceKafkaSink`): at-least-once — each epoch waits for Kafka to acknowledge its records. If the epoch does not commit after a successful write, the same records are written again on restart. For more details, see [Kafka extension](https://ytsaurus.tech/docs/extensions/kafka.md#guarantees). ### ClickHouse {#clickhouse-guarantees} The ClickHouse extension provides three guarantee levels across four sink classes; your choice directly affects message processing guarantees: - **Exactly-once sinks** (`TClickHouseBatchingSink`, recommended by default, and `TShardedClickHouseBatchingSink`): messages are stored in `output_messages` and deterministically grouped into batches by the same `MessageId` boundaries. In an unsharded sink, `insert_deduplication_token` equals the batch’s maximum `MessageId`. With `shard_hosts`, each shard receives that batch token with a `:` suffix. A byte-identical replay carries the **same** deduplication token even after `group_by` repartitioning, so exactly-once survives repartitioning. ClickHouse deduplicates the repeat by the token. - **At-least-once sink** (`TAtLeastOnceClickHouseSink`): the epoch batch is written synchronously during processing without intermediate storage and without a deduplication token. Lower latency and load on YTsaurus. A failed write prevents the epoch from committing, so messages are not lost. Known transient errors are retried without a limit; unclassified errors are attempted at most `max_insert_attempts` times in total (10 by default), and permanent errors fail immediately. On failure after a successful write but before epoch commit, the same batch is written again, so duplicates are possible. - **At-most-once sink** (`TAtMostOnceClickHouseSink`): the guarantee applies in both strategy modes. With the default static setting `at_most_once_strategy.enabled = false`, pending messages stay in `output_messages` and use ordered delivery, so a connection or session failure before `INSERT` starts may be retried. Set the option to `true` to send each message independently through a bounded in-memory queue without waiting for delivery. Its dynamic limit is `at_most_once_strategy.total_queue_bytes_limit`; the sink logs a warning when it drops messages on overflow. In either mode, once `INSERT` has started, a failed insert is acknowledged instead of retried. Each message generates a separate `INSERT`, so to avoid creating many small parts in ClickHouse, aggregate the stream with batching upstream in the pipeline. Exactly-once requires one of the ClickHouse table engines that deduplicate inserted blocks (see [Data Replication](https://clickhouse.com/docs/engines/table-engines/mergetree-family/replication) and [SharedMergeTree](https://clickhouse.com/docs/cloud/reference/shared-merge-tree) in the ClickHouse documentation): - `ReplicatedMergeTree` - `ReplicatedReplacingMergeTree` - `ReplicatedSummingMergeTree` - `ReplicatedAggregatingMergeTree` - `SharedMergeTree` Each sink instance creates its writer session lazily on the first write and keeps it until the instance stops. Complete target metadata validation runs when that session starts. Dynamic reconfiguration recreates clients when the write timeout changes and updates the relevant deduplication-window checks, but it does not repeat engine, schema, or replication-identity validation. After changing the target table, pause and start the pipeline so new sink instances validate it. `Distributed` tables, engines outside the `MergeTree` family, and multi-host or failover targets within a shard that cannot be proven to identify the same logical table are rejected. A single-host, non-replicated `MergeTree` without block deduplication is accepted with a warning, but exactly-once degrades to at-least-once. The deduplication window is finite and gets evicted: if a replay reaches ClickHouse later than the deduplication token’s eviction, exactly-once also degrades to at-least-once. If a known deduplication window is shorter than `replay_horizon`, the sink writes a structured warning with the attributes `Database`, `Table`, `DedupWindowSetting`, `ReplayHorizon`, and `DedupWindow`. Summary table of guarantees by sink class: #| || | **Exactly-once sink** | **At-least-once sink** | **At-most-once sink** || || Intermediate storage in YTsaurus | Yes (`output_messages`) | No | Yes with `at_most_once_strategy.enabled = false`; no with `true` || || Deduplication in ClickHouse | `insert_deduplication_token` (max `MessageId`; with `shard_hosts`, plus `:`) | No | No || || Latency | Higher (extra write) | Lower | Lower || || Loss on failure | No | No | Possible after `INSERT` starts; with `enabled = true`, also on queue overflow || || Duplicates on failure | No | Possible | No || |# For more details, see [ClickHouse extension](https://ytsaurus.tech/docs/extensions/clickhouse.md). ### Service log {#servicelog-guarantees} - **Source**: exactly-once — deduplication by read ranges. The service log is an infinite source that cycles through the table. Exactly-once guarantees apply within each cycle. - No sink. The sorted dynamic table used as a service log source is often populated by the pipeline itself via [external state](https://ytsaurus.tech/docs/en/stateful.md#external-state) operations. For example, a `TTransformComputation` type computation updates table records at each epoch. For more details, see [Service log](https://ytsaurus.tech/docs/connectors/servicelog.md). ## Fault tolerance {#fault-tolerance} - The [pipeline](https://ytsaurus.tech/docs/en/glossary.md#pipeline) survives the failure of individual machines and data centers. - While the pipeline remains active and scheduling continues, if a job fails, the controller removes it, aborts its [Lease](https://ytsaurus.tech/docs/en/#lease) if it is still active, and schedules the necessary replacement jobs for the affected current partitions, with a new Lease for each replacement job. See [Lease transaction](https://ytsaurus.tech/docs/en/#lease) for the fencing and recovery sequence. - Internal streams are stored in YTsaurus dynamic tables — data loss isn’t possible during normal storage operation. - [Automatic partition balancing](https://ytsaurus.tech/docs/about.md) redistributes load when the cluster topology changes. ## See also - [Flow overview](https://ytsaurus.tech/docs/about.md) - [Message processing order](https://ytsaurus.tech/docs/en/ordering.md) - [Timers](https://ytsaurus.tech/docs/en/timers.md) - [Stateful processing](https://ytsaurus.tech/docs/en/stateful.md) - [Connectors](https://ytsaurus.tech/docs/connectors/about.md) # Message processing order in YTsaurus Flow In a distributed streaming system, it’s not practical to ensure a strict global order for all messages: data comes from multiple sources and is processed in parallel across different [partitions](https://ytsaurus.tech/docs/en/glossary.md#partition) and [computations](https://ytsaurus.tech/docs/en/glossary.md#computation). Flow addresses this differently, using the `AlignmentTimestamp` mechanism. Each message carries an [AlignmentTimestamp](https://ytsaurus.tech/docs/en/#alignment-timestamp)—a monotonically increasing timestamp that defines the processing priority: - **Within a single [stream](https://ytsaurus.tech/docs/en/glossary.md#stream)**: messages are processed in the order `(AlignmentTimestamp, message_id)`—this gives a fully deterministic order. - **Across different [streams](https://ytsaurus.tech/docs/en/glossary.md#stream)**: a merging priority queue is used based on [StabilizedEventTimestamp](https://ytsaurus.tech/docs/en/#stabilized-event-timestamp) (calculated from `AlignmentTimestamp`) with an adjustment for [stream_delays](https://ytsaurus.tech/docs/en/spec.md#inputordering); if values are equal, ordering uses `TaskId`. In addition to prioritization, there’s an order guarantee for **derived messages**—see the [Order guarantees](https://ytsaurus.tech/docs/en/#ordering-guarantees) section for details. ## AlignmentTimestamp {#alignment-timestamp} Along with [SystemTimestamp and EventTimestamp](https://ytsaurus.tech/docs/en/glossary.md#timestamps-and-watermarks), each message in Flow includes the `AlignmentTimestamp` field—a timestamp used to align processing progress across streams and partitions. Rules for setting `AlignmentTimestamp`: - **Messages from source streams**: equal to `WriteTimestamp`, which is the time when the message was written to the persistent queue (for example, in QYT). - **Output messages from `TransformComputation`**: equal to `SystemTimestamp` at the time the message is created. - **In all other cases**: inherited unchanged from parent messages. ## StabilizedEventTimestamp {#stabilized-event-timestamp} `StabilizedEventTimestamp` is a computed timestamp equal to `AlignmentTimestamp` + `bias`, where `bias` is the average difference between `EventTimestamp` and `AlignmentTimestamp` within a stream. `bias` is calculated based on the messages in the stream that are currently being processed. It’s used as a non-decreasing substitute for `EventTimestamp` to prioritize the processing of messages from different streams. ## Order guarantees {#ordering-guarantees} Message order in a stream isn’t guaranteed in general, but there is a specific guarantee for derived messages. If, in the input stream, message A precedes message B, and they have derived messages A' and B' such that: - their [lineage](https://ytsaurus.tech/docs/en/lineage.md) keys match (meaning the [grouping keys](https://ytsaurus.tech/docs/en/glossary.md#key) are the same in all intermediate computations along the path from the source to the current computation), - and in the current computation, A' and B' share the same key, then A' will be processed before B'. ## Partition prioritization within a single stream {#partition-prioritization} Within a single source stream, partitions are prioritized by the `WriteTimestamp` of their messages: partitions with earlier messages are processed first. This prioritization is implemented through the processing order of output messages in subsequent computations. ## Cross-stream prioritization {#cross-stream-prioritization} Across different streams, prioritization is based on `EventTimestamp`, but indirectly: ordering actually happens by `AlignmentTimestamp` with a stream-specific adjustment. The adjustment is calculated as the average difference between `EventTimestamp` and `AlignmentTimestamp` among the messages currently in all output buffers for that stream: $$ordering\_priority = AlignmentTimestamp + avg(EventTimestamp − AlignmentTimestamp)$$ This approach accounts for each stream’s specifics (for example, the systematic delay between the time a message is written to the queue and the event time) and ensures a fairer cross-stream ordering. ## See also - [Computation](https://ytsaurus.tech/docs/en/computation.md) - [Watermarks](https://ytsaurus.tech/docs/en/watermarks.md) - [Timers](https://ytsaurus.tech/docs/en/timers.md) # Autoscaler The autoscaler adjusts the partition count of each [Computation](https://ytsaurus.tech/docs/en/computation.md). Several criteria propose counts; the controller selects the largest, subject to delays and the `min_partition_count` and `max_partition_count` limits. An explicit `desired_partition_count` sets the count manually. ## CPU {#cpu} The CPU criterion increases the partition count when average partition CPU usage exceeds `desired_average_partition_cpu_load`. This distributes processing across more jobs. As load decreases, the recommendation declines gradually. ## Throughput {#throughput} The Throughput algorithm adjusts the partition count based on message processing rates. It helps `Computation` nodes that wait for external services while using little CPU. The algorithm uses two quantities: - **Demand** is the required rate: new input plus the rate needed to catch up on backlog within the configured catch-up period. Messages from Source, timers and Key Visitor are propagated to each `Computation` using the input/output message ratios recorded in [lineage](https://ytsaurus.tech/docs/en/lineage.md). Ordinary batching backlog is excluded. - **Capacity** estimates what the current partitions can process. The total completed-work rate, `Processed`, is divided by the busiest job's busy-time share. Both quantities are averaged over a common window; waiting for input or output distribution is excluded from busy time. Each `Computation` then follows this cycle: 1. **Measurement.** The algorithm collects statistics from all current jobs over a common window. Job load determines whether Capacity before and after a partition-count change can be compared reliably. 1. **Growth.** If Demand persistently exceeds Capacity and the Computation is sufficiently loaded, the algorithm increases its partition count. The step depends on the capacity shortage; catch-up uses smaller steps than new input. 1. **Capacity comparison.** With sufficiently loaded jobs, the algorithm compares Capacity with the point before growth. If the gain justifies the additional partitions, exploration continues. Two probes without sufficient gain indicate a plateau: the algorithm keeps the larger count. 1. **Bounded exploration.** When capacities before and after growth cannot be compared reliably, the algorithm checks completed throughput, Processed. Growth is bounded by an allowance based on the original partition count; confirmed progress in Processed expands this allowance. Without progress, exploration stops. 1. **Recovery.** After a reliably measured capacity drop, the algorithm restores the original partition count and repeats measurement over a longer window. Repeated degradation appears in the Computation status. 1. **Downsizing.** Once catch-up is complete, the algorithm returns towards the beginning of the plateau while retaining capacity for Demand and twice the historical steady-demand peak. The peak is retained for `peak_demand_window` (nine days by default) and survives restarts; catch-up throughput does not contribute to it. Further reductions also retain spare capacity relative to Processed and are periodically batched across the pipeline. The historical peak only limits downsizing. Unsuccessful probe rollbacks run separately. At an unchanged plateau, the algorithm waits for confirmed improvement in measurements or an explicit search-state reset. Other criteria can still increase the partition count during Throughput warm-up. The Throughput algorithm is disabled by default. Enable it with `dynamic_spec.job_manager.partitioning.enable_throughput_autoscaler`. See the [Throughput configuration reference](https://ytsaurus.tech/docs/generated_docs/all_yson_structs.md#NYT_NFlow_TThroughputSpec). ## Timer memory {#timers} The timer criterion increases the partition count when the average number of timers per partition exceeds `desired_average_partition_timer_count`. This limits timer accumulation and memory use. A similar criterion tracks average job memory usage through `desired_average_partition_memory_used`. ## Weighted boundaries {#weighted-boundaries} With `experimental_enable_non_uint_key`, the controller can move partition boundaries using sampled keys (pivots). This works with both legacy count criteria and Throughput, including a fixed `desired_partition_count`. Key-count partitioning uses its own policy. With Throughput enabled, partition weights are their current input message rates (RPS). This also applies during warm-up and with a fixed partition count. With Throughput disabled, boundary selection uses the highest current average-to-target ratio among enabled criteria with partition weights. For example, CPU can determine the boundaries even when memory usage remains uneven. A retained historical count peak does not select the criterion. A partition without pivots remains indivisible. Its pivots are optional when its input message rate is below `0.5 * total_input_rate / current_partition_count`. A zero-flow partition needs no pivots. The denominator is the current count, including when planning growth. Missing status or input metrics defer weighted rebalancing. When preserving the partition count, the controller moves boundaries only if the candidate reduces estimated weight imbalance. The delay decreases with the current imbalance, from `partition_count_double_delay` down to one tenth of that value. Boundary moves do not restart the count-change cooldown. If growth lacks enough pivots, the controller can still rebalance at the current count. # Lineage in YTsaurus Flow Lineage (from English, “genealogy”) is information about which input [messages](https://ytsaurus.tech/docs/en/glossary.md#message) and [timers](https://ytsaurus.tech/docs/en/glossary.md#timer) a specific output result of a [computation](https://ytsaurus.tech/docs/en/glossary.md#computation) was derived from. You use this information during processing so the framework can compute metadata and ensure ordering guarantees. Note that this information isn’t stored in the output message itself. ## Why you need lineage {#why-lineage} The framework uses lineage for two purposes: 1. **Computing meta-fields.** Based on parent messages, Flow automatically populates the meta-fields of output messages, such as `EventTimestamp`, `AlignmentTimestamp`, and others. For Swift computations and [passthrough](https://ytsaurus.tech/docs/en/glossary.md#passthrough), `AlignmentTimestamp` is inherited from the parents unchanged. This ensures correct [prioritization](https://ytsaurus.tech/docs/en/ordering.md) of messages in downstream computations. 2. **Guaranteeing the order of derived messages.** If two messages share the same [grouping keys](https://ytsaurus.tech/docs/en/glossary.md#key) across the entire lineage chain from the source to the current computation, their relative processing order is preserved. For more details, see the section [Message Processing Order](https://ytsaurus.tech/docs/en/ordering.md#ordering-guarantees). ## Default behavior {#default-behavior} In most cases, you don’t need to manage lineage explicitly—the framework automatically sets the parents: | Function type | Parent of the output message | | --- | --- | | `IProcessFunction::ProcessMessage` / `RowFunction` | The current input message | | `IBatchProcessFunction::Process` / `BatchFunction` | All messages in the current batch | | Timer handler | The current timer | ## When to set lineage explicitly {#explicit-lineage} By default, the entire current batch is considered the parent of the output message. Setting lineage explicitly lets you narrow this set to a specific subset of input objects. This makes the computation of `EventTimestamp` and `AlignmentTimestamp` more precise. In a Swift computation the narrowing is mandatory: every output message must have exactly one parent (multiple only with [`allow_batching_with_relaxed_guarantees`](https://ytsaurus.tech/docs/en/guarantees.md#swift-allow-batching-with-relaxed-guarantees)). With per-message processing the framework assigns the parent automatically; in batch processing user code sets it. In a Transform computation explicit lineage is optional. ## API {#api} You set lineage using the `SetParents` / `set_parent_ids` / `setParentIds` / `WithParentIDs` method on the `OutputCollector` object. The method returns a **new** collector with the lineage context attached. All calls to `AddMessage` / `add_message` / `addMessage` on this collector will carry that lineage. For more details on how to use this in each language: - [C++](https://ytsaurus.tech/docs/cpp/process-functions.md) - [Java](https://ytsaurus.tech/docs/java/computation.md#output-collector) - [Python](https://ytsaurus.tech/docs/python/computation.md#output-collector) - [Go](https://ytsaurus.tech/docs/go/computation.md#output-collector) ## Conversion statistics {#conversion-statistics} Flow estimates the output-to-input ratio for each stream pair, separately for message count and byte size. These statistics describe processed messages, not guaranteed deliveries. ## See also - [Message Processing Order](https://ytsaurus.tech/docs/en/ordering.md) - [Computation](https://ytsaurus.tech/docs/en/computation.md) - [Core Concepts (Glossary)](https://ytsaurus.tech/docs/en/glossary.md) - [Computation (Go)](https://ytsaurus.tech/docs/go/computation.md) # Companion in YTsaurus Flow In Flow, you can run user code in a separate process. This process is called a companion process. ## Companion use cases ### Currently used - Supporting computations in languages other than [C++](https://ytsaurus.tech/docs/cpp/getting-started.md), such as [Python](https://ytsaurus.tech/docs/python/getting-started.md), [Java and Kotlin](https://ytsaurus.tech/docs/java/getting-started.md), and [Go](https://ytsaurus.tech/docs/go/getting-started.md). - Running user C++ code in a separate process through the [C++ companion](https://ytsaurus.tech/docs/en/#cpp-companion). ### Planned - Hot updating user code without stopping the [pipeline](https://ytsaurus.tech/docs/en/glossary.md#pipeline). ## Workflow {#schema} When you use a companion, the [Computation](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation) consists of two parts: a specialized Computation on the [Worker](https://ytsaurus.tech/docs/en/glossary.md#worker) side and a lightweight Computation on the companion side. {% note info %} You develop all business logic in the chosen programming language on the companion side, while the pipeline structure is still configured via the [spec](https://ytsaurus.tech/docs/en/spec.md). In this workflow, the worker becomes an infrastructure binary that doesn’t depend on the pipeline logic. So, when you use Python, Go, Java, or Kotlin, you don’t need to write any C++ user code. {% endnote %} The Computation on the Worker side collects a batch of messages, enriches it with all the information needed for processing (states, parameters, watermark values, and so on), and sends it to the companion via gRPC locally, within a single host. The batch is formed without regard to [keys](https://ytsaurus.tech/docs/en/glossary.md#key) — one request may contain messages with different keys; this is how the worker collects batches for all computations. There is no per-key grouping in the companion protocol: if the business logic needs per-key processing, it is done in the companion code — see the [Python](https://ytsaurus.tech/docs/python/computation.md#batch-function) example. The companion returns its output in groups; each group carries [lineage](https://ytsaurus.tech/docs/en/lineage.md) — the list of ids of the input messages of the batch its output was derived from (lineage is unrelated to keys). For when lineage must be set explicitly and what exactly to pass, see [When to set lineage explicitly](https://ytsaurus.tech/docs/en/lineage.md#explicit-lineage). In the future, you’ll also be able to use Unix sockets. ![](https://ytsaurus.tech/docs/_images/companion_v1.svg) You manage the companion process through the [resource](https://ytsaurus.tech/docs/en/glossary.md#resource) `CompanionManager`. ### Configuration Here’s an example of declaring the resource in a static spec for Java: ```yson "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TJavaCompanionManager"; "parameters" = { "timeout" = "10s"; "jdk_bin_path" = "/app/ytflow/jdk/bin/java"; "main_class" = "tech.ytsaurus.flow.examples.wordcount.WordCountApplication"; "classpath" = "/app/ytflow/lib/*"; }; "dependencies" = {}; }; ``` For a detailed description of all `TCompanionManagerParameters` parameters, see the section [CompanionManager resource configuration](https://ytsaurus.tech/docs/java/computation.md#companion-manager). The Python companion configuration is described in the section [CompanionManager resource configuration (Python)](https://ytsaurus.tech/docs/python/computation.md#companion-manager). Here’s an example of declaring a Computation in a static spec: ```yson "computations" = { "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(word)"; "type" = "uint64"; required = %true;}; {"name" = "word"; "type" = "string";}; ]; "input_stream_ids" = ["words"]; "output_stream_ids" = []; "required_resource_ids" = { "CompanionManager" = { "controller" = false; "worker" = true; }; }; "parameters" = { "internal_states" = ["word-state"]; }; }; }; ``` The key point in this example is using the `CompanionManager` resource to launch the companion process and the specialized Computation class `NYT::NFlow::NCompanion::TTransformCompanionComputation`. ### C++ companion {#cpp-companion} You can move user C++ code out of the worker into a separate process as well. The SDK is located in `yt/yt/flow/library/cpp/companion/server`: you declare the served Computations in `TPipeline`, specifying the process function type (this typed declaration replaces `YT_FLOW_DEFINE_PROCESS_FUNCTION`), and build a separate binary with the `RunCompanionMain` entry point: ```cpp int main(int argc, const char** argv) { NYT::NFlow::NCompanionServer::TPipeline pipeline; pipeline.AddSource("reader"); pipeline.AddTransform("mapper"); return NYT::NFlow::NCompanionServer::RunCompanionMain(argc, argv, std::move(pipeline)); } ``` The function is selected by the name from the `processing_function` field of the Computation spec, the same way as in the in-process `TProcessFunctionComputation` adapters. The worker launches the binary through the generic `TCompanionManager` resource: ```yson "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { "entrypoint" = { "executable" = "/path/to/my_companion"; }; }; }; ``` A process function in a companion can take the HTTP clients from `IRuntimeInitContext` in `Init` (`GetHttpClient()`, `GetHttpsClient()`). The clients are configured by the `http_client_config`, `https_client_config` and `http_poller_threads` fields of the `companion` block of the worker node config ([TCompanionConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs.md#NYT_NFlow_NCompanion_TCompanionConfig)). Limitations of the first version of the C++ companion: - sync process functions aren’t supported (the companion protocol has no Sync phase); - static [resources](https://ytsaurus.tech/docs/en/glossary.md#resource), distributed throttlers, and the epoch timestamp (`GetCurrentTimestamp`) aren’t available; - `GetStreamSpecs()->ComputeKey()` can’t compute a key when `group_by_schema` has computed columns: a companion doesn’t evaluate expressions. The key arrives with the message — use `message->Key`; - external states are supported only as `TSimpleExternalState`; - output timers can only reference the key of one of the parent entities of the batch; - the companion runs as a single multithreaded process (`companion_process_count` is 0 or 1). For an example, see `yt/yt/flow/examples/cpp/companion_word_count`. ### Types of Computations for working with companions {#computation-types} - `NYT::NFlow::NCompanion::TSwiftMapCompanionComputation`: An implementation of [TSwiftMapComputation](https://ytsaurus.tech/docs/en/computation.md#tswiftmapcomputation) that delegates data processing to the companion process. - `NYT::NFlow::NCompanion::TSwiftOrderedSourceCompanionComputation`: An implementation of [TSwiftOrderedSourceComputation](https://ytsaurus.tech/docs/en/computation.md#tswiftorderedsourcecomputation) that delegates data processing to the companion process. - `NYT::NFlow::NCompanion::TTransformCompanionComputation`: An implementation of [TTransformComputation](https://ytsaurus.tech/docs/en/computation.md#ttransformcomputation) that delegates data processing to the companion process. - `NYT::NFlow::NCompanion::TTransformOrderedSourceCompanionComputation`: An implementation of [TTransformOrderedSourceComputation](https://ytsaurus.tech/docs/en/computation.md#ttransformorderedsourcecomputation) that delegates data processing to the companion process. Two modes are available for a Source computation. `TSwiftOrderedSourceCompanionComputation` doesn’t materialize the output and requires deterministic processing without user state. `TTransformOrderedSourceCompanionComputation` materializes the output and commits it together with the internal state and the source offset in the epoch transaction. Choose it for non-deterministic processing or for working with internal state; the key of such a state is the source partition key. The spec limitations are the same as for [TTransformOrderedSourceComputation](https://ytsaurus.tech/docs/en/computation.md#ttransformorderedsourcecomputation). For more details on implementing pipelines using companions, see [Java and Kotlin](https://ytsaurus.tech/docs/java/getting-started.md), [Python](https://ytsaurus.tech/docs/python/getting-started.md), and [Go](https://ytsaurus.tech/docs/go/getting-started.md). ## See also - [Computation](https://ytsaurus.tech/docs/en/computation.md) - [Quick start (Java)](https://ytsaurus.tech/docs/java/getting-started.md) - [Quick start (Python)](https://ytsaurus.tech/docs/python/getting-started.md) - [Quick start (Go)](https://ytsaurus.tech/docs/go/getting-started.md) # Companion resources: brief design A companion resource is a heavyweight object managed by Flow that lives in a companion process and is available to user process functions. Examples include an unpacked dictionary, model, or connection pool. The resource belongs to the whole companion process rather than a single job, so it must not hold mutable job state and must support concurrent use. ## Components {#components} - The resource controller determines the desired revision and publishes it to Flow. - `TCompanionResource` on a worker acts as a proxy: it prepares the revision, manages its lifecycle, and sends commands to the companion. - `TResourceStore` in the C++ companion stores resources by `resource_id`. The store belongs to the process and survives job creation and removal. - A process function receives only resources listed in `required_resource_ids`, including aliases and transitive dependencies. ## Delivery and references {#delivery-and-references} The worker calls `ResourceExecute(resource_id, command, argument)` over RPC. The command is an enum with two values: `init` and `unload`. The result contains a separate resource-command status and a structured `TError`. The main path uses idempotent `init`. Its argument contains the static and dynamic specs, the prepared revision, incarnation identifier and generation, configuration generation, and exact dependency references. Dependencies are initialized before the resource that depends on them. `TJobInfo` receives the same exact references: `resource_id`, `incarnation_id`, `configuration_generation`, and an optional alias. Before user code starts, the companion checks each reference and exposes only matching instances to the process function. ## Reconfiguration and recovery {#reconfiguration-and-recovery} - A new target revision is first prepared asynchronously on the worker through `PrepareResourceRevision`; a failed result is not published to the companion. - Successful preparation increments the configuration generation and starts a convergent `init`. - The companion publishes the new generation only after the resource applies the target revision. By contract, `Reconfigure` may only pass the target to an asynchronous switch. While `GetRevisionState` reports an applied revision that lags behind, `init` returns a temporary `resource_not_initialized`; retries converge without recreating the instance. - A monotonic incarnation generation prevents a delayed command from reviving an old instance or removing a new one. - The worker periodically retries `init`, and each computation delivers its required resources through its own channel. This restores state after the companion process restarts. - If `ProcessBatch` returns `RESOURCE_NOT_INITIALIZED`, the worker reinitializes the resource graph, refreshes the references and job information, and retries the request a bounded number of times. - `unload` is best effort. Running jobs retain references to the old object, but new jobs no longer receive it. A complete server-side implementation currently exists in the C++ companion. Java, Go, and Python currently return `Unsupported` from `ResourceExecute`. # Worker Groups in YTsaurus Flow ## Description {#desc} Worker Groups are a mechanism for grouping [workers](https://ytsaurus.tech/docs/en/glossary.md#worker) and binding [computations](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation) to specific worker groups. This lets you: - Isolate the execution of different computations on separate sets of workers. - Configure balancing parameters separately for each group. - Optimize resource usage in heterogeneous clusters. ## Configuring Worker Groups {#setup} ### Worker configuration {#worker-config} You assign workers to groups via the `YT_FLOW_WORKER_GROUPS` environment variable. The value is a comma-separated list of group names. Examples: ```bash # The worker belongs to a single group export YT_FLOW_WORKER_GROUPS="gpu" # The worker belongs to multiple groups export YT_FLOW_WORKER_GROUPS="cpu,memory-intensive" # The worker has no groups (default) # YT_FLOW_WORKER_GROUPS is unset or set to an empty string ``` {% note info %} If the variable is unset or set to an empty string, the worker is considered part of the default group and can run computations without a specified group. {% endnote %} ### Computation configuration {#computation-config} You bind computations to a worker group using the `worker_group` parameter in the [spec](https://ytsaurus.tech/docs/en/glossary.md#spec-and-dynamic-spec). Example computation spec: ```yson { "computations" = { "my_computation" = { "computation_class_name" = "MyComputation"; "worker_group" = "gpu"; # This computation runs only on workers in the "gpu" group "input_stream_ids" = ["input_stream"]; "output_stream_ids" = ["output_stream"]; }; }; } ``` Rules: - A computation runs only on workers that list the corresponding group in `YT_FLOW_WORKER_GROUPS`. - If `worker_group` isn’t specified, the computation can run only on workers that don’t list any group in `YT_FLOW_WORKER_GROUPS`. - If no available workers exist for the specified group, the computation won’t run (see [Computation doesn’t run](https://ytsaurus.tech/docs/en/#computation-does-not-run)). ## Balancer settings for groups {#balancer-config} ### Common balancer settings {#common-balancer-config} By default, all worker groups use the common balancer settings defined in the `job_manager` section of the pipeline’s dynamic [spec](https://ytsaurus.tech/docs/en/glossary.md#pipeline): ```yson { "job_manager" = { "use_cpu_aware_balancer" = %true; "rebalance_delay_after_pipeline_sync" = 30000; # 30s in milliseconds "rebalance_target_deviation" = 0.05; # ... other balancer parameters }; } ``` ### Overriding settings for a group {#group-override} You can override balancer settings for a specific worker group using the `worker_group_override` parameter in the `job_manager` section: ```yson { "job_manager" = { # Common balancer settings "use_cpu_aware_balancer" = %true; "rebalance_delay_after_pipeline_sync" = 30000; # 30s in milliseconds "rebalance_target_deviation" = 0.05; # Override settings for specific groups "worker_group_override" = { "gpu" = { # Use more aggressive balancing for GPU workers "rebalance_target_deviation" = 0.02; "rebalance_hot_mode_coeff" = 3.0; "rebalance_sync_period" = 5000; # 5s in milliseconds }; "memory-intensive" = { # Balance less frequently for memory-intensive workers "rebalance_delay_after_pipeline_sync" = 60000; # 60s in milliseconds "rebalance_sync_period" = 20000; # 20s in milliseconds }; }; }; } ``` `worker_group_override` format: - Key: the worker group name (string). - Value: balancer settings (all parameters from [`TDynamicJobBalancerSpec`](https://ytsaurus.tech/docs/en/spec.md#jobmanager)). For a full description of the parameters, see the [JobManager documentation](https://ytsaurus.tech/docs/en/spec.md#jobmanager). ## Usage examples {#examples} ### Example 1: Separating CPU and GPU computations {#example-1} ```yson # Static pipeline spec { "computations" = { "preprocessing" = { "computation_class_name" = "PreprocessingComputation"; # No worker_group — runs on default-group workers only }; "gpu_inference" = { "computation_class_name" = "GPUInferenceComputation"; "worker_group" = "gpu"; }; "postprocessing" = { "computation_class_name" = "PostprocessingComputation"; "worker_group" = "cpu"; }; }; } # Dynamic pipeline spec { "job_manager" = { "worker_group_override" = { "gpu" = { # GPU workers are expensive, so balance more carefully "rebalance_target_deviation" = 0.01; "rebalance_sync_period" = 15000; # 15s in milliseconds }; }; }; } ``` Example worker configuration: ```bash # GPU workers export YT_FLOW_WORKER_GROUPS="gpu" # CPU workers export YT_FLOW_WORKER_GROUPS="cpu" ``` ### Example 2: Isolating critical computations {#example-2} ```yson # Static pipeline spec { "computations" = { "critical_computation" = { "computation_class_name" = "CriticalComputation"; "worker_group" = "critical"; }; "regular_computation" = { "computation_class_name" = "RegularComputation"; # No worker_group }; }; } # Dynamic pipeline spec { "job_manager" = { "minimum_worker_count" = 10; "worker_group_override" = { "critical" = { # Use stable balancing for critical computations "rebalance_delay_after_pipeline_sync" = 120000; # 120s in milliseconds "async_balancing" = %false; }; }; }; } ``` ### Example 3: A worker in multiple groups {#example-3} ```bash # The worker can handle both CPU and memory-intensive tasks export YT_FLOW_WORKER_GROUPS="cpu,memory-intensive" ``` ```yson # Static pipeline spec { "computations" = { "cpu_computation" = { "worker_group" = "cpu"; }; "memory_computation" = { "worker_group" = "memory-intensive"; }; # Both computations can run on this worker }; } ``` ## Monitoring and debugging {#monitoring} ### Checking worker groups {#checking-worker-groups} You can view worker group information via the yt interface: ```bash # List active workers and their groups yt --proxy= flow get-flow-view --view-path="/state/workers" ``` Example output: ```yson { "[2a02:6b8:c42:cec3:7800:18:6687:0]:81" = { "worker_groups" = []; # No groups assigned, default group #... other fields }; "[2a02:6b8:c42:d6ca:7800:18:22a7:0]:81" = { "worker_groups" = [ "GPU_1"; # A specific group is assigned ]; #... other fields }; #... } ``` ### Common issues {#troubleshooting} #### Computation doesn’t run {#computation-does-not-run} Symptoms: - On the pipeline graph page, all its streams show `0 pcs/s 0 B/S`. - The node with the computation shows `CPU Usage –` and `RAM Usage 0 B`. - On the pipeline page `Computations/`, the table has rows, but the `Worker` column is empty for all of them. Possible causes: - A typo in the group name. - No available workers with the specified group. How to fix it: 1. Check that the group name is spelled correctly in the spec and the environment variable (they must match, including case). 2. Check for available workers with the needed group using [yt](https://ytsaurus.tech/docs/en/#checking-worker-groups). 3. Review the controller logs for balancing errors. 4. Review the logs of workers in that group for operational errors. ## See also - [YT Flow core concepts](https://ytsaurus.tech/docs/en/glossary.md) - [Pipeline spec](https://ytsaurus.tech/docs/en/spec.md) - [Load balancing](https://ytsaurus.tech/docs/en/spec.md#jobmanager) # Distributed Throttler in YTsaurus Flow Use the distributed throttler to globally limit the rate across the entire [pipeline](https://ytsaurus.tech/docs/en/glossary.md#pipeline). The Controller holds a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) for each named throttler; [Jobs](https://ytsaurus.tech/docs/en/glossary.md#job) request a quota from the Controller before processing messages or performing any custom user action. You can apply quotas to either the number of processed messages or the total size of their payload in bytes — separately or together. Typical use cases: - Limit the total load on an external API from all Jobs. - Evenly distribute bandwidth across [partitions](https://ytsaurus.tech/docs/en/glossary.md#partition) of a single [`Computation`](https://ytsaurus.tech/docs/en/computation.md). - Slow down reading from a source. ## Priority by lag {#priority} Each Job attaches its timestamp to the quota request — the same timestamp that determines lag in the input buffer. The server takes the minimum value across all `stabilized_event_timestamp + stream_delay` from [input streams](https://ytsaurus.tech/docs/en/glossary.md#stream) and `read_alignment_timestamp` from [source streams](https://ytsaurus.tech/docs/en/glossary.md#source). The server ranks requests by this timestamp in ascending order: it grants quota first to those that are lagging more. If there’s a load imbalance across [partitions](https://ytsaurus.tech/docs/en/glossary.md#partition) of a single [`Computation`](https://ytsaurus.tech/docs/en/computation.md), the lagging ones automatically get more quota, while the leading ones slow down. ## Weighted quota classes {#quota-classes} A named throttler can declare classes with weights. Quota is distributed among the classes that have pending requests in proportion to their weights, and in a work-conserving way. If a class goes idle, its share is immediately redistributed among the active classes. A weight sets the long-run share of the bandwidth, not a strict priority. Priority by lag keeps working inside each class. An idle class banks no credit, but once a request appears it gets a chance to be served before the next long pass over another class's continuous backlog. If `classes` is absent, the previous single-queue behavior and the previous token bucket semantics are preserved. The reserved `default` class always has weight `1.0`. A missing, empty, or unknown `quota_class_id` falls into `default` on the server; that is how a throttler with no class configured is served. The class covers every request the Computation makes to that throttler: if the same id is also obtained manually via `GetThrottlerOrThrow`, those requests carry the same class rather than `default`. The class is set per automatic throttler — `input_rows_throttler_class_id` and `input_bytes_throttler_class_id` — and each is checked against the classes its own throttler declares: an unknown class is rejected by validation before the pipeline starts. ```yson "dynamic_spec" = { "throttlers" = { "output_quota" = { "limit" = 1000.0; "period" = "1s"; "request_period" = "500ms"; "max_grant_amount" = 50; "classes" = { "vip" = {"weight" = 5.0;}; "regular" = {"weight" = 3.0;}; "bulk" = {"weight" = 1.0;}; }; }; }; "computations" = { "ReaderVip" = { "input_rows_throttler_id" = "output_quota"; "input_rows_throttler_class_id" = "vip"; }; }; } ``` ### Weights as absolute rates Setting `use_class_weights_as_limit = %true` makes the bucket rate the sum of the declared class weights, so a weight reads as quota units per second instead of a share of a separately chosen `limit`. With `5.0`, `3.0` and `1.0` the throttler emits 9 units per second, and each class is served at its weight while all of them stay backlogged. Work conservation is unchanged: a class still exceeds its weight while the others are idle, so the weight is a floor under contention, not a cap. The option requires at least one class and is mutually exclusive with `limit` — setting both is rejected by validation. The reserved `default` class is not part of the sum. Classless traffic on such a throttler competes at weight `1.0` on top of the declared rate and pushes every declared class below its weight; the throttler's `/classless_requests` and `/unknown_class_requests` sensors show when that happens. ### Class switching latency The scheduler reconsiders the active classes after the current server chunk completes. `max_grant_amount` sets the maximum chunk size in absolute quota units; leaving it unset lets a single request hold the token bucket for as long as its whole prefetch window takes, which delays every other class by that much. The observed latency also includes `drain_period`, the RPC, the local prefetch, the source's `empty_batch_backoff`, and the Flow commit. ### Weights and reader throughput Observed shares match the configured weights only while every measured class stays backlogged **and** each reader can actually consume its entitlement. A reader iteration carries a fixed overhead independent of the granted rate, so with a small `max_rows_per_batch` a high-weight class can hit its own throughput ceiling before it reaches its share; work conservation then hands the surplus to the lower-weight classes and the observed split drifts toward equality. Scale `max_rows_per_batch` together with `limit` so the per-iteration overhead stays a small fraction of the time each class spends waiting for quota. ## Live reconfiguration {#reconfigure} All throttler parameters (`limit`, `period`, `request_period`, `retrying_channel`, `rpc_timeout`, `classes`, `max_grant_amount`, `use_class_weights_as_limit`) are applied without restarting the pipeline — just update `dynamic_spec`. The `IThroughputThrottlerPtr` cached in your user code remains valid after the config change. Weights and the server chunk size change without rebuilding the prefetch client. The exception is `use_class_weights_as_limit`: a weight is then part of the rate, so a weight change that resizes the prefetch window rebuilds the client — a change too small to move that window leaves the client, and its prefetched quota, in place. The new rate applies on the server either way. Changing `input_rows_throttler_class_id` or `input_bytes_throttler_class_id` affects subsequent RPCs; quota already fetched locally or still in flight stays accounted to the previous class. ## Quota classes and watermark alignment {#watermark-alignment} Quota availability does not override watermark alignment: reading is allowed only when the output limits, the alignment, and the read window restriction are all satisfied at once. So a heavier class will not speed reading up while alignment is holding the source back — weight distributes quota only. Configuring alignment itself is covered in [watermark strategy](https://ytsaurus.tech/docs/en/watermarks.md). ## What happens if the Controller is unavailable {#controller-unavailable} The throttler client reopens the connection via `retrying_channel`. While the Controller doesn’t respond, the local prefetch cache is consumed, and `Throttle()` blocks while waiting for a quota — the Job makes no progress. When the Controller comes back, the wait resolves automatically. If the Controller stays unresponsive longer than the total retry budget of `retrying_channel`, the throttler operation throws an exception and the Job fails. ## What happens if there isn’t enough quota {#quota-insufficient} If the total demand from Jobs far exceeds the possible supply, individual `RequestQuota` calls may wait a long time in the server queue and eventually miss their `rpc_timeout` × `retry_attempts` deadline. In this case, `Throttle()` also throws an exception, which causes the Job to fail. Fallback to a local limit isn’t supported in either of these cases right now. ## Configuration {#configuration} All settings are in `DynamicSpec`. You declare throttlers at the root. In the example below, `external_api_quota` is a user-defined throttler ID, and `RequestEnricher` is the Computation name: ```yson "dynamic_spec" = { "throttlers" = { "external_api_quota" = { "limit" = 100.0; }; }; "computations" = { "RequestEnricher" = { "input_rows_throttler_id" = "external_api_quota"; }; }; } ``` Fields of `TDynamicThrottlerSpec`: Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) Configuration of a named distributed throttler. #| || **Parameter** | **Description** || || `limit` | **Type**: `std::optional` Average quota issuance rate in units per second. `none` means `unlimited`. || || `period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` Averaging period for the average rate in the [token bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm: over `period`, the bucket is replenished to the volume of `limit * period` units. The same volume is the maximum possible burst. || || `request_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` Target interval between client requests to the controller (the prefetch size adjusts to this interval). || || `retrying_channel` | **Type**: `NYT::TIntrusivePtr<`[NYT::NRpc::TRetryingChannelConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NRpc_TRetryingChannelConfig)`>` **Default value**: ```yson { "enable_exponential_retry_backoffs" = %true; "retry_attempts" = 100; "retry_timeout" = 600000; } ``` Retry parameters for requests to the throttler service. The defaults are designed to survive a controller leader change even if it takes a long time. || || `rpc_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `30s` Timeout for a single `RequestQuota` request. || || `classes` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicThrottlerClassSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicThrottlerClassSpec)`>>` **Default value**: `{}` Named weighted quota classes. Keys must match `[0-9A-Za-z_-]+`. Backlogged classes share bandwidth in proportion to their weights and idle shares are redistributed. || || `max_grant_amount` | **Type**: `std::optional` Maximum server scheduling chunk in absolute quota units. It bounds the delay before active classes are reconsidered. If unset, a single request is granted whole, so it holds the token bucket for its entire prefetch window and delays every other class by that long. || || `use_class_weights_as_limit` | **Type**: `bool` **Default value**: `false` Reads the class weights as absolute rates: the issuance rate becomes the sum of the declared class weights, so a backlogged class is served at its weight in units per second. Requires at least one class and is mutually exclusive with `limit`. The reserved `default` class is not part of the sum. || |# ## Usage {#usage} You have two options — automatic and manual. They don’t conflict: you can use one or both at the same time. ### Automatic: limit the input batch rate {#auto} `TDynamicComputationSpec` includes two fields — `input_rows_throttler_id` and `input_bytes_throttler_id`. If you set them, the Computation waits for a quota before each `Process` iteration: - By the number of messages in the input batch (`input_rows_throttler_id`). - By the sum of the `byte_size` of the batch messages — this is the system size of the serialized representation, excluding network-level compression or encryption (`input_bytes_throttler_id`). The wait is recorded as a separate `Input.Throttle` span in the Computation’s tracing (visible in Jaeger and in the “Epoch parts time” charts in the UI). The ID must be declared in `dynamic_spec/throttlers`. Both fields may be set at once — the iteration then waits for both quotas — but they must name **different** throttlers: a throttler meters either a message count or a byte size, and a shared token bucket would be summing counts with bytes. Such a spec is rejected by validation. ### Manual: `GetThrottlerOrThrow(id)` from user code {#manual} If automatic throttling on the input batch isn’t enough — for example, if you need a rate limit for each external request — a process function gets the throttler from `IRuntimeContext`. `context->GetThrottlerOrThrow(id)` returns `IThroughputThrottlerPtr`, the standard YT throttler. Call `Throttle(amount)` on it and wait for the result. `GetThrottlerOrThrow` throws if the id isn’t declared in `throttlers`; `context->TryGetThrottler(id)` returns `nullptr` instead — useful when declaring a throttler in `dynamic_spec` enables throttling for the matching entity, such as one throttler per model or external service. The returned pointer stays stable for the entire life of the Job: the factory replaces the internal client during `Reconfigure`, so a process function can store it in `Init` and keep using it without re-fetching. This does not apply to a `nullptr` from `TryGetThrottler`: it only means "not declared right now", and once the throttler is added to `dynamic_spec` you have to query it again. Right now, this mechanism is supported only in C++. ## Monitoring {#monitoring} The Flow dashboard has a separate tab called **Distributed throttler**. Key sensors include: - Server-side (Controller): `value.rate` — the actual quota issued, `queue_size` — how many requests are waiting, `wait_time.max` — the tail of the wait time. - Client-side (Worker, on each `Computation`): `consumed.rate` / `released.rate`, `wait_time.max` — the time the Job spent blocked in `Throttle()`, `request_count.rate` — the prefetch-RPC frequency. ## See also {#see-also} - [Spec, DynamicSpec and Config](https://ytsaurus.tech/docs/en/spec.md) - [Computation](https://ytsaurus.tech/docs/en/computation.md) # Spec, DynamicSpec, and Config in YTsaurus Flow Use a spec to describe the pipeline you want. Any high-level API to Flow generates a pipeline spec. A pipeline has two specs: `Spec` and `DynamicSpec`: - `Spec` is a static object that contains the topology of the entire pipeline. You can change this object only if the pipeline is stopped. The system guarantees that at runtime, every part of the system has the same view of the `Spec`. - `DynamicSpec` is the dynamic part of the spec. You can change it at any time, and the changes apply asynchronously to all parts of the system. `DynamicSpec` usually stores buffer sizes, timeouts, the number of partitions, and so on. The node’s `Config` is also an important part. Unlike the spec, you set the config exactly once per release, and you can’t change it dynamically. In the future, some config parameters might move to the specs. {% note warning %} All parameters in Flow have optimal default values. Don’t change the settings unless you’re sure what they do and how they can affect the system. {% endnote %} You can find the default values by searching for the parameter name in the Flow codebase. They’re mostly grouped in the file [spec.cpp](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.cpp). ## Spec Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `validate_binary_version` | **Type**: `bool` **Default value**: `false` || || `binary_version` | **Type**: `std::string` **Default value**: `TString("")` || || `validate_binary_checksum` | **Type**: `bool` **Default value**: `false` || || `binary_checksum` | **Type**: `std::string` **Default value**: `TString("")` || || `computations` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TComputationSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TComputationSpec)`>>` **Default value**: `{}` A named enumeration of all pipeline nodes. Keys must match `[0-9A-Za-z_-]+`. || || `resources` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TResourceSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TResourceSpec)`>>` **Default value**: `{}` A named enumeration of all resources. Keys must match `[0-9A-Za-z_-]+`. || || `streams` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TStreamSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TStreamSpec)`>>` **Default value**: `{}` A named enumeration of all data streams. Keys must match `[0-9A-Za-z_-]+`. || |# ### Computation Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `computation_class_name` | **Type**: `std::string` **Required parameter** The built-in Computation or process-function adapter class name. For new C++ user logic, select one of the built-in `TProcessFunction*Computation` adapters. Register the function with `YT_FLOW_DEFINE_PROCESS_FUNCTION` and name it in `processing_function`. Don’t create custom Computation classes. || || `process_function_graph` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TProcessFunctionGraphSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TProcessFunctionGraphSpec)`>` Several process functions connected by stream IDs and run in one `TProcessFunctionComputation` or `TProcessFunctionSwiftMapComputation`. Batch and per-message functions can be combined. Internal message IDs are derived from their boundary parents, output stream, and suffix; timestamps and external IDs follow the host computation's policy. Swift requires deterministic replay; batch functions set each result's parents with `SetParents`. Merging different input messages requires `parameters/allow_batching_with_relaxed_guarantees = %true` and provides at-least-once delivery. The runner replaces the graph with `processing_function`, `processing_function_parameters`, `timer_streams`, `streams_dependency`, and `allow_timer_self_dependency`, so do not set these fields together with the graph. || || `processing_function` | **Type**: `std::optional` The fully qualified process-function name registered with `YT_FLOW_DEFINE_PROCESS_FUNCTION`. Required by `TProcessFunction*Computation` adapters unless `process_function_graph` is set. || || `processing_function_parameters` | **Type**: `NYT::TIntrusivePtr` Static process-function parameters. Their type is declared in `YT_FLOW_DEFINE_PROCESS_FUNCTION` registration. || || `group_by_schema` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{'value': [], 'attributes': {'strict': true, 'unique_keys': false}}` A schema for grouping all input streams. It is a dynamic table schema. Schema properties and requirements: * Does not contain any ordering rules. * Can contain computed fields. * Must be empty only for `SourceComputation`. * For other `Computation` instances, the first column must be of type `uint64`. It is recommended that this column be a computed hash of the other columns (for example, `farm_hash`). Note that using the modulo operator `%` and `bigb_hash` in `expression` is prohibited. || || `experimental_enable_non_uint_key` | **Type**: `std::optional` || || `input_stream_ids` | **Type**: `THashSet>` **Default value**: `[]` Input streams. The schemas of all streams must be castable to `group_by_schema`. || || `output_stream_ids` | **Type**: `THashSet>` **Default value**: `[]` Output streams of the Computation. They can be used as inputs for other Computations. || || `streams_dependency` | **Type**: `THashMap, THashSet>>` **Default value**: `{}` A description of dependencies between streams, including internal `source` and `timer` streams. Example: `{stream1, {stream2, stream3}}` — `stream1` depends on streams `stream2` and `stream3`. Specified only for `timer` and `output` streams. If no description is provided for a stream, the dependency list is auto-generated: * for `timer` streams, the list includes all `input` and `source` streams; * for `output` streams, the list includes all `timer`, `input`, and `source` streams. || || `watermark_strategy` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TWatermarkStrategySpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TWatermarkStrategySpec)`>` **Default value**: `{}` Settings related to `EventWatermark`. An optional parameter. Must be specified within `SourceComputation`. || || `required_resource_ids` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TResourceDescription](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TResourceDescription)`>>` **Default value**: `{}` A list of resources required for the `Computation` to work. Has an `alias` parameter for locally renaming a resource (for example, `Computation` expects a resource named `YTClient`, but the provided resource has a different name). The `controller` and `worker` parameters control whether the resource is created on the `Controller` or on the `Worker`. || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Custom parameters for `Computation` and `ComputationController`. || || `timer_streams` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TTimerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TTimerSpec)`>>` **Default value**: `{}` Settings for all `timer` streams. Keys must match `[0-9A-Za-z_-]+`. || || `key_visitor_streams` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TKeyVisitorStreamSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TKeyVisitorStreamSpec)`>>` **Default value**: `{}` Settings for all `key_visitor` streams. Keys must match `[0-9A-Za-z_-]+`. || || `source_streams` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TSourceSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TSourceSpec)`>>` **Default value**: `{}` Settings for all `source` streams. Keys must match `[0-9A-Za-z_-]+`. || || `sinks` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TSinkSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TSinkSpec)`>>` **Default value**: `{}` Settings for all sinks. Keys must match `[0-9A-Za-z_-]+`. || || `external_state_managers` | **Type**: `THashMap>` **Default value**: `{}` A declarative declaration of [external state](https://ytsaurus.tech/docs/cpp/state.md#external-state) managers for this `Computation`. The key is the client name that the process function passes to `InitExternalStateClient` (must start with `/`, for example `/state`); the value contains the manager class name and its parameters. || || `external_state_joiners` | **Type**: `THashMap>` **Default value**: `{}` A declarative declaration of [external state](https://ytsaurus.tech/docs/cpp/state.md#external-state-joiner) joiners (read-only access to external states via key join) for this `Computation`. The key is the client name that the process function passes to `InitExternalStateClient` (must start with `/`, for example `/state`); the value contains the joiner class name and its parameters. || || `state_joiners` | **Type**: `THashMap>` **Default value**: `{}` A declarative declaration of [state](https://ytsaurus.tech/docs/cpp/state.md#state-joiner) joiners (read-only access to the internal state of another `Computation` via key join) for this `Computation`. The key is the client name that the process function passes to `InitClient` (must start with `/`); the value specifies the target `computation_id`, its `state_name`, and `join_on`. || || `heavy_hitters` | **Type**: [NYT::NFlow::THeavyHittersSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_THeavyHittersSpec) **Default value**: `{}` Settings for detecting high-frequency keys. || || `pivot_finder` | **Type**: [NYT::NFlow::TPivotFinderSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TPivotFinderSpec) **Default value**: `{}` || || `input_ordering` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TInputOrderingSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TInputOrderingSpec)`>` **Default value**: `{}` Settings for the processing order of input streams. || || `distribution_ordering` | **Type**: [NYT::NFlow::EDistributionOrdering](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EDistributionOrdering) **Default value**: `strict` Settings for the distribution order of output messages. Controls whether a race condition is possible between stopping and running partitions from the perspective of the order of output messages generated from input messages with the same key. || || `worker_group` | **Type**: `NYT::TStrongTypedef` **Default value**: `TString("")` || || `allow_timer_self_dependency` | **Type**: `bool` **Default value**: `false` || || `use_compact_input_messages` | **Type**: `std::optional` Use the `compact_input_messages` table for [input message deduplication](https://ytsaurus.tech/docs/en/guarantees.md) instead of `input_messages`. The compact deduplication key stores only the hash of `key[0]` and `CityHash128(message_id)`, which reduces the row size (~200 bytes → ~71 bytes). An optional parameter. If not set, the table is chosen automatically: the compact table is always used except when `experimental_enable_non_uint_key` is enabled (in that case, the first key column may not be `uint64`, and deduplication requires the full key). An explicitly set value takes priority over automatic selection. Enabling `use_compact_input_messages` together with `experimental_enable_non_uint_key` is prohibited. || |# #### WatermarkStrategy {#watermark-strategy} Fill this for `SourceComputation`. Other `Computation` types might not support it, or might support it only partially. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `event_timestamp_assigner` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TEventTimestampAssignerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TEventTimestampAssignerSpec)`>` **Default value**: `{}` Automatic population of `EventTimestamp` for all `output` messages based on the value of a specified column. || || `watermark_generator` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TWatermarkGeneratorSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TWatermarkGeneratorSpec)`>` Settings for the `EventWatermark` generation algorithm. For a `Computation` with at least one `source`, omitting this parameter is equivalent to an empty block with default settings. A multi-source `Computation` is processed as independent single-source computations: for each `source`, unavailable-group, idle-partition, and late-data-partition heuristics run independently in that order, and the prepared partitions are merged only afterwards. `idle_partitions.max_ratio` is applied separately to each `source`, and availability-group suppression decisions in one `source` do not affect another. Therefore, a fully idle `source` is not treated as a partial-idle stall and does not by itself raise `/idle_partitions_watermark_stall`, even if it gates the merged `Computation` watermark while another `source` remains active. Availability-group identifiers do not need to match across sources. || || `watermark_alignment` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TWatermarkAlignmentSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TWatermarkAlignmentSpec)`>` Settings for aligning `source` across different partitions, including those of different `Computation` instances. || || `watermark_percentile` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TWatermarkPercentileSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TWatermarkPercentileSpec)`>` **Default value**: `{}` Settings for a "percentile" `EventWatermark`. Used to ignore the oldest events within a configured percentage. || |# ##### EventTimestampAssigner {#event-timestamp-assigner} Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `column` | **Type**: `std::optional` The time column. || || `format` | **Type**: [NYT::NFlow::ETimestampFormat](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_ETimestampFormat) **Default value**: `seconds` The time format used (`seconds` and `milliseconds`). || || `limit_by_system_timestamp` | **Type**: `bool` **Default value**: `false` Limit `EventTimestamp` to the `SystemTimestamp` value to protect against corrupted values from the future. || |# ##### WatermarkGenerator {#watermark-generator} Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `use_source_watermark` | **Type**: `bool` **Default value**: `false` Use `SourceWatermark` for `EventWatermark` generation. This option completely disables all heuristics for determining `EventWatermark`. Use it strictly when the input stream contains special markers with `EventWatermark`. || || `out_of_orderness_bound` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `0` The upper bound on possible event reordering, used for estimating `EventWatermark`. Event reordering is not accounted for by default. || || `idle_partitions` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TIdlePartitionsSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TIdlePartitionsSpec)`>` Heuristic settings for ignoring partitions with zero write throughput. || || `late_data_partitions` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TLateDataPartitionsSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TLateDataPartitionsSpec)`>` || |# Settings for `IdlePartitions`: Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `duration` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` The time a partition must be empty before it is considered to have no writes. || || `max_ratio` | **Type**: `double` **Default value**: `0.4` The proportion of partitions that can be excluded from the watermark calculation by this logic. The conceptual purpose of this option is to protect against watermark advancement when the data provider stops writing messages to the queue due to an incident on their side. Setting the option to `1.0` effectively disables this protection. || |# Settings for `UnavailablePartitionGroups`: Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) **This structure has no main parameters.** ##### WatermarkAlignment {#watermark-alignment} Use this module to align input [streams](https://ytsaurus.tech/docs/en/glossary.md#stream-and-computation). It can cause a complete read stop if there are availability issues or problems estimating the `Watermark` for individual [partitions](https://ytsaurus.tech/docs/en/glossary.md#partition) or `SourceComputation`. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `group_name` | **Type**: `NYT::TStrongTypedef` **Default value**: `default-alignment-group` The group name. || || `drift_bound` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `20m` The maximum possible lead of an individual partition's watermark over the group's watermark. || || `read_delays` | **Type**: `std::optional, `[TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration)`>>` An advanced use option. If the `EventTimestamp` of an output message is greater than `EventWatermark - Delay` for at least one specified stream, reading is paused. If misconfigured, this option can cause a complete read stall. || |# ##### WatermarkPercentile {#watermark-percentile} Use this module to ignore old data in streams when calculating `EventWatermark`. Each partition maintains a set of unprocessed messages for each outgoing stream. By default, the system calculates `EventWatermark` using the minimum `EventTimestamp` value across all messages (that is, it accounts for `100%` of events). If an event falls within the `value` percentile, it’s always included in the `EventWatermark` calculation. If an event doesn’t fall within the specified percentile but its `EventTimestamp` differs from those events by no more than `delay`, it’s also included in the `EventWatermark` calculation. So the calculation algorithm is `EventWatermark = max(MinEventTimestamp, PercentileEventTimestamp - delay)`, where `MinEventTimestamp` is the minimum across all events, and `PercentileEventTimestamp` is the `EventTimestamp` value at the corresponding `value` percentile. Keep in mind that the `inflight` set for a single stream within a partition can contain dozens of messages at a time. That’s why values like `99` or `99.9` can behave almost identically to `100`—meaning they won’t produce the desired effect. Also, even if old events make up less than 1% of the entire stream, they might be unevenly distributed across partitions. That’s why it’s better to use values like `80` or `90`. To avoid negative effects like ignoring recent events, there’s the `delay` parameter: it ensures that only truly old events are excluded from the calculation. If there are no such old events, the system works the same as when `value` is `100`, without losing any information. This module affects only the `EventWatermark` calculation and doesn’t affect `SystemWatermark`. {% note warning %} The settings don’t affect how the system aggregates information from different partitions. The system always uses the minimum value across all partitions. {% endnote %} Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `value` | **Type**: `NYT::TStrongTypedef` **Default value**: `100.0` The percentage of events unconditionally considered for `EventWatermark` calculation. || || `delay` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` The freshness threshold for events below the selected percentile. || |# ##### LateDataPartitions {#late-data-partitions} Use this module to ignore partitions that contain only old data. It works similarly to `WatermarkPercentile`, but at the level of all partitions, not individual messages inside a partition. `WatermarkPercentile` lets you ignore old messages **inside a partition**, but if an entire partition contains only old data, it will still slow down the overall `EventWatermark`. The `LateDataPartitions` module solves this by letting you ignore such partitions entirely. The calculation algorithm is `FinalWatermark = max(MinPartitionWatermark, PercentilePartitionWatermark - Delay)`, where `MinPartitionWatermark` is the minimum watermark across all partitions, and `PercentilePartitionWatermark` is the partition’s watermark at the specified `value` percentile (partitions are sorted by watermark). If a partition falls within the `value` percentile, it’s always included. If a partition doesn’t fall within the specified percentile but its watermark differs from the percentile value by no more than `delay`, it’s also included. Partitions with a watermark below the threshold (`PercentileWatermark - Delay`) are considered to contain only late data, and their watermark is “hidden” (set to `FinalWatermark`). For example, with `value=90` and 100 partitions: 90% of the partitions (those with the highest watermarks) are always included, and the remaining 10% (those with the lowest watermarks) are included only if their lag from the 90th percentile doesn’t exceed `delay`. {% note warning %} This module runs **after** `IdlePartitions`. Idle partitions are already excluded from the calculation before the `LateDataPartitions` logic applies. {% endnote %} Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `value` | **Type**: `NYT::TStrongTypedef` **Default value**: `100.0` || || `delay` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` || |# #### Timer Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `time_type` | **Type**: [NYT::NFlow::ETimeType](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_ETimeType) **Default value**: `event_time` The time type used by the timer: `event_time`, `system_time`, `real_time` || || `streams` | **Type**: `std::optional>>` List of streams that the timer monitors. || || `streams_with_delays` | **Type**: `std::optional, `[TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration)`>>` List of streams that the timer monitors with an individual delay specified || || `deduplicate_equal_timestamps` | **Type**: `bool` **Default value**: `true` Enables timer deduplication: if you try to create a timer with the same key and `trigger_timestamp`, only one timer remains — the one with the smaller `EventTimestamp` value || |# If you don’t specify `streams` and `streams_with_delays`, the timer monitors the streams according to `streams_dependency` with zero extra delay. #### Source {#source_spec} Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `source_class_name` | **Type**: `std::string` **Default value**: `TString("")` Name of the corresponding class. Must be registered using `YT_FLOW_DEFINE_SOURCE`. || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Arbitrary parameters for the corresponding class. || |# Built-in [source](https://ytsaurus.tech/docs/en/glossary.md#source) implementations are described in the [connector documentation](https://ytsaurus.tech/docs/connectors/about.md). #### Sink {#sink_spec} Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `sink_class_name` | **Type**: `std::string` **Default value**: `TString("")` Name of the corresponding class. Must be registered using `YT_FLOW_DEFINE_SINK`. || || `input_stream_ids` | **Type**: `THashSet>` **Default value**: `[]` List of input streams. Only streams specified in `output_stream_ids` of the entire `Computation` are allowed here. || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Arbitrary parameters for the corresponding class. || |# Built-in [sink](https://ytsaurus.tech/docs/en/glossary.md#sink) implementations are described in the [connector documentation](https://ytsaurus.tech/docs/connectors/about.md). #### HeavyHitters This module detects high-frequency keys. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `window` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5m` Time window. || || `threshold` | **Type**: `double` **Default value**: `0.01` Keys that appear in the stream more often than `threshold` are considered heavy hitters. The algorithm uses `O(1 / threshold)` memory. || || `limit` | **Type**: `long` **Default value**: `5` Maximum number of heavy hitter keys to show. || |# #### InputOrdering These settings control the order in which the system processes events from input streams. Events with the earliest timestamps are processed first. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `time_type` | **Type**: [NYT::NFlow::ETimeType](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_ETimeType) **Default value**: `event_time` Time for sorting: `event_time`, `system_time`, `real_time` || || `stream_delays` | **Type**: `THashMap, `[TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration)`>` **Default value**: `{}` Delays for input streams. If a stream is not specified, there is no delay || |# ### Streams Register only `output` streams in `streams`. `Source` streams exist only inside their corresponding partitions and aren’t persisted in YT. `Timer` streams have a fixed schema. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `class_name` | **Type**: `std::optional` || || `schema` | **Type**: `NYT::TIntrusivePtr` **Required parameter** Stream schema. It is a dynamic table schema. Cannot contain sorting or computed columns. We recommend making schema changes by completely stopping the pipeline. In this case, no `output` messages remain inside the pipeline. || || `migration_function` | **Type**: `std::string` **Default value**: `NYT::NFlow::DefaultMigrationFunction` || |# ### Resource Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `resource_class_name` | **Type**: `std::string` **Required parameter** Name of the corresponding class. Must be registered using the `YT_FLOW_DEFINE_RESOURCE` macro. || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Arbitrary parameters for the corresponding class. || || `file_providers` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TFileProviderSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TFileProviderSpec)`>>` **Default value**: `{}` Named file providers of the resource. The controller attaches a file snapshot to a target revision only after discovering every name; a controller-specific target spec may be published independently. Workers materialize the exact revisions from that target. Such resources are worker-only and must have `controller = %false` on every `required_resource_ids` path that can reach them. Keys must match `[0-9A-Za-z_-]+`. || || `dependencies` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TResourceDescription](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TResourceDescription)`>>` **Default value**: `{}` Resources that this resource depends on. Mirrors the structure of `required_resource_ids` from `Computation`. || || `required_capabilities` | **Type**: `THashMap` **Default value**: `{}` || || `preload_required` | **Type**: `bool` **Default value**: `false` || || `always_on` | **Type**: `bool` **Default value**: `false` If `true`, the resource is loaded in advance when the unit starts and stays in memory for its entire lifetime (never unloaded) instead of being loaded lazily on first access. It is loaded only on units where at least one computation requires it (see `worker` / `controller` in `required_resource_ids`): a resource needed only on a worker is not loaded on a controller, and a resource not required by any computation is not loaded at all. Mutually exclusive with `preload_required`. || |# ## DynamicSpec Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `target_state` | **Type**: [NYT::NFlow::EPipelineState](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EPipelineState) **Default value**: `unknown` Target state of the system. Use the `start-pipeline`, `stop-pipeline`, and `pause-pipeline` commands to change it. || || `computations` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicComputationSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicComputationSpec)`>>` **Default value**: `{}` Named list of all pipeline nodes. || || `resources` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicResourceSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicResourceSpec)`>>` **Default value**: `{}` || || `throttlers` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicThrottlerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicThrottlerSpec)`>>` **Default value**: `{}` A set of named distributed throttlers common to all `Computation` pipeline nodes. Keys are throttler IDs and must match `[0-9A-Za-z_-]+`. For more information, see [Distributed Throttler](https://ytsaurus.tech/docs/en/distributed_throttler.md). || || `job_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicJobManagerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicJobManagerSpec)`>` **Default value**: `{}` `JobManager` settings. || || `message_distributor` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicMessageDistributorSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicMessageDistributorSpec)`>` **Default value**: `{}` `MessageDistributor` settings. || || `job_tracker` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicJobTrackerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicJobTrackerSpec)`>` **Default value**: `{}` `JobTracker` settings. || || `controller_connector` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicControllerConnectorSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicControllerConnectorSpec)`>` **Default value**: `{}` `ControllerConnector` settings. || || `singletons` | **Type**: `NYT::TIntrusivePtr<`[NYT::TSingletonsDynamicConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_TSingletonsDynamicConfig)`>` **Default value**: `{}` || || `enable_mermaid_graph_describe` | **Type**: `bool` **Default value**: `false` || || `flow_view_cache_codec` | **Type**: [NYT::NCompression::ECodec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NCompression_ECodec) **Default value**: `zstd_2` || |# `Stream` and `Resource` currently don’t have or don’t support dynamic parameters. ### Computation Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `batch_duration` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Maximum time to collect an input batch. For YTsaurus queues, a non-full batch keeps collecting until this time passes since the previous batch; after a non-full queue response, the next request is also made no earlier than that moment. || || `max_rows_per_batch` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `1K` Maximum size of an input batch in rows. Calculated separately for `input_streams`, `timer_streams`, and for each of the `source_streams`. For YTsaurus queues, unpacked Flow messages are counted; a batch may exceed the limit by one queue row. || || `max_bytes_per_batch` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `10Mi` Maximum size of an input batch in bytes. Calculated similarly to `max_rows_per_batch`. For YTsaurus queues, the size of unpacked messages is counted; the limit may be exceeded as for `max_rows_per_batch`. || || `max_keys_per_batch` | **Type**: `std::optional<`[NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize)`>` || || `draining` | **Type**: `bool` **Default value**: `false` The `Draining` mode is used to drain the pipeline. In this mode, the `Computation` stops running timers and fetching new events from the `Source`. The `stop-pipeline` command stops the entire pipeline through a full drain. You can set this mode for individual `Computation` nodes for debugging. || || `empty_batch_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `250ms` If the epoch is empty, the `Computation` sleeps for the specified additional time. We do not recommend setting this parameter to less than 100 ms. || || `lease_check_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` || || `retryable_request` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicRetryableRequestSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicRetryableRequestSpec)`>` **Default value**: `{}` || || `tracer` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicPartitionTracerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicPartitionTracerSpec)`>` **Default value**: `{}` || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Arbitrary dynamic parameters of the `Computation` class. || || `processing_function_parameters` | **Type**: `NYT::TIntrusivePtr` Dynamic process-function parameters. Their type is declared in `YT_FLOW_DEFINE_PROCESS_FUNCTION` registration. For a process function graph, it contains the dynamic parameters of the nodes in `nodes`. || || `source_streams` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicSourceSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicSourceSpec)`>>` **Default value**: `{}` Dynamic parameters of all `Sources`. || || `key_visitor_streams` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicKeyVisitorStreamSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicKeyVisitorStreamSpec)`>>` **Default value**: `{}` || || `sinks` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicSinkSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicSinkSpec)`>>` **Default value**: `{}` Dynamic parameters of all `Sinks`. || || `external_state_managers` | **Type**: `THashMap>` **Default value**: `{}` || || `external_state_joiners` | **Type**: `THashMap>` **Default value**: `{}` || || `state_joiners` | **Type**: `THashMap>` **Default value**: `{}` || || `state_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicStateManagerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicStateManagerSpec)`>` **Default value**: `{}` || || `input_store` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicInputStoreSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicInputStoreSpec)`>` **Default value**: `{}` || || `timer_store` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicTimerStoreSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicTimerStoreSpec)`>` **Default value**: `{}` || || `output_store` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicOutputStoreSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicOutputStoreSpec)`>` **Default value**: `{}` || || `timer_store_count_limit` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `250K` || || `timer_store_byte_size_limit` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `100Mi` || || `blocked_time_window` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10m` Averaging window for the share of time the job spent blocked on each limit (`blocked_share` in diagnostics). The share is normalized to the job lifetime, so a job blocked from the start shows `~1` regardless of its age. || || `input_rows_throttler_id` | **Type**: `std::optional>` Throttler ID for limiting the message processing rate. If set, before each iteration the `Computation` waits for a quota equal to the number of messages in the input batch. The ID must be present in `dynamic_spec/throttlers`. For more information, see [Distributed Throttler](https://ytsaurus.tech/docs/en/distributed_throttler.md). || || `input_bytes_throttler_id` | **Type**: `std::optional>` Similar to `input_rows_throttler_id`, but quotas the total `byte_size` of messages in the batch — the system size of their serialized representation. || || `input_rows_throttler_class_id` | **Type**: `std::optional>` Quota class for the `input_rows_throttler_id` throttler. The class must be declared by that throttler; `default` selects the reserved class with weight `1.0`. Requires `input_rows_throttler_id` to be set. || || `input_bytes_throttler_class_id` | **Type**: `std::optional>` Quota class for the `input_bytes_throttler_id` throttler. The class must be declared by that throttler; `default` selects the reserved class with weight `1.0`. Requires `input_bytes_throttler_id` to be set. || || `skip_if_expression` | **Type**: `std::optional` YTQL predicate for filtering (skipping) input messages before `Computation` processing. A message is skipped (dropped) if the predicate evaluates to boolean `true`. The predicate is evaluated on the message `payload` columns plus the meta-columns `$message_id`, `$stream_id`, `$system_timestamp`, `$event_timestamp`, `$alignment_timestamp`. The result must be boolean: `NULL` or a non-boolean result causes an exception. Applies to both `input` and `source` streams. The number of skipped messages is exported via the `input_streams/skipped_by_expression_count` and `source_streams/skipped_by_expression_count` metrics. || |# #### Source Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Arbitrary dynamic parameters of the corresponding `Source` class. || || `draining` | **Type**: `bool` **Default value**: `false` || |# #### Sink Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `parameters` | **Type**: `NYT::TIntrusivePtr` **Default value**: `{}` Arbitrary dynamic parameters of the corresponding `Sink` class. || |# ### JobManager This is the config for the module that manages [jobs](https://ytsaurus.tech/docs/en/glossary.md#job) on the [controller](https://ytsaurus.tech/docs/en/glossary.md#controller). Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `balancer_type` | **Type**: [NYT::NFlow::EJobBalancerType](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EJobBalancerType) **Default value**: `cpu_aware` || || `use_cpu_aware_balancer` | **Type**: `std::optional` Enables experimental CPU-aware balancing. Default balancing can lead to severe CPU resource imbalance among workers. || || `rebalance_delay_after_pipeline_sync` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `30s` || || `rebalance_target_deviation` | **Type**: `double` **Default value**: `0.1` Maximum acceptable deviation. || || `rebalance_hot_mode_coeff` | **Type**: `double` **Default value**: `2.0` || || `rebalance_action_min_time` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `4s` || || `rebalance_action_max_time` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `12s` || || `rebalance_sync_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` Period of `cpu_aware` balancing. It is not recommended to set a value lower than `15min` because the balancer looks at `10min` metrics. || || `rebalance_count_exceeded_allowed` | **Type**: `double` **Default value**: `1.2` || || `rebalance_even_load_thresholds` | **Type**: `THashMap<`[NYT::NFlow::EBalanceResource](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EBalanceResource)`, NYT::TIntrusivePtr<`[NYT::NFlow::TEvenLoadThresholds](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TEvenLoadThresholds)`>>` **Default value**: `{}` Per-resource thresholds of the "load is even" gate, for example `{cpu = {spread = 2.0; ratio = 1.5}}`. Only resources with a non-zero weight are consulted. Unset fields fall back to the defaults: a spread of `1.0` core for CPU and `1 GB` for memory, a ratio of `1.2`. || || `rebalance_min_cpu_spread` | **Type**: `std::optional` Deprecated: the same as `spread` of the `cpu` entry of `rebalance_even_load_thresholds`. A value set explicitly in `rebalance_even_load_thresholds` takes precedence. || || `rebalance_min_cpu_ratio` | **Type**: `std::optional` Deprecated: the same as `ratio` of the `cpu` entry of `rebalance_even_load_thresholds`. A value set explicitly in `rebalance_even_load_thresholds` takes precedence. || || `balance_weights` | **Type**: `THashMap<`[NYT::NFlow::EBalanceResource](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EBalanceResource)`, double>` **Default value**: `{'cpu': 1.0, 'memory': 0.0}` Relative importance of the resources for `cpu_aware` balancing, for example `{cpu = 80; memory = 20}`. The given keys are merged into the default `{cpu = 1; memory = 0}`; the weights are normalized, so only the proportions matter. A resource with a zero weight does not take part in balancing, so by default only CPU is balanced. The `rebalance_target_deviation` threshold applies to the normalized weighted sum: adding a second resource proportionally shrinks the contribution of the first one. With a non-zero `memory` weight, partition memory includes the measured job memory and the input buffer bytes of its streams. Each input stream is charged the larger of its `demand`, as requested before the buffer pool scales it down, and its resident bytes. A stream without `demand`, for example one with an explicit limit override or on a worker without buffer manager v2, is charged its resident bytes. || || `balancer_metrics_source` | **Type**: [NYT::NFlow::EBalancerMetricsSource](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EBalancerMetricsSource) **Default value**: `job` Where the `cpu_aware` balancer takes a partition's CPU usage from. `partition`: the 10-minute rate of the running job, and until it exists the partition's persisted history (measured by the previous job, survives moves and restarts); 30-second and immediate rates are never used. `job`: the previous behaviour, only the running job's metrics with a fallback to the 30-second and immediate rates. Off by default while the change is rolled out pipeline by pipeline; temporary switch. || || `balance_warmup_protection` | **Type**: `bool` **Default value**: `false` Never move a running job whose metrics are not mature yet: the 10-minute rate exists and one more 10-minute window has passed since it appeared. Such a job is still paying for its previous move and its weight is unknown. The protection applies to every balancing resource: a memory-only configuration waits for the CPU rate to mature as well. A worker over the count limit whose kick candidate is immature is skipped for the round. Jobs younger than two minutes stay movable: they have invested nothing yet, and on a pipeline restart the workers do not register all at once. Partitions without a job are placed as usual. Off by default while the change is rolled out pipeline by pipeline; temporary switch. || || `balance_warmup_idle_worker_share` | **Type**: `double` **Default value**: `0.2` Share of the group's workers without a job of the group at which warm-up protection is lifted: filling idle workers matters more than the metrics of the jobs that would move, e.g. when workers register minutes apart after a pipeline restart. Temporary switch for rollback. || || `worker_coef_mode` | **Type**: [NYT::NFlow::EWorkerCoefMode](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EWorkerCoefMode) **Default value**: `legacy` How the `cpu_aware` balancer estimates the relative speed of workers. `probing`: from partitions that moved between workers, comparing CPU per message before and after the move; observations per worker pair accumulate in `balancer_state` and are solved together; without moves all coefficients are 1. `legacy`: the previous estimate from the current usage of each worker's partitions against the computation averages. Off by default while the change is rolled out pipeline by pipeline; temporary switch. || || `worker_coef_half_life` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1d` Half-life of an accumulated worker-pair observation: when a new observation merges in, the old one loses weight in proportion to the time passed. Without new observations the estimate does not change. || || `worker_coef_retention` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `7d` How long the observations of a worker absent from the group are kept; a worker returning earlier gets its coefficient back. || || `worker_coef_prior_weight` | **Type**: `double` **Default value**: `0.05` Weight of the prior "the worker's coefficient is 1" in observation weight units (a move of one partition weighs `1 / N`, `N` being the number of partitions on the receiving worker). Pins the group's mean coefficient to 1 and damps single observations. || || `worker_coef_max_ratio` | **Type**: `double` **Default value**: `4.0` Safety limit: worker coefficients are clamped to `[1 / max_ratio, max_ratio]`. || || `disable_even_load_gate` | **Type**: `std::optional` || || `async_balancing` | **Type**: `bool` **Default value**: `true` || || `graceful_move` | **Type**: `bool` **Default value**: `true` || || `zero_queue_latency` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` || || `planning_horizon` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10m` || || `preloading_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `30m` How long the `resource_queue` balancer keeps a model preload that is still loading, counted from the request. Until then the preload is not released and the worker counts as a worker of the computations that need the model; after it the usual rules apply. || || `minimum_worker_count` | **Type**: `unsigned long` **Default value**: `1` Minimum number of workers to distribute the load across. Must be filled in to avoid out-of-memory errors and overloading individual workers when the system starts. Recommended value is about 60% of the target number of workers (to survive the loss of a subset of workers, for example, if a data center goes offline). || || `lost_job_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` Time after which a `Job` is considered lost if no status update reaches the controller. The same timeout is used to consider a worker lost. || || `faulty_address_window` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5m` Window within which the exponentially decaying counter of connection losses between the controller and a worker is calculated. If this counter exceeds `faulty_address_attempts`, the worker is considered `faulty` and is ignored during load balancing. Solves the problem of workers that constantly lose connection to the controller. Also solves the problem of two workers with different incarnation IDs but the same address (when deployment has two pods that think they share the same FQDN). || || `faulty_address_attempts` | **Type**: `unsigned long` **Default value**: `5` See the description of the `faulty_address_window` parameter. || || `partitioning` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicPartitioningCoordinatorSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicPartitioningCoordinatorSpec)`>` **Default value**: `{}` Pipeline partitioning settings. || || `worker_group_override` | **Type**: `THashMap, NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicJobManagerGroupSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicJobManagerGroupSpec)`>>` **Default value**: `{}` || || `partition_history_limit` | **Type**: `long` **Default value**: `4096` Most partition histories the balancer keeps across job restarts, see `balancer_metrics_source`. The histories live in one persisted document with a size limit, so when the limit is reached the lightest history gives way to a heavier one and lighter ones are not saved. A pipeline stop saves no histories at all. || |# ### JobTracker This is the config for the module that manages jobs on the [worker](https://ytsaurus.tech/docs/en/glossary.md#worker). Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `job_control_threads` | **Type**: `int` **Default value**: `3` Number of control threads. In most cases, the default value is sufficient. || || `job_threads` | **Type**: `std::optional` Number of threads for performing jobs. If not specified, the pool size is calculated automatically based on the node's CPU limit. If the node limit cannot be calculated, 30 threads are created. || || `buffer_state_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicBufferStateManagerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicBufferStateManagerSpec)`>` **Default value**: `{}` Configuration of the buffer management module for incoming and outgoing messages. || || `load_throughput_throttler` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TLoadThroughputThrottlerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TLoadThroughputThrottlerSpec)`>` **Default value**: `{'limit': 134217728.0}` || || `state_cache` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicStateCacheSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicStateCacheSpec)`>` **Default value**: `{}` || || `mark_performance_metrics_steady_after_first_iteration` | **Type**: `bool` **Default value**: `false` Report the job's rate counters (CPU, input messages, memory) as steady only once its first iteration with input has completed, so that the balancer reads windowed metrics such as `cpu_usage_10m` after the job's initialization (state download, index build) has decayed out of them; the counters themselves are never reset. A job that has received no input for a minute is reported as steady as well, so an idle job reports its near-zero rates instead of staying unmeasured. Off by default while the change is rolled out pipeline by pipeline; temporary switch. || |# #### BufferStateManager This is the config for the module that manages buffer sizes. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `manage_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Frequency of buffer size recalculation. || || `demand_window` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` Time window over which the utilization of a single buffer is estimated. || || `input_buffer` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicBufferStateManagerSpec::TOneSideBufferSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicBufferStateManagerSpec_TOneSideBufferSpec)`>` **Default value**: `{}` Input buffer settings. || || `output_buffer` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicBufferStateManagerSpec::TOneSideBufferSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicBufferStateManagerSpec_TOneSideBufferSpec)`>` **Default value**: `{}` Output buffer settings. || || `enable_v2` | **Type**: `bool` **Default value**: `true` Enables the v2 buffer-sizing strategy: limit = peak usage + headroom, with a `v2_gain_epochs × demand × epoch` bandwidth-delay-product floor. The `demand × max_duration` cap is raised by announced input backlog or, for a producing output, to an equal share of half `fair_share_pool` because producer epochs do not reveal the downstream acknowledgement period. Demand-backed limits are allocated before speculative output probes. On both sides, Σ(limits) ≤ `fair_share_pool` including other streams' in-flight bytes; `job_limit` remains the per-stream bound. Streams with `job_overrides` remain fully outside the pool as in v1, so worker buffer memory should cover `fair_share_pool` plus the actual in-flight bytes of overridden streams. Enabled by default; set this option to `false` to use the previous v1 formula. || |# {% cut "**Additional parameters**" %} #| || `epoch_cycle_window_samples` | **Type**: `int` **Default value**: `16` Number of samples in the window used to estimate the median job epoch cycle. || || `max_rate_estimator_buckets` | **Type**: `int` **Default value**: `8` Number of buckets in the windowed-max drain-rate estimator; changing it resets the estimate. || || `warmup_refresh_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `30s` How often a job polls the converged warmup state for persistence. || || `v2_gain_epochs` | **Type**: `double` **Default value**: `2.0` Minimum buffer target in units of job epochs, used as a bandwidth-delay-product floor. Applies only when `enable_v2` is set. || || `v2_use_offered_rate` | **Type**: `bool` **Default value**: `true` Whether announced backlog rate contributes to demand. Disable it when a producer systematically overstates its offered rate. Applies only when `enable_v2` is set. || || `v2_floor` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `2Mi` Minimum grant for a stream with backlog; it must fit the largest message. Applies only when `enable_v2` is set. || || `v2_headroom_growth_factor` | **Type**: `double` **Default value**: `2.0` Headroom growth factor per management tick while utilization is high. Applies only when `enable_v2` is set. || || `v2_high_utilization_threshold` | **Type**: `double` **Default value**: `0.65` Utilization threshold above which headroom grows; below half this value headroom decays. Applies only when `enable_v2` is set. || || `v2_publish_threshold` | **Type**: `double` **Default value**: `0.25` Suppress a new limit when its relative increase is smaller than this value; decreases are always published. Applies only when `enable_v2` is set. || |# {% endcut %} All dimensional parameters in this config (measured in number of items or bytes) can be parsed from strings like "50K". ### ControllerConnector This is the config for managing the connection between the worker and the controller. Don’t change it unless necessary. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `controller_wait_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5m` Time after which the worker forcibly disconnects all jobs in case of loss of connection with the controller. || || `controller_discover_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` Period for rediscovering the controller. || || `controller_heartbeat_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Period of worker heartbeats to the controller. || || `worker_statistics_report_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `30s` || || `controller_heartbeat_rpc_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` Heartbeat timeout. || || `controller_heartbeat_failure_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Backoff time in case of handshake failure. || || `controller_handshake_rpc_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` Handshake timeout. || || `controller_handshake_failure_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` || || `orchid_update_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Period for updating `orchid` on the controller. || |# ## Parameters {#parameters} The `parameters` field exists in the `Computation`, `Source`, `Sink`, and `Resource` specs. It contains parameters of the selected built-in implementation, such as a process-function adapter. Check the selected class’s documentation for supported fields, and don’t use this block for user-logic parameters. Set C++ process-function parameters separately in `processing_function_parameters`. Declare a regular [yson struct](https://ytsaurus.tech/user-guide/storage/data-types.md#yson_struct), register its type with the function, and read it through the runtime context: ```cpp struct TMyParameters : public NYTree::TYsonStruct { i64 Threshold; REGISTER_YSON_STRUCT(TMyParameters); static void Register(TRegistrar registrar); }; YT_FLOW_DEFINE_PROCESS_FUNCTION(TMyFunction, TMyParameters); ``` ```yson "processing_function_parameters" = { "threshold" = 10; }; ``` Static parameters are available in `Init` through `initContext->GetParameters()`; dynamic parameters are available through `context->GetDynamicParameters()`. For more details, see [Process functions](https://ytsaurus.tech/docs/cpp/process-functions.md#parameters). ## Config This is the config for a Flow cluster node. Source: [yt/yt/flow/library/cpp/runner/config.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/runner/config.h) #| || **Parameter** | **Description** || || `logging` | **Type**: `NYT::TIntrusivePtr<`[NYT::NLogging::TLogManagerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NLogging_TLogManagerConfig)`>` **Default value**: ```yson { "high_backlog_watermark" = 100000; "low_backlog_watermark" = 100000; "min_disk_space" = 0; "rules" = [ { "exclude_categories" = []; "max_level" = "maximum"; "min_level" = "info"; "writers" = [ "Stderr"; ]; }; ]; "writers" = { "Stderr" = { "common_fields" = {}; "enable_host_field" = %false; "enable_native_tags" = %false; "enable_source_location" = %false; "enable_span_id_field" = %false; "enable_system_fields" = %true; "format" = "plain_text"; "type" = "stderr"; "yson_format" = "text"; }; }; } ``` Logging settings. || || `jaeger` | **Type**: `NYT::TIntrusivePtr<`[NYT::NTracing::TJaegerTracerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NTracing_TJaegerTracerConfig)`>` **Default value**: `{}` || || `pipe_io_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NPipeIO::TPipeIODispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NPipeIO_TPipeIODispatcherConfig)`>` **Default value**: `{}` || || `solomon_registry` | **Type**: `NYT::TIntrusivePtr<`[NYT::NProfiling::TSolomonRegistryConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NProfiling_TSolomonRegistryConfig)`>` **Default value**: `{}` || || `error_backtrace_enricher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TBacktraceEnricherSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TBacktraceEnricherSpec)`>` **Default value**: `{}` || || `query_engine_config` | **Type**: `NYT::TIntrusivePtr<`[NYT::NQueryClient::TQueryEngineConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NQueryClient_TQueryEngineConfig)`>` **Default value**: `{}` || || `cluster_url` | **Type**: `std::string` **Required parameter** YTsaurus cluster to use. || || `path` | **Type**: `TString` **Required parameter** Working directory of the pipeline on the `cluster_url` cluster. || || `proxy_role` | **Type**: `std::optional` `rpc proxy` role. || || `rpc_port` | **Type**: `int` **Default value**: `0` Main port for communication between workers and controllers, can be different for each instance. || || `monitoring_port` | **Type**: `int` **Default value**: `0` API for obtaining metrics. || || `companion` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::NCompanion::TCompanionConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_NCompanion_TCompanionConfig)`>` Companion process parameters. Needed by any worker that runs a companion (Python, Java, Go, C++ companion). Filled in automatically for a vanilla launch. || || `controller` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::NController::TControllerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_NController_TControllerConfig)`>` **Default value**: `{}` `Controller` parameters. || || `worker` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::NWorker::TWorkerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_NWorker_TWorkerConfig)`>` **Default value**: `{}` `Worker` parameters. || || `authenticator` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TAuthenticatorConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TAuthenticatorConfig)`>` **Default value**: `{}` || || `bus_server` | **Type**: `NYT::TIntrusivePtr<`[NYT::NBus::NTcp::TBusServerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NBus_NTcp_TBusServerConfig)`>` **Default value**: `{}` || || `rpc_server` | **Type**: `NYT::TIntrusivePtr<`[NYT::NRpc::TServerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NRpc_TServerConfig)`>` **Default value**: `{}` || || `core_dumper` | **Type**: `NYT::TIntrusivePtr<`[NYT::NCoreDump::TCoreDumperConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NCoreDump_TCoreDumperConfig)`>` || || `solomon_exporter` | **Type**: `NYT::TIntrusivePtr<`[NYT::NProfiling::TSolomonExporterConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NProfiling_TSolomonExporterConfig)`>` **Default value**: `{'enable_solomon_aggregates': true}` || || `solomon_proxy` | **Type**: `NYT::TIntrusivePtr<`[NYT::NProfiling::TSolomonProxyConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NProfiling_TSolomonProxyConfig)`>` **Default value**: `{}` || || `http_client_config` | **Type**: `NYT::TIntrusivePtr<`[NYT::NHttp::TClientConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NHttp_TClientConfig)`>` **Default value**: `{}` || || `https_client_config` | **Type**: `NYT::TIntrusivePtr<`[NYT::NHttps::TClientConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NHttps_TClientConfig)`>` **Default value**: `{}` || || `http_poller_threads` | **Type**: `int` **Default value**: `1` || || `abort_on_unrecognized_options` | **Type**: `bool` **Default value**: `true` When set to true, Flow Node will not start if there are unknown options in the config. || || `ignore_singletons_dynamic_config` | **Type**: `bool` **Default value**: `false` || || `enable_porto_resource_tracker` | **Type**: `bool` **Default value**: `true` || |# {% cut "**Additional parameters**" %} #| || `address_resolver` | **Type**: `NYT::TIntrusivePtr<`[NYT::NNet::TAddressResolverConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NNet_TAddressResolverConfig)`>` **Default value**: `{}` || || `grpc_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NRpc::NGrpc::TDispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NRpc_NGrpc_TDispatcherConfig)`>` **Default value**: `{}` || || `tcmalloc` | **Type**: `NYT::TIntrusivePtr<`[NYT::NTCMalloc::TTCMallocConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NTCMalloc_TTCMallocConfig)`>` **Default value**: `{'guarded_sampling_rate': 16777216}` || || `fiber_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NConcurrency::TFiberManagerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NConcurrency_TFiberManagerConfig)`>` **Default value**: `{}` || || `resource_tracker` | **Type**: `NYT::TIntrusivePtr<`[NYT::NProfiling::TResourceTrackerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NProfiling_TResourceTrackerConfig)`>` **Default value**: `{}` || || `tcp_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NBus::NTcp::TDispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NBus_NTcp_TDispatcherConfig)`>` **Default value**: `{}` || || `rpc_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NRpc::TDispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NRpc_TDispatcherConfig)`>` **Default value**: `{}` || || `protobuf_interop` | **Type**: `NYT::TIntrusivePtr<`[NYT::NYson::TProtobufInteropConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYson_TProtobufInteropConfig)`>` **Default value**: `{}` || || `clients_cache` | **Type**: `NYT::TIntrusivePtr<`[NYT::NClient::NCache::TClientsCacheConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NClient_NCache_TClientsCacheConfig)`>` **Default value**: `{}` Connection settings of the node's YT clients cache. It backs the pipeline connector, the queue log writer and everything that takes a client from `TComputationContext::ClientsCache` — computations, connectors and state managers. The pipeline authenticator builds its own client and is not governed by these settings. || || `clients_cache_factory` | **Type**: `NYT::TIntrusivePtr` Parameters of the root YT clients cache factory installed via `SetRootClientsCacheFactory()`. Their schema is defined by the factory itself; the built-in factory ignores this block. || || `enable_phdr_cache` | **Type**: `bool` **Default value**: `true` || |# {% endcut %} ### Controller Source: [yt/yt/flow/library/cpp/controller/config.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/controller/config.h) #| || **Parameter** | **Description** || || `controller_threads` | **Type**: `int` **Default value**: `5` Number of threads. || || `orchid_update_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` `orchid` recalculation period. || || `warm_up_time` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` Warm-up period when the controller starts. || || `scheduler_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` Scheduler invocation period. || || `cache_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` || || `feedback_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` || || `metrics_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` || || `write_own_retryable_errors_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` || || `publish_retry_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` Period during which the controller attempts to tell YTsaurus that it is the leader. || || `publish_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `2h` || || `election_manager` | **Type**: `NYT::NYTree::TPolymorphicYsonStruct, NYT::NFlow::NController::EElectionBackend, NYT::NYTree::NDetail::TOptionalValue, NYT::NFlow::NController::TElectionBackendConfigBase, NYT::NYTree::NDetail::TLeafTag<(NYT::NFlow::NController::EElectionBackend)0, NYT::NFlow::NController::TCypressElectionBackendConfig>, NYT::NYTree::NDetail::TLeafTag<(NYT::NFlow::NController::EElectionBackend)1, NYT::NFlow::NController::TDyntableElectionBackendConfig>, NYT::NYTree::NDetail::TLeafTag<(NYT::NFlow::NController::EElectionBackend)2, NYT::NFlow::NController::TChaosElectionBackendConfig>>>` **Default value**: ```yson { "backend" = "cypress"; "leader_cache_update_period" = 1000; "leader_lease_ping_period" = 1000; "leader_lease_ttl" = 5000; "lock_acquisition_period" = 1000; } ``` Leader election settings for multiple controllers. || || `persisted_state_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::NController::TPersistedStateManagerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_NController_TPersistedStateManagerConfig)`>` **Default value**: `{}` `PersistedStateManager` settings. || || `lease_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::NController::TLeaseManagerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_NController_TLeaseManagerConfig)`>` **Default value**: `{}` `LeaseManager` settings. || || `controller_service` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::NController::TControllerServiceConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_NController_TControllerServiceConfig)`>` **Default value**: `{}` `ControllerService` settings. || || `bus` | **Type**: `NYT::TIntrusivePtr<`[NYT::NBus::NTcp::TBusConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NBus_NTcp_TBusConfig)`>` **Default value**: `{}` || |# {% cut "**Additional parameters**" %} #| || `publish_request_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Timeout of a single request of a publication attempt. A failed attempt is retried, so this only decides how long one attempt may hang before the retry. || |# {% endcut %} #### PersistedStateManager Source: [yt/yt/flow/library/cpp/controller/config.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/controller/config.h) #| || **Parameter** | **Description** || || `timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` Save timeout in YTsaurus. || || `max_reads_per_transaction` | **Type**: `long` **Default value**: `10000` || || `max_writes_per_transaction` | **Type**: `long` **Default value**: `10000` || |# #### LeaseManager Source: [yt/yt/flow/library/cpp/controller/config.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/controller/config.h) #| || **Parameter** | **Description** || || `lease_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10m` How long a job stays fenced without the controller refreshing the fence. With the Cypress election backend this is the timeout of the job's master lease transaction, with the chaos one the timeout of its chaos lease, and with the dyntable one the ttl of the pipeline-wide deadline row that gates every worker commit. || || `lease_ping_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `30s` Period at which the leader prolongs the leases. Every backend prolongs from the leader and none from the worker, but the machinery differs: the chaos leases are pinged by one periodic executor, the Cypress ones by a client-side pinger per lease transaction, and the shared dyntable deadline is simply rewritten. || || `max_concurrent_requests` | **Type**: `long` **Default value**: `500` Maximum number of concurrent requests the manager keeps in flight when it attaches to leases or terminates them. Do not increase to avoid overloading the master. Prolongation is not capped by it: the chaos pings are dispatched in one fire-and-forget round, and the Cypress ones belong to the lease transactions themselves. || |# #### ControllerService Source: [yt/yt/flow/library/cpp/controller/config.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/controller/config.h) #| || **Parameter** | **Description** || || `set_spec_retry_count` | **Type**: `int` **Default value**: `3` Number of attempts to update the spec in case of errors. || || `set_spec_retry_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5s` Time between attempts. || || `tables_throttler` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TLoadThroughputThrottlerSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TLoadThroughputThrottlerSpec)`>` **Default value**: `{}` || |# Plus a small buffer for the case where some live partitions also won’t receive any writes. ## Runner config {#runner-config} This is the config for running a pipeline. The format of this config isn’t standardized; you can use whatever config works best for your tasks. Flow provides a simple version of this config, `NYT::NFlow::TSimpleRunnerConfig`, and a program, `NYT::NFlow::TSimpleRunnerProgram`, to run a pipeline using this config. The Flow test framework relies on this config format to inject the cluster and the pipeline path in the local YTsaurus, and to apply test default parameters for faster test runs. ### SimpleRunnerConfig Source: [yt/yt/flow/library/cpp/runner/simple_runner_program.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/runner/simple_runner_program.h) #| || **Parameter** | **Description** || || `logging` | **Type**: `NYT::TIntrusivePtr<`[NYT::NLogging::TLogManagerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NLogging_TLogManagerConfig)`>` **Default value**: ```yson { "high_backlog_watermark" = 100000; "low_backlog_watermark" = 100000; "min_disk_space" = 0; "rules" = [ { "exclude_categories" = []; "max_level" = "maximum"; "min_level" = "info"; "writers" = [ "Stderr"; ]; }; ]; "writers" = { "Stderr" = { "common_fields" = {}; "enable_host_field" = %false; "enable_native_tags" = %false; "enable_source_location" = %false; "enable_span_id_field" = %false; "enable_system_fields" = %true; "format" = "plain_text"; "type" = "stderr"; "yson_format" = "text"; }; }; } ``` Logging settings. || || `jaeger` | **Type**: `NYT::TIntrusivePtr<`[NYT::NTracing::TJaegerTracerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NTracing_TJaegerTracerConfig)`>` **Default value**: `{}` || || `pipe_io_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NPipeIO::TPipeIODispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NPipeIO_TPipeIODispatcherConfig)`>` **Default value**: `{}` || || `solomon_registry` | **Type**: `NYT::TIntrusivePtr<`[NYT::NProfiling::TSolomonRegistryConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NProfiling_TSolomonRegistryConfig)`>` **Default value**: `{}` || || `error_backtrace_enricher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TBacktraceEnricherSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TBacktraceEnricherSpec)`>` **Default value**: `{}` || || `query_engine_config` | **Type**: `NYT::TIntrusivePtr<`[NYT::NQueryClient::TQueryEngineConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NQueryClient_TQueryEngineConfig)`>` **Default value**: `{}` || || `cluster_url` | **Type**: `std::string` **Required parameter** Name of the cluster where the pipeline is located. The tail after the last `/` is treated as a proxy role and takes precedence over `proxy_role` — e.g. `hahn/flow` means cluster `hahn` with role `flow`. || || `proxy_role` | **Type**: `std::optional` **Required parameter** The [proxy role](https://ytsaurus.tech/user-guide/proxy/about#rpc_proxy) to use for performing operations on the pipeline. || || `path` | **Type**: `TString` **Required parameter** Path to the pipeline on YTsaurus. || || `spec` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TPipelineSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TPipelineSpec)`>` **Required parameter** Static spec with which to start the pipeline. || || `dynamic_spec` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicPipelineSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicPipelineSpec)`>` **Default value**: `{}` Dynamic spec with which to start the pipeline. || || `abort_on_unrecognized_options` | **Type**: `bool` **Default value**: `false` || || `abort_on_specs_parseability_error` | **Type**: `bool` **Default value**: `false` || || `vanilla` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TVanillaConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TVanillaConfig)`>` If set and `enable=%true`, the runner starts a vanilla operation on YTsaurus and runs the federation inside it, instead of starting the controller/workers locally. || || `set_flow_core_target` | **Type**: `bool` **Default value**: `true` || || `direct_controller_commands` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDirectControllerCommandsConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDirectControllerCommandsConfig)`>` **Default value**: `{}` || |# {% cut "**Additional parameters**" %} #| || `address_resolver` | **Type**: `NYT::TIntrusivePtr<`[NYT::NNet::TAddressResolverConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NNet_TAddressResolverConfig)`>` **Default value**: `{}` || || `grpc_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NRpc::NGrpc::TDispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NRpc_NGrpc_TDispatcherConfig)`>` **Default value**: `{}` || || `tcmalloc` | **Type**: `NYT::TIntrusivePtr<`[NYT::NTCMalloc::TTCMallocConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NTCMalloc_TTCMallocConfig)`>` **Default value**: `{}` || || `fiber_manager` | **Type**: `NYT::TIntrusivePtr<`[NYT::NConcurrency::TFiberManagerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NConcurrency_TFiberManagerConfig)`>` **Default value**: `{}` || || `resource_tracker` | **Type**: `NYT::TIntrusivePtr<`[NYT::NProfiling::TResourceTrackerConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NProfiling_TResourceTrackerConfig)`>` **Default value**: `{}` || || `tcp_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NBus::NTcp::TDispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NBus_NTcp_TDispatcherConfig)`>` **Default value**: `{}` || || `rpc_dispatcher` | **Type**: `NYT::TIntrusivePtr<`[NYT::NRpc::TDispatcherConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NRpc_TDispatcherConfig)`>` **Default value**: `{}` || || `protobuf_interop` | **Type**: `NYT::TIntrusivePtr<`[NYT::NYson::TProtobufInteropConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYson_TProtobufInteropConfig)`>` **Default value**: `{}` || || `clients_cache` | **Type**: `NYT::TIntrusivePtr<`[NYT::NClient::NCache::TClientsCacheConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NClient_NCache_TClientsCacheConfig)`>` **Default value**: `{}` Connection settings of the YT clients cache the runner uses to talk to the pipeline cluster. They do not extend to the vanilla launcher, which builds its own clients. || || `clients_cache_factory` | **Type**: `NYT::TIntrusivePtr` Parameters of the root YT clients cache factory installed via `SetRootClientsCacheFactory()`. Their schema is defined by the factory itself; the built-in factory ignores this block. || |# {% endcut %} ## See also - [Autoscaler](https://ytsaurus.tech/docs/en/autoscaler.md) - [Computation](https://ytsaurus.tech/docs/en/computation.md) - [Watermarks and Timers](https://ytsaurus.tech/docs/en/watermarks.md) - [Connectors](https://ytsaurus.tech/docs/connectors/about.md) [*idle_partitions_max_ratio_default_value]: Why 0.4: if the source stops writing to one of the data centers, about a third of the partitions will become `idle`. [*paramsClasses]: `Computation`, `Source`, `Sink`, `Resource` # Quick start with YTsaurus Flow (C++) In this section, you’ll learn step by step how to implement your first [pipeline](https://ytsaurus.tech/docs/concepts/glossary.md#pipeline) in C++ using Flow. As an example, we’ll walk through a word count task: reading text messages from a queue, splitting them into words, and counting the occurrences of each word. ## Prerequisites - Check out the [repository](https://github.com/ytsaurus/ytsaurus/tree/main). - Set up `ya make` (the build system). - Familiarize yourself with the [basic concepts](https://ytsaurus.tech/docs/concepts/glossary.md) of Flow. ## Step-by-step guide ### 1. Define message types {#define-messages} To work with messages in a type-safe way, use `TYsonMessage`, a special subclass of `NYTree::TYsonStruct`. You need to register each message type in the global registry using the `YT_FLOW_DEFINE_YSON_MESSAGE` macro. ```cpp #include struct TWordMessage : public TYsonMessage { std::string Word; REGISTER_YSON_STRUCT(TWordMessage); static void Register(TRegistrar registrar) { registrar.Parameter("word", &TThis::Word) .Default(); } }; YT_FLOW_DEFINE_YSON_MESSAGE(TWordMessage); ``` For more details on message conversion, see [Process functions (C++)](https://ytsaurus.tech/docs/en/process-functions.md). ### 2. Prepare external state {#define-state} In this example, the counters live in an external YTsaurus table, and `TWordCountFunction` reads and updates them through `TMutableStateKeyClient`. You don't need a separate state class: create the table in step 8 and configure `TSimpleExternalStateManager` in the step 6 spec. For details, see [Working with states (C++)](https://ytsaurus.tech/docs/en/state.md). ### 3. Implement a process function for the [Source](https://ytsaurus.tech/docs/concepts/glossary.md#source) {#implement-source} Implement C++ user logic only as a [process function](https://ytsaurus.tech/docs/en/process-functions.md). For element-wise processing, inherit from `IProcessFunction` and implement `ProcessMessage`: ```cpp class TTextReadFunction : public IProcessFunction { public: void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override { auto text = GetColumnValue(message, "text"); for (const auto& word : StringSplitter(text).SplitBySet(" \t\n\r").SkipEmpty()) { auto wordMessage = New(); wordMessage->Word = word; output->AddMessage(context->ConvertToMessage(wordMessage)); } } }; YT_FLOW_DEFINE_PROCESS_FUNCTION(TTextReadFunction); ``` In the spec, the built-in `TProcessFunctionSourceComputation` executes this function. It selects source mode: output messages aren’t materialized in YT, and only recovery metadata is stored. For more details, see [Process functions](https://ytsaurus.tech/docs/en/process-functions.md#how-it-works). ### 4. Implement a stateful process function {#implement-transform} Use `IProcessFunction` for stateful processing as well. To work with external state, use `TSimpleExternalStateManager`: ```cpp class TWordCountFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitExternalStateClient(StateClient_, "/state"); } void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto state = StateClient_.GetState(message->Key); i64 count = state->GetColumnValue>("count").value_or(0); TPayloadBuilder builder(state->Schema); builder.Set(count + 1, "count"); state->Payload = builder.Finish(); } private: TMutableStateKeyClient StateClient_; }; YT_FLOW_DEFINE_PROCESS_FUNCTION(TWordCountFunction); ``` Key points: - `TMutableStateKeyClient` is a type-safe client for external state. You set the manager’s parameters in the `Computation` spec (see below), not in your own `TParameters`/`TDynamicParameters`. - `InitExternalStateClient(StateClient_, "/state")` binds the client to the external state manager named `"/state"`, which is declared in the `external_state_managers` spec. - In the spec, the built-in `TProcessFunctionComputation` executes the function and provides transform-mode exactly-once state commits. ### 5. Write main.cpp {#write-main} The `main` function ties all components together: ```cpp #include #include int main(int argc, const char** argv) { NYT::NFlow::Initialize(argc, argv); TSimpleSpecBuilder builder; builder.RegisterStream("words"); return NYT::NFlow::TSimpleRunnerProgram(std::move(builder)).Run(argc, argv); } ``` Here: - `Initialize(argc, argv)` initializes the Flow runtime. - `TSimpleSpecBuilder` is a builder where you register all type-safe streams. It automatically infers schemas from the registered `TYsonMessage` types. - `RegisterStream("words")` registers the `words` stream with the `TWordMessage` message type. - `TSimpleRunnerProgram` is a standard runner that handles starting and managing computations. Full source code for the example: ```cpp #include #include #include #include #include using namespace NYT; using namespace NYT::NFlow; using namespace NYT::NFlow::NExample; //////////////////////////////////////////////////////////////////////////////// YT_FLOW_DEFINE_YSON_MESSAGE(TWordMessage); // Register the functions; the pipeline spec picks the hosting computation (the mode) and // names the function via the `processing_function` parameter. YT_FLOW_DEFINE_PROCESS_FUNCTION(TTextReadFunction, TTextReaderParameters); YT_FLOW_DEFINE_PROCESS_FUNCTION(TWordCountFunction); //////////////////////////////////////////////////////////////////////////////// int main(int argc, const char** argv) { NYT::NFlow::Initialize(argc, argv); TSimpleSpecBuilder builder; builder.RegisterStream("words"); return NYT::NFlow::TSimpleRunnerProgram(std::move(builder)).Run(argc, argv); } ``` ### 6. Describe the pipeline spec {#write-spec} The spec describes the pipeline topology in YSON format. Here’s an example for word count: ```yson { "spec" = { "computations" = { "reader" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionSourceComputation"; "processing_function" = "NYT::NFlow::NExample::TTextReadFunction"; "output_stream_ids" = ["words"]; "source_streams" = { "queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { "queue_path" = "//path/to/queue"; "consumer_path" = "//path/to/consumer"; }; }; }; }; "counter" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "NYT::NFlow::NExample::TWordCountFunction"; "input_stream_ids" = ["words"]; "output_stream_ids" = []; "group_by_schema" = [ {"name" = "hash"; "type" = "uint64"; "expression" = "farm_hash(word)";}; {"name" = "word"; "type" = "string";}; ]; "external_state_managers" = { "/state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state/table"; }; }; }; }; }; }; } ``` You don’t need to fill the `streams` section when you use `TYsonMessage` and `TSimpleSpecBuilder`; the stream information is inferred automatically. For more details on the spec format, see [Spec & DynamicSpec](https://ytsaurus.tech/docs/concepts/spec.md). ### 7. Build the project {#build} The code fragments above explain the checked-in Word Count example; they aren't a standalone project. Build the example from the repository. Its dependencies are listed in [ya.make](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/word_count/ya.make): ```bash ya make yt/yt/flow/examples/cpp/word_count ``` The binary is `yt/yt/flow/examples/cpp/word_count/word_count`. The simplified step 6 spec omits `min_word_length`, so the default `0` applies; the checked-in config sets it to `4`. If you move the process functions into another namespace, use their fully qualified names in the spec's `processing_function` fields. ### 8. Create objects in YT {#create-yt-objects} Before you run the pipeline, you need to create: - An input queue (if it doesn’t exist yet). - A consumer for the input queue, registered on it. - A state table (for `ExternalState`). For a working example of creating the queue, consumer, and state table, see [Prepare the pipeline](https://ytsaurus.tech/docs/quickstart.md#prepare) in the quick start. The C++ runner creates the pipeline and internal tables on first launch; for a separate preparation step, see [Creating a pipeline](https://ytsaurus.tech/docs/concepts/pipeline-object.md#create). ### 9. Run and test {#run-and-test} Save the YSON from step 6 as ``. Replace the queue, consumer, and external state table paths, then add `cluster_url`, the pipeline `path`, and a `vanilla` block with `enable = %true`, your scheduler `pool`, and `worker = {"count" = 1;}`, as shown in [Prepare the pipeline](https://ytsaurus.tech/docs/quickstart.md#prepare). Launch the built binary with `--config ` as in [Validate and launch](https://ytsaurus.tech/docs/quickstart.md#launch). Check the state with `yt --proxy flow get-pipeline-state `, where `` is the `path` value from your config; the CLI should print `working`. Before you release a new version, read [Release and table change rules](https://ytsaurus.tech/docs/devops/vanilla/releases.md#release-and-configure-basic-rules). For lifecycle commands, see [Pipeline operations](https://ytsaurus.tech/docs/devops/vanilla/pipeline-operations.md). ## See also - [Process functions (C++)](https://ytsaurus.tech/docs/en/process-functions.md) - [Computation modes (C++)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (C++)](https://ytsaurus.tech/docs/en/state.md) - [Watermarks](https://ytsaurus.tech/docs/concepts/watermarks.md) - [Timers](https://ytsaurus.tech/docs/concepts/timers.md) - [Spec & DynamicSpec](https://ytsaurus.tech/docs/concepts/spec.md) # Computation in YTsaurus Flow (C++) {% note info %} This page describes Computation execution modes in C++. For a language-agnostic description of the concept, see [Computation](https://ytsaurus.tech/docs/concepts/computation.md). {% endnote %} {% note warning %} Implement new C++ user logic only as a [process function](https://ytsaurus.tech/docs/en/process-functions.md). Don’t create subclasses of the `Computation` base classes. This page documents execution modes, built-in computations, and the low-level API needed for framework and legacy maintenance. {% endnote %} This section describes the base `Computation` classes used by built-in process-function adapters and important execution-mode details. Flow currently implements four base `Computation` classes: - `TTransformComputation` - `TTransformOrderedSourceComputation` - `TSwiftOrderedSourceComputation` - `TSwiftMapComputation` Classes that include `Swift` in their name implement the [Swift](https://ytsaurus.tech/docs/concepts/swift.md) principle. See the [Swift](https://ytsaurus.tech/docs/concepts/swift.md) section for more details. ## General The following direct-API details are for framework and existing legacy maintenance; they aren’t an authoring guide for new user computations. - Declare process-function parameters as a regular `TYsonStruct`, name its type in `YT_FLOW_DEFINE_PROCESS_FUNCTION`, and pass values through `processing_function_parameters`. - Perform complex process-function initialization in `Init(const IRuntimeInitContextPtr&)`. - Select one processing granularity: `IProcessFunction` for individual messages, timers, and visits; `IBatchProcessFunction` for the whole epoch; or `IKeyedBatchProcessFunction` for one key’s batch. - To write manually in the transform-mode transaction, also implement `ISyncProcessFunction`; otherwise use a [Sink](https://ytsaurus.tech/docs/concepts/glossary.md#sink) or [ExternalState](https://ytsaurus.tech/docs/en/state.md#external-state). - Add output messages and timers through `IOutputCollector`; create and convert them through `IRuntimeContext`. - Code in one partition runs strictly single-threaded. Parallelize processing by increasing the number of [partitions](https://ytsaurus.tech/docs/concepts/glossary.md#partition). - You can convert input messages to `NYTree::TYsonStruct`. To do this, you need to: - Create a class that inherits from `TYsonMessage` (this is a special child of `NYTree::TYsonStruct`). - Register it in the global registry using `YT_FLOW_DEFINE_YSON_MESSAGE`. - In the `main` function, create a `TSimpleSpecBuilder` object and register the corresponding `stream_id` in it. - If you use `TSimpleRunnerProgram`, you can pass this `TSimpleSpecBuilder` directly to the `TSimpleRunnerProgram` constructor. - If you implement `main` yourself, you’ll need to pass specs to `TSimpleSpecBuilder` to enrich them with stream information. - You don’t need to manually fill `spec/streams` when using `TYsonMessage` — all information will be derived from the registered `TYsonMessage + stream_id` using `TSimpleSpecBuilder`. - In a process function, use `context->ConvertToYsonMessage(message)` and `context->ConvertToMessage(ysonMessage)` to convert between `TMessage => TYsonMessage` and back. ### OutputCollector {#output-collector} The `IOutputCollectorPtr output` object is passed to process-function methods and sends processing results: | Method | Description | | --- | --- | | `output->AddMessage(message)` | Add an output message (a `TMessage` object obtained via `context->MakeOutputMessageBuilder().Finish()`) | | `output->AddTimer(timer)` | Add a [timer](https://ytsaurus.tech/docs/concepts/glossary.md#timer) | | `output->SetParents(parentIds)` | Set the parent ID for tracking [lineage](https://ytsaurus.tech/docs/concepts/lineage.md). Returns a new `IOutputCollectorPtr` with the attached lineage context | Use `SetParents` when the output message is logically derived from a specific subset of inputs, not the entire batch. Non-batch `ProcessMessage` and `ProcessTimer` set lineage automatically. ### TMessage {#tmessage} The `TMessage` structure is used in `ProcessMessage` and `AddMessage` methods: Source: [yt/yt/flow/library/cpp/common/message-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/message-inl.h) #| || **Parameter** | **Description** || || `message_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** Unique message ID. || || `system_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Timestamp of the specific message creation. || || `alignment_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** || || `event_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Timestamp of the real event associated with this message. || || `stream_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** The stream this message belongs to. || || `payload` | **Type**: `NYT::TStrongTypedef` **Required parameter** The data itself. || || `payload_schema` | **Type**: `NYT::TIntrusivePtr` **Required parameter** Payload schema, populated based on `stream_id`. || |# ## TTransformComputation Transform mode handles arbitrary transformations of input data. It doesn’t work with `Source`. The result is always saved in YT, so the transformation needn’t be deterministic. Properties of `TTransformComputation`: - It can write to YT “idle”, that is, without real changes, by overwriting existing content. You should expect such a stream to create a negligible load. For new user logic, select this mode with `TProcessFunctionComputation`. Implement `IProcessFunction`, `IBatchProcessFunction`, or `IKeyedBatchProcessFunction`; add `ISyncProcessFunction` when you need a sync phase. For a complete example, see [Process functions](https://ytsaurus.tech/docs/en/process-functions.md). ### TTimer {#ttimer} The `TTimer` structure is used in `ProcessTimer` and `AddTimer` methods: Source: [yt/yt/flow/library/cpp/common/timer-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/timer-inl.h) #| || **Parameter** | **Description** || || `message_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** Unique timer ID. || || `system_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Timestamp of the timer creation. || || `event_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Timestamp of the real event associated with this timer. || || `stream_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** The stream this message belongs to. || || `key` | **Type**: `NYT::TStrongTypedef` **Required parameter** Key of the timer. || || `key_schema` | **Type**: `NYT::TIntrusivePtr` **Required parameter** Key schema, matches the `group_by_schema` of the corresponding `Computation`. || || `trigger_timestamp` | **Type**: `NYT::TStrongTypedef` **Required parameter** Time for the timer to fire. || |# ### ProcessingMode {#processing-mode} `TransformComputation` has a `parameters/processing_mode` parameter that lets you reduce processing guarantees in exchange for lowering the load on YTsaurus. #| || Possible values | **Description** || || `exactly_once` | Default value. The result of `Transform`, including output messages, processed messages, etc., is committed to `YTsaurus` within a single transaction. All input messages are deduplicated by `message_id`. || || `at_least_once_consistent` | In this mode, input message deduplication is disabled. In this mode, `TransformComputation` stops interacting with the `input_messages` table. Input messages may be processed multiple times, and the processing result is saved to `output_messages` each time and is guaranteed to be processed by subsequent `Computation` instances. This mode can be used if duplicates are not a problem, for example, because the user logic itself can deduplicate redundant messages. Duplicates can occur during any job restarts/crashes (including rescheduling). System shutdown via `draining` using `stop-pipeline`, however, does not lead to a violation of guarantees (unless jobs are restarted for some other reason during the process). || |# ### TPassthroughComputation This is a child of `TTransformComputation`. It implements a [passthrough computation](https://ytsaurus.tech/docs/concepts/computation.md#passthrough) and is mainly created to demonstrate capabilities. Features: - It has no overridable methods. - It can’t have more than one output. - All incoming `input` streams are turned into `output` by converting the message using `ConvertMessageToNewSchema`. ### TTransformOrderedSourceComputation {#ttransformorderedsourcecomputation} This mode processes `source` messages with arbitrary custom logic: parsing, filtering, or expanding one input message into several outputs. Run new user process functions under `TProcessFunctionTransformOrderedSourceComputation`. It replaces the `TSwiftPassthroughOrderedSourceComputation` → `TProcessFunctionComputation` chain when the intermediate computation only processes source data. The transformation result is materialized in YTsaurus the same way as in `TTransformComputation`: - Output messages receive unique `MessageId` values and are durably saved in YTsaurus before they are distributed downstream. After a restart, materialized messages that haven’t been delivered yet are distributed with the same `MessageId` values instead of being recomputed, so the transformation has no determinism requirements. - A message can be added to `output` with an explicit `distribute` flag, for example `output->AddMessage(std::move(message), /*distribute*/ false)`. Such a message isn’t published downstream, but it participates in watermark estimation on equal terms with published ones: the watermark generator registers the read over the full set of output messages before the publication filter is applied, so the watermark can be estimated correctly over the full stream even when a significant part of it is filtered out. The source offset advances in the epoch transaction in either case. - The `source` offset, the materialized output, and the states are committed in a single epoch transaction, so processing of each source message is applied exactly once. For new user logic, implement a process function: use `Init(const IRuntimeInitContextPtr&)` for initialization, `ProcessMessage` or `Process` for processing, and, when needed, `ISyncProcessFunction::Sync` for manual writes in the epoch transaction. A process function keeps its state in a `TMutableStateKeyClient` field (see [Working with states](https://ytsaurus.tech/docs/en/state.md#internal-state)), initializes it through `initContext->InitClient(...)`, and reads it through `GetState(message->Key)`. Before processing, the adapter loads state for the current epoch’s message keys. A computation instance is always bound to a single `source` key, so all messages in an epoch carry the same key and address the same state row. The framework synchronizes state clients created through `IRuntimeInitContext` in the epoch transaction atomically with the `source` offset, so an ordinary mutation, such as incrementing a counter, is exactly-once correct: no additional deduplication by `MessageId` is needed. The computation spec is validated at startup; the following fields cause a validation error: * `input` streams; * [timers](https://ytsaurus.tech/docs/concepts/glossary.md#timer); * [key-visitor streams](https://ytsaurus.tech/docs/concepts/key_visitor.md); * a non-empty `group_by_schema`; * `external_state_managers`; * `external_state_joiners` that have no `join_on/key_schema_override` set (the source message key isn’t described by `group_by_schema`, so the key schema must be set explicitly). `watermark_strategy` is supported: `watermark_generator` estimates the source watermarks, `watermark_alignment` aligns reading of the source relative to other streams (`read_delays` delay reading, not publication), and `event_timestamp_assigner` assigns `event_timestamp` to output messages. The atomicity of the commit of the `source` offset, the materialized output messages, and the state doesn’t depend on alignment. `skip_if_expression` is supported as well. `skip_if_expression` is applied before processing, but after the input batch has been counted in the metrics and in the number of late messages: a filtered-out message reaches neither the state nor the output. It doesn’t affect watermark estimation either: the generator registers a read only over output messages, so a fully filtered-out batch doesn’t move `EventWatermark`, and over a long series of such batches the partition watermark stands still. The source takes the `EventWatermark` markers of input records into account when reading regardless of the filter, but with `use_source_watermark = false` (the default value) the source watermark only bounds the estimate from above and never moves it forward; it becomes the only source of the partition watermark with `use_source_watermark = true`. This exactly matches the behavior of `TSwiftOrderedSourceComputation`. Write user logic as a [process function](https://ytsaurus.tech/docs/en/process-functions.md) and specify the `NYT::NFlow::TProcessFunctionTransformOrderedSourceComputation` adapter in the spec. It runs the function in this mode with the same output materialization, states, and spec validation. For an example, see `NYT::NFlow::NExample::TLogParserProcessFunction` from [`examples/cpp/log_parser`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/log_parser): it splits a log line into records, emits the `TLogRecordMessage` YSON structure (`level`, `text`, `worst_level_so_far`), and maintains the `TWorstSeverityState` state, a running maximum severity per source partition. For more details, together with the full source code, see the [Log parser](https://ytsaurus.tech/docs/en/examples/log_parser.md) section. #### TProtoTransformOrderedSourceComputation {#tprototransformorderedsourcecomputation} For new user logic, use `TProtoParsingProcessFunctionBase` from `yt/yt/flow/library/cpp/parsers/proto.h`. The base reads the string column selected by `processing_function_parameters/data_column` (`"data"` by default), parses it into `TProto`, and calls `ProcessProto(message, proto, output, context)`. It routes a read or parsing error to `ProcessUnparsed(message, error, output, context)`, which rethrows by default. Keep state in `TMutableStateKeyClient` and initialize it in `Init`; the key is available as `message->Key`. For materialized ordered-source mode, run the function under `TProcessFunctionTransformOrderedSourceComputation`. `TProtoTransformOrderedSourceComputation` is the low-level counterpart for maintaining existing legacy code. Don’t use it as the base of a new user class. For a process-function example with the same parsing pattern, see `NYT::NFlow::NExample::TProtoLogParserFunction` from [`examples/cpp/proto_parser`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/proto_parser). It inherits from `TProtoParsingProcessFunctionBase` and runs under `TProcessFunctionTransformOrderedSourceComputation`: it parses `TLogRecordProto`, emits `TLogRecordMessage` (`level`, `text`, `seen_at_level`), and maintains the `TLevelCountsState` state, a counter of records of each level per source partition. The counter isn’t idempotent under reprocessing and is correct exactly because the state is committed in the same transaction as the `source` offset. For more details, see the [Proto parser](https://ytsaurus.tech/docs/en/examples/proto_parser.md) section. ## TSwiftMapComputation Swift-map mode implements a deterministic simple `Map` without materializing results in YT. Run user process functions under `TProcessFunctionSwiftMapComputation`. Features: - It doesn’t support `sources` and `sinks`. - It supports `timer_streams` and `key_visitor_streams` only for working with state: emitting output messages from timer or visit processing is prohibited, so output streams can’t depend on timer and visit streams in `streams_dependency`. - It must return the same result (including order) for each input row. If the result changes on repeated runs, various negative effects can occur. It’s possible that separate parts of the system will process different versions of the output, up to duplicates, if the field values for subsequent `group-by` change. - As a consequence, each resulting message must have exactly one parent. ### TSwiftPassthroughComputation This is a child of `TSwiftMapComputation`. It’s similar to `TPassthroughComputation`: it simply turns `input` into `output` by converting messages to a new schema. ## TSwiftOrderedSourceComputation Swift ordered-source mode reads data from external sources and requires the data stream from each instance to be ordered. Run user process functions under `TProcessFunctionSourceComputation`. Features: - There must be exactly one `source`. - The `Source` must be a child of `IOrderedSource`. - It can use `watermark_strategy/event_timestamp_assigner` to assign `event_timestamp` to output messages, provided a column is specified. Otherwise, the `event_timestamp` of the output message will be taken from the `event_timestamp` of the message from `source` — that is, the creation time of the original message. - It uses `watermark_strategy/watermark_generator` to estimate [watermarks](https://ytsaurus.tech/docs/concepts/glossary.md#timestamps-and-watermarks) of input sources. - It uses `watermark_strategy/watermark_alignment` to align stream reading relative to other streams. - It lets you filter part of the events: a message added to `output` with `distribute=false` isn’t published but is still considered when estimating the watermark. This lets you estimate the watermark using the full stream even when a significant part of the stream is filtered out. - `system_timestamp` is assigned at the moment the message is registered in `output`. - It reliably saves part of the data in YT to guarantee recovery of all metadata. It doesn’t save the messages themselves in YT. - It can write to YT “idle”, that is, without real changes. You should expect such a stream to create a minimal load. ### TSwiftPassthroughOrderedSourceComputation This is a child of `TSwiftOrderedSourceComputation`. It’s similar to `TPassthroughComputation`: it converts `source` to `output` by converting messages to a new schema. ## FAQ ### How to configure Source and Sink? {#source-sink-configuration} You configure `Source` and `Sink` in the `Computation` spec via the `sources` and `sinks` sections, respectively. Each `Source`/`Sink` is defined in a separate subsection with a type specified (for example, `TQueueSource`) and connection parameters. For more details about available connectors, see the [Connectors](https://ytsaurus.tech/docs/connectors/about.md) section. ### How do batching and partitioning work? {#batching-partitions} Each partition is processed strictly single-threaded. You achieve parallelism by increasing the number of partitions (`partition_count` in the spec). `IBatchProcessFunction::Process` receives all messages and timers for the current [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch), which lets you optimize processing. The process-function API intentionally doesn’t expose `PoolInvoker`; parallelize processing through partitions. ### How to estimate the load on internal tables? {#internal-tables-load} The load on YTsaurus internal tables depends on the `Computation` type and the number of partitions. Below is an approximate estimate: #| || **Computation type** | **Records per epoch per partition** | **Comment** || || `TTransformComputation` | ~2–4 | State writes + commit || || `TTransformOrderedSourceComputation` | ~2–4 | Output materialization + offsets + states || || `TSwiftMapComputation` | 0 | Doesn’t write to YT || || `TSwiftOrderedSourceComputation` | ~1–2 | Metadata for recovery || |# Total load = records per partition × number of partitions × epoch frequency. For a [pipeline](https://ytsaurus.tech/docs/concepts/glossary.md#pipeline) with 1000 partitions and an epoch of 1 second, `TTransformComputation` will create ~2000–4000 records/s. ## Pipeline state {#pipeline-state} Possible pipeline states (type `EPipelineState`): #| || Possible values | **Description** || || `unknown` | Default state value for a pipeline that has not been started yet. || || `stopped` | Pipeline is stopped. `Draining` has been performed — all intermediate messages in the pipeline are processed, the actual processed offsets in the source queues are committed. All jobs are stopped. In this state, you can safely roll out a pipeline release and update its static spec. To initiate a transition to this state, use the `stop-pipeline` command. || || `paused` | Pipeline is paused. All jobs are stopped, but intermediate messages in the pipeline may not have been processed. To initiate a transition to this state, use the `pause-pipeline` command. || || `working` | Pipeline is working. Messages are being processed. To initiate a transition to this state, use the `start-pipeline` command. || || `draining` | Intermediate state. Pipeline is in the process of stopping (transition to `stopped` state). All messages that the pipeline has already seen from sources and all internal intermediate messages between computations are being final-processed. || || `pausing` | Intermediate state. Pipeline is in the process of pausing. All jobs are being stopped. || || `completed` | Final state. Pipeline is complete. All sources were finite, and all messages from them have been processed. It is currently not possible to exit this state; you can only recreate the pipeline. || |# ## See also - [Process functions (C++)](https://ytsaurus.tech/docs/en/process-functions.md) - [Computation (concept)](https://ytsaurus.tech/docs/concepts/computation.md) - [Working with states (C++)](https://ytsaurus.tech/docs/en/state.md) - [Quick start (C++)](https://ytsaurus.tech/docs/en/getting-started.md) # Working with states in YTsaurus Flow (C++) {% note info %} This page describes the specifics of working with states in C++. For a language-agnostic description of the concept, see the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. {% endnote %} All user-logic examples on this page use [process functions](https://ytsaurus.tech/docs/en/process-functions.md). Don’t create a custom `Computation` subclass to work with state. ## Internal State {#internal-state} This is the simplest way to store a state inside a `Computation`. Data is automatically loaded at the start of the [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch) and written on commit. You don’t need to create tables yourself — Flow manages them automatically. ### Usage To work with Internal State, you need to: You can use **any type** as a state type, as long as it has serialization and deserialization functions defined. By default, all types with standard `Serialize`/`Deserialize` functions defined in YT are supported (including `TYsonStruct` subclasses); for a custom type, you just need to provide overloads for these functions. By default, a state is considered empty if it equals the default value (`TMyState{}`), and clearing it recreates it. To override this behavior, the state type can inherit optional mixins from `NYT::NFlow`: - `ICustomStateOps` — custom logic for `Clear()` and `IsEmpty()` (specified as a pair); - `ICustomYsonView` — custom representation for `ToYsonView()` for read-state introspection. You need to: 1. Declare a `TMutableStateKeyClient MyStateClient_` field in your process function. 2. Override `Init(const IRuntimeInitContextPtr& initContext)` and call `initContext->InitClient(MyStateClient_, "my_state")` in it. Use a string that’s unique within the computation as the name. 3. To get the state by key, use `MyStateClient_.GetState(message->Key)`. The returned accessor `TStateAccessor` behaves like a smart pointer to `TMyState` (`state->...`, `*state`) and is valid only within the current epoch — you can’t store it in fields. 4. To clear the state (delete the row from the table), call `state.Clear()` on the accessor. If the state is empty, the corresponding row will be deleted. Example: ```cpp struct TMyState : public NYTree::TYsonStruct { std::optional SomeValue; REGISTER_YSON_STRUCT(TMyState); static void Register(TRegistrar registrar) { registrar.Parameter("some_value", &TThis::SomeValue) .Default(); } }; class TMyFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitClient(MyStateClient_, "my_state"); } void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto state = MyStateClient_.GetState(message->Key); state->SomeValue = 42; // ... } void ProcessTimer( const TInputTimerConstPtr& timer, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto state = MyStateClient_.GetState(timer->Key); // Clear the state (delete the row from the table): state.Clear(); } private: TMutableStateKeyClient MyStateClient_; }; ``` ### Compression Internal State supports data compression. You configure it in [DynamicSpec](https://ytsaurus.tech/docs/concepts/spec.md) for each state separately — in the `state_manager` section of the computation, in `overrides//format`: - `compress` — enable compression (default `false`); - `recode_probability` — probability to recode the state to the specified format during the next processing; this ensures gradual migration of states after changing the format (default `0.1`). ## Yson State Reader {% note warning %} This functionality is not yet implemented. {% endnote %} ## External State {#external-state} External State is a state stored in a user-defined dynamic table in YTsaurus. Unlike Internal State, the tables are created and managed by the user. The external state manager is declared in the `Computation` spec under a unique name in the `external_state_managers` section and connected to the `Computation` via a typed client `TMutableStateKeyClient`. The names in the spec must start with `/` (for example, `"/state"`) and match the name passed to `InitExternalStateClient`. The implementation is looked up in the registry by `external_state_manager_class_name`. ### Usage To work with External State, you need to: 1. Declare a `TMutableStateKeyClient StateClient_` field in your process function, where `TState` is the state type returned by the corresponding external state manager (see below for specific implementations). 2. Override `Init(const IRuntimeInitContextPtr& initContext)` and call `initContext->InitExternalStateClient(StateClient_, "/state")` in it. Use a string that’s unique within the computation as the name — this same name must appear in the spec. 3. To get the state by key, use `StateClient_.GetState(message->Key)`. The returned accessor `TStateAccessor` behaves like a smart pointer to `TState` and is valid only within the current epoch. Example: ```cpp class TMyFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitExternalStateClient(StateClient_, "/state"); } void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto state = StateClient_.GetState(message->Key); i64 count = state->GetColumnValue>("count").value_or(0); TPayloadBuilder builder(state->Schema); builder.Set(count + 1, "count"); state->Payload = builder.Finish(); } private: TMutableStateKeyClient StateClient_; }; ``` `Computation` spec with `TSimpleExternalStateManager` connected: ```yson "external_state_managers" = { "/state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state/table"; }; }; }; ``` ### Preload and `auto_preload` {#external-state-preload} By default (`auto_preload = %true`), the framework preloads every external state manager before processing an epoch with all message, timer, and visit keys — one lookup per key. With `"auto_preload" = %false;`, the process function loads only the keys it needs through the client: `PreloadKeyStates(THashSet)`, `PreloadKeyStates(IInputContextPtr)`, or `PreloadKeyStates(IInputContextPtr, TExtractKeysOptions)`. The options select the entity kinds whose keys are taken, for example `{.Visits = false}` for a state that a visit handler doesn’t read. Preload is incremental: it may be called several times within an epoch, only keys not loaded yet are fetched, and an already loaded, possibly modified state is left as is. `GetState` on a key that wasn’t preloaded in the epoch throws. `auto_preload = %false` isn’t allowed for companion computations. In `IBatchProcessFunction::Process`, dispatch entities through the helpers described in [Process functions](https://ytsaurus.tech/docs/en/process-functions.md): ```cpp void Process(const IInputContextPtr& input, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override { if (!context->GetSpec()->ExternalStateManagers.at("/state")->AutoPreload) { WaitFor(StateClient_.PreloadKeyStates(input, {.Visits = false})).ThrowOnError(); } ProcessMessages(input, output, context, BIND(&TMyFunction::ProcessMessage, MakeStrong(this))); ProcessVisits(input, output, context, BIND(&TMyFunction::ProcessVisit, MakeStrong(this))); } ``` ### Deleting without reading: `EraseState` {#external-state-erase} `StateClient_.EraseState(key)` stages the deletion of a row without preloading or reading it — a key visitor that expires rows by a key column (say, a timestamp) never pays a lookup for them. Erase is terminal for the epoch: afterwards `GetState` throws and `PreloadKeyStates` does not bring the key back, whether or not it was loaded before. `TSimpleExternalStateManager`, `TProfileStateManager` and the internal per-key state delete by key alone; a user-defined manager that does not override `EraseKeyState` rejects the call. ```cpp for (const auto& visit : input->GetVisits()) { if (GetColumnValue(visit->Key, "ts") + Week < now) { StateClient_.EraseState(visit->Key); // No lookup. } } ``` ### TSimpleExternalStateManager `TSimpleExternalStateManager` is the standard implementation of an external state manager. It works with a single dynamic table whose keys match the `group_by_schema`. `GetState` returns an accessor over `TSimpleExternalState` with `Payload` and `Schema` fields; columns are retrieved and written via `GetColumn[Value]` / `TPayloadBuilder` by name or index. State caching happens automatically via the shared [StateCache](https://ytsaurus.tech/docs/en/#state-cache). You don’t need to register `TSimpleExternalStateManager` in `register.cpp` — it’s already registered in the Flow library itself. Spec: Source: [yt/yt/flow/library/cpp/computation/simple_external_state_manager.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/computation/simple_external_state_manager.h) #| || **Parameter** | **Description** || || `path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the dynamic table with states (including the cluster in a rich path). || |# Dynamic spec: Source: [yt/yt/flow/library/cpp/computation/simple_external_state_manager.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/computation/simple_external_state_manager.h) **This structure has no main parameters.** For a complete example of using `TSimpleExternalStateManager`, see the breakdown of the [word_count](https://ytsaurus.tech/docs/en/examples/word_count.md) example. ## External State Joiner {#external-state-joiner} Use the External State Joiner to get read-only access to external states via a key-based join. You cache the loaded states in the shared StateCache with a TTL. Before the TTL expires, repeated requests for the same key are served from the cache without contacting YT. You declare an external state joiner in the `Computation` spec under a unique name in the `external_state_joiners` section. Connect it to the `Computation` using the typed client `TJoinedStateKeyClient`. The names in the spec must start with `/` (for example, `"/reference"`) and match the name you pass to `InitExternalStateClient`. The system looks up the implementation in the registry by `external_state_joiner_class_name`. ### Usage This works similarly to the external state manager: ```cpp class TMyFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitExternalStateClient(StateReaderClient_, "/reference"); } void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto state = StateReaderClient_.GetState(message->Key); // state is a read-only accessor TConstStateAccessor (valid within the epoch). } private: TJoinedStateKeyClient StateReaderClient_; }; ``` Here is a `Computation` spec with `TSimpleExternalStateJoiner` attached: ```yson "external_state_joiners" = { "/reference" = { "external_state_joiner_class_name" = "NYT::NFlow::TSimpleExternalStateJoiner"; "parameters" = { "path" = "//path/to/reference/table"; }; }; }; ``` ### TSimpleExternalStateJoiner {#simple-external-state-joiner} `TSimpleExternalStateJoiner` is the standard implementation of an external state joiner. Its interface is similar to `TSimpleExternalStateManager`: it reads rows from a single dynamic table whose keys match the `group_by_schema`. The `GetState` method returns a read-only accessor over `TSimpleExternalState`, which contains `TPayload` and the schema of the table’s value columns. You retrieve columns via `GetColumn[Value]` by name or index. You don’t need to register `TSimpleExternalStateJoiner` in `register.cpp`; it’s already registered in the Flow library. Spec: Source: [yt/yt/flow/library/cpp/computation/simple_external_state_manager.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/computation/simple_external_state_manager.h) #| || **Parameter** | **Description** || || `path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the dynamic table with states (including the cluster in a rich path). || |# Dynamic spec (set the cache TTL via the `cache` section): Source: [yt/yt/flow/library/cpp/computation/simple_external_state_manager.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/computation/simple_external_state_manager.h) #| || **Parameter** | **Description** || || `cache` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicExpiringJobNamedStateCacheSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicExpiringJobNamedStateCacheSpec)`>` **Default value**: `{}` TTL cache settings for loaded states. || |# ### TStaticTableKeyVisitorJoiner {#static-table-key-visitor-joiner} `TStaticTableKeyVisitorJoiner` is an external state joiner over a **static** sorted table. It works only with the [key-visitor stream](https://ytsaurus.tech/docs/concepts/key_visitor.md#static-table-joiner). Unlike `TSimpleExternalStateJoiner`, it doesn’t perform random reads. The background visitor scan reads the table sequentially using the same key ranges as the internal state, and the table row becomes available in `ProcessVisit` as a read-only state for the visit key. If a key isn’t in the table, it returns an empty state (`IsEmpty() == true`). You bind the joiner to a visit stream by listing its name in the `external_names` field of the `key_visitor_streams` section (see [static spec parameters](https://ytsaurus.tech/docs/concepts/key_visitor.md#static-params)). You can bind one joiner to no more than one visit stream (the system checks this when you submit the spec). `join_on/key_schema_override` isn’t supported; the key is always the joiner’s own `group_by_schema`. Requirements for the source table: - It must be static; the system rejects a dynamic table when reading the schema. - It must be sorted so that the prefix of its key columns matches the `group_by_schema` of the `Computation` by name and type. The table can reside on another cluster (`` in the rich path). {% note warning %} The system doesn’t validate computed column expressions. The source’s partition column must be **materialized** with values that match the expression in `group_by_schema` (usually `farm_hash(key)`). The framework can’t distinguish a differently materialized table from a correct one; each visit key silently resolves as missing. {% endnote %} The canonical pattern is mirroring (reconciliation). The table’s keys participate in the scan alongside the keys of your own state. That means a visit arrives for a key that isn’t yet in the state (you need to create it) and for a key that’s no longer in the table (you need to delete it). A periodic scan thus aligns the `Computation`’s state with the external table: ```cpp class TMirrorFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitClient(MirroredState_, "/mirror"); initContext->InitExternalStateClient(SourceState_, "/source"); } void ProcessVisit( const TInputVisitConstPtr& visit, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto src = SourceState_.GetState(visit->Key); if (!src.IsInitialized()) { // The source range wasn’t read (mark_unreadable): the key’s state is unknown. return; } auto mirror = MirroredState_.GetState(visit->Key); if (src.IsEmpty()) { mirror.Clear(); } else { mirror->Payload = src->GetColumnValue>("payload").value_or(std::string{}); } } private: TMutableStateKeyClient MirroredState_; TJoinedStateKeyClient SourceState_; }; ``` Here is the `Computation` spec for this example: ```yson "external_state_joiners" = { "/source" = { "external_state_joiner_class_name" = "NYT::NFlow::TStaticTableKeyVisitorJoiner"; "parameters" = { "path" = "//path/to/source"; }; }; }; "key_visitor_streams" = { "visit_iter" = { "names" = ["/mirror"]; "external_names" = ["/source"]; }; }; ``` The `unavailable_source_policy` defines the behavior when the source is unavailable. A read is considered unsuccessful if it exhausts the `read_attempts` limit: - `retry` (default): the error fails the scan iteration, and the read retries. The scan doesn’t advance past the unread range. - `mark_unreadable`: the error is ignored, and the scan continues. Keys in the unread range resolve to an **uninitialized** accessor: `IsInitialized() == false`, and dereferencing throws an exception. The `Computation` decides how to handle a key whose state is unknown in the source (usually you skip the visit, as shown in the example above). After an unsuccessful read, the source is marked unavailable for `unavailable_source_backoff`. While this flag is active, the system doesn’t contact the source; reads immediately resolve according to the policy. The first read after the window expires tries the source again. You can see the joiner’s state in the worker sensors `static_table_key_visitor_joiner/{source_unavailable,failed_reads,reader_opens,listed_size}` with the tag `external_state_joiner=`. You don’t need to register `TStaticTableKeyVisitorJoiner` in `register.cpp`; it’s already registered in the Flow library. Spec: Source: [yt/yt/flow/library/cpp/computation/static_table_key_visitor_joiner.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/computation/static_table_key_visitor_joiner.h) #| || **Parameter** | **Description** || || `path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to a static sorted source table (including the cluster in a rich path). Its key column prefix must match the computation's `group_by_schema` by names and types. The computed (partition) column must be materialized with the values of its expression. || || `unavailable_source_policy` | **Type**: [NYT::NFlow::EUnavailableSourcePolicy](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EUnavailableSourcePolicy) **Default value**: `retry` Behavior when reading the source fails (exhausting the `read_attempts` budget). `retry` (default) — the error drops the traversal iteration and reading is retried. The traversal does not advance past the unread range. `mark_unreadable` — the error is swallowed, the traversal continues, and keys in the unread range are resolved with an uninitialized accessor (`IsInitialized() == false`). || |# Dynamic spec: Source: [yt/yt/flow/library/cpp/computation/static_table_key_visitor_joiner.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/computation/static_table_key_visitor_joiner.h) #| || **Parameter** | **Description** || || `read_attempts` | **Type**: `int` **Default value**: `3` Budget of attempts for a single source read. A read that exhausts the budget is considered unsuccessful and is handled according to `unavailable_source_policy`. || || `unavailable_source_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5m` How long an unsuccessful read marks the source as unavailable. While the marker is active, there are no accesses to the source: each read immediately resolves according to `unavailable_source_policy`. The first read after the window expires retries the source. || |# ## State Joiner {#state-joiner} Use the State Joiner to get read-only access to the internal state of another `Computation`. One `Computation` can enrich its data with the state that another `Computation` accumulates, even when the join key doesn’t match its own `group_by_schema`. You declare the Joiner in the `Computation` spec under a unique name in the `state_joiners` section and connect it via the typed client `TJoinedStateKeyClient`. Names in the spec must start with `/` and match the name you pass to `InitClient`. You don’t need a separate implementation or registration—this is a built-in Flow feature. In the spec, you specify: - `computation_id` — the `Computation` whose internal state is read. - `state_name` — the state client name of the target `Computation` (the prefix it passed to `InitClient`; it must start with `/`). - `join_on/key_schema_override` — the columns in the current row that define the key. If you don’t set this, the `Computation` uses its own `group_by_schema` (join on the same key). - `join_on/key_provider_streams` — the streams from which keys are taken from messages and timers (`nullopt` means all input streams). - `auto_preload` — if `true` (the default), the framework loads the keys before each `DoProcess`; otherwise, the `Computation` calls `PreloadKeyStates` itself. {% note warning %} The framework checks key types at startup: `key_schema_override` (or the `Computation`’s own `group_by_schema` if you don’t set an override) must match the target `Computation`’s `group_by_schema` in the number of columns and their types. The Joiner calculates the full key for the target state. Column names and expressions can differ. You’re responsible for the following (the framework doesn’t check them): - *Value mapping* — you must ensure each column maps correctly so the calculated key points to the right row in the other `Computation`’s state. - `TState` must match the state type of the target `Computation`. The Joiner deserializes its rows the same way `TMutableStateKeyClient` does on the owner side. {% endnote %} ### Usage ```cpp class TMyFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitClient(UpstreamClient_, "/upstream"); } void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) override { auto state = UpstreamClient_.GetState(message); // state — read-only accessor TConstStateAccessor (valid within the epoch). } private: TJoinedStateKeyClient UpstreamClient_; }; ``` Spec for a `Computation` with a Joiner connected using a key that differs from its own `group_by_schema`: ```yson "state_joiners" = { "/upstream" = { "computation_id" = "accumulator"; "state_name" = "/total"; "join_on" = { "key_schema_override" = [ {name = "Hash"; expression = "farm_hash(UserId)"; type = "uint64"; required = %true;}; {name = "UserId"; type = "string"; required = %true;}; ]; }; }; }; ``` By default, the cache is disabled (`ttl = 0`). The Joiner reads the target `Computation`’s state at `SyncLastCommittedTimestamp` each epoch, so you always see the latest commits. You enable a cache with TTL in the dynamic spec via the `cache` section (loaded states stay valid until `ttl` expires; repeated reads for the same key are served without contacting YT). For `Swift` computations that require strict determinism, you must consider the cache and reading at `SyncLastCommittedTimestamp`. Spec: Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `computation_id` | **Type**: `NYT::NFlow::TStrongIdentifierTypedef` **Required parameter** The `Computation` whose internal state is read by this joiner. || || `state_name` | **Type**: `std::string` **Required parameter** The state client name of the target `Computation` — the prefix passed to `InitClient` (starts with `/`). || || `join_on` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TStateJoinSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TStateJoinSpec)`>` **Default value**: `{}` Description of the mapping between the computation input stream keys and the target `Computation`'s state key (see `TStateJoinSpec`). `key_schema_override` must match the target `Computation`'s `group_by_schema`. || || `auto_preload` | **Type**: `bool` **Default value**: `true` If `true` (default), the framework automatically calls `PreloadKeyStates` on this joiner before each `DoProcess`, forming keys using `join_on`. If `false`, the computation is responsible for calling `PreloadKeyStates` before `GetState`. || |# Dynamic spec (set cache TTL via the `cache` section): Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `cache` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TDynamicExpiringJobNamedStateCacheSpec](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TDynamicExpiringJobNamedStateCacheSpec)`>` **Default value**: `{}` TTL cache settings for loaded states. Disabled by default (`ttl = 0`). || |# ## StateCache {#state-cache} Flow provides a shared two-level (uncompressed + compressed) LRU cache for states. You configure it via `/dynamic_spec/job_tracker/state_cache`. It’s primarily used for manager states; Joiners have the cache disabled by default, and data is loaded at `SyncLastCommittedTimestamp`. Source: [yt/yt/flow/library/cpp/common/spec.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/spec.h) #| || **Parameter** | **Description** || || `compressed_cache_weight` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `1Gi` Limit for `compressed`. || || `uncompressed_cache_weight` | **Type**: [NYT::NYTree::TSize](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NYTree_TSize) **Default value**: `100Mi` Limit for `uncompressed`. || |# For more details on `group_by_schema` consistency and general principles for working with states, see [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md). ## See also - [Stateful processing (concept)](https://ytsaurus.tech/docs/concepts/stateful.md) - [Computation (C++)](https://ytsaurus.tech/docs/en/computation.md) - [Quick start (C++)](https://ytsaurus.tech/docs/en/getting-started.md) # Resources A resource is an object shared by all jobs in one Flow process. A computation can use a resource to access an external-system client, reference data, a model, or another object that would be expensive to create for every message. Declare resources in the `resources` section of the pipeline spec. Computations list the resources they need in `required_resource_ids`. One resource instance is created per worker process. Keep the constructor lightweight and perform long-running initialization in `Load()`. The examples below show only resource-related fragments of classes and the pipeline spec. See the [getting-started guide](https://ytsaurus.tech/docs/en/getting-started.md) for a complete pipeline project and build commands. A user-defined resource usually derives from `TResourceBase` and is registered with `YT_FLOW_DEFINE_RESOURCE`: ```cpp class TMyResource : public TResourceBase { public: using TResourceBase::TResourceBase; TFuture Load( const THashMap& dependencies) override; }; YT_FLOW_DEFINE_RESOURCE(TMyResource); ``` Specify class parameters in `resources..parameters`; they are available through `GetParameters()`. Declare dependencies on other resources in `resources..dependencies`. ## Where to load a resource For each requirement, independently specify whether the resource is needed on workers and on the controller: ```yson required_resource_ids = { geobase = { worker = %true; controller = %false; }; }; ``` Most resources that hold user data are needed only on workers. Load a resource on the controller only if it is required by computation logic that actually runs there. This restriction also applies to resource dependencies. ## Resource controller A resource needs a controller-side component when Flow must track an external data version and tell workers which version to use. For files, this logic is already implemented by `TResourceControllerBase`. Define a custom controller to add resource-specific logic: ```cpp class TMyResourceController : public TResourceControllerBase { public: using TResourceControllerBase::TResourceControllerBase; protected: NYTree::INodePtr DoBuildTargetRevisionSpec() override; void DoCollectStatuses( const THashMap& workerStatuses, const TWorkerResourceStatusPtr& controllerStatus) override; NYTree::IMapNodePtr DoGetView() override; }; class TMyResource : public TResourceBase { public: using TController = TMyResourceController; using TResourceBase::TResourceBase; }; ``` The controller publishes a target resource revision. Workers receive it through regular reconfiguration and report which revision they have applied. This lets the controller distinguish between a target that has not been delivered yet and a delivered target that is still being prepared. `DoCollectStatuses()` receives resource states from live workers, while `DoGetView()` builds the resource section of the Flow view. If custom state must survive a controller restart, persist it through the `DoInit()` context. ## File-backed resources A file-backed resource is suitable for reference data, models, and other immutable structures that can be built from one or more files. Declare providers as the named `file_providers` map next to the ordinary resource parameters: ```yson resources = { geobase = { resource_class_name = "TGeobaseResource"; parameters = { format = "binary"; }; file_providers = { countries = { file_provider_class_name = "NYT::NFlow::TYTFileProvider"; parameters = { path = "//models/countries"; }; }; cities = { file_provider_class_name = "NYT::NFlow::TYTFileProvider"; parameters = { path = "//models/cities"; }; }; }; }; }; ``` Such a resource must be loaded only on workers: set `controller = %false` in every reachable requirement. An invalid spec is rejected before the pipeline starts. ### Implementing the resource For the standard workflow, derive from `TFileResourceBase` and implement `Initialize()`. Override `Validate()` when the structure needs additional validation: ```cpp class TGeobaseResource : public TFileResourceBase { protected: TGeobasePtr Initialize( const TMaterializedFileProviderSnapshotPtr& files) override { const auto& countries = files->GetFileProvider(TFileProviderId("countries")); const auto& cities = files->GetFileProvider(TFileProviderId("cities")); return LoadGeobase(countries->GetRootPath(), cities->GetRootPath()); } void Validate(const TGeobasePtr& geobase) override; }; ``` Flow discovers a download generation for every named provider and combines them into one snapshot. On each worker, the snapshot is downloaded, initialized, and validated in order. New data becomes available only after all three steps succeed for every file. If a step fails, the resource retries after `file_provider_update_retry_period`. During initial loading, `Load()` remains pending until a retry succeeds or a replacement target arrives. During an update, the resource keeps serving the previous valid version. The controller keeps an active snapshot and the next snapshot being prepared. A new worker first loads the active snapshot and then prepares the next one. The controller admits the preparing snapshot after one current resource instance on the current target revision validates it. This is a canary admission check, not an all-worker barrier or quorum. After admission, workers converge asynchronously and may complete the switch at different times, but each worker receives the same set of sources and download generations. For `TYTFileProvider` and `TYTDirectoryLastFileProvider`, the cache key consists of the cluster, selected path, resolved object ID, and `content_revision`. Changed contents or a replaced object create a new download generation that passes the usual preparation and admission checks. Changes to other attributes do not trigger a reload. Jobs and worker processes continue running; existing accessors retain their previous data until released. `file_snapshot_min_creation_period` limits generation creation frequency; multiple changes during that period are coalesced into the latest discovered state. If a user-defined resource does not need the standard preparation order, `TResourceBase` also provides the protected `MaterializeFileProvider()` and `MaterializeFileProviders()` methods for downloading named files from an already delivered target revision. ### Reading data `Lock()` returns a `TFileResourceAccessor` that pins one data version. An accessor that has already been returned does not change when the resource switches versions. Acquire one accessor per `RunIteration` or per batch-function call, and release it before the next iteration. A long-lived accessor retains the old version and prevents the switch from completing. If the wait exceeds `file_snapshot_rollout_warning_period`, the worker reports `/file_snapshot_activation`; an issued accessor cannot be revoked forcibly. Do not read a mutable file-backed resource from user logic in a Swift computation. When an epoch is retried, the computation could observe a different file version and become nondeterministic. Use a materializing transformation such as `TTransformComputation`, or use a provider that is guaranteed not to change during the entire run. ### Pinning a version A provider may define dynamic parameters. For example, `TYTDirectoryLastFileProvider` can temporarily select a specific table revision instead of the latest one: ```yson dynamic_spec = { resources = { geobase = { file_providers = { release = { parameters = { pinned_file_name = "000001"; }; }; }; }; }; }; ``` After the parameters change, the controller discovers revisions again. Workers keep using the previous snapshot until a complete new set has been discovered successfully. ### Postprocessing downloaded files Any file provider can declare a static postprocessing command. For example, a BLOB table may contain one archive that must be unpacked before `Initialize()` runs: ```yson file_providers = { model = { file_provider_class_name = "NYT::NFlow::TYTFileProvider"; parameters = { path = "//path/to/model-archive"; }; postprocess_command = """ /usr/bin/tar -xf "$YT_FLOW_RESOURCE_PATH/model.tar" \ -C "$YT_FLOW_POSTPROCESSING_PATH" """; postprocess_timeout = "5m"; }; }; ``` Flow runs `postprocess_command` as `/bin/bash -e -o pipefail -c `. The default timeout is one minute. The command receives only the following environment; it does not inherit worker process variables: - `YT_FLOW_RESOURCE_PATH` is a directory containing the immutable downloaded tree; - `YT_FLOW_POSTPROCESSING_PATH` is a new empty result directory; - `PATH=/usr/bin:/bin`, `LANG=C`, `LC_ALL=C`, and `TZ=UTC`. The working directory is `YT_FLOW_POSTPROCESSING_PATH`. The command must exit with code zero and leave only regular files and directories in the result directory; links and special entries are rejected. The command must wait synchronously for all child processes and must not daemonize. On timeout, Flow kills the process group. Flow continuously drains stdout and stderr and retains only the final 16 KiB of each stream, so command output cannot grow worker memory without bound. This is arbitrary shell code with worker-job permissions, not an additional sandbox. The command must write only under the result directory, must not read mutable external state, and must produce the same result for the same provider revision and command bytes. Use absolute paths to stable or versioned executables. If a helper implementation changes at the same path, also change the visible `postprocess_command`, for example by adding a version argument. A successful result is cached atomically by the provider revision identity and the exact command bytes. A cache hit reruns neither download nor postprocessing. Changing the command invalidates the postprocessing result but not the separately cached download, so Flow reruns only the command while that raw object remains cached. Changing only `postprocess_timeout` can reuse an existing result. A command failure does not terminate the worker: the incomplete result is removed and the resource retries after `file_provider_update_retry_period`. On initial loading, dependent computations wait for successful preparation. During an update, the previous valid snapshot remains available. The resource `/file_update` error contains the phase, exit code or signal, command digest, and bounded stdout/stderr tails. Repeated `command not found`, invalid-input, timeout, or helper-crash failures require the user to fix the image, data, timeout, or command. Volume and cache-capacity errors are also reported under `/file_storage`. ## Built-in file providers ### Immutable local file `TLocalFileProvider` is intended primarily for tests and environments where the same absolute path refers to the same file on every worker: ```yson file_providers = { file = { file_provider_class_name = "NYT::NFlow::TLocalFileProvider"; parameters = { path = "/absolute/path/visible/to/every/worker/data.bin"; }; }; }; ``` The file is treated as immutable. Do not replace its contents in place; publish a new version at a new path. ### Files in a YTsaurus BLOB table `TYTFileProvider` materializes all files from one static sorted BLOB table. `path` may point either to the table itself or to a link to it. The table must have exactly this strict, unique-key schema: ```yson [ {name="filename";type="string";sort_order="ascending";}; {name="part_index";type="int64";sort_order="ascending";}; {name="data";type="string";}; ] ``` Every filename becomes a regular file in the materialized root: ```yson file_providers = { model = { file_provider_class_name = "NYT::NFlow::TYTFileProvider"; parameters = { path = "//path/to/current-model-files"; }; }; }; ``` Every file starts at part index zero and part indexes are consecutive. Filenames must be single path components. The provider also supports a regular YTsaurus file: its contents are materialized as `data` in the provider root. The controller observes the object ID and `content_revision` at the configured path. A change to either invalidates the cache and triggers a resource reload on workers. For each download, a worker snapshot-locks the object at the selected path and checks its ID and content revision against the locator before reading any data. If either differs, the download fails and the resource waits for a newly discovered version. The worker reads the matching version within the same transaction. Repointing a link to another table also triggers a reload, even if its numeric content revision is the same. Workers switch to the latest discovered generation asynchronously. Publish new versions at separate immutable paths to keep previous versions available for downloading. ### Latest BLOB table in a YTsaurus directory `TYTDirectoryLastFileProvider` treats every immediate directory child as a separate revision of the complete file set. It selects the child with the lexicographically greatest name and materializes all files from the selected BLOB table: ```yson file_providers = { release = { file_provider_class_name = "NYT::NFlow::TYTDirectoryLastFileProvider"; parameters = { path = "//path/to/releases"; }; }; }; ``` Add new immutable tables under lexicographically sortable timestamp names such as `2026-08-31T07:00:00Z` and `2026-08-31T08:00:00Z`. The dynamic `pinned_file_name` parameter selects one exact child table name. ## Worker disk cache Every worker that loads file-backed resources needs `worker.file_storage`: ```yson worker = { file_storage = { path = "/absolute/dedicated/file-resource-cache"; soft_size_limit = 1073741824; hard_size_limit = 1342177280; cleanup_period = "5m"; }; }; ``` `path` is the exact cache root for one process. Flow does not append a pipeline or worker ID. The process holds the `/.lock` lock and refuses to start if another process already uses the same directory. Tests and environments with a shared file system must therefore assign a separate path to every worker that runs concurrently. Successfully materialized versions survive resource recreation and can be reused after a worker restart if the volume and path are preserved. System tools required by postprocessing, such as `tar`, must be available in the worker environment. The cache evicts only resource versions that are not currently in use, following LRU order. `soft_size_limit` is the target size after cleanup; `hard_size_limit` is the admission boundary for new data. It is not a physical volume quota: leave space for metadata and downloads whose size is not known in advance. If pinned data occupies more than half of the hard limit, the component reports a warning. The resource status shows file preparation progress. A discovery error identifies the provider. Download, initialization, and validation errors identify the resource, the snapshot, and the revisions of its providers. Snapshot and individual-revision state distributions are reported through the `/resource_controller/file_snapshot_instance_count` and `/resource_controller/file_provider_revision_instance_count` metrics. Cache state and insufficient- space errors are reported under `/file_storage`. ## File revision monitoring The controller dashboard has four panels for each resource and named file provider: - **Resource/File/Active**: the controller's active target revision, with its source version in `display_version` and cache identity in `revision_id`. - **Resource/File/Preparing**: the candidate revision awaiting validation. - **Resource/File/Instances**: revisions actually reported by workers, grouped by state. An active controller target does not mean that all workers have switched to it. - **Resource/File/Age**: `Now - Timestamp` in seconds for Active and Preparing targets. A missing timestamp produces no age series, not a zero age. A file provider can supply an optional `TFileProviderRevision::Timestamp` from source metadata. It must not substitute discovery or download time. The timestamp survives controller state restoration and rollback, does not affect disk cache identity, and changing only this metadata does not create a new snapshot: an existing snapshot retains the timestamp recorded when it was created. YT File and YT Directory Last use the snapshot-locked node's `modification_time`. This includes metadata modifications, not only content changes. It is not the training-data time of a model. Local File has no authoritative source publication timestamp and leaves it unset. # Testing in YTsaurus Flow (C++) {% note info %} C++ [workers](https://ytsaurus.tech/docs/concepts/glossary.md#worker) currently don't have a separate framework for unit testing computations. The main approach is to run the entire pipeline with final data sources. You can find a test example in [examples/cpp/wait_click_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/wait_click_join). If you still need to write unit tests, you can move the business logic out of the computations into separate classes and write unit tests for those classes. {% endnote %} Use these principles when writing integration tests for pipelines: * Test end-to-end scenarios. That is: * Write input/source data to the local YT. * In the pipeline spec, mark the sources as `finite=%true`. * For a [Key Visitor stream](https://ytsaurus.tech/docs/concepts/key_visitor.md) that should run during the test, set `finite=%false`. * Run the pipeline. * When the Key Visitor should finish, call the integration test base method [`self.ask_key_visitor_to_complete("", "")`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base/yt_flow_base.py); it switches that stream's [dynamic `finite` parameter](https://ytsaurus.tech/docs/concepts/key_visitor.md#dynamic-params) to `%true`. * Wait for the pipeline to finish. * Check the output data. * Prepare the environment with the same code you use in production: * Test the same pipeline binary that will run in production. * Generate pipeline specs with the same code that generates them for production. * If input/output data require non-trivial serialization/parsing, implement this logic with code shared with production. * Use the [shared test framework](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base) (a `README.md` is available). * Implement a failover test. * Many errors surface when workers fail and other workers take over their tasks. So, run a test with `problems=True` and more than one worker. * Write stable tests. * Remember that in CI, any part of a test might run unexpectedly long. * The ideal runtime for a single test is 20 seconds for any build type. * For sanitizer builds, reduce the amount of input data. * Set all local timeouts with a multiple safety margin. * The test execution logic shouldn’t depend significantly on the current time. For example, a test shouldn’t fail if it starts before midnight and finishes after. ## Debugging tests {#debug} ### Logs {#debug-logs} After a test finishes, you can find its logs in `test-results/py3test/testing_out_stuff` in the test directory. Key logs include: * `run.log` — Python test logs. * `//Controller_...` — Controller logs (`.err` is the process’s stderr; `.log` contains regular logs written via `YT_LOG_...`). * `//Worker_...` — Worker logs. * `//Runner...` — Runner logs. If something isn’t working, check errors in all these logs. The approach to reviewing them is the same as in production — see [Logs](https://ytsaurus.tech/docs/devops/vanilla/diagnostics/logs.md). You can also view logs before the test finishes. To do this, locate the temporary directory where the test runs. The simplest way is to run the test with the `--keep-temps` flag: `ya make --keep-temps -ttt `. In this case, `ya make` won’t delete the temporary directory after the test finishes and will print a link to it in the output. You can also use these bash aliases: ```bash alias curtestdir="ps -f -u $USER | python3 -c \"import sys, re; drs = set(e for e in re.findall(r'[\s=](https://ytsaurus.tech/docs/en//S*testing'%20+%20r'_out_stuff)\b', sys.stdin.read())); print('' if len(drs) == 1 else 'Select first from ' + repr(drs), file=sys.stderr); print(list(drs)[0])\"" alias cdcurtestdir='cd $(curtestdir)' # Get the link to the local YTsaurus UI from the logs. alias curlocalyt='cat $(curtestdir)/stderr 2>/dev/null | grep YT' ``` ### Test framework {#debug-test-framework} You can read about how to set up the environment for better test framework performance and how to influence test parameters in the framework’s [README.md](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base). You configure the behavior of integration tests using `--test-param NAME=VAL`: #| || **Parameter** | **Default** | **Values** | **Action** || || `RUNNER_LOG_LEVEL` | | `Error`, `Info`, `Debug`, … | Sets the logging level for the process that runs the pipeline (runner). || || `PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN` | `0` | `0`, `1` | Pauses the test before stopping Flow processes. Combined with `--test-disable-timeout`, this lets you keep the local YTsaurus and the Flow process federation running for a long time so you can study them at your own pace via the UI. || || `EXTERNAL_YT_CONFIG` | (not set) | YSON — see below | Runs the pipeline on real external YTsaurus clusters instead of the local recipe. || |# Examples: ```bash ya make -A --test-param RUNNER_LOG_LEVEL=Debug ya make -A --test-disable-timeout --test-param PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN=1 ``` ### `EXTERNAL_YT_CONFIG` {#external-yt-config} The local YTsaurus still starts via the recipe, but the test ignores it. Required fields that are common for all clusters: - `path` — base directory. - `tablet_cell_bundle` — bundle for the dynamic tables that are created. - `proxy_role` — RPC proxy role. Optional: `primary_medium` (default `"default"`). The `clusters` list — the first element is primary. Record fields: `cluster_name` (required), `proxy_url` (defaults to `cluster_name`). Authorization: when using an external YTsaurus, `YT_TOKEN`/`YT_USER` from the local recipe are cleared; yt-wrapper picks up the token from `~/.yt/token`. Isolation: `work_yt_path = path//`; the `path/` directory is deleted and recreated once per class in `setup_class`. Example: ```bash ya make -A --test-param 'EXTERNAL_YT_CONFIG={path="//tmp/yt_flow";tablet_cell_bundle=default;proxy_role=default;clusters=[{cluster_name=};];}' ``` ## See also - [Basic release rules](https://ytsaurus.tech/docs/devops/vanilla/releases.md#release-and-configure-basic-rules) - [Testing (Java)](https://ytsaurus.tech/docs/java/testing.md) - [Testing (Python)](https://ytsaurus.tech/docs/python/testing.md) - [Quick start (C++)](https://ytsaurus.tech/docs/en/getting-started.md) - [Quick start (Java)](https://ytsaurus.tech/docs/java/getting-started.md) - [Quick start (Python)](https://ytsaurus.tech/docs/python/getting-started.md) # Process function in YTsaurus Flow (C++) ## Why you need it Implement new C++ user logic only as a process function. Direct inheritance from `TTransformComputation`, `TSwiftMapComputation`, `TSwiftOrderedSourceComputation`, or `TTransformOrderedSourceComputation` is a low-level API for framework and legacy maintenance, not an authoring model for new user computations. Such logic is tightly coupled with the `Computation` object, inherits dozens of protected methods, and can only be constructed from a fully built `TComputationContext`, so it’s nearly impossible to test in isolation with unit tests. A process function moves your custom logic into a separate, lightweight object. This object receives its dependencies (`IOutputCollector`, `IRuntimeContext`) as narrow interfaces and doesn’t depend on the `Computation` object itself. This lets you test the function in isolation with unit tests. ## How it works {#how-it-works} The process function isn’t run directly. Instead, a built-in `Computation` adapter executes it. You specify this adapter in the spec via the `computation_class_name` field (see [Registration](https://ytsaurus.tech/docs/en/#registration)). The adapter gives the function the same environment as a regular `Computation`: the same spec, the same stores and states, and the same epoch-processing logic. That’s why a pipeline with a process function provides the same [processing guarantees](https://ytsaurus.tech/docs/concepts/guarantees.md) (including exactly-once) as a manually written `Computation`. The adapter also defines the function’s execution mode. There are four built-in adapters: | `computation_class_name` | Mode | | --- | --- | | `NYT::NFlow::TProcessFunctionComputation` | transform | | `NYT::NFlow::TProcessFunctionSwiftMapComputation` | swift-map | | `NYT::NFlow::TProcessFunctionSourceComputation` | source | | `NYT::NFlow::TProcessFunctionTransformOrderedSourceComputation` | [ordered source with output materialization and state](https://ytsaurus.tech/docs/en/computation.md#ttransformorderedsourcecomputation) | You can run the same function under different adapters without rebuilding the binary. ## Interfaces The library is `library/cpp/common` (`common/process_function.h`). The function selects **one** processing granularity by inheriting the corresponding interface. The spec determines which `Computation` (source, swift map, or transform) the function attaches to (see [Registration](https://ytsaurus.tech/docs/en/#registration)). The base `IProcessFunctionBase` only includes `Init(initContext)` — initialization at the start of an [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch). By default, this is a no-op. The granularity interfaces add the actual processing methods. Choose the interface based on how you want to process the epoch’s input: one entity at a time, the entire batch at once, or by key. Then override only the methods you need. The worker will call them with the state and exactly-once semantics of the selected mode. The function inherits one granularity interface and, if needed, the `ISyncProcessFunction` mix-in: ```mermaid classDiagram class TRefCounted { <> } class IProcessFunctionBase { <> +Init(initContext) } class IProcessFunction { <> +ProcessMessage(message, output, context) +ProcessTimer(timer, output, context) +ProcessVisit(visit, output, context) } class IBatchProcessFunction { <> +Process(input, output, context) } class IKeyedBatchProcessFunction { <> +ProcessKey(input, output, context) } class ISyncProcessFunction { <> +Sync(transaction, context) } class TUserFunction { <> } TRefCounted <|-- IProcessFunctionBase IProcessFunctionBase <|-- IProcessFunction : element-wise IProcessFunctionBase <|-- IBatchProcessFunction : entire epoch IProcessFunctionBase <|-- IKeyedBatchProcessFunction : by key IProcessFunction <|-- TUserFunction : granularity ISyncProcessFunction <|.. TUserFunction : sync mix-in ``` - `IProcessFunction` — element-wise processing (the most common case). The worker calls a method for each entity in the epoch. Override the methods you need; all are no-op by default: - `ProcessMessage(message, output, context)` — handles a single message. - `ProcessTimer(timer, output, context)` — handles a single [timer](https://ytsaurus.tech/docs/concepts/glossary.md#timer). - `ProcessVisit(visit, output, context)` — handles a single visit. In source mode, only messages arrive, so `ProcessTimer` and `ProcessVisit` aren’t called. - `IBatchProcessFunction` — processes the entire epoch input in a single call. Override `Process(input, output, context)` when your logic works with the whole batch at once (for example, a single batched external request). The input isn’t grouped by key. To combine batch work with per-entity handling — say, [one state preload](https://ytsaurus.tech/docs/en/state.md#external-state-preload) for the whole batch — call the dispatch helpers `ProcessMessages` / `ProcessTimers` / `ProcessVisits(input, output, context, callback)` from `Process`: each sets the parents and tags errors with the key exactly as the worker does around `IProcessFunction` hooks, and takes a `TCallback` — `BIND(&TMyFunction::ProcessMessage, MakeStrong(this))` or a `BIND` of a lambda. - `IKeyedBatchProcessFunction` — processes by key using group-by, for keyed modes (swift map and transform). The worker groups the epoch’s input by key and calls `ProcessKey` for each key: - `ProcessKey(input, output, context)` — handles all input for a single key (messages, timers, and visits together). It’s no-op by default. Override it when your logic relies on the entire key batch at once (for example, to reconcile messages and timers via the key’s shared state). - `ISyncProcessFunction` — an optional mix-in for functions that commit side effects in a separate sync phase at the end of the epoch. You inherit it in addition to the granularity interface: - `Sync(transaction, context)` — commits side effects in the `transaction`. The `context` gives access to runtime accessors. You must implement this method. It’s called only by a `Computation` adapter that has a sync phase — among the built-in adapters, that’s `TProcessFunctionComputation` (transform) and `TProcessFunctionTransformOrderedSourceComputation` (ordered source). The spec validation checks this match: you can’t attach a function with `Sync` to a `Computation` without a sync phase. In `Process` (`IBatchProcessFunction`) and `ProcessKey`, the `output` doesn’t have parent messages set — you must set them yourself via `output->SetParents(...)`. In the element-wise methods of `IProcessFunction` (`ProcessMessage`, `ProcessTimer`, `ProcessVisit`), they’re already set for the corresponding entity. The `Distribute` field in `TAddMessageOptions` mirrors the `OutputCollector` semantics in `Computation`: for source, a message with `Distribute = false` isn’t published but is still considered when evaluating the [watermark](https://ytsaurus.tech/docs/concepts/glossary.md#timestamps-and-watermarks); for other `Computation` types, such a message is simply discarded. ### Message ID suffixes in Swift {#message-id-suffixes} In Swift mode, you can select the derived message ID suffix in `TAddMessageOptions`: ```cpp output->AddMessage(std::move(message)); output->AddMessage( std::move(hashedMessage), TAddMessageOptions{ .MessageIdSuffix = TOutputMessageIdSuffix::FromPayloadHash(), }); output->AddMessage( std::move(keyedMessage), TAddMessageOptions{ .Distribute = false, .MessageIdSuffix = TOutputMessageIdSuffix::FromUserDefined(semanticKey), }); ``` - With default options, or with `FromSequenceNumber()`, Flow uses the current message sequence number for the parent message ID and output stream pair. - `FromPayloadHash()` uses a 128-bit CityHash of the canonical payload wire representation. Equal payloads with the same parent and stream get the same message ID; a hash collision is theoretically possible. - `FromUserDefined(...)` accepts a non-empty user-defined suffix. Flow encodes it as a separate lexicographic component, so it cannot impersonate a sequence-number suffix. The caller is responsible for keeping it stable and unique among distinct logical messages with the same parent and stream. Payload hashes and user-defined suffixes are supported only by Swift computations. They let message identity be independent of emission order, but they don’t remove the [Swift determinism requirement](https://ytsaurus.tech/docs/concepts/swift.md#determinism): the same message ID must still denote the same logical output. {% note warning %} A process function is `TRefCounted`, so you must always create it via `New<...>()`. {% endnote %} ## IRuntimeContext `context` (of type `IRuntimeContext`, `common/runtime_context.h`) is an interface that collects everything a `Computation` usually reads from `this`: | Method | Description | | --- | --- | | `GetWatermark(streamId)` / `GetInputEventWatermark()` | Event-time [watermarks](https://ytsaurus.tech/docs/concepts/glossary.md#timestamps-and-watermarks) | | `GetSpec()` / `GetStreamSpecs()` / `GetKeySchema()` | Spec and stream schemas | | `MakeOutputMessageBuilder(streamId)` | Output message builder | | `ConvertToOutputMessage(message, streamId)` | Converts a message to the output stream’s schema | | `ConvertToMessage(ysonMessage)` | `TYsonMessage` → `TMessage` | | `ConvertToYsonMessage(message)` | `TMessage` → typed `TYsonMessage` | | `MakeTimer(key, streamId, trigger, event)` | Creates a [timer](https://ytsaurus.tech/docs/concepts/glossary.md#timer) | | `GetThrottlerOrThrow(throttlerId)` | Gets a distributed throttler | | `TryGetThrottler(throttlerId)` | Same, but returns `nullptr` if the throttler is not declared | ## States Typed state clients (`TMutableStateKeyClient` and others) are stored as function fields and initialized in `Init` via `IRuntimeInitContext` (`common/runtime_init_context.h`): ```cpp void Init(const IRuntimeInitContextPtr& initContext) override { initContext->InitExternalStateClient(StateClient_, "/state"); // or: initContext->InitClient(Client_, "my-state"); } ``` For more details on state types, see the [Working with states](https://ytsaurus.tech/docs/en/state.md) section. ## Parameters {#parameters} A function can declare its own parameter structure — a regular `TYsonStruct` — and read it in a typed way. You pass parameters in the `processing_function_parameters` field at the top level of the `Computation` spec (next to `processing_function`): - Static — `processing_function_parameters` in `spec`, read once in `Init` via `initContext->GetParameters()`. - Dynamic — `processing_function_parameters` in `dynamic_spec`, read via `context->GetDynamicParameters()` and reflect the latest reconfiguration. Both blocks are parsed into the parameter types declared when registering the function (see below), so `T` in `GetParameters()` / `GetDynamicParameters()` must be the registered type — a mismatch throws. If the `processing_function_parameters` field is missing, the structure is filled with default values. The static block is parsed once at job init; the dynamic one is reparsed only when it changes (that is, on reconfiguration). You specify parameter types when registering the function via macro arguments: `YT_FLOW_DEFINE_PROCESS_FUNCTION(function, TStaticParams)` for the static block, `YT_FLOW_DEFINE_PROCESS_FUNCTION(function, TStaticParams, TDynamicParams)` also for the dynamic one. Then the corresponding `processing_function_parameters` block (in `spec` and `dynamic_spec`) is validated against the schema when loading the spec, just like `parameters` for `Computation`: an unknown field or incorrect type causes an error before the run. A block for which you didn’t declare a type (including for a parameterless `YT_FLOW_DEFINE_PROCESS_FUNCTION(function)`) is treated as empty — any passed field will be rejected. ```cpp struct TMyParameters : public NYTree::TYsonStruct { i64 Threshold; REGISTER_YSON_STRUCT(TMyParameters); static void Register(TRegistrar registrar) { registrar.Parameter("threshold", &TThis::Threshold).Default(0); } }; void Init(const IRuntimeInitContextPtr& initContext) override { Threshold_ = initContext->GetParameters()->Threshold; } void ProcessMessage(const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override { auto currentThreshold = context->GetDynamicParameters()->Threshold; // ... } // Registering with a parameter type enables validation in the spec. YT_FLOW_DEFINE_PROCESS_FUNCTION(TMyFunction, TMyParameters); ``` In the spec: ```yson "counter" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "NYT::NFlow::NExample::TWordCountFunction"; "processing_function_parameters" = { "threshold" = 5; }; }; ``` In unit tests, set static parameters via `TTestStateEnvironment::SetStaticParameters(...)` — the passed struct is served to `GetParameters()` as is. Dynamic ones go through the production path: name the function via `TTestRuntimeContextBuilder().SetProcessingFunction()` (it must be registered in the test binary) and pass the struct to `SetDynamicParameters(...)`; the context reparses it into the registered dynamic type. ## Composing functions {#function-graph} When several process functions handle data in sequence or split a stream, you can run them in one `Computation` as a graph. Each node of the graph is a separately registered process function; messages between nodes stay inside the worker instead of passing through intermediate pipeline streams. Describe the graph in the static spec, in the `process_function_graph` field of a computation with `computation_class_name = "NYT::NFlow::TProcessFunctionComputation"` (Transform) or `"NYT::NFlow::TProcessFunctionSwiftMapComputation"` (Swift map). A node has the same fields as a computation that hosts one process function: `processing_function`, `processing_function_parameters`, `input_stream_ids`, `output_stream_ids`, `timer_streams`, and `streams_dependency`. Nodes are connected by stream IDs, the same way computations are connected in a pipeline: - The `input_stream_ids` and `output_stream_ids` of the computation are the boundary of the graph. - A node reads the streams listed in its `input_stream_ids`: inputs of the computation or outputs of other nodes. Several nodes can read one stream, and one node can read several streams. - Each stream is written by one node. A stream written by a node and not listed in the `output_stream_ids` of the computation is internal to the graph. In the pipeline spec of the integration test, the `graph` computation counts messages of two input streams in separate nodes and merges the results in a third one: ```yson { "spec" = { "computations" = { "left_reader" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionSourceComputation"; "processing_function" = "NYT::NFlow::NProcessFunctionGraphTest::TQueueReaderFunction"; "output_stream_ids" = ["left"]; "source_streams" = { "queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { "queue_path" = "//path/to/left_queue"; "consumer_path" = "//path/to/left_consumer"; "finite" = %false; }; }; }; }; "right_reader" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionSourceComputation"; "processing_function" = "NYT::NFlow::NProcessFunctionGraphTest::TQueueReaderFunction"; "output_stream_ids" = ["right"]; "source_streams" = { "queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { "queue_path" = "//path/to/right_queue"; "consumer_path" = "//path/to/right_consumer"; "finite" = %false; }; }; }; }; "graph" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"; "required" = %true;}; {"name" = "key"; "type" = "uint64";}; ]; "input_stream_ids" = ["left"; "right"]; "output_stream_ids" = ["result"]; "process_function_graph" = { "nodes" = { "left" = { "processing_function" = "NYT::NFlow::NProcessFunctionGraphTest::TStateAndTimerFunction"; "processing_function_parameters" = {"increment" = 2;}; "input_stream_ids" = ["left"]; "output_stream_ids" = ["left_counts"]; "timer_streams" = {"timer" = {"time_type" = "current_time";};}; }; "right" = { "processing_function" = "NYT::NFlow::NProcessFunctionGraphTest::TStateAndTimerFunction"; "processing_function_parameters" = {"increment" = 3;}; "input_stream_ids" = ["right"]; "output_stream_ids" = ["right_counts"]; "timer_streams" = {"timer" = {"time_type" = "current_time";};}; }; "merge" = { "processing_function" = "NYT::NFlow::NProcessFunctionGraphTest::TMergeFunction"; "input_stream_ids" = ["left_counts"; "right_counts"]; "output_stream_ids" = ["result"]; }; }; }; "sinks" = { "queue" = { "sink_class_name" = "NYT::NFlow::TSyncQueueSink"; "input_stream_ids" = ["result"]; "parameters" = { "queue_path" = "//path/to/output_queue"; }; }; }; }; }; }; "dynamic_spec" = { "computations" = { "left_reader" = { "batch_duration" = 100; "parameters" = {"desired_partition_count" = 1;}; }; "right_reader" = { "batch_duration" = 100; "parameters" = {"desired_partition_count" = 1;}; }; "graph" = { "batch_duration" = 100; "parameters" = {"desired_partition_count" = 1;}; }; }; "job_tracker" = {"job_threads" = 4;}; }; } ``` Register every stream the nodes use, including internal ones, like any other pipeline stream. The same test does it in `PrepareSpec`: ```cpp void PrepareSpec(const TPipelineSpecPtr& spec, const TDynamicPipelineSpecPtr& dynamicSpec) { TSimpleSpecBuilder streamBuilder; streamBuilder.RegisterStream("left"); streamBuilder.RegisterStream("right"); // Streams between graph nodes are registered like any other stream. streamBuilder.RegisterStream("left_counts"); streamBuilder.RegisterStream("right_counts"); streamBuilder.RegisterStream("result"); if (spec->Computations.contains("delivery")) { streamBuilder.RegisterStream("delivered"); } streamBuilder(spec, dynamicSpec); } ``` No other C++ code is needed: the functions are registered with `YT_FLOW_DEFINE_PROCESS_FUNCTION` as usual, and a function sees the same stream IDs in a graph as in a computation of its own. ### Graph rules {#function-graph-rules} The runner resolves the graph after `PrepareSpec` and stops with an error if the graph breaks any of these rules: - Every node reads at least one stream. Each stream it reads is an input of the computation or an output of another node. - Every stream a node writes is read by another node or listed in the `output_stream_ids` of the computation. To drop messages, read the stream with a node that has no `output_stream_ids`. - Every input of the computation is read by a node, and every output is written by a node. - The graph has no cycles. Node, timer, and internal stream IDs match `[0-9A-Za-z_-]+`. - An internal stream is not used by other computations. To pass a stream to another computation, list it in `output_stream_ids`. - Every stream read by a node contains the non-expression columns of `group_by_schema`. Columns with expressions are computed into the key from the payload, so they do not have to be present in the stream schema. - The computation does not set `processing_function`, `processing_function_parameters`, `timer_streams`, `streams_dependency`, or `allow_timer_self_dependency`: they are generated from the graph. - The computation keeps the default precise watermarks: `watermark_strategy.watermark_percentile.value` is 100. - The computation does not set `watermark_strategy.event_timestamp_assigner`: a message written to an internal stream inherits the minimum event timestamp of its parents, so set event timestamps in the node that emits the messages. - In Swift, message outputs do not depend on timer streams: timers cannot be parents of messages. After resolution, the computation runs `NYT::NFlow::TGraphProcessFunction`: `processing_function_parameters` contains the graph and the specs of its streams, and `timer_streams` and `streams_dependency` are computed from the nodes. Internal streams are removed from the pipeline `streams`. Only the resolved spec reaches the controller and workers; it is validated again before the pipeline starts. The controller rejects a spec with `process_function_graph`, so set such a spec by starting the pipeline binary, not with `set_pipeline_spec`. Applications with their own runner call `ResolveProcessFunctionGraphs(spec)` from `common/process_function_graph.h` before validating the spec. ### Node parameters {#function-graph-parameters} Each node has its own parameters, typed as described in [Parameters](https://ytsaurus.tech/docs/en/#parameters): - Static parameters are set in `processing_function_parameters` of the node. Unknown fields and invalid values are errors. Several nodes can run one function with different parameters and separate state. - Dynamic parameters are set in `dynamic_spec.computations..processing_function_parameters.nodes.`, for example `nodes = {left = {multiplier = 2;};}`. Unknown nodes and invalid values are rejected before the dynamic spec is saved. Updates take effect at epoch boundaries within each partition. ### Keys, state, and timers {#function-graph-state} - All nodes share the `group_by_schema` and partitioning of the computation. A message written to an internal stream must have the key of its parents: a key change requires a separate computation. A batch function whose input contains several keys must set the parents of each output with `output->SetParents(...)`. - The state of each node uses the `functions//` prefix, so nodes can reuse state names. Renaming a node starts it with empty state. Do not rename or remove a node or its timer while the timer has pending instances: the jobs of the computation fail to start until the timer stream is restored. This is the same rule as for removing a timer stream of a computation. - A timer is declared in the `timer_streams` of its node and returns only to that node. The `streams` or `streams_with_delays` of a timer name input streams of the node; without them, the timer watches all inputs of the node. A timer sets at most one of these fields. - By default, an output of a node depends on all inputs and timers of the node, and a timer depends on all inputs of the node and on itself. Unlike a computation with one function, a timer can reschedule itself without `allow_timer_self_dependency`, but it cannot schedule another timer of the node unless `streams_dependency` allows it. `streams_dependency` of the node narrows or changes these sets; include every actual dependency, including effects through state. A message or timer without an allowed parent fails the epoch. - When a timer handler schedules a timer again, set the event timestamp of the new timer explicitly. Otherwise the new timer inherits the event timestamp of the fired one and holds back the event watermark of every output and timer that depends on it. - A node sees the watermarks of all pipeline streams. For an internal stream or a local timer ID, the event watermark is the minimum over the computation inputs and timers it derives from, and `GetInputEventWatermark()` covers the inputs of the node. A timer emits messages with the current system timestamp, so the system watermark of an internal stream covers only the computation inputs it derives from. As a result, an `event_time` timer that watches an internal stream waits for the pending timers of the upstream nodes, while a `system_time` timer does not. - Nodes share the computation and partition IDs. Transform nodes also share the epoch commit and sequence number. Swift does not publish an epoch sequence number, so `GetEpochUniqueSeqNo()` is unavailable in its nodes. A function that derives external identifiers from shared context values must add a node-specific part, such as `IRuntimeInitContext::GetPrefix()`. ### Execution {#function-graph-execution} Nodes run once per epoch in topological order; independent nodes run in the order of their IDs. A node that reads several streams receives the input messages of the computation first and then the outputs of other nodes in that order. As between computations, do not rely on the relative order of messages from different streams. Messages between nodes stay in memory until the end of the epoch and have no separate limit: if nodes multiply messages, reduce the input batch of the computation with `max_rows_per_batch` and `max_bytes_per_batch`. Source connectors, key visitors, companions, external state managers, joined state clients, nested graphs, and functions implementing `ISyncProcessFunction` are not supported inside a graph; keep them in separate computations. Node state and timers are persisted by the hosting computation without a user `Sync` hook. ### Message IDs and retries {#function-graph-message-ids} A graph hosted by `TProcessFunctionComputation` (Transform) shares the computation's commit. In the default `exactly_once` processing mode, input confirmations, node state, timers, and materialized outputs are committed atomically. If a job fails before commit, its replacement restores committed state and processes the input again. A node can therefore run more than once for the same logical input. Messages received from the computation's inputs retain their IDs. Both hosts derive internal IDs deterministically from the original parents at the computation boundary, the output stream, and the resolved suffix. The default suffix is a sequence number scoped to the boundary-parent set and stream. User-defined and payload-hash suffixes are additionally scoped to the immediate parents. A replay with the same parents and logical outputs reproduces internal IDs independently of the epoch sequence number. Changed parent sets or output count/order can change default IDs; Transform does not require the node logic itself to be deterministic. Timestamps and exported IDs follow the host policy. Transform assigns the current epoch timestamp internally and fresh IDs to exported messages; persisted redelivery restores the saved external IDs. Swift inherits the maximum system timestamp and minimum alignment timestamp of the boundary parents and derives external IDs with the normal Swift metadata setter. In both modes, an unset event timestamp inherits the minimum event timestamp of the immediate parents. Scheduled timer IDs are assigned by the host. Swift requires deterministic replay. A batch node receives the full batch; use `output->SetParents(...)` for each result that belongs to one input. The strict mode requires one original boundary message parent, so fan-in of descendants of that same input is allowed. Merging different boundary inputs requires `parameters/allow_batching_with_relaxed_guarantees = %true` and provides at-least-once delivery; changing batch boundaries can also change merged identities. Combining computations into a graph removes the materialization boundary between their functions. Internal IDs are suitable for replay deduplication only while their parents and suffixes remain stable; nondeterministic Transform logic or changed batch parent sets can produce different IDs. When those conditions do not hold, keep a materialized stream before the function, use a stable application identifier, or send the effect through a sink with the required delivery guarantee. Direct external requests from a node do not join the computation's transaction automatically. Use output collectors and parent messages or timers only during the processing invocation that supplied them. To continue processing in a later epoch, use managed state and timers instead of retaining a collector or a parent handle. ### Testing a graph {#function-graph-testing} `TProcessFunctionGraphTestHarness` from `process_function/testing/process_function_graph_test_harness.h` runs graph processing and commits managed test state between epochs. It does not simulate job recovery or roll back a failed epoch; use integration tests with worker restarts to check those paths. It takes the computation spec in YSON, as in the pipeline spec, and resolves it the same way as the runner, so graph rule violations fail the test. Outputs pass the production checks of `streams_dependency`. `computation_class_name` and `group_by_schema` can be omitted: they default to `TProcessFunctionComputation` and the key schema passed to the harness, `DefaultTestKeySchema()` by default. A `group_by_schema` set in the spec must equal that key schema. The unit test of the same graph: ```cpp TTestStateEnvironment environment; TProcessFunctionGraphTestHarness graph(environment, R"({ input_stream_ids = [left; right]; output_stream_ids = [result]; process_function_graph = { nodes = { left = { processing_function = "NYT::NFlow::NProcessFunctionGraphTest::TStateAndTimerFunction"; processing_function_parameters = {increment = 2;}; input_stream_ids = [left]; output_stream_ids = [left_counts]; timer_streams = {timer = {time_type = current_time;};}; }; right = { processing_function = "NYT::NFlow::NProcessFunctionGraphTest::TStateAndTimerFunction"; processing_function_parameters = {increment = 3;}; input_stream_ids = [right]; output_stream_ids = [right_counts]; timer_streams = {timer = {time_type = current_time;};}; }; merge = { processing_function = "NYT::NFlow::NProcessFunctionGraphTest::TMergeFunction"; input_stream_ids = [left_counts; right_counts]; output_stream_ids = [result]; }; }; }; })"); graph .RegisterStream("left") .RegisterStream("right") .RegisterStream("left_counts") .RegisterStream("right_counts") .RegisterStream("result"); auto key = MakeKey(7); auto fillKey = [] (TMessageBuilder& builder) { builder.Payload().Set(7, "key"); }; graph.RunEpoch({graph.MakeMessage("left", key, fillKey)}); ASSERT_EQ(graph.GetMessages().size(), 1u); EXPECT_EQ(graph.ReadNodeState("left", "count", key), 2); // Timers scheduled in an epoch fire in the next one. graph.RunEpoch({}, graph.MakeFiringTimers()); EXPECT_EQ(graph.GetMessages().size(), 1u); ``` `GetMessages()` returns messages written to computation outputs with the IDs of their input parents. `GetTimers()` returns scheduled timers with the node and local timer ID. `SetNodeDynamicParameters`, `SetWatermark` and `SetCurrentTimestamp` configure the following epochs; node dynamic parameters are validated like the controller validates them. ## Registration {#registration} You link the function and `Computation` via the spec. Register the function with a single macro from `common/registry.h` (in the same `TRegistry` as computation / source / sink) under its `TypeName`; the optional second argument is the type of its parameters (see [Parameters](https://ytsaurus.tech/docs/en/#parameters)): ```cpp YT_FLOW_DEFINE_PROCESS_FUNCTION(TWordCountFunction); // no parameters YT_FLOW_DEFINE_PROCESS_FUNCTION(TTextReadFunction, TTextReaderParameters); // with parameters ``` In the `Computation` spec, the `computation_class_name` field points to the built-in `Computation` adapter (which also sets the mode — see the [list of adapters](https://ytsaurus.tech/docs/en/#how-it-works)), and the adjacent `processing_function` field names the function: ```yson "counter" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "NYT::NFlow::NExample::TWordCountFunction"; }; ``` ## Testing The `library/cpp/process_function/testing` library provides a ready-made set of utilities for unit tests with sensible defaults (all its utilities live in the separate `NYT::NFlow::NTesting` namespace to keep test code separate from production): - `TRecordingOutputCollector` — an `IOutputCollector` that records messages and timers in vectors (`GetMessages()` / `GetTimers()`). - `TTestRuntimeContextBuilder` — builds an `IRuntimeContext`; by default, it uses zero watermarks, one output stream for each registered `RegisterStream(id)`, and the key schema `DefaultTestKeySchema()`. - `TTestStateEnvironment` — starts a `TJobStateManager` over in-memory mock tables and provides an `IRuntimeInitContext`; `PreloadKeyStates(inputContext)` loads keys before processing, and `ReadKeyState(name, key)` reads the state afterward. - `entity_builders.h` — `MakeTestMessage`, `MakeTestRawMessage`, `MakeTestTimer`, `MakeTestVisit`. - `TProcessFunctionGraphTestHarness` — runs a [process function graph](https://ytsaurus.tech/docs/en/#function-graph-testing) across epochs. - `TProcessFunctionTestHarness` — runs the function across epochs like a worker does: it wraps the function as batch (via `WrapAsBatch`, so per-element, whole-batch, and per-key forms all work the same way, and timers and visits are dispatched alongside messages), calls `Init` once, and for each `RunEpoch(...)` it preloads the state, processes the input, executes the end-of-epoch `Sync` for `ISyncProcessFunction`, and commits the state. You can access the last epoch’s messages and timers via `GetMessages()` / `GetTimers()`. The umbrella header `process_function/testing/unittest.h` includes all the listed utilities; in a test, you only need to include it instead of the individual harness headers. `TTestStateEnvironment` covers all state types the function uses: - Internal (`InitClient`) — work directly via `TJobStateManager` over in-memory tables; the function writes them and reads them back via `ReadKeyState(name, key)`. - External managers (`InitExternalStateClient` with `TMutableStateKeyClient`) — register via `RegisterExternalState(name, ...)`; there’s a ready-made in-memory `TInMemorySimpleExternalStateManager` for `TSimpleExternalState`, and you can read the result via `ReadExternalKeyState(name, key)`. - External joiners (`InitExternalStateClient` with `TJoinedStateKeyClient`) — register via `RegisterExternalStateJoiner(name, ...)`; the in-memory `TInMemorySimpleExternalStateJoiner` is seeded via `GetMutableState(key)` before the function starts. - Internal joiners of another computation’s state (`InitClient` with `TJoinedStateKeyClient`) — register via `RegisterStateJoiner(name, stateName)`; in a single environment, you can run a producer function (it writes the state), call `Sync()`, and then run a joiner function that reads it. `RegisterExternalState` / `RegisterExternalStateJoiner` also accept an arbitrary `IExternalStateManagerPtr` / `IExternalStateJoinerPtr` — this way, you connect a real manager built over a YTsaurus mock client in the test. Example of a test for a row function with state: ```cpp TTestStateEnvironment stateEnv; auto context = TTestRuntimeContextBuilder().Build(); auto output = New(); auto function = New(); function->Init(stateEnv.GetInitContext()); auto key = MakeKey(7); auto message = MakeTestMessage("input", key, New()); stateEnv.PreloadKeyStates(New( std::vector{message}, std::vector{})); function->ProcessMessage(message, output, context); EXPECT_EQ(stateEnv.ReadKeyState("counter", key), 1); ``` The same test using `TProcessFunctionTestHarness`, which hides `Init`, preload, context/output setup, and epoch commit: ```cpp TTestStateEnvironment stateEnv; TProcessFunctionTestHarness harness(stateEnv, New()); auto key = MakeKey(7); harness.RunEpoch({MakeTestMessage("input", key, New())}); EXPECT_EQ(stateEnv.ReadKeyState("counter", key), 1); ``` For more details on the general approach to testing C++ computations, see the [Testing](https://ytsaurus.tech/docs/en/testing.md) section. ## Example Full example — [examples/cpp/word_count](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/word_count) (both classes are `IProcessFunction` in the `NYT::NFlow::NExample` namespace, registered via `YT_FLOW_DEFINE_PROCESS_FUNCTION`). In `pipeline.yson`, `TTextReadFunction` is connected to `TProcessFunctionSourceComputation`, and `TWordCountFunction` is connected to `TProcessFunctionComputation` via `processing_function`; `TTextReadFunction` reads its static `min_word_length` parameter from `processing_function_parameters`. A more complex example with timers and window-based joining is [examples/cpp/wait_click_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/wait_click_join). ## See also - [Computation (C++)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (C++)](https://ytsaurus.tech/docs/en/state.md) - [Testing (C++)](https://ytsaurus.tech/docs/en/testing.md) - [Quick start (C++)](https://ytsaurus.tech/docs/en/getting-started.md) # Word Count in YTsaurus Flow (C++) This is a simple introductory example in C++. The [pipeline](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/word_count) reads text messages from the input queue, splits them into words, and counts the occurrences of each word using an external state table. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/word_count) Both units of user logic are written as [process functions](https://ytsaurus.tech/docs/process-functions.md) (subclasses of `IProcessFunction`). The function itself is lightweight and testable, and its execution mode is selected by the built-in `Computation` adapter in the spec via the `processing_function` field. ## Pipeline components ### TTextReadFunction `TTextReadFunction` is a process function (`IProcessFunction`) that is executed by `TProcessFunctionSourceComputation` (the source adapter). It reads text messages from the input queue, splits the text into words (by whitespace characters), and for each word with a length of at least `min_word_length`, it generates a `TWordMessage` object in the `words` output stream. The `min_word_length` parameter is read in `Init` via `initContext->GetParameters()` from the `processing_function_parameters` block of the spec. `TProcessFunctionSourceComputation` runs the function in Swift ordered-source mode, so output messages are not stored in YTsaurus — only the metadata required for deterministic operation is saved. For more details about computation modes, see the [Computations](https://ytsaurus.tech/concepts/computation.md) section. ### TWordCountFunction `TWordCountFunction` is a process function (`IProcessFunction`) that is executed by `TProcessFunctionComputation` (the transform adapter). It uses `TSimpleExternalStateManager` to work with the [state](https://ytsaurus.tech/concepts/glossary.md#state). For each input word, it: - Gets the current counter value from the external state table. - Increases the counter by 1. - Writes the updated value back. For more details about working with state, see the [State](https://ytsaurus.tech/concepts/stateful.md) section. ## Message types `TWordMessage` is a subclass of `TYsonMessage`. It contains a single `Word` field. It is registered via the `YT_FLOW_DEFINE_YSON_MESSAGE` macro. ## Pipeline structure The pipeline consists of two adapter computations connected by the `words` stream: 1. **source** (input queue) → **TProcessFunctionSourceComputation** (`processing_function = TTextReadFunction`) → `words` stream. 2. `words` stream → **TProcessFunctionComputation** (`processing_function = TWordCountFunction`) → state table (word → count). In the spec for the counter, you specify `group_by_schema` with a hash of the word and the word itself — to ensure correct [partitioning](https://ytsaurus.tech/concepts/glossary.md#partition). The state parameters (`TSimpleExternalStateManager` and the table path) are declared in the `external_state_managers` section of the `Computation` spec. ## The main function In `main`, you do the following: 1. `NYT::NFlow::Initialize(argc, argv)` — initialize the Flow library. 2. Register the functions via `YT_FLOW_DEFINE_PROCESS_FUNCTION(TTextReadFunction)` and `YT_FLOW_DEFINE_PROCESS_FUNCTION(TWordCountFunction)`. 3. `TSimpleSpecBuilder` — a builder for registering streams. You register the `words` stream with the `TWordMessage` message type via `RegisterStream("words")`. 4. `TSimpleRunnerProgram` — run the pipeline. ## Source code ### TTextReadFunction ```cpp void TTextReadFunction::Init(const IRuntimeInitContextPtr& initContext) { MinWordLength_ = initContext->GetParameters()->MinWordLength; } void TTextReadFunction::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto text = GetColumnValue(message, "text"); for (const auto& word : StringSplitter(text).SplitBySet(" \t\n\r").SkipEmpty()) { if (std::ssize(word) < MinWordLength_) { continue; } auto wordMessage = New(); wordMessage->Word = word; output->AddMessage(context->ConvertToMessage(wordMessage)); } } ``` ### TWordCountFunction ```cpp void TWordCountFunction::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitExternalStateClient(StateClient_, "/state"); } void TWordCountFunction::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) { auto state = StateClient_.GetState(message->Key); i64 count = state->GetColumnValue>("count").value_or(0); TPayloadBuilder builder(state->Schema); builder.Set(count + 1, "count"); state->Payload = builder.Finish(); } ``` ## See also - [Getting started (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (C++)](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) # Shuffle in YTsaurus Flow (C++) You use the [pipeline](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/shuffle) to read a JSON stream from a sorted dynamic table, group it multiple times by different keys, and then count the number of unique keys in all resulting streams. A [test](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/shuffle/test/test_shuffle.py) is also described for the pipeline. This pipeline doesn’t aim to solve any business problem. ## General pipeline overview ### Reader. Reading input data The first computation is `Reader`. It reads and performs initial transformation of data from the queue. Here are the parts of the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec) related to this computation: ```yson { "spec" = { "computations" = { "reader" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionSourceComputation"; "processing_function" = "NYT::NFlow::NExample::TQueueReader"; "output_stream_ids" = ["event"]; "source_streams" = { "queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { "queue_path" = "//path/to/queue"; "consumer_path" = "//path/to/consumer"; "finite" = false; }; }; }; }; }; "streams" = { "event" = { "schema" = [ {"name" = "key_a"; "type" = "uint64";}; {"name" = "key_b"; "type" = "uint64";}; {"name" = "key_c"; "type" = "uint64";}; {"name" = "key_d"; "type" = "uint64";}; {"name" = "value"; "type" = "string";}; ]; } }; }; } ``` Let’s break it down. - `computations/reader/source_streams` contains the `queue` source with the type `NYT::NFlow::TQueueSource`. This `source` reads data from a sorted dynamic table using a `consumer`. In `parameters`, you specify which queue to read from and which consumer to use. You can learn more about the parameters in the `NYT::NFlow::TQueueSourceParameters` class. - Use `NYT::NFlow::TQueueSourceController` to manage the `Computation`, because you need to define the number and settings of the [partitions](https://ytsaurus.tech/concepts/glossary.md#partition) based on the input sorted dynamic table. - `streams` contains a single `event` flow — the parsed flow output from `reader`, which is available to other `Computation` components. It has an associated schema. This same flow is also registered in `computations/reader/output_stream_ids`. - The `TQueueReader` class implements `IProcessFunction`; the `TProcessFunctionSourceComputation` named in `computation_class_name` hosts it. A built-in passthrough isn’t suitable here because parsing `JSON` requires a custom `ProcessMessage` implementation. ```cpp void TQueueReader::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto data = GetColumnValue(message, "data"); ::NJson::TJsonValue json; ::NJson::ReadJsonTree(data, &json, /*throwOnError*/ true); auto builder = context->MakeOutputMessageBuilder("event"); builder.Payload().Set(json["value"].GetStringSafe(), "value"); builder.Payload().Set(json["key_a"].GetUIntegerSafe(), "key_a"); builder.Payload().Set(json["key_b"].GetUIntegerSafe(), "key_b"); builder.Payload().Set(json["key_c"].GetUIntegerSafe(), "key_c"); builder.Payload().Set(json["key_d"].GetUIntegerSafe(), "key_d"); output->AddMessage(builder.Finish()); } ``` - The source adapter runs in Swift mode: it doesn’t materialize output streams in YTsaurus and saves only the metadata required for deterministic operation. - `TQueueSource` and the adapter manage queue access, including access to a remote cluster. The process function receives an already-read message and doesn’t access a YTsaurus client directly. ### Shuffle The pipeline includes several shuffles: `shuffle_a`, `shuffle_b`, `shuffle_c`, and `shuffle_d`. Each of them groups the input stream by the corresponding key — `key_a`, `key_b`, `key_c`, or `key_d`. They don’t transform the data; they just demonstrate the ability to group different objects. Let’s examine the spec for `shuffle_b`: ```yson { "spec" = { "computations" = { "shuffle_b" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key_b)"; "type" = "uint64";}; {"name" = "key_b"; "type" = "uint64";}; ]; "input_stream_ids" = ["event_a"]; "output_stream_ids" = ["event_b"]; }; }; streams = { "event_a" = { "schema" = [ {"name" = "key_a"; "type" = "uint64";}; {"name" = "key_b"; "type" = "uint64";}; {"name" = "key_c"; "type" = "uint64";}; {"name" = "key_d"; "type" = "uint64";}; {"name" = "value"; "type" = "string";}; ]; }; "event_b" = { "schema" = [ {"name" = "key_a"; "type" = "uint64";}; {"name" = "key_b"; "type" = "uint64";}; {"name" = "key_c"; "type" = "uint64";}; {"name" = "key_d"; "type" = "uint64";}; {"name" = "value"; "type" = "string";}; ]; }; }; }; } ``` - Since this example doesn’t include any data transformation, `NYT::NFlow::TSwiftPassthroughComputation` is sufficient. If transformation is required, implement the custom logic as `IProcessFunction` and host it with `NYT::NFlow::TProcessFunctionSwiftMapComputation`. - `NYT::NFlow::TSwiftPassthroughComputation` doesn’t materialize data in YTsaurus. - `group_by_schema` contains the corresponding `key_b` key. The `hash` column is added, because partitioning in `Flow` works only under the assumption that the first column contains uniformly distributed `uint64` values. - `input_stream_ids` and `output_stream_ids` contain `event_a` and `event_b`, respectively. - `spec/streams` also contains `event_a` and `event_b` with the full schema description. ### Reduce This is the final `Computation`. It reads the `event_a`, `event_b`, `event_c`, and `event_d` streams and counts how many times each `value` appears. In effect, the original stream is processed four times. ```yson { "spec" = { "computations" = { "reducer" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "NYT::NFlow::NExample::TReducer"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(value)"; "type" = "uint64";}; {"name" = "value"; "type" = "string";}; ]; "input_stream_ids" = ["event_a"; "event_b"; "event_c"; "event_d";]; "output_stream_ids" = []; "external_state_managers" = { "/state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state"; }; }; }; }; }; }; }; ``` - The process function `TReducer`, hosted by `TProcessFunctionComputation`, implements the logic. - To work with the [state](https://ytsaurus.tech/concepts/glossary.md#state), the example uses `TSimpleExternalStateManager`, which provides direct access to the table. `TReducer` stores a `TMutableStateKeyClient` and binds it to `"/state"` in `Init(const IRuntimeInitContextPtr&)`. ```cpp void TReducer::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitExternalStateClient(StateClient_, "/state"); } void TReducer::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) { auto state = StateClient_.GetState(message->Key); i64 count = state->GetColumnValue>("count").value_or(0); count += 1; TPayloadBuilder builder(state->Schema); builder.Set(count, "count"); state->Payload = builder.Finish(); } ``` ## See also - [Quick start (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (C++)](https://ytsaurus.tech/docs/computation.md) - You must include the `value` column (and its hash) in `group_by_schema` to group by `value`. - List all streams — `event_a`, `event_b`, `event_c`, and `event_d` — in `input_stream_ids` to read all resulting streams. From a business logic perspective, this isn’t the most meaningful action. However, the original goal of this pipeline was to test the `exactly-once` guarantees, even in the case of a `Swift` chain. - `TReducer` implements `IProcessFunction` and is hosted by `TProcessFunctionComputation`. The adapter persists `input_messages` and `output_messages` in YTsaurus, but this example produces no output messages. In effect, the pipeline saves the `reader` metadata, the (`message_id`, `key`) metadata for each `reducer` input message, and the `value => count` table. The intermediate passthrough computations don’t interact with YTsaurus. ### DynamicSpec - Fill the `dynamic_spec/computations//parameters/desired_partition_count` field for each `computation` except `reader`. The `test_shuffle.py` test changes the number of partitions. - Specify the required number of threads for running all jobs in `dynamic_spec/job_tracker/job_threads`. ### Config for running - Key settings for running: `cluster_url`, `proxy_role`, `path`, `rpc_port`, and `monitoring_port`. - Set `controller/scheduler_period` to 200 for this specific test — in reality, the default value should be sufficient. - `logging` contains the logging settings. ```yson { "cluster_url" = "cluster_name"; "path" = "//path/to/pipeline"; "rpc_port" = 81; "monitoring_port" = 80; "controller" = { "scheduler_period" = 200; }; "logging" = { "suppressed_messages" = [ ]; "rules" = [ { "exclude_categories" = [ "Bus"; "Dns"; "Concurrency"; "QueryClient"; "Profiling"; "RpcClient"; "Monitoring"; "Net"; "Solomon"; "Jaeger"; "RpcProxyClient"; "RpcServer"; "Dns"; "BufferMetrics"; ]; "min_level" = "debug"; "writers" = [ "Stderr"; ]; }; ]; "writers" = { "Stderr" = { "type" = "file"; "file_name" = "/path/to/file.log"; }; }; } } ``` # Async Request in YTsaurus Flow (C++) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) demonstrates a pattern for asynchronous external requests using [process functions](https://ytsaurus.tech/docs/process-functions.md). You send events to the input, convert them into requests, process them with a deterministic function, and accumulate the results in the [state](https://ytsaurus.tech/concepts/glossary.md#state). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/async_request) ## Pipeline components ### TStateKeeper `TStateKeeper` implements `IProcessFunction` and is hosted by `TProcessFunctionComputation`. The function uses `TSimpleExternalStateManager` to work with the external state and handles two types of input messages: - **The `event` stream**: when you receive an event, it creates a `TRequestMessage` with a unique `RequestId` and sends it to the `request` stream. - **The `response` stream**: when you receive a response, it updates the state — it sums `total_length` from all received responses. You distinguish the streams using `message->StreamId`. ### TRequestProcessor `TRequestProcessor` implements `IProcessFunction` and is hosted by `TProcessFunctionSwiftMapComputation`, which doesn’t store input and output messages in YTsaurus. The function receives a `TRequestMessage`, performs processing (in this example, it calculates the request length), and generates a `TResponseMessage`. You use the SwiftMap adapter because the request processing is a pure function: the same input data always generates the same result. ## Message types - **TEventMessage** — a descendant of `TYsonMessage`. It contains the `Key` and `Data` fields. - **TRequestMessage** — a descendant of `TYsonMessage`. It contains the `RequestId`, `Key`, and `Request` fields. - **TResponseMessage** — a descendant of `TYsonMessage`. It contains the `RequestId`, `Key`, and `Length` fields. You register all message types using the `YT_FLOW_DEFINE_YSON_MESSAGE` macro. ## Key pattern: request-response cycle The main idea of this example is to build a request-response cycle within the pipeline using multiple streams: 1. **events** → `TStateKeeper` → **request** (request generation) 2. **request** → `TRequestProcessor` → **response** (request processing) 3. **response** → `TStateKeeper` → state (result accumulation) `TStateKeeper` is both an event consumer and a response consumer. It uses `input_stream_ids = ["event", "response"]` and determines the input message type by `StreamId`. The spec binds each process function to its adapter explicitly: - `state` uses `computation_class_name = "NYT::NFlow::TProcessFunctionComputation"` and `processing_function = "NYT::NFlow::NExample::TStateKeeper"`. - `processor` uses `computation_class_name = "NYT::NFlow::TProcessFunctionSwiftMapComputation"` and `processing_function = "NYT::NFlow::NExample::TRequestProcessor"`. ## State management `TStateKeeper` uses `TSimpleExternalStateManager` to store the sum of lengths of all processed requests. You bind the state client (`TMutableStateKeyClient`) in `Init(const IRuntimeInitContextPtr&)` via `initContext->InitExternalStateClient(StateClient_, "/state")`. You declare the state parameters (the `path` to the table, etc.) in the `external_state_managers` section of the computation [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec). ## The main function In `main`, you register three streams; the process functions are registered via `YT_FLOW_DEFINE_PROCESS_FUNCTION`: - `RegisterStream("event")` — input events - `RegisterStream("request")` — requests to the processor - `RegisterStream("response")` — responses from the processor ## Source code ### TRequestProcessor ```cpp void TRequestProcessor::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto request = context->ConvertToYsonMessage(message); auto response = New(); response->RequestId = request->RequestId; response->Key = request->Key; response->Length = std::ssize(request->Request); output->AddMessage(context->ConvertToMessage(response)); YT_TLOG_DEBUG("Processed request") .With("RequestId", request->RequestId) .With("FailedAttempts", 0); } ``` ### TStateKeeper ```cpp void TStateKeeper::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitExternalStateClient(StateClient_, "/state"); } void TStateKeeper::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { if (message->StreamId == "event") { auto event = context->ConvertToYsonMessage(message); auto request = New(); request->RequestId = RandomNumber(); request->Key = event->Key; request->Request = event->Data; output->AddMessage(context->ConvertToMessage(request)); YT_TLOG_DEBUG("Send request") .With("RequestId", request->RequestId); } else if (message->StreamId == "response") { auto response = context->ConvertToYsonMessage(message); YT_TLOG_DEBUG("Received response") .With("RequestId", response->RequestId); auto state = StateClient_.GetState(message->Key); i64 totalLength = state->GetColumnValue>("total_length").value_or(0); totalLength += response->Length; TPayloadBuilder builder(state->Schema); builder.Set(totalLength, "total_length"); state->Payload = builder.Finish(); } else { THROW_ERROR_EXCEPTION("Unexpected stream_id") .With("stream_id", message->StreamId); } } ``` ## See also - [Getting started (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (C++)](https://ytsaurus.tech/docs/computation.md) # Retryable Async Request in YTsaurus Flow (C++) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) is similar to [Async Request](https://ytsaurus.tech/docs/en/async_request.md), but it adds retry logic using [timers](https://ytsaurus.tech/concepts/glossary.md#timer) and the internal `YsonState`. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/retryable_async_request) ## Difference from Async Request The key difference is that `TRequestProcessor` is hosted by `TProcessFunctionComputation` rather than `TProcessFunctionSwiftMapComputation` because it needs to: - Store the internal [state](https://ytsaurus.tech/concepts/glossary.md#state) to track the number of failed attempts. - Use timers for retries at a specified interval. ## Pipeline Components ### TRequestProcessor `TRequestProcessor` implements `IProcessFunction` and uses `TMutableStateKeyClient` to store the internal state (Internal YsonState). You initialize the state in `Init(const IRuntimeInitContextPtr& initContext)` via `initContext->InitClient(RequestStateClient_, "request_state")`. When you process an incoming message (`ProcessMessage`): 1. You save the request in the state with `FailedAttempts = 0`. 2. You call `TryRequest` to perform the attempt. The `TryRequest` method contains the main retry logic: - If the request fails (determined by `IsRequestSuccessful`), you increment the `FailedAttempts` counter and set a timer via `output->AddTimer(GetNextAttempt(context))`. - If the request succeeds, you create a `TResponseMessage`, reset the state via `state.Clear()`, and send the response. When the timer fires (`ProcessTimer`), you call `TryRequest` again with the current state. ### TStateKeeper It’s fully similar to `TStateKeeper` from the [Async Request](https://ytsaurus.tech/docs/en/async_request.md) example: it accepts incoming events and responses and stores the accumulated result in the external state. ## Retry Pattern The retry logic is based on the following elements: - **TDelayedRequestState** — a descendant of `NYTree::TYsonStruct`, it stores `FailedAttempts` and the `Request` itself. - **TMutableStateKeyClient** — a client for working with the internal YsonState. Unlike `TSimpleExternalStateManager`, the state is stored in Flow’s internal tables, not in an external user table. - **Timers** — on a failed attempt, you set a timer with a `Delay` via `output->AddTimer(GetNextAttempt(context))`. - **Constants** — `MaxRetries = 3` defines the simulated success period: a request succeeds immediately or after no more than two retries. `Delay = 5` defines the delay between attempts. You calculate the time for the next attempt via `context->GetCurrentTimestamp()`, which ensures correct operation with Flow’s system time. ## Pipeline Structure 1. **events** → `TStateKeeper` → **request** (request generation). 2. **request** → `TRequestProcessor` → **response** (processing with retries). 3. **response** → `TStateKeeper` → state (result accumulation). In the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec) for `TRequestProcessor`, you must register the `timer_streams` section to support retries. The process function class is specified in `processing_function`. ## Source Code ### TRequestProcessor ```cpp void TRequestProcessor::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitClient(RequestStateClient_, "request_state"); } void TRequestProcessor::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto request = context->ConvertToYsonMessage(message); auto state = RequestStateClient_.GetState(message->Key); state->Request = request; state->FailedAttempts = 0; TryRequest(state, output, context); } void TRequestProcessor::ProcessTimer( const TInputTimerConstPtr& timer, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto state = RequestStateClient_.GetState(timer->Key); TryRequest(state, output, context); } TSystemTimestamp TRequestProcessor::GetNextAttempt(const IRuntimeContextPtr& context) const { return TSystemTimestamp{context->GetCurrentTimestamp().Underlying() + Delay}; } bool TRequestProcessor::IsRequestSuccessful(ui64 requestId, i64 failedAttempts) const { return (requestId + failedAttempts) % MaxRetries == 0; } void TRequestProcessor::TryRequest( TStateAccessor& state, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) const { if (!IsRequestSuccessful(state->Request->RequestId, state->FailedAttempts)) { state->FailedAttempts += 1; output->AddTimer(GetNextAttempt(context)); YT_TLOG_DEBUG("Failed request") .With("RequestId", state->Request->RequestId) .With("FailedAttempts", state->FailedAttempts); return; } auto response = New(); response->RequestId = state->Request->RequestId; response->Key = state->Request->Key; response->Length = std::ssize(state->Request->Request); YT_TLOG_DEBUG("Processed request") .With("RequestId", state->Request->RequestId) .With("FailedAttempts", state->FailedAttempts); state.Clear(); output->AddMessage(context->ConvertToMessage(response)); } ``` ### TStateKeeper ```cpp void TStateKeeper::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitExternalStateClient(StateClient_, "/state"); } void TStateKeeper::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { if (message->StreamId == "event") { auto event = context->ConvertToYsonMessage(message); auto request = New(); request->RequestId = RandomNumber(); request->Key = event->Key; request->Request = event->Data; output->AddMessage(context->ConvertToMessage(request)); YT_TLOG_DEBUG("Send request") .With("RequestId", request->RequestId); } else if (message->StreamId == "response") { auto response = context->ConvertToYsonMessage(message); YT_TLOG_DEBUG("Received response") .With("RequestId", response->RequestId); auto state = StateClient_.GetState(message->Key); i64 totalLength = state->GetColumnValue>("total_length").value_or(0); totalLength += response->Length; TPayloadBuilder builder(state->Schema); builder.Set(totalLength, "total_length"); state->Payload = builder.Finish(); } else { THROW_ERROR_EXCEPTION("Unexpected stream_id") .With("stream_id", message->StreamId); } } ``` ## See also - [Quick Start (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Example: Asynchronous Requests (C++)](https://ytsaurus.tech/docs/en/async_request.md) # Join hits, impressions, and clicks over a time window (YTsaurus Flow, C++) ## Problem statement Imagine a system with three types of events: - hit — a user request event. It contains some hit_payload with useful data. - action show — an event indicating that the user saw the response; it contains action_time. - action click — an event indicating that the user clicked the response they saw; it contains action_time. All events include hit_id and hit_time. For simplicity, assume that within a user request, a hit must be present, and there can be no more than one show and no more than one click. The data arrives in two topics: hit and action. The output will be a join of all three logs. In general, there are many possible join variants, but here we’ll focus on one specific approach: 1. An action can occur several hours after a hit. However, this [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) only considers actions where action_time < hit_time + wait_for_actions. 2. The system ignores all events that happen after the hit is closed. In other words, it doesn’t allow any updates to previously emitted events. 3. Hits without impressions are ignored. 4. For each show, you get information about whether a click occurred and the hit_payload from the hit. Points 1 and 2 can have other variants depending on business goals. The article [The Dataflow Model](https://static.googleusercontent.com/media/research.google.com/ru//pubs/archive/43864.pdf) covers various possible system behaviors quite well. We strongly recommend reading it. You can find the code [here](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/wait_click_join). ## Reading data This part is pretty standard. Since the data is already in the required format in the input queue, you can use the combination `TSwiftPassthroughOrderedSourceComputation + TQueueSource`. You register `action_reader`, which generates the `action` stream, and `hit_reader`, which generates the `hit` stream. However, for the pipeline to work correctly, you need to configure time handling properly. For both `SourceComputation` instances, you must set up `watermark_strategy`: - You need to set `event_timestamp` to the appropriate time. Use `event_timestamp_assigner` for this. For `action_reader`, specify the `action_time` column; for `hit_reader`, use `hit_time`. - You explicitly set `watermark_generator/out_of_orderness_bound` to `10s`. This is a key parameter for the heuristic that estimates the [watermark](https://ytsaurus.tech/concepts/glossary.md#timestamps-and-watermarks). This value is only for tests; for real tasks, choose it based on the properties of the stream you’re reading. You can read about all the settings in detail [here](https://ytsaurus.tech/concepts/spec.md#watermark-strategy). Here’s an example [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec) for `action_reader`: ```yson "spec" = { "computations" = { "action_reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "input_stream_ids" = []; "output_stream_ids" = ["action"]; "watermark_strategy" = { "event_timestamp_assigner" = { "column" = "action_time"; }; "watermark_generator" = { "out_of_orderness_bound" = "10s"; }; }; "sources" = { "source_stream" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "stream_id" = "injected_stream"; "parameters" = { }; }; }; "parameters" = {}; }; }; "streams" = { "action" = { "schema" = [ {name = "hit_id"; type = "string";}; {name = "hit_time"; type = "uint64";}; {name = "is_click"; type = "boolean";}; {name = "action_time"; type = "uint64";}; ]; }; }; }; ``` Keep in mind that our watermark generator is heuristic, so errors are inevitable. That means that from time to time, some events will violate the watermark rules. These are usually called `late data`. Depending on your system’s requirements, you can handle these events in a special way. In this pipeline, they’re completely ignored, which inevitably leads to some data loss. ## Joining streams For convenience, the join is performed by the key `(hit_id, hit_time)`. When the first event for this key arrives, a timer starts with `max_time == hit_time + wait_for_actions`. The system waits until all events with `event_time < max_time` are processed (allowing for the accuracy of our `watermark`, of course) and then permanently closes the corresponding hit. It generates the necessary `output` event and deletes the original hit profile. Any data that arrives after the hit is closed is ignored. First, let’s look more closely at the join spec: ```yson { "computations" = { "join" = { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "NYT::NFlow::NExample::TJoinFunction"; "processing_function_parameters" = { "wait_for_actions" = "10s"; }; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(hit_id)"; "type" = "uint64"}; {"name" = "hit_id"; "type" = "string"}; {"name" = "hit_time"; "type" = "uint64"}; ]; "input_stream_ids" = ["action"; "hit"]; "output_stream_ids" = ["joined_action";]; "timers" = { "timer" = {}; }; "sinks" = { "queue" = { "sink_class_name" = "NYT::NFlow::TAsyncQueueSink"; "input_stream_ids" = ["joined_action"]; "parameters" = { }; }; }; }; }; streams = { "joined_action" = { "schema" = [ {name = "hit_id"; type = "string";}; {name = "hit_time"; type = "uint64";}; {name = "is_click"; type = "boolean";}; {name = "show_time"; type = "uint64";}; {name = "click_time"; type = "uint64";}; {name = "hit_payload"; type = "string";}; ]; }; }; }; ``` - In `group_by_schema`, in addition to `hit_id` and `hit_time`, we include `hash`. This is for the correct operation of the [partitioning](https://ytsaurus.tech/concepts/glossary.md#partition) algorithm. - The pipeline needs a [timer](https://ytsaurus.tech/concepts/glossary.md#timer) to close the hit, so we register `timer` in `timers`. We don’t specify extra settings because timers, by default, use `event_time` and the input streams. - To send the `joined_event` to an ordered dynamic table (which might be on another cluster), we use an asynchronous `TAsyncQueueSink`. The join itself is implemented as a [process function](https://ytsaurus.tech/docs/process-functions.md) called `TJoinFunction` (a subclass of `IProcessFunction` that processes messages and timers element by element). It’s executed by the built-in `TProcessFunctionComputation`. In the spec, you set it via `processing_function`, and `wait_for_actions` is passed through `processing_function_parameters`. We recommend reading the code in the repository because it’s continuously improved. Key ideas: - When the first event for a key arrives, you create a timer to close it. - The state for each key is described by an arbitrary type (in this example, a `TYsonStruct` subclass). This object is stored in the internal `Flow` tables. You access the state via `TMutableStateKeyClient`, which you initialize in `Init` using `IRuntimeInitContext::InitClient`. - For each event, you record the necessary information from that event in the profile. - When the timer fires, you generate an output event (if you have the required data) and clear the profile by calling `state.Clear()`. - You get the current watermark with `context->GetInputEventWatermark()` and discard all late-arriving events. ```cpp void TJoinFunction::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto ysonKey = context->ConvertToYsonKey(message->Key); auto state = JoinStateClient_.GetState(message->Key); auto windowEnd = GetWindowEnd(ysonKey); auto eventTimestamp = message->GetMeta().EventTimestamp; // All messages out of the wait range are dropped. if (eventTimestamp >= windowEnd) { YT_TLOG_WARNING("Message is out of wait range, skip") .With("HitId", ysonKey->HitId) .With("WindowEnd", windowEnd); return; } // Drop late data. auto inputEventWatermark = context->GetInputEventWatermark(); if (eventTimestamp < inputEventWatermark) { YT_TLOG_WARNING("Late message, skip") .With("HitId", ysonKey->HitId) .With("InputEventWatermark", inputEventWatermark); return; } if (message->StreamId == "hit") { auto hit = context->ConvertToYsonMessage(message); state->HitPayload = hit->HitPayload; } else if (message->StreamId == "action") { auto action = context->ConvertToYsonMessage(message); if (action->IsClick) { state->ClickTime = action->ActionTime; } else { state->ShowTime = action->ActionTime; } } else { THROW_ERROR_EXCEPTION("Unknown stream %Qv", message->StreamId); } // Set a timer at the end of the wait window. With DeduplicateEqualTimestamps (on by // default) timers are deduplicated by trigger timestamp; the timer fires once the event // watermark reaches windowEnd, i.e. after (almost) all messages within the window. output->AddTimer(windowEnd, TSystemTimestamp(ysonKey->HitTime)); } ``` ```cpp void TJoinFunction::ProcessTimer( const TInputTimerConstPtr& timer, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto ysonKey = context->ConvertToYsonKey(timer->Key); auto state = JoinStateClient_.GetState(timer->Key); if (timer->StreamId != "timer") { THROW_ERROR_EXCEPTION("Unknown stream %Qv", timer->StreamId); } if (state.IsEmpty()) { return; } if (state->ShowTime && state->HitPayload) { auto joinedAction = New(); joinedAction->HitId = ysonKey->HitId; joinedAction->HitTime = ysonKey->HitTime; joinedAction->ShowTime = *state->ShowTime; if (state->ClickTime) { joinedAction->IsClick = true; joinedAction->ClickTime = *state->ClickTime; } else { joinedAction->IsClick = false; joinedAction->ClickTime = 0; } joinedAction->HitPayload = *state->HitPayload; output->AddMessage(context->ConvertToMessage(joinedAction)); } state.Clear(); } ``` ## Managing objects in YTsaurus {#yt-sync} The imaginary system described above includes the following objects: - `action_queue` and `hit_queue` — input [queues](https://ytsaurus.tech/user-guide/dynamic-tables/queues.md#data_model). - `consumer` — a [consumer](https://ytsaurus.tech/user-guide/dynamic-tables/queues.md#data_model) for reading the input queues and notifying the queue broker which messages are fully processed and no longer needed by the system. - `output_queue` — the output queue with the joined input logs. - `producer` — a producer for [exactly-once](https://ytsaurus.tech/concepts/glossary.md#exactly-once) writes to the output queue. - `state` — a [table](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md) for storing the key profile. - `pipeline` — the pipeline itself, which is a collection of tables and files. ## See also - [Quick start (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) You must create all these objects in YTsaurus before you [start the pipeline](https://ytsaurus.tech/devops/index.md#launch-flow). # URL Downloader in YTsaurus Flow (C++) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) shows how you run external calls (similar to HTTP requests) from [computations](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). You control the download speed (throttling), shard by host, and manage the [state](https://ytsaurus.tech/concepts/glossary.md#state). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/url_downloader) ## Pipeline components ### TUrlDownloader This helper class (a descendant of `TRefCounted`) manages download queues for each host. It lets you: - **Register hosts** (`RegisterHost`) — for each host, it creates an asynchronous executor that processes URLs from the queue one after another with an artificial delay (to emulate throttling). - **Add URLs** (`RegisterUrl`) — it adds a URL to the corresponding host’s queue. - **Extract results** (`ExtractProcessedUrls`) — it returns processed URLs with their results. - **Unregister a host** (`UnregisterHost`) — it stops processing and clears the queue. The key pattern is using `AsyncVia(GetCurrentInvoker())` to run background processing within a serialized invoker. This lets you safely work with shared state without locks. ### TLimitedUrlDownloadFunction This is the main process function. It implements `IProcessFunction`, is hosted by `TProcessFunctionComputation`, and coordinates URL downloads using `TUrlDownloader`. When you process an input message (`ProcessMessage`): 1. You read a `TUrlMessage` with the `Host` and `Url` fields. 2. You save the URL in the host’s state. 3. You register the host and URL in `TUrlDownloader`. 4. You set a [timer](https://ytsaurus.tech/concepts/glossary.md#timer) to periodically check results via `GetNextHostCheck`. 5. You apply a limit to the state size using `EnforceLimit`. When the timer fires (`ProcessTimer`): 1. You restore the host from the state. 2. You extract processed URLs from `TUrlDownloader`. 3. You remove processed URLs from the state. 4. You generate a `TProcessedUrlMessage` for each processed URL. 5. If the queue is empty, you unregister the host and reset the state; otherwise, you set the next timer. ## Message types - **TUrlMessage** — a descendant of `TYsonMessage`. It contains the `Host` and `Url` fields. - **TProcessedUrlMessage** — a descendant of `TYsonMessage`. It contains the `Host`, `Url`, and `Data` fields (the processing result). ## Key patterns ### Internal YsonState You use `TMutableStateKeyClient` to store the URL queue for each host. The `TLimitedHostState` state contains: - `Host` — the host name. - `Urls` — the URL queue (`std::deque`) waiting to be processed. ### Timers for periodic checks The `GetNextHostCheck` method calculates the time for the next host check. The time is calculated based on: - `CheckHostPeriod` — the check period (default is 5 seconds). - The host name hash — to evenly distribute checks for different hosts over time. ### Dynamic parameters `TDynamicLimitedUrlDownloadParameters`, passed through the dynamic `processing_function_parameters` field, lets you change parameters without restarting the pipeline: - `CheckHostPeriod` — the host check period (default is 5 seconds and must be greater than 1 second). - `PersistLimit` — the maximum number of URLs stored in the state for a single host (default is 1000). ### PersistLimit `EnforceLimit` limits the size of the URL queue in the state. If the number of URLs exceeds `PersistLimit`, the older URLs are removed. This keeps the state within the row size limits for a dynamic table. {% note warning %} During [partition](https://ytsaurus.tech/concepts/glossary.md#partition) rebalancing, URLs that don’t fit within the `PersistLimit` will be lost. Choose the limit value considering the acceptable loss. {% endnote %} ## Pipeline structure 1. **Input queue** → the `urls` stream (`TUrlMessage`). 2. The `urls` stream → **TLimitedUrlDownloadFunction** (with timers and state) → the `processed_urls` stream (`TProcessedUrlMessage`). In the static spec, `url_downloader` uses `computation_class_name = "NYT::NFlow::TProcessFunctionComputation"` and `processing_function = "NYT::NFlow::NExample::TLimitedUrlDownloadFunction"`. Function settings are passed through `dynamic_spec/computations/url_downloader/processing_function_parameters`. ## main function In `main`, you register two streams; `TLimitedUrlDownloadFunction` is registered via `YT_FLOW_DEFINE_PROCESS_FUNCTION`: - `RegisterStream("urls")` — input URLs. - `RegisterStream("processed_urls")` — processed URLs. ## Source code ### TUrlDownloader ```cpp void TUrlDownloader::RegisterHost(const std::string& host, const std::deque& urls) { if (Executors_.contains(host)) { return; } Queue_[host] = urls; // Allows to write code without spin locks. YT_ASSERT_SERIALIZED_INVOKER_AFFINITY(GetCurrentInvoker()); Executors_[host] = BIND([weakThis = MakeWeak(this), host] { // Emulates slow processing with throttle. while (true) { if (auto strongThis = weakThis.Lock()) { if (!strongThis->Queue_[host].empty()) { auto url = strongThis->Queue_[host].front(); strongThis->Queue_[host].pop_front(); strongThis->ProcessedUrls_[host].push_back({url, strongThis->ProcessUrl(url)}); } } else { return; } NConcurrency::TDelayedExecutor::WaitForDuration(TDuration::MilliSeconds(500 + RandomNumber(1000))); } }) .AsyncVia(GetCurrentInvoker()) .Run(); } void TUrlDownloader::UnregisterHost(const std::string& host) { if (auto iter = Executors_.find(host); iter != Executors_.end()) { auto future = iter->second; Executors_.erase(iter); future.Cancel(TError("Host unregistered")); Y_UNUSED(NConcurrency::WaitFor(future)); } ProcessedUrls_.erase(host); Queue_.erase(host); } void TUrlDownloader::RegisterUrl(const std::string& host, const std::string& url) { YT_VERIFY(Executors_.contains(host)); Queue_[host].push_back(url); } std::deque> TUrlDownloader::ExtractProcessedUrls(const std::string& host) { std::deque> processed; std::swap(processed, ProcessedUrls_[host]); return processed; } std::string TUrlDownloader::ProcessUrl(const std::string& url) const { int digits = 0; for (const char ch : url) { if (ch >= '0' && ch <= '9') { digits += 1; } } return Format("length: %v, digits: %v", std::ssize(url), digits); } ``` ### TLimitedUrlDownloadFunction ```cpp void TLimitedUrlDownloadFunction::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitClient(HostStateClient_, "host_state"); UrlDownloader_ = New(); } void TLimitedUrlDownloadFunction::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto request = context->ConvertToYsonMessage(message); auto state = HostStateClient_.GetState(message->Key); state->Host = request->Host; state->Urls.push_back(request->Url); UrlDownloader_->RegisterHost(state->Host, state->Urls); UrlDownloader_->RegisterUrl(request->Host, request->Url); output->AddTimer(GetNextHostCheck(state->Host, context)); EnforceLimit(state, context); } void TLimitedUrlDownloadFunction::ProcessTimer( const TInputTimerConstPtr& timer, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto state = HostStateClient_.GetState(timer->Key); EnforceLimit(state, context); UrlDownloader_->RegisterHost(state->Host, state->Urls); for (const auto& [url, data] : UrlDownloader_->ExtractProcessedUrls(state->Host)) { if (auto iter = std::find(state->Urls.begin(), state->Urls.end(), url); iter != state->Urls.end()) { state->Urls.erase(iter); } auto processedUrl = New(); processedUrl->Host = state->Host; processedUrl->Url = url; processedUrl->Data = data; output->AddMessage(context->ConvertToMessage(processedUrl)); } if (state->Urls.empty()) { UrlDownloader_->UnregisterHost(state->Host); state.Clear(); } else { output->AddTimer(GetNextHostCheck(state->Host, context)); } } TSystemTimestamp TLimitedUrlDownloadFunction::GetNextHostCheck( const std::string& host, const IRuntimeContextPtr& context) const { auto parameters = context->GetDynamicParameters(); ui64 period = parameters->CheckHostPeriod.Seconds(); ui64 shift = THash()(host) % period; ui64 timestamp = context->GetCurrentTimestamp().Underlying(); ui64 nextTimestamp = timestamp - timestamp % period + shift + period; return TSystemTimestamp{nextTimestamp}; } void TLimitedUrlDownloadFunction::EnforceLimit( const TStateAccessor& state, const IRuntimeContextPtr& context) const { auto persistLimit = context->GetDynamicParameters()->PersistLimit; while (std::ssize(state->Urls) > persistLimit) { state->Urls.pop_front(); } } ``` ## See also - [Getting started (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (C++)](https://ytsaurus.tech/docs/computation.md) # Multiplexer in YTsaurus Flow (C++) Multiplexer is a process-function pattern that reads a set of records associated with an input key and sends each record as a separate output message. A typical example: key X arrives at the input, and you need to output all rows of a sorted dynamic table where X is a key prefix. Here’s what the base class provides: - **Uniform progress** across multiple active keys (a single large key doesn’t block the processing of others). - **Collapse handling** — if a new input message arrives for a key that’s already being processed, the iteration restarts so that all rows are output with the latest version of the payload from the input. ## How the iteration works The base class stores per-key state with a cursor (`Offset`). On each timer tick, it calls `FetchBatch` on the derived class, passing the current `startOffsetExclusive`. The derived class returns the next cursor; if there’s no more data, it returns `nullopt`. When a collapse happens (a repeated input message for an active key), the base class saves the current position (`InitialStartOffset`) and reads the data in two passes: 1. It reads the remainder from the current position to the end (phase 1). 2. It goes back to the start and reads up to the saved collapse point (phase 2). This ensures that after a collapse, all rows with the new payload version are output — including those that were already emitted before the collapse with the old version. ## Ready-made class: `TDynamicTableMultiplexerProcessFunction` {#dynamic-table-multiplexer} This class covers a typical scenario: you have a sorted dynamic table, and input messages arrive with a key and a payload. For each input message, you need to output one row for every table record with that key. [Class header](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/multiplexer/dynamic_table_multiplexer_process_function.h) ### Parameters ```yson { "computation_class_name" = "NYT::NFlow::TProcessFunctionComputation"; "processing_function" = "TMyMultiplexerProcessFunction"; "processing_function_parameters" = { "table_path" = "//path/to/lookup_table"; }; } ``` Register the derived function with the same static and dynamic parameter types: ```cpp YT_FLOW_DEFINE_PROCESS_FUNCTION( TMyMultiplexerProcessFunction, TDynamicTableMultiplexerParameters, TDynamicMultiplexerParameters); ``` `table_path` is a required cluster-qualified path. Before the first batch, the class reads the set of columns for iteration (key columns after `group_by_schema`) and payload columns from the table schema and caches them. The computation’s `group_by_schema` must match the leading key columns of the table, and the table must have at least one secondary sort-key column after that prefix. The base class persists the secondary-key schema with the offset and resets the offset when that schema changes between pipeline runs. Changes only to non-key payload columns don’t trigger this reset. The table schema is cached for the lifetime of a process-function instance. The configuration fragment above shows only the process-function fields. The computation also needs `group_by_schema`, input and output streams, a current-time timer stream, a timer dependency on the timer and input streams, and `allow_timer_self_dependency = %true`. See the [function implementation](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/multiplexer/tests/pipeline/main.cpp) and its [pipeline spec](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/multiplexer/tests/pipeline/pipeline.yson). ### What you need to implement Inherit from the class and expose its constructor with `using TDynamicTableMultiplexerProcessFunction::TDynamicTableMultiplexerProcessFunction`, or define a constructor that accepts `TProcessFunctionContextPtr`. Override `BuildOutputForRow` to build an output message for one selected row. The method receives the input key, row payload and schema, user state, output collector, and runtime context. If you need to pass data from an input message to the output, define your own `TUserState` and override `OnInputMessage`. Otherwise, keep the default `TEmptyMultiplexerUserState` and don’t override `OnInputMessage`. ```cpp void BuildOutputForRow( const TKey& key, const TPayload& rowPayload, const NTableClient::TTableSchemaPtr& rowSchema, TStateAccessor& userState, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override; ``` The complete example linked above uses input `(key, payload)`, lookup table `[hash, key, secondary_key, region]`, and output `(key, secondary_key, region, payload)`. `rowPayload` is a full row of the table (without group_by columns) as [`TPayload`](https://ytsaurus.tech/docs/state.md). `rowSchema` describes its columns (names and types). You retrieve columns by name using `GetColumnValue` ([`payload.h`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/payload.h)). ### Dynamic parameters These are inherited from the base class: - `timer_period` (default 5 seconds) — how often the per-key timer triggers. - `batch_size` (default 1000) — the size of a single batch; it’s passed to the `LIMIT` clause of the query. Place both fields in the dynamic computation spec under `processing_function_parameters`: ```yson { "processing_function_parameters" = { "timer_period" = "10s"; "batch_size" = 1000; }; } ``` ## Base class: `TMultiplexerProcessFunction` {#multiplexer-process-function} If your data source isn’t a sorted dynamic table but something else (an in-memory structure, a custom RPC service, etc.), inherit directly from [`TMultiplexerProcessFunction`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/multiplexer/multiplexer_process_function.h) and implement `FetchBatch`. The `FetchBatch` contract: - **Offset** (`TKey`) — must be comparable and monotonically increasing within a single iteration. The base class checks monotonicity and fails if the contract is violated. - **`startOffsetExclusive`** — read data strictly **after** this position. `nullopt` means “from the very beginning”. - **`endOffsetInclusive`** — is set only in phase 2 (after a collapse). Your implementation must not exceed this boundary — the base class checks this. - **`limit`** — the recommended maximum number of rows in a single batch. - **Returning `nullopt`** — indicates there’s no more data in the current range. The base class will either switch to phase 2 or finish the iteration. `OnInputMessage` is called for every input message for a key — both when a new key appears and when a collapse occurs. If the implementation needs to distinguish these cases, store an explicit initialization marker in `TUserState`; don’t rely on `userState.IsEmpty()`, because default-valued state remains empty. If you don’t need to save anything from the input, don’t override this method. ## See also - [Process functions (C++)](https://ytsaurus.tech/docs/process-functions.md) - [Working with states (C++)](https://ytsaurus.tech/docs/state.md) # Log parser in YTsaurus Flow (C++) This example shows a [process function](https://ytsaurus.tech/docs/process-functions.md) in materialized ordered-source mode. The [pipeline](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/log_parser) reads log lines from a queue and parses them while reading the source, without an intermediate passthrough computation. The built-in `TProcessFunctionTransformOrderedSourceComputation` executes the function. The example also shows reading from a `Source` and maintaining durable state that survives restarts (see [State](https://ytsaurus.tech/docs/en/#state)). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/log_parser) ## Pipeline components ### TLogParserProcessFunction The user logic is written as a [process function](https://ytsaurus.tech/docs/process-functions.md): a subclass of `IProcessFunction` that doesn’t depend on the `Computation` object, so you can cover it with unit tests without a cluster ([unittest](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/log_parser/unittest/log_parser_process_function_ut.cpp)). It’s executed by the built-in `TProcessFunctionTransformOrderedSourceComputation` adapter, which also sets the mode to ordered source (see the [list of adapters](https://ytsaurus.tech/docs/process-functions.md#how-it-works)). In `ProcessMessage(const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context)`, the function reads the `line` column of the raw `source` message via `GetColumnValue(message, "line")` and parses it with `ParseLogLine` into records of the form `"level:text"` separated by `;`, discarding records without the `:` separator, with empty text, or with a level other than `info`, `warning`, and `error`. For each valid record, it updates the state through the `StateClient_.GetState(message->Key)` accessor (see [State](https://ytsaurus.tech/docs/en/#state)), builds a `TLogRecordMessage`, and emits it into the `records` output stream by calling `output->AddMessage(context->ConvertToMessage(outputRecord))`. The transformation result, the `records` stream, is materialized in YTsaurus as in `TTransformComputation`, so the transformation has no determinism requirements: after a restart, Flow distributes the already materialized messages with the `MessageId` values previously assigned to them instead of recomputing them. The [Proto parser](https://ytsaurus.tech/docs/en/proto_parser.md) example uses the same hosting mode through a reusable process-function base: `TProtoLogParserFunction` inherits from `TProtoParsingProcessFunctionBase` and runs under `TProcessFunctionTransformOrderedSourceComputation`. The host adapter’s spec validator is the same as the computation base class’s: a non-empty `group_by_schema`, timers, key-visitor streams, and `external_state_managers` are rejected (for the full [list of limitations](https://ytsaurus.tech/docs/computation.md#ttransformorderedsourcecomputation)). ### The parser computation spec Two fields of the `parser` computation spec link the function to the adapter (see [Registration](https://ytsaurus.tech/docs/process-functions.md#registration)): ```yson "computation_class_name" = "NYT::NFlow::TProcessFunctionTransformOrderedSourceComputation"; "processing_function" = "NYT::NFlow::NExample::TLogParserProcessFunction"; ``` The remaining fields of the `parser` record describe the connections: `source_streams.queue` is a `TQueueSource` with the paths to the queue and the consumer, and `sinks.queue` is a direct external `TSyncQueueSink` for the `records` stream, so no separate sink computation is needed. For the full file, see [pipeline.yson](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/log_parser/pipeline.yson). ## Message types `TLogRecordMessage` is a subclass of `TYsonMessage` (a YSON structure registered via `YT_FLOW_DEFINE_YSON_MESSAGE`) with the following fields: - `level`: the record level (`info`, `warning`, or `error`); - `text`: the record text; - `worst_level_so_far`: the highest severity level (`info < warning < error`) encountered in this source partition at the time of the record (see [State](https://ytsaurus.tech/docs/en/#state)). ## State {#state} `TLogParserProcessFunction` is stateful. It keeps the `TWorstSeverityState` state in the `TMutableStateKeyClient StateClient_` field (see [Working with states (C++)](https://ytsaurus.tech/docs/state.md#internal-state)). The `TProcessFunctionTransformOrderedSourceComputation` adapter calls `Init(const IRuntimeInitContextPtr& initContext)`, where the client connects to the state via `initContext->InitClient(StateClient_, WorstSeverityStateName)` (the state name is `worst_severity`), and `ProcessMessage`, where the state is read through `GetState(message->Key)` and output records are converted into messages via `context->ConvertToMessage(...)`. The computation instance is bound to a single source partition, so all messages carry the same key and address the same state row: `state->WorstSeverity = std::max(state->WorstSeverity, SeverityRank(record.Level))`. The framework synchronizes this state in the same epoch transaction as the advance of the source offset (see [Computation](https://ytsaurus.tech/docs/computation.md#ttransformorderedsourcecomputation)); the function itself does nothing for this. Therefore, user state is exactly-once correct and doesn’t have to be idempotent under reprocessing: an ordinary, “naive” counter of processed records would be just as correct here. The example keeps a running maximum severity simply because it’s a natural aggregate for such a pipeline, not because it’s in any way safer than a counter. ## The main function The `main` function does the following: 1. `NYT::NFlow::Initialize(argc, argv)`: initializes the Flow library. 2. `TSimpleSpecBuilder`: the builder for registering streams. `RegisterStream("records")` registers the `records` stream with the `TLogRecordMessage` message type. 3. `TSimpleRunnerProgram(std::move(builder)).Run(argc, argv)`: starts the pipeline. You don’t need to register the function in `main`: the `YT_FLOW_DEFINE_PROCESS_FUNCTION(TLogParserProcessFunction)` macro is placed at file level in `lib/log_parser_process_function.cpp`, and the file itself is linked into the library as `GLOBAL`, so the registry entry appears when the binary is initialized. ## Source code ### TLogParserProcessFunction ```cpp #pragma once #include "log_line_parser.h" #include #include #include #include #include namespace NYT::NFlow::NExample { //////////////////////////////////////////////////////////////////////////////// struct TLogRecordMessage : public TYsonMessage { std::string Level; std::string Text; std::string WorstLevelSoFar; REGISTER_YSON_STRUCT(TLogRecordMessage); static void Register(TRegistrar registrar) { registrar.Parameter("level", &TThis::Level) .Default(); registrar.Parameter("text", &TThis::Text) .Default(); registrar.Parameter("worst_level_so_far", &TThis::WorstLevelSoFar) .Default(); } }; //////////////////////////////////////////////////////////////////////////////// struct TWorstSeverityState : public NYTree::TYsonStruct { int WorstSeverity = -1; REGISTER_YSON_STRUCT(TWorstSeverityState); static void Register(TRegistrar registrar) { registrar.Parameter("worst_severity", &TThis::WorstSeverity) .Default(-1); } }; //////////////////////////////////////////////////////////////////////////////// inline constexpr TStringBuf WorstSeverityStateName = "worst_severity"; class TLogParserProcessFunction : public IProcessFunction { public: void Init(const IRuntimeInitContextPtr& initContext) override; void ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override; private: TMutableStateKeyClient StateClient_; }; //////////////////////////////////////////////////////////////////////////////// } // namespace NYT::NFlow::NExample ``` ```cpp #include "log_parser_process_function.h" #include #include #include #include namespace NYT::NFlow::NExample { //////////////////////////////////////////////////////////////////////////////// void TLogParserProcessFunction::Init(const IRuntimeInitContextPtr& initContext) { initContext->InitClient(StateClient_, WorstSeverityStateName); } void TLogParserProcessFunction::ProcessMessage( const TInputMessageConstPtr& message, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto state = StateClient_.GetState(message->Key); for (const auto& record : ParseLogLine(GetColumnValue(message, "line"))) { state->WorstSeverity = std::max(state->WorstSeverity, SeverityRank(record.Level)); auto outputRecord = New(); outputRecord->Level = record.Level; outputRecord->Text = record.Text; outputRecord->WorstLevelSoFar = SeverityName(state->WorstSeverity); output->AddMessage(context->ConvertToMessage(outputRecord)); } } //////////////////////////////////////////////////////////////////////////////// YT_FLOW_DEFINE_YSON_MESSAGE(TLogRecordMessage); YT_FLOW_DEFINE_PROCESS_FUNCTION(TLogParserProcessFunction); //////////////////////////////////////////////////////////////////////////////// } // namespace NYT::NFlow::NExample ``` ### ParseLogLine ```cpp #include "log_line_parser.h" #include namespace NYT::NFlow::NExample { //////////////////////////////////////////////////////////////////////////////// int SeverityRank(TStringBuf level) { if (level == "info") { return 0; } if (level == "warning") { return 1; } if (level == "error") { return 2; } return -1; } std::string SeverityName(int rank) { switch (rank) { case 0: return "info"; case 1: return "warning"; case 2: return "error"; default: return {}; } } std::vector ParseLogLine(const std::string& line) { std::vector records; for (const auto& entry : StringSplitter(line).Split(';').SkipEmpty()) { TStringBuf token = entry.Token(); auto separator = token.find(':'); if (separator == TStringBuf::npos) { continue; } auto level = token.substr(0, separator); auto text = token.substr(separator + 1); if (text.empty() || SeverityRank(level) < 0) { continue; } records.push_back({.Level = std::string(level), .Text = std::string(text)}); } return records; } //////////////////////////////////////////////////////////////////////////////// } // namespace NYT::NFlow::NExample ``` ## See also - [Getting started (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Process function (C++)](https://ytsaurus.tech/docs/process-functions.md) - [Computation (C++)](https://ytsaurus.tech/docs/computation.md#ttransformorderedsourcecomputation) - [Computation (concept)](https://ytsaurus.tech/concepts/computation.md#ttransformorderedsourcecomputation) - [Working with states (C++)](https://ytsaurus.tech/docs/state.md) # Proto parser in YTsaurus Flow (C++) This example shows `TProtoParsingProcessFunctionBase`, a base class for [process functions](https://ytsaurus.tech/docs/process-functions.md) that handles `Protobuf` message parsing. The [pipeline](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/proto_parser) reads serialized log records from a queue, parses them without calling `ParseFromStringOrThrow` manually, and maintains a state counter for parsed records of each level. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/cpp/proto_parser) ## Pipeline components ### TProtoLogParserFunction `TProtoLogParserFunction` inherits from `TProtoParsingProcessFunctionBase`, where `TLogRecordProto` is a `Protobuf` message with the `level` and `text` fields. The base class reads the input column named by the `data_column` parameter (the `"data"` column by default), parses it into `TLogRecordProto`, and invokes one of the user methods: - `ProcessProto(...)`: after successful parsing, gets the state for the message key, increments the level counter, builds a `TLogRecordMessage`, and emits it to the `records` stream; - `ProcessUnparsed(...)`: if the input column named by the `data_column` parameter is missing (`null`) or `Protobuf` parsing fails. The implementation is empty, so such messages are silently dropped. An empty but present string parses successfully into a `TLogRecordProto` with default values because the message has no required fields. The process function is registered via `YT_FLOW_DEFINE_PROCESS_FUNCTION`. In the `parser` spec, `TProcessFunctionTransformOrderedSourceComputation` hosts it and `processing_function` contains `NYT::NFlow::NExample::TProtoLogParserFunction`. If necessary, the input column name can be set in `processing_function_parameters/data_column`. The `records` output stream is configured with a direct external `TSyncQueueSink`, so no separate sink computation is required. ### The TLevelCountsState state `TProtoLogParserFunction` stores a `TMutableStateKeyClient StateClient_` and initializes it in `DoInit(const IRuntimeInitContextPtr&)` via `initContext->InitClient(StateClient_, "level_counts")`. `ProcessProto` accesses the state through `StateClient_.GetState(message->Key)`. The state contains `record_counts`, the number of parsed records of each level per source partition; the current value is written to the output message as `seen_at_level`. `TProcessFunctionTransformOrderedSourceComputation` materializes output and persists state in the same epoch transaction that advances the source offset. This keeps the counter consistent with queue consumption across restarts. ## Message types `TLogRecordMessage` is a `TYsonMessage` subclass registered via `YT_FLOW_DEFINE_YSON_MESSAGE` with the following fields: - `level`: the record level from `TLogRecordProto`; - `text`: the record text from `TLogRecordProto`; - `seen_at_level`: the number of parsed records of this level in the source partition, including the current record. ## The main function The example library contains process-function and type registration. `main` initializes Flow, registers the `records` stream through `TSimpleSpecBuilder`, and starts `TSimpleRunnerProgram`. ## Source code ### TProtoLogParserFunction ```cpp #pragma once #include #include #include #include namespace NYT::NFlow::NExample { //////////////////////////////////////////////////////////////////////////////// struct TLogRecordMessage : public TYsonMessage { std::string Level; std::string Text; i64 SeenAtLevel = 0; REGISTER_YSON_STRUCT(TLogRecordMessage); static void Register(TRegistrar registrar); }; //////////////////////////////////////////////////////////////////////////////// struct TLevelCountsState : public NYTree::TYsonStruct { THashMap RecordCounts; REGISTER_YSON_STRUCT(TLevelCountsState); static void Register(TRegistrar registrar); }; //////////////////////////////////////////////////////////////////////////////// class TProtoLogParserFunction : public TProtoParsingProcessFunctionBase< TLogRecordProto, TProtoParsingProcessFunctionParameters, /*PropagateHookErrors*/ true> { protected: void DoInit(const IRuntimeInitContextPtr& initContext) override; void ProcessProto( const TInputMessageConstPtr& message, TLogRecordProto&& proto, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override; void ProcessUnparsed( const TInputMessageConstPtr& message, TError error, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) override; private: TMutableStateKeyClient StateClient_; }; //////////////////////////////////////////////////////////////////////////////// } // namespace NYT::NFlow::NExample ``` ```cpp #include "proto_parser_function.h" #include #include #include namespace NYT::NFlow::NExample { //////////////////////////////////////////////////////////////////////////////// void TLogRecordMessage::Register(TRegistrar registrar) { registrar.Parameter("level", &TThis::Level) .Default(); registrar.Parameter("text", &TThis::Text) .Default(); registrar.Parameter("seen_at_level", &TThis::SeenAtLevel) .Default(); } void TLevelCountsState::Register(TRegistrar registrar) { registrar.Parameter("record_counts", &TThis::RecordCounts) .Default(); } //////////////////////////////////////////////////////////////////////////////// void TProtoLogParserFunction::DoInit(const IRuntimeInitContextPtr& initContext) { initContext->InitClient(StateClient_, "level_counts"); } void TProtoLogParserFunction::ProcessProto( const TInputMessageConstPtr& message, TLogRecordProto&& proto, const IOutputCollectorPtr& output, const IRuntimeContextPtr& context) { auto state = StateClient_.GetState(message->Key); auto record = New(); record->Level = proto.level(); record->Text = proto.text(); record->SeenAtLevel = ++state->RecordCounts[proto.level()]; output->AddMessage(context->ConvertToMessage(record)); } void TProtoLogParserFunction::ProcessUnparsed( const TInputMessageConstPtr& /*message*/, TError /*error*/, const IOutputCollectorPtr& /*output*/, const IRuntimeContextPtr& /*context*/) { } //////////////////////////////////////////////////////////////////////////////// YT_FLOW_DEFINE_YSON_MESSAGE(TLogRecordMessage); YT_FLOW_DEFINE_PROCESS_FUNCTION(TProtoLogParserFunction, TProtoParsingProcessFunctionParameters); //////////////////////////////////////////////////////////////////////////////// } // namespace NYT::NFlow::NExample ``` ## See also - [Getting started (C++)](https://ytsaurus.tech/docs/getting-started.md) - [Process functions (C++)](https://ytsaurus.tech/docs/process-functions.md) - [Computation (C++)](https://ytsaurus.tech/docs/computation.md) - [Log parser](https://ytsaurus.tech/docs/en/log_parser.md) # Quick start with YTsaurus Flow (Java) Java and Kotlin computation support in Flow is implemented through the [companion](https://ytsaurus.tech/docs/concepts/glossary.md#companion) mechanism. Java or Kotlin code runs in a separate gRPC process that communicates with a C++ [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker). [Java SDK source code for Flow](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow) [Examples](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java) ## Application architecture {#architecture} Any Flow pipeline consists of three components: - `Runner` — starts the pipeline and sets a new spec version. - `Controller` — manages the pipeline’s operation. - `Worker` — performs the actual data processing. You use Java and Kotlin in the `Runner` and `Worker`. ![](https://ytsaurus.tech/docs/_images/java_flow_cluster.svg) ## Two configuration approaches The Java SDK for Flow (with Kotlin support) provides two approaches to configure a companion: 1. **Manual** (`PipelineContext` + `FlowApplication.run`) — suitable for simple cases where you don’t need dependency injection. 2. **Spring Boot** (auto-config with `@FlowComputation` annotations) — the recommended approach for production services with complex configuration and dependencies. ## Computation and SourceComputation To create a Java [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation), select the appropriate builder that corresponds to the [C++ computation type](https://ytsaurus.tech/docs/concepts/companion.md#computation-types): - `Computation.builder()` — for `TTransformCompanionComputation` and `TSwiftMapCompanionComputation`. - `SourceComputation.builder()` — for `TSwiftOrderedSourceCompanionComputation` and `TTransformOrderedSourceCompanionComputation`. {% list tabs group=lang %} - Java ```java // SourceComputation for reading data from a source var reader = SourceComputation.builder() .setComputationId("reader") .build(); // Computation for data processing var mapper = Computation.builder() .setComputationId("mapper") .setProcessFunction(new WordCountMapper()) .build(); ``` - Kotlin ```kotlin // SourceComputation for reading data from a source val reader = SourceComputation.builder() .setComputationId("reader") .build() // Computation for data processing val mapper = Computation.builder() .setComputationId("mapper") .setProcessFunction(WordCountMapper()) .build() ``` {% endlist %} `Computation.builder()` requires two mandatory parameters: - **Computation id** — used to map requests between the [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker) and the companion. - **Process function** — the function that contains the message-processing logic. ## Process Function There are two types of ProcessFunction: - `RowFunction` — receives [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message) and [timers](https://ytsaurus.tech/docs/concepts/glossary.md#timer) one at a time and provides the `onMessage` and `onTimer` methods. - `BatchFunction` — receives the entire batch of messages and timers; it provides the `onMessages` and `onTimers` methods. For more details, see the [Computation (Java)](https://ytsaurus.tech/docs/en/computation.md) section. ## Entry point {#entry-point} Without Spring Boot, the pipeline entry point is a class whose `main` method calls `FlowApplication.run(args, context)`. With Spring Boot, the application `main` starts Spring, which configures Flow; see [Spring Boot integration](https://ytsaurus.tech/docs/en/spring.md). The process chooses its role from `YT_FLOW_MODE`: - When the variable is unset, the process runs as the **runner**: it enriches the pipeline spec and hands the launch to `flow_server`. - With `YT_FLOW_MODE=Worker`, the process runs as a **companion**: it starts a gRPC server and handles worker requests. The worker sets this variable when it starts the companion. Configure computations and streams in `main`, and add them to `PipelineContext`: {% list tabs group=lang %} - Java ```java import tech.ytsaurus.flow.computation.Computation; import tech.ytsaurus.flow.context.PipelineContext; import tech.ytsaurus.flow.pipeline.FlowApplication; public class PipelineMain { public static void main(String[] args) throws Exception { var mapper = Computation.builder() .setComputationId("mapper") .setProcessFunction(new WordCountMapper()) .build(); var context = new PipelineContext(); context.registerComputation(mapper); context.registerTypedStreams(Word.class); FlowApplication.run(args, context); } } ``` - Kotlin ```kotlin import tech.ytsaurus.flow.computation.Computation import tech.ytsaurus.flow.context.PipelineContext import tech.ytsaurus.flow.pipeline.FlowApplication object PipelineMain { @JvmStatic fun main(args: Array) { val mapper = Computation.builder() .setComputationId("mapper") .setProcessFunction(WordCountMapper()) .build() val context = PipelineContext() context.registerComputation(mapper) context.registerTypedStreams(Word::class.java) FlowApplication.run(args, context) } } ``` {% endlist %} The runner requires `--config` with the pipeline config path and accepts `--flow-bin` with the `flow_server` binary path. If `--flow-bin` is omitted, it uses the path from the `YT_FLOW_BIN` environment variable. ```bash ./run.sh com.example.pipeline.PipelineMain --config pipeline.yson --flow-bin flow_server ``` Use the fully qualified class name: `run.sh` passes its first argument directly to `java`. The runner fills in `spec.streams` from registered message types when the spec does not define those schemas explicitly. Set the `main_class` of the `TJavaCompanionManager` resource in the pipeline spec. If your functions need extra resources, such as a dictionary or cache, create them in `main` and make them thread-safe. ### Spring Boot approach {#spring-boot-approach} When using Spring Boot, the `mapper` computation is registered by applying the `@FlowComputation` annotation directly to the process function class (the `reader` source is passthrough: it is declared in the pipeline spec and is not registered in the Java companion): {% list tabs group=lang %} - Java ```java @FlowComputation(id = "mapper") public class WordCountMapper implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { // message processing } } ``` - Kotlin ```kotlin @FlowComputation(id = "mapper") class WordCountMapper : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { // message processing } } ``` {% endlist %} Typed streams are declared by annotating the message POJO class with `@FlowMessage` and listing the stream identifiers it handles in `streamIds`. The existing JPA `@Entity` annotation supplies the schema. Spring Boot discovers these classes by scanning the application packages and registers their streams automatically: {% list tabs group=lang %} - Java ```java @Entity @FlowMessage(streamIds = {"words"}) public class Word { // fields, constructors, getters, setters... } ``` - Kotlin ```kotlin @Entity @FlowMessage(streamIds = ["words"]) class Word { // fields and constructors... } ``` {% endlist %} The Spring Boot application has one entry point for both roles: {% list tabs group=lang %} - Java ```java @SpringBootApplication public class WordCountApplication { public static void main(String[] args) { new SpringApplicationBuilder(WordCountApplication.class) .run(args); } } ``` - Kotlin ```kotlin @SpringBootApplication open class WordCountApplication { companion object { @JvmStatic fun main(args: Array) { SpringApplicationBuilder(WordCountApplication::class.java).run(*args) } } } ``` {% endlist %} `getStreams()` remains available when you want to register typed [streams](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) with `FlowStreams.typed(...)` instead of annotations. The `@SpringBootApplication` class needs no separate runner entry point. In runner mode, the starter collects the declared streams and launches the pipeline without starting the gRPC or monitoring servers, then exits. ## See also - [Computation (Java)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (Java)](https://ytsaurus.tech/docs/en/state.md) - [Examples](https://ytsaurus.tech/docs/en/examples/wordcount.md) - [Spring Boot registration](https://ytsaurus.tech/docs/en/spring.md) - [Companion](https://ytsaurus.tech/docs/concepts/companion.md) # Computation in YTsaurus Flow (Java) {% note info %} Use this page to learn the details for working with computations in Java and Kotlin. For general concepts, see the [Computation](https://ytsaurus.tech/docs/concepts/computation.md) section. {% endnote %} ## Computation types {#computation-types} In Flow, you have two types of `Computation`: [`Swift`](https://ytsaurus.tech/docs/concepts/glossary.md#swift) and `Transform`. Your choice affects how you ensure [exactly-once guarantees](https://ytsaurus.tech/docs/concepts/guarantees.md) and what transformations you can implement. | Type | Guarantee approach | Use case | |------|--------------------|----------| | `Swift` | The transformation code is deterministic and can be rerun if needed | Stateless transformations | | `Transform` | The result is always stored in YTsaurus, so no determinism requirements apply | [Stateful](https://ytsaurus.tech/docs/concepts/stateful.md) transformations | For more on processing guarantees, see the [Processing guarantees](https://ytsaurus.tech/docs/concepts/guarantees.md) section. For Java and Kotlin pipelines, you choose between `Swift` or `Transform` by setting `computation_class_name` in the static spec: - `NYT::NFlow::NCompanion::TTransformCompanionComputation` — for `Transform`. - `NYT::NFlow::NCompanion::TSwiftMapCompanionComputation` — for `Swift`. - `NYT::NFlow::NCompanion::TTransformOrderedSourceCompanionComputation` — for `Transform` source. - `NYT::NFlow::NCompanion::TSwiftOrderedSourceCompanionComputation` — for `Swift` source. ## Creating a Computation {#computation} In Java and Kotlin code, you create a `Computation` using `Computation.Builder` and register it in `PipelineContext`. {% list tabs group=lang %} - Java ```java var join = Computation.builder() .setComputationId("join") .setProcessFunction(new JoinProcessFunction()) .build(); ``` - Kotlin ```kotlin val join = Computation.builder() .setComputationId("join") .setProcessFunction(JoinProcessFunction()) .build() ``` {% endlist %} In the static spec, you create a `Computation` with the same `id` (in this example, `join`): ```yson "join" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ ... ]; "input_stream_ids" = [...]; "output_stream_ids" = [...]; "parameters" = { ... }; "timers" = {}; }; ``` For more on specs, see the [Spec, DynamicSpec, and Config](https://ytsaurus.tech/docs/concepts/spec.md) section. {% note warning %} You must provide `processFunction` (`null` is not allowed): you don’t register computations without business logic in Java. If you need [passthrough](https://ytsaurus.tech/docs/concepts/glossary.md#passthrough), don’t register the computation in Java at all. Instead, specify the C++ passthrough class in `computation_class_name` in the static spec (see [Passthrough Computation](https://ytsaurus.tech/docs/concepts/computation.md#passthrough)). {% endnote %} ## SourceComputation {#sourcecomputation} `SourceComputation` is the top node in the pipeline graph that reads data from external sources. On the worker side, it corresponds to [TSwiftOrderedSourceComputation](https://ytsaurus.tech/docs/concepts/computation.md#tswiftorderedsourcecomputation) or [TTransformOrderedSourceComputation](https://ytsaurus.tech/docs/concepts/computation.md#ttransformorderedsourcecomputation). In Java, `SourceComputation` extends `Computation`. Like `Computation`, it requires the `processFunction` parameter. In the static spec, for deterministic processing without custom state, you specify `TSwiftOrderedSourceCompanionComputation`. If `SourceComputation` uses internal state or non-deterministic logic, you specify `TTransformOrderedSourceCompanionComputation`: the worker materializes the output and records it together with the state and the source offset. The internal state key in such a computation is the source partition key. ### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `computationId` | Yes | Unique identifier | | `processFunction` | Yes | Function for processing messages | ### Creating a SourceComputation {% list tabs group=lang %} - Java ```java var reader = SourceComputation.builder() .setComputationId("hit_reader") .setProcessFunction(new HitParsingFunction()) .build(); ``` - Kotlin ```kotlin val reader = SourceComputation.builder() .setComputationId("hit_reader") .setProcessFunction(HitParsingFunction()) .build() ``` {% endlist %} For a passthrough Source, don’t use Java. Specify `NYT::NFlow::TSwiftPassthroughOrderedSourceComputation` in `computation_class_name` in the spec and leave the computation unregistered in the Java companion. For more details, see [Passthrough Computation](https://ytsaurus.tech/docs/concepts/computation.md#passthrough). ### Interaction with Worker {#companion-info} When you initialize `Worker`, it requests information about registered `Computation` and `SourceComputation` objects from the Java [companion](https://ytsaurus.tech/docs/concepts/glossary.md#companion). The source computation on the worker side sends input messages to the Java companion, which applies `ProcessFunction` to them and returns the result. ## Process Function You implement the business logic for data processing with a Process Function. Choose one of these two interfaces: [RowFunction](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/function/RowFunction.java) or [BatchFunction](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/function/BatchFunction.java). {% note info %} Choosing between `RowFunction` and `BatchFunction` depends only on your business logic. `RowFunction` doesn’t add extra processing overhead compared to `BatchFunction` because Flow internally transfers data in batches. {% endnote %} ### RowFunction [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/function/RowFunction.java) `RowFunction` receives [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message) and [timers](https://ytsaurus.tech/docs/concepts/glossary.md#timer) one at a time. The interface provides two methods: - `onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx)` — called for each input message. - `onTimer(Timer timer, OutputCollector output, RuntimeContext ctx)` — called when a timer fires. #### Example of a stateless function {% list tabs group=lang %} - Java ```java public class X2Mapper implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { var messageBuilder = ctx.createMessageBuilder("x2_numbers"); //1 Long number = message.get("number", Long.class); //2 messageBuilder.set("number_x2", number * 2); //3 output.addMessage(messageBuilder.finish()); //4 } } ``` - Kotlin ```kotlin class X2Mapper : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val messageBuilder = ctx.createMessageBuilder("x2_numbers") //1 val number: Long? = message.get("number", Long::class.java) //2 messageBuilder.set("number_x2", number!! * 2) //3 output.addMessage(messageBuilder.finish()) //4 } } ``` {% endlist %} Let’s walk through the code line by line: 1. `ctx.createMessageBuilder("x2_numbers")` — you create a `MessageBuilder` for the output [stream](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) with id = `x2_numbers`. The stream with this identifier must be present in the `output_stream_ids` list in the computation’s static [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec). 2. `message.get("number", Long.class)` — you get the value of the `number` field from the incoming message. You must pass the value’s class to the `Message#get` method to unambiguously convert the serialized form to a Java object. 3. `messageBuilder.set("number_x2", number * 2)` — you write the value to the `number_x2` field. This field must be present in the schema of the `x2_numbers` stream in the static spec. 4. `output.addMessage(messageBuilder.finish())` — the `finish` method returns the completed message, which you add to the `OutputCollector`. ### BatchFunction [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/function/BatchFunction.java) `BatchFunction` receives the entire list of messages and timers that come from the [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker). The interface provides two methods: - `onMessages(List messages, OutputCollector output, RuntimeContext ctx)` — called for a batch of messages. - `onTimers(List timers, OutputCollector output, RuntimeContext ctx)` — called for a batch of timers. A batch corresponds to one worker request and may contain messages with different [keys](https://ytsaurus.tech/docs/concepts/glossary.md#key); per-key grouping, if needed, is done in user code (see [Companion](https://ytsaurus.tech/docs/concepts/companion.md#schema)). #### Example of a batch function {% list tabs group=lang %} - Java ```java public class X2BatchMapper implements BatchFunction { @Override public void onMessages(List messages, OutputCollector output, RuntimeContext ctx) { var messageBuilder = ctx.createMessageBuilder("x2_numbers"); //1 for (var message : messages) { //2 Long number = message.get("number", Long.class); //3 messageBuilder.set("number_x2", number * 2); //4 output.addMessage(messageBuilder.finish()); //5 } } } ``` - Kotlin ```kotlin class X2BatchMapper : BatchFunction { override fun onMessages(messages: List, output: OutputCollector, ctx: RuntimeContext) { val messageBuilder = ctx.createMessageBuilder("x2_numbers") //1 for (message in messages) { //2 val number: Long? = message.get("number", Long::class.java) //3 messageBuilder.set("number_x2", number!! * 2) //4 output.addMessage(messageBuilder.finish()) //5 } } } ``` {% endlist %} The key differences from `RowFunction` are: - You create `MessageBuilder` once for the entire batch (line 1). - The `finish()` method returns the completed message and resets `MessageBuilder` to its initial state, so you can reuse it for the next message (line 5). ## Registering in PipelineContext {#pipeline-context} You must register all `Computation` objects and typed streams (created via `FlowStreams.typed`) in `PipelineContext` before you run `GrpcServerExecution`. You don’t need to register untyped streams (created via `FlowStreams.raw`). Flow creates them automatically based on the `streams` block in the static spec. Learn more about [Typed Streams](https://ytsaurus.tech/docs/en/typed-streams.md). {% list tabs group=lang %} - Java ```java var context = new PipelineContext(); // Register Computation objects. Computation join = Computation.builder() .setComputationId("join") .setProcessFunction(new JoinProcessFunction()) .build(); context.registerComputation(join); SourceComputation reader = SourceComputation.builder() .setComputationId("hit_reader") .setProcessFunction(new HitParsingFunction()) .build(); context.registerComputation(reader); // Register typed streams. context.registerStream(FlowStreams.typed("hit", Hit.class)); context.registerStream(FlowStreams.typed("action", Action.class)); context.registerStream(FlowStreams.typed("joined_action", JoinedAction.class)); ``` - Kotlin ```kotlin val context = PipelineContext() // Register Computation objects. val join: Computation = Computation.builder() .setComputationId("join") .setProcessFunction(JoinProcessFunction()) .build() context.registerComputation(join) val reader: SourceComputation = SourceComputation.builder() .setComputationId("hit_reader") .setProcessFunction(HitParsingFunction()) .build() context.registerComputation(reader) // Register typed streams. context.registerStream(FlowStreams.typed("hit", Hit::class.java)) context.registerStream(FlowStreams.typed("action", Action::class.java)) context.registerStream(FlowStreams.typed("joined_action", JoinedAction::class.java)) ``` {% endlist %} {% note warning %} Each Computation and stream must have a unique ID that matches the IDs in the static spec. If you try to register a Computation or stream with an ID that already exists, you’ll get an error and won’t be able to start the companion. {% endnote %} ## RuntimeContext [RuntimeContext source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/context/RuntimeContext.java) [StatefulContext source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/context/StatefulContext.java) `RuntimeContext` gives you access to the computation’s execution context. Key methods: | Method | Description | | --- | --- | | `ctx.createMessageBuilder(streamId)` | Create a `MessageBuilder` for the specified output stream | | `ctx.getComputationParameters()` | Get the computation’s parameters from the spec | | `ctx.getEpochInputEventWatermark()` | Get the current [watermark](https://ytsaurus.tech/docs/concepts/glossary.md#timestamps-and-watermarks) for the [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch) | | `ctx.getProtoStateAccessor(name, message, Class)` | Get the state as a protobuf object linked to the message’s [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) | | `ctx.getYsonStateAccessor(name, message, Class)` | Get the YSON state linked to the message’s [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) | | `ctx.getStateAccessor(name, message, Class, ser, deser)` | Get the state with custom serialization/deserialization | | `ctx.getRawStateAccessor(name, message)` | Get the state as a byte array without interpretation | | `ctx.getNoOpStateAccessor(name, message)` | Get the state that only stores the presence fact (no value) | | `ctx.getExternalStateAccessor(name, message)` | Get the external state linked to the message’s [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) | Learn more about working with states in the [Working with States (Java)](https://ytsaurus.tech/docs/en/state.md) section. ## OutputCollector {#output-collector} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/computation/OutputCollector.java) Use `OutputCollector` to send processing results: | Method | Description | | --- | --- | | `output.addMessage(message)` | Add an output message | | `output.addMessage(message, options)` | Add a message with `AddMessageOptions` controlling distribution and the Swift message ID suffix | | `output.addTimer(triggerTimestamp)` | Add a [timer](https://ytsaurus.tech/docs/concepts/glossary.md#timer) with the specified trigger time (eventTimestamp = 0) | | `output.addTimer(triggerTimestamp, eventTimestamp)` | Add a timer with the specified trigger time and event time | | `output.addTimer(timerStreamId, triggerTimestamp, eventTimestamp)` | Add a timer for a specific timer stream | | `output.setParentIds(parentIds)` | Set the parent ID to track the [lineage](https://ytsaurus.tech/docs/concepts/lineage.md) of messages. Returns a new `OutputCollector` | For a Swift computation, `MessageIdSuffix.payloadHash()` or `MessageIdSuffix.userDefined(value)` makes message identity independent of emission order: ```java output.addMessage( message, AddMessageOptions.builder() .setMessageIdSuffix(MessageIdSuffix.payloadHash()) .build()); ``` Default options use the message sequence number. Payload-hash and user-defined suffixes are supported only by Swift computations. ## Spring Boot When you use Spring Boot, you register the computation with the `@FlowComputation` annotation (or `@FlowSourceComputation` for a source) directly on the `ProcessFunction` class. The annotation is meta-annotated with `@Component`, so the class automatically becomes a Spring bean: {% list tabs group=lang %} - Java ```java @FlowComputation(id = "mapper") public class WordCountMapper implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { // process the message } } ``` - Kotlin ```kotlin @FlowComputation(id = "mapper") class WordCountMapper : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { // process the message } } ``` {% endlist %} Declare streams as Spring beans of type `FlowStream` (or via `ComputationProvider.getStreams()`): {% list tabs group=lang %} - Java ```java @Configuration public class StreamConfiguration { @Bean public FlowStream wordsStream() { return FlowStreams.typed("words", Word.class); } } ``` - Kotlin ```kotlin @Configuration class StreamConfiguration { @Bean fun wordsStream(): FlowStream = FlowStreams.typed("words", Word::class.java) } ``` {% endlist %} `FlowStreams.typed(...)` creates a typed stream that automatically serializes and deserializes messages into Java objects of the specified type. Learn more in the [Typed Streams](https://ytsaurus.tech/docs/en/typed-streams.md) section. Learn more about registration via annotations and `ComputationProvider` in the [Spring Boot Integration](https://ytsaurus.tech/docs/en/spring.md) section. ## CompanionManager Resource Configuration {#companion-manager} To run a companion in Java or Kotlin, you must declare the `CompanionManager` resource in the static spec: ```yson "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TJavaCompanionManager"; "parameters" = { "timeout" = "10s"; "jdk_bin_path" = "/app/ytflow/jdk/bin/java"; "main_class" = "tech.ytsaurus.flow.examples.waitclickjoin.PipelineMain"; "classpath" = "/app/ytflow/lib/*"; }; "dependencies" = {}; }; ``` The `resource_class_name` parameter specifies the resource class that will run the companion. For a Java or Kotlin companion, `resource_class_name` must always be `NYT::NFlow::NCompanion::TJavaCompanionManager` (it supports both languages via the JVM). Learn more about the spec in the [Spec, DynamicSpec and Config](https://ytsaurus.tech/docs/concepts/spec.md) section. ## See also - [Computation (concept)](https://ytsaurus.tech/docs/concepts/computation.md) - [Working with States (Java)](https://ytsaurus.tech/docs/en/state.md) - [Quick Start (Java)](https://ytsaurus.tech/docs/en/getting-started.md) - [Companion](https://ytsaurus.tech/docs/concepts/companion.md) # The distribute flag in YTsaurus Flow (Java) The `distribute` flag is a per-message flag that you set when you add an output message to [SourceComputation](https://ytsaurus.tech/docs/en/computation.md#sourcecomputation). It controls whether the message is published further along the processing graph. The `distribute` flag ensures: - Correct watermark evaluation: messages with `distribute=false` are still accounted for by the watermark generator (unlike filtering in `onMessage`, which can break the watermark). - Assignment of deterministic identifiers to messages. {% note warning %} To filter a message in `SourceComputation`, don’t skip it in `onMessage`. Instead, emit it with `distribute=false`. This way, the message isn’t published further, but it remains accounted for in watermark evaluation. {% endnote %} ## When to use distribute=false Use the `distribute=false` flag when: - You need to filter some output messages at the source computation stage. - Correct watermark evaluation is important. If you don’t set the flag, it defaults to `true`, and the message is published further. ## Usage {#usage} Move the filtering logic to the processing function: instead of a separate filtering step, emit the message with the required `AddMessageOptions`. {% list tabs %} - Java ```java public class HitParsingFunction implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { var hit = ProtoUtils.parseBytes(message.get("data", byte[].class), THit.class); // Duplicates are emitted but not published further. var distribute = !hit.getHitPayload().equals("duplicate_payload"); output.addMessage( ctx.createMessageBuilder("hit") .set("hit_id", hit.getHitId()) .set("hit_payload", hit.getHitPayload()) .finish(), AddMessageOptions.builder().setDistribute(distribute).build() ); } } ``` - Kotlin ```kotlin class HitParsingFunction : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val hit = ProtoUtils.parseBytes(message.get("data", ByteArray::class.java), THit::class.java) // Duplicates are emitted but not published further. val distribute = hit.hitPayload != "duplicate_payload" output.addMessage( ctx.createMessageBuilder("hit") .set("hit_id", hit.hitId) .set("hit_payload", hit.hitPayload) .finish(), AddMessageOptions.builder().setDistribute(distribute).build() ) } } ``` {% endlist %} ## Registering a source computation {#registration} You create a source computation using `SourceComputation.builder()`. You no longer need a separate filtering parameter — the decision about publishing is made in the processing function. ```java var hitReader = SourceComputation.builder() .setComputationId("hit_reader") .setProcessFunction(new HitParsingFunction()) .build(); ``` ## See also - [Computation (Java)](https://ytsaurus.tech/docs/en/computation.md) - [Watermarks](https://ytsaurus.tech/docs/concepts/watermarks.md) - [The distribute flag (Python)](https://ytsaurus.tech/docs/python/distribute.md) # Typed Streams in YTsaurus Flow (Java) Use the Java SDK Flow (Java and Kotlin) to work with typed streams: declare them with the `@FlowMessage` annotation (the recommended way) or register them imperatively via `FlowStreams.typed`. Either way, messages are serialized and deserialized into POJO objects automatically, which simplifies working with data in [ProcessFunction](https://ytsaurus.tech/docs/en/computation.md#rowfunction). Both registration modes are described in [Registering streams](https://ytsaurus.tech/docs/en/#registering-streams). The binary format for untyped (`FlowStreams.raw`) and typed (`FlowStreams.typed`) streams is fully identical. It matches the binary format of `UnversionedRow` in varint encoding, which Flow uses to transfer data between cluster nodes. ## Entity You describe POJO classes for streams using the JPA annotations `@Entity` and `@Column`. Make sure the class has a default constructor. {% list tabs group=lang %} - Java ```java @Entity public class Hit { @Column(name = "hit_id") private String hitId; @Column(name = "hit_time", columnDefinition = "uint64") private Long hitTime; @Column(name = "hit_payload") private String hitPayload; // constructors, getters, setters... } ``` - Kotlin ```kotlin @Entity class Hit { @Column(name = "hit_id") var hitId: String? = null @Column(name = "hit_time", columnDefinition = "uint64") var hitTime: Long? = null @Column(name = "hit_payload") var hitPayload: String? = null // constructors, getters, setters... } ``` {% endlist %} {% note warning %} You must ensure that the order of fields in the typed data model matches the order of columns in the stream definition in the static spec. If you break the field order, it can lead to hard-to-diagnose errors. If the stream is not described in the static spec, the typed model sets the column order. Keep that order compatible with the output queues and tables. {% endnote %} If you register streams via `FlowStreams.raw`, the messages are available in an untyped form. To get field values from an untyped message, use `message.get("field_name", Type.class)`. For more details on working with typed and untyped messages, see [Process Function](https://ytsaurus.tech/docs/en/computation.md#rowfunction). ## Column The `@Column` annotation is optional. If a class field doesn’t have this annotation, the field name is used as the column name. With `@Column`, you can set the column name via the `name` attribute and specify the column type via the `columnDefinition` attribute. The `columnDefinition` attribute accepts a string with a Type V3 name. See the [full list of types](https://ytsaurus.tech/user-guide/storage/data-types.md#schema). ### Registering streams You must register all typed streams before the companion server starts. #### Via the `@FlowMessage` annotation (recommended) Mark the message POJO class with the `@FlowMessage` annotation listing the stream identifiers (`streamIds`) it serves. The annotation is used together with `@Entity`, which the schema is derived from, and doesn’t replace it. A single POJO can serve several streams with the same schema; in that case, list several identifiers in `streamIds`. {% list tabs group=lang %} - Java ```java @Entity @FlowMessage(streamIds = {"hit"}) public class Hit { // fields... } ``` - Kotlin ```kotlin @Entity @FlowMessage(streamIds = ["hit"]) class Hit { // fields... } ``` {% endlist %} In Spring Boot applications, such classes are found by scanning the application packages and are registered automatically. By default, the Spring Boot autoconfiguration packages are scanned: the package of the class annotated with `@SpringBootApplication` and its nested packages. You can specify additional packages with the `flow.entity-scan-packages` property. Without Spring Boot, pass the classes directly to `PipelineContext.registerTypedStreams`: {% list tabs group=lang %} - Java ```java context.registerTypedStreams(Hit.class, Action.class, JoinedAction.class); ``` - Kotlin ```kotlin context.registerTypedStreams(Hit::class.java, Action::class.java, JoinedAction::class.java) ``` {% endlist %} #### Via `FlowStreams.typed` (imperative) You can also create and register typed streams manually via the `FlowStreams.typed` factory method, which takes two arguments: `streamId` and the message class. In Spring Boot applications, you can declare streams via `ComputationProvider` (the `getStreams()` method) or as separate `FlowStream` beans. {% list tabs group=lang %} - Java ```java context.registerStream(FlowStreams.typed("hit", Hit.class)); context.registerStream(FlowStreams.typed("action", Action.class)); context.registerStream(FlowStreams.typed("joined_action", JoinedAction.class)); ``` - Kotlin ```kotlin context.registerStream(FlowStreams.typed("hit", Hit::class.java)) context.registerStream(FlowStreams.typed("action", Action::class.java)) context.registerStream(FlowStreams.typed("joined_action", JoinedAction::class.java)) ``` {% endlist %} # Working with states in YTsaurus Flow (Java) {% note info %} This page describes details for Java and Kotlin when working with states. General concepts are described in the [Stateful computations](https://ytsaurus.tech/docs/concepts/stateful.md) section. {% endnote %} The Java SDK Flow (Java and Kotlin) provides several types of state accessors for working with state. The most commonly used are: - **YsonStateAccessor** — for YSON states stored in Flow’s internal tables. - **ExternalStateAccessor** — for external states stored in separate dynamic tables in YTsaurus. You also have access to **ProtoStateAccessor**, **DefaultStateAccessor**, **RawStateAccessor**, and **NoOpStateAccessor**. For more details on all types, see the [Internal State](https://ytsaurus.tech/docs/en/internal-state.md) section. ## YsonStateAccessor {#yson-state} `YsonStateAccessor` provides typed access to a YSON state tied to a message key. You get the accessor via `RuntimeContext`: {% list tabs group=lang %} - Java ```java StateAccessor stateAccessor = ctx.getYsonStateAccessor("state-name", message, StateClass.class); ``` - Kotlin ```kotlin val stateAccessor: StateAccessor = ctx.getYsonStateAccessor("state-name", message, StateClass::class.java) ``` {% endlist %} Parameters: - `"state-name"` — the state name; it must match the name registered in the [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec) of the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) (`parameters/internal_states`). - `message` — the current message from which you extract the [grouping key](https://ytsaurus.tech/docs/concepts/glossary.md#key). - `StateClass.class` — the Java class for serializing or deserializing the state. ### StateAccessor methods | Method | Description | | --- | --- | | `get()` | Get the current state value (`T`, `null` when there is no value) | | `set(T value)` | Set a new state value | | `getOrDefault(T defaultValue)` | Get the value or return the default value | | `clear()` | Delete the state for the current key | ### Example: WordCountMapper {% list tabs group=lang %} - Java ```java @Component public class WordCountMapper implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { Word input = message.getPayload(); StateAccessor stateAccessor = ctx.getYsonStateAccessor("word-state", message, WordCountState.class); var state = stateAccessor.getOrDefault(new WordCountState(input.getWord(), 0)); state.setCount(state.getCount() + 1); stateAccessor.set(state); } } ``` - Kotlin ```kotlin @Component class WordCountMapper : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val input: Word = message.getPayload() val stateAccessor = ctx.getYsonStateAccessor("word-state", message, WordCountState::class.java) val state = stateAccessor.getOrDefault(WordCountState(input.word, 0)) state.count = state.count + 1 stateAccessor.set(state) } } ``` {% endlist %} In this example: - You extract a `Word` object with the `word` field from the message. - You get an accessor for the `"word-state"` state, which is tied to the current message’s key. - If the state for this key doesn’t exist, you create a new `WordCountState` object with an initial counter value of 0. - You increase the counter value and update the state. Annotate the state class with `@YTreeObject` to serialize it to YSON: {% list tabs group=lang %} - Java ```java @YTreeObject public class WordCountState { private String word; private long count; public WordCountState() {} public WordCountState(String word, long count) { this.word = word; this.count = count; } // getters and setters public String getWord() { return word; } public void setWord(String word) { this.word = word; } public long getCount() { return count; } public void setCount(long count) { this.count = count; } } ``` - Kotlin ```kotlin @YTreeObject class WordCountState { var word: String = "" var count: Long = 0 constructor() constructor(word: String, count: Long) { this.word = word this.count = count } } ``` {% endlist %} ## ExternalStateAccessor {#external-state} `ExternalStateAccessor` provides access to an external state stored in a separate dynamic table in YTsaurus. You describe the external state with an `ExternalStateDescriptor` constant, which you create via `StateDescriptors.external(...)`: {% list tabs group=lang %} - Java ```java ExternalStateAccessor externalStateAccessor = ctx.getExternalStateAccessor("state-name", message); ``` - Kotlin ```kotlin val externalStateAccessor = ctx.getExternalStateAccessor("state-name", message) ``` {% endlist %} ### ExternalStateAccessor methods | Method | Description | | --- | --- | | `get()` | Get the current state value (`Payload`, `null` when there is no value) | | `getOrDefault()` | Get the value or an empty `Payload` | | `set(Payload value)` | Set a new state value | | `clear()` | Delete the state for the current key | `Payload` is an untyped container with field access by name. Use `PayloadBuilder` to modify it. ### Example: EventReducer {% list tabs group=lang %} - Java ```java public class EventReducer implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { ExternalStateAccessor externalStateAccessor = ctx.getExternalStateAccessor("shuffle-state", message); Payload state = externalStateAccessor.getOrDefault(); PayloadBuilder stateBuilder = state.toBuilder(); if (state.get("count", Long.class) == null) { stateBuilder.set("count", 1L); } else { stateBuilder.set("count", state.get("count", Long.class) + 1); } externalStateAccessor.set(stateBuilder.finish()); } } ``` - Kotlin ```kotlin class EventReducer : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val externalStateAccessor = ctx.getExternalStateAccessor("shuffle-state", message) val state = externalStateAccessor.getOrDefault() val stateBuilder = state.toBuilder() if (state.get("count", Long::class.java) == null) { stateBuilder.set("count", 1L) } else { stateBuilder.set("count", state.get("count", Long::class.java) + 1) } externalStateAccessor.set(stateBuilder.finish()) } } ``` {% endlist %} In this example: - You declare the `SHUFFLE_STATE` descriptor for the external state `"/shuffle-state"`. - You get the accessor for the message key via `ctx.getState(SHUFFLE_STATE, message)`. - You extract the current state value as `Payload`. - You use `PayloadBuilder` to create an updated version of the state with an increased counter. - You save the updated state back. ## State in timers {#state-in-timers} When you process timers, the state is available via the `timer` object, which contains the [grouping key](https://ytsaurus.tech/docs/concepts/glossary.md#key): {% list tabs group=lang %} - Java ```java @Override public void onTimer(Timer timer, OutputCollector output, RuntimeContext ctx) { ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("join-state", timer); Payload joinState = Objects.requireNonNull(stateAccessor.get()); // process the state and generate output messages var messageBuilder = ctx.createMessageBuilder("output_stream"); messageBuilder.set("hit_id", joinState.get("hit_id", String.class)); // ... fill in the remaining fields ... output.addMessage(messageBuilder.finish()); // clear the state after processing stateAccessor.clear(); } ``` - Kotlin ```kotlin override fun onTimer(timer: Timer, output: OutputCollector, ctx: RuntimeContext) { val stateAccessor = ctx.getExternalStateAccessor("join-state", timer) val joinState = stateAccessor.get()!! // process the state and generate output messages val messageBuilder = ctx.createMessageBuilder("output_stream") messageBuilder.set("hit_id", joinState.get("hit_id", String::class.java)) // ... fill in the remaining fields ... output.addMessage(messageBuilder.finish()) // clear the state after processing stateAccessor.clear() } ``` {% endlist %} The `clear()` method removes the state for the given key. Make sure to call it after closing the window or finalizing the processing to avoid accumulating outdated data. ## Binding to group_by_schema {#group-by} In `TTransformCompanionComputation`, the key used to access state is defined by `group_by_schema` in the computation spec. The state accessor extracts it from the message or timer. `TTransformOrderedSourceCompanionComputation` does not support `group_by_schema`. Its internal state uses the source partition key, so messages from one partition share that state. See [Computation (Java)](https://ytsaurus.tech/docs/en/computation.md#sourcecomputation) for choosing a SourceComputation class. For more details on `group_by_schema` and its impact on state handling, see the [Stateful computations](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## State configuration in the spec {#spec-config} ### YSON state (internal_states) You register YSON states in the computation spec via `parameters/internal_states`: ```yson "computations" = { "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "parameters" = { "internal_states" = ["word-state"]; }; }; }; ``` ### External state You register the external state in the `external_state_managers` section of `Computation`. The name must start with `/` and match the value passed to `StateDescriptors.external(...)`: ```yson "computations" = { "reducer" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "external_state_managers" = { "/shuffle-state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state/table"; }; }; }; "parameters" = {}; }; }; ``` The `external_state_manager_class_name` field specifies the registered external state manager class. For a typical scenario, this is `"NYT::NFlow::TSimpleExternalStateManager"`. For more details on available managers, see the [C++ documentation](https://ytsaurus.tech/docs/cpp/state.md#external-state). You need to create the state table in advance. ## See also - [Stateful computations](https://ytsaurus.tech/docs/concepts/stateful.md) - [Internal State (Java)](https://ytsaurus.tech/docs/en/internal-state.md) - [External State (Java)](https://ytsaurus.tech/docs/en/external-state.md) - [Computation (Java)](https://ytsaurus.tech/docs/en/computation.md) - [Getting started (Java)](https://ytsaurus.tech/docs/en/getting-started.md) # StateAccessor in YTsaurus Flow (Java) StateAccessor is an interface for reading, modifying, and deleting state values. For general information about stateful processing, see the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## How it works {#how-it-works} The [state](https://ytsaurus.tech/docs/concepts/glossary.md#state) in Flow is stored in [sorted dynamic tables](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md). If you use [external state](https://ytsaurus.tech/docs/en/external-state.md), you create this table. If you use [internal state](https://ytsaurus.tech/docs/en/internal-state.md), Flow creates and manages these tables automatically. For simplicity, the following description focuses on an example with external state. You can think of each row in the state table as having two parts: key columns and value columns: ![](https://ytsaurus.tech/docs/_images/state_line_example.svg) For `TTransformCompanionComputation`, the key columns in the state table match the `group_by_schema` of the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation). For the internal state of `TTransformOrderedSourceCompanionComputation`, the source partition key is used instead; this SourceComputation does not support `group_by_schema`. The value columns are available for reading and modifying through `StateAccessor`. The format in which you can read and modify these values in Java code depends on the `StateAccessor` implementation. ## Reading and writing data {#reading-and-writing-data} The [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker) handles direct operations on the table, including reading, writing, and deleting data. When the worker receives the next batch of [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message), it loads the state values for all [keys](https://ytsaurus.tech/docs/concepts/glossary.md#key) in the batch and sends them to the [companion](https://ytsaurus.tech/docs/concepts/companion.md) along with the messages and [timers](https://ytsaurus.tech/docs/concepts/glossary.md#timer). For more details, see the [interaction schema](https://ytsaurus.tech/docs/concepts/companion.md#schema). You write new values to the state table as a transaction within an [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch). ## StateAccessor interface {#state-accessor-interface} {% list tabs group=lang %} - Java ```java public interface StateAccessor { /** Get the state value. */ @Nullable T get(); /** Get the state value or a default value. */ default T getOrDefault(T defaultValue); /** Set the state value. */ void set(T value); /** Clear or delete the state for the key. */ void clear(); /** Get the state class. */ Class getStateClass(); /** Get a read-only view of the accessor. */ default StateAccessor readOnly(); } ``` - Kotlin ```kotlin interface StateAccessor { /** Get the state value. */ fun get(): T? /** Get the state value or a default value. */ fun getOrDefault(defaultValue: T): T /** Set the state value. */ fun set(value: T) /** Clear or delete the state for the key. */ fun clear() /** Get the state class. */ fun getStateClass(): Class /** Get a read-only view of the accessor. */ fun readOnly(): StateAccessor } ``` {% endlist %} The value of an internal state returned by `get()` and `getOrDefault()` is live: the changes made to it are written without a `set()` call, and `readOnly()` returns an untracked view — see [Changing the value in place](https://ytsaurus.tech/docs/en/internal-state.md#in-place). A default returned by `getOrDefault()` is not written to YTsaurus: it becomes the state value only if the computation changes it. # Internal State in YTsaurus Flow (Java) Internal State is a mechanism for working with internal state stored in Flow’s internal tables. Unlike [External State](https://ytsaurus.tech/docs/en/external-state.md), you don’t need to create the tables yourself — Flow manages them automatically. Learn more about `StateAccessor` and working with state: [State Accessor](https://ytsaurus.tech/docs/en/state-accessor.md). General information about stateful processing is in the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## Overview The Java SDK Flow (Java and Kotlin) provides several types of state accessors for working with Internal State; they differ by serialization format: | Accessor | Format | Description | |----------|--------|-------------| | [YsonStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#yson-state-accessor) | YSON | Serialization via `@YTreeObject` annotations | | [ProtoStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#proto-state-accessor) | Protobuf | Serialization via Protobuf | | [DefaultStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#default-state-accessor) | Custom | User-defined serializer/deserializer | | [RawStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#raw-state-accessor) | `byte[]` | No serialization (raw bytes) | | [NoOpStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#noop-state-accessor) | — | Stores only the fact that the state exists | All accessors implement the common `StateAccessor` interface. ## StateAccessor interface {#state-accessor} {% list tabs group=lang %} - Java ```java public interface StateAccessor { /** Get the state value. */ @Nullable T get(); /** Get the state value or a default value. */ default T getOrDefault(T defaultValue); /** Set the state value. */ void set(T value); /** Clear/delete the state for the key. */ void clear(); /** Get the state class. */ Class getStateClass(); } ``` - Kotlin ```kotlin interface StateAccessor { /** Get the state value. */ fun get(): T? /** Get the state value or a default value. */ fun getOrDefault(defaultValue: T): T /** Set the state value. */ fun set(value: T) /** Clear/delete the state for the key. */ fun clear() /** Get the state class. */ fun getStateClass(): Class } ``` {% endlist %} ### Changing the value in place {#in-place} The value returned by `get()` and `getOrDefault()` is live: it is decoded once per key and batch, every accessor for that key returns the same object, and the changes made to it are written to the state at the end of the batch without a `set()` call. Nothing is written when the value did not change. The default from `getOrDefault()` is attached to the key but not written: it can be changed right away, and it becomes the state value only once it is changed — an untouched default creates no row in the state table: {% list tabs group=lang %} - Java ```java ctx.getState(COUNTER, message).getOrDefault(new CounterState()).count += 1; ``` - Kotlin ```kotlin ctx.getState(COUNTER, message).getOrDefault(CounterState()).count += 1 ``` {% endlist %} `set()` still replaces the whole value, and `clear()` removes the state. Protobuf messages are immutable, so their new value goes through `set()`. To detect the changes, a value that was read is re-encoded once at the end of the batch — even when the computation only inspects the state. `readOnly()` takes that work away: the accessor returns the same value but does not track it, so the state is neither re-encoded at the end of the batch nor sent to the worker. Use it wherever a state is only read. Its `getOrDefault()` does not create the state, and `set()` and `clear()` throw: {% list tabs group=lang %} - Java ```java long count = ctx.getState(COUNTER, message).readOnly().getOrDefault().count; ``` - Kotlin ```kotlin val count = ctx.getState(COUNTER, message).readOnly().getOrDefault().count ``` {% endlist %} The same intent can be declared once on the descriptor: `InternalStateDescriptor.readOnly()` returns a descriptor of the same state whose accessors are read-only, so no access site needs a `readOnly()` call of its own. Declare it once as a constant next to the original: {% list tabs group=lang %} - Java ```java private static final InternalStateDescriptor COUNTER_READ_ONLY = COUNTER.readOnly(); long count = ctx.getState(COUNTER_READ_ONLY, message).getOrDefault().count; ``` - Kotlin ```kotlin private val COUNTER_READ_ONLY: InternalStateDescriptor = COUNTER.readOnly() val count = ctx.getState(COUNTER_READ_ONLY, message).getOrDefault().count ``` {% endlist %} An internal state lives within a single computation: the worker serves only the names listed in the `internal_states` of its parameters. A read-only descriptor therefore reads the state of the computation it is used in: it is not a way to read a state that another computation writes — that is what [External State](https://ytsaurus.tech/docs/en/external-state.md) joiners are for. Read-only is a discipline of access, not a property of the state: the tracking lives on the state itself. If the value for the same key was already read through a writable accessor in this batch, the state is already tracked, and a change made in place afterwards reaches the worker whichever accessor handed the value out. The descriptor guarantees "this accessor does not write", not "this state is not tracked". ## YsonStateAccessor {#yson-state-accessor} `YsonStateAccessor` uses YSON serialization. The state class must be annotated with `@YTreeObject`. ### Getting the accessor {% list tabs group=lang %} - Java ```java // For a message YsonStateAccessor stateAccessor = ctx.getYsonStateAccessor("state-name", message, MyState.class); // For a timer YsonStateAccessor stateAccessor = ctx.getYsonStateAccessor("state-name", timer, MyState.class); ``` - Kotlin ```kotlin // For a message val stateAccessor: YsonStateAccessor = ctx.getYsonStateAccessor("state-name", message, MyState::class.java) // For a timer val stateAccessor: YsonStateAccessor = ctx.getYsonStateAccessor("state-name", timer, MyState::class.java) ``` {% endlist %} ### Example state class {% list tabs group=lang %} - Java ```java import ru.yandex.inside.yt.kosher.impl.ytree.object.annotation.YTreeObject; import ru.yandex.inside.yt.kosher.impl.ytree.object.annotation.YTreeField; @YTreeObject public class CounterState { @YTreeField(key = "count") private long count; @YTreeField(key = "last_update") private long lastUpdate; // A default constructor is required public CounterState() {} // Getters and setters... public long getCount() { return count; } public void setCount(long count) { this.count = count; } public long getLastUpdate() { return lastUpdate; } public void setLastUpdate(long lastUpdate) { this.lastUpdate = lastUpdate; } } ``` - Kotlin ```kotlin import ru.yandex.inside.yt.kosher.impl.ytree.object.annotation.YTreeObject import ru.yandex.inside.yt.kosher.impl.ytree.object.annotation.YTreeField @YTreeObject class CounterState { @YTreeField(key = "count") var count: Long = 0 @YTreeField(key = "last_update") var lastUpdate: Long = 0 // A default constructor is required constructor() } ``` {% endlist %} ### Example usage {% list tabs group=lang %} - Java ```java public class CounterFunction implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { YsonStateAccessor stateAccessor = ctx.getYsonStateAccessor("counter", message, CounterState.class); // Get the current state or create a new one CounterState state = stateAccessor.getOrDefault(new CounterState()); // Modify the state state.setCount(state.getCount() + 1); state.setLastUpdate(message.getEventTimestamp()); } } ``` - Kotlin ```kotlin class CounterFunction : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val stateAccessor: YsonStateAccessor = ctx.getYsonStateAccessor("counter", message, CounterState::class.java) // Get the current state or create a new one val state: CounterState = stateAccessor.getOrDefault(CounterState()) // Modify the state state.count = state.count + 1 state.lastUpdate = message.getEventTimestamp() } } ``` {% endlist %} ## ProtoStateAccessor {#proto-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/context/ProtoStateAccessor.java) `ProtoStateAccessor` uses Protobuf serialization. Your state class must inherit from `com.google.protobuf.MessageLite`. ### Get the accessor {% list tabs group=lang %} - Java ```java // For a message ProtoStateAccessor stateAccessor = ctx.getProtoStateAccessor("state-name", message, MyProtoState.class); // For a timer ProtoStateAccessor stateAccessor = ctx.getProtoStateAccessor("state-name", timer, MyProtoState.class); ``` - Kotlin ```kotlin // For a message val stateAccessor: ProtoStateAccessor = ctx.getProtoStateAccessor("state-name", message, MyProtoState::class.java) // For a timer val stateAccessor: ProtoStateAccessor = ctx.getProtoStateAccessor("state-name", timer, MyProtoState::class.java) ``` {% endlist %} ### The getOrDefault method `ProtoStateAccessor` provides a parameterless `getOrDefault()` method that returns a default Protobuf object: {% list tabs group=lang %} - Java ```java ProtoStateAccessor stateAccessor = ctx.getProtoStateAccessor("state-name", message, MyProtoState.class); // Get the state or a default Protobuf object MyProtoState state = stateAccessor.getOrDefault(); ``` - Kotlin ```kotlin val stateAccessor: ProtoStateAccessor = ctx.getProtoStateAccessor("state-name", message, MyProtoState::class.java) // Get the state or a default Protobuf object val state: MyProtoState = stateAccessor.getOrDefault() ``` {% endlist %} ### Usage example {% list tabs group=lang %} - Java ```java public class ProtoCounterFunction implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { ProtoStateAccessor stateAccessor = ctx.getProtoStateAccessor("counter", message, CounterProto.class); CounterProto state = stateAccessor.getOrDefault(); // Modify using the Protobuf builder CounterProto updatedState = state.toBuilder() .setCount(state.getCount() + 1) .setLastUpdate(message.getEventTimestamp()) .build(); stateAccessor.set(updatedState); } } ``` - Kotlin ```kotlin class ProtoCounterFunction : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val stateAccessor: ProtoStateAccessor = ctx.getProtoStateAccessor("counter", message, CounterProto::class.java) val state: CounterProto = stateAccessor.getOrDefault() // Modify using the Protobuf builder val updatedState: CounterProto = state.toBuilder() .setCount(state.getCount() + 1) .setLastUpdate(message.getEventTimestamp()) .build() stateAccessor.set(updatedState) } } ``` {% endlist %} ## DefaultStateAccessor {#default-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/context/DefaultStateAccessor.java) `DefaultStateAccessor` lets you use custom serialization and deserialization functions. ### Get the accessor {% list tabs group=lang %} - Java ```java // For a message DefaultStateAccessor stateAccessor = ctx.getStateAccessor( "state-name", message, MyState.class, state -> serialize(state), // Function bytes -> deserialize(bytes) // Function ); // For a timer DefaultStateAccessor stateAccessor = ctx.getStateAccessor( "state-name", timer, MyState.class, state -> serialize(state), bytes -> deserialize(bytes) ); ``` - Kotlin ```kotlin // For a message val stateAccessor: DefaultStateAccessor = ctx.getStateAccessor( "state-name", message, MyState::class.java, { state -> serialize(state) }, // (MyState) -> ByteArray { bytes -> deserialize(bytes) } // (ByteArray) -> MyState ) // For a timer val stateAccessor: DefaultStateAccessor = ctx.getStateAccessor( "state-name", timer, MyState::class.java, { state -> serialize(state) }, { bytes -> deserialize(bytes) } ) ``` {% endlist %} ### Example with Jackson {% list tabs group=lang %} - Java ```java public class JsonCounterFunction implements RowFunction { private static final ObjectMapper mapper = new ObjectMapper(); @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { DefaultStateAccessor stateAccessor = ctx.getStateAccessor( "counter", message, CounterState.class, state -> { try { return mapper.writeValueAsBytes(state); } catch (Exception e) { throw new RuntimeException(e); } }, bytes -> { try { return mapper.readValue(bytes, CounterState.class); } catch (Exception e) { throw new RuntimeException(e); } } ); CounterState state = stateAccessor.getOrDefault(new CounterState()); state.setCount(state.getCount() + 1); } } ``` - Kotlin ```kotlin class JsonCounterFunction : RowFunction { companion object { private val mapper = ObjectMapper() } override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val stateAccessor: DefaultStateAccessor = ctx.getStateAccessor( "counter", message, CounterState::class.java, { state -> try { mapper.writeValueAsBytes(state) } catch (e: Exception) { throw RuntimeException(e) } }, { bytes -> try { mapper.readValue(bytes, CounterState::class.java) } catch (e: Exception) { throw RuntimeException(e) } } ) val state: CounterState = stateAccessor.getOrDefault(CounterState()) state.count = state.count + 1 } } ``` {% endlist %} ## RawStateAccessor {#raw-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/context/RawStateAccessor.java) Use `RawStateAccessor` to work with raw bytes without serialization or deserialization. ### Get the accessor {% list tabs group=lang %} - Java ```java RawStateAccessor stateAccessor = ctx.getRawStateAccessor("state-name", message); ``` - Kotlin ```kotlin val stateAccessor: RawStateAccessor = ctx.getRawStateAccessor("state-name", message) ``` {% endlist %} ### Example usage {% list tabs group=lang %} - Java ```java RawStateAccessor stateAccessor = ctx.getRawStateAccessor("raw-state", message); byte[] data = stateAccessor.get(); if (data != null) { // Process raw data... } // Write raw data stateAccessor.set(new byte[]{0x01, 0x02, 0x03}); // Clear the data stateAccessor.clear(); ``` - Kotlin ```kotlin val stateAccessor: RawStateAccessor = ctx.getRawStateAccessor("raw-state", message) val data: ByteArray? = stateAccessor.get() if (data != null) { // Process raw data... } // Write raw data stateAccessor.set(byteArrayOf(0x01, 0x02, 0x03)) // Clear the data stateAccessor.clear() ``` {% endlist %} ## NoOpStateAccessor {#noop-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-core/src/main/java/tech/ytsaurus/flow/context/NoOpStateAccessor.java) `NoOpStateAccessor` stores only the fact that a state exists for a [key](https://ytsaurus.tech/docs/concepts/glossary.md#key), with no payload. Use it to track already processed keys (for deduplication). ### Get the accessor {% list tabs group=lang %} - Java ```java NoOpStateAccessor stateAccessor = ctx.getNoOpStateAccessor("seen-keys", message); ``` - Kotlin ```kotlin val stateAccessor: NoOpStateAccessor = ctx.getNoOpStateAccessor("seen-keys", message) ``` {% endlist %} ### Example usage {% list tabs group=lang %} - Java ```java public class DeduplicationFunction implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { NoOpStateAccessor stateAccessor = ctx.getNoOpStateAccessor("seen-keys", message); // Check if the key was already processed if (stateAccessor.get() != null) { // The key is already processed, skip it return; } // Mark the key as processed stateAccessor.touch(); // Process the message... output.addMessage(new Message("output", message.getPayload())); } } ``` - Kotlin ```kotlin class DeduplicationFunction : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val stateAccessor: NoOpStateAccessor = ctx.getNoOpStateAccessor("seen-keys", message) // Check if the key was already processed if (stateAccessor.get() != null) { // The key is already processed, skip it return } // Mark the key as processed stateAccessor.touch() // Process the message... output.addMessage(Message("output", message.getPayload())) } } ``` {% endlist %} ## Configuration in the static spec {#static-spec} Internal State doesn’t require you to create external tables. States are automatically stored in Flow’s internal tables (`states` and `partition_states`). You must declare the names of internal states in the `internal_states` section of the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) parameters in the static spec: ```yson "computations" = { "counter" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"}; {"name" = "key"; "type" = "string"}; ]; "input_stream_ids" = ["input"]; "output_stream_ids" = ["output"]; "parameters" = { "internal_states" = ["counter"]; }; }; }; ``` The state name in your code (the first argument of `ctx.getYsonStateAccessor(...)`, `ctx.getProtoStateAccessor(...)`, and so on) must match the name declared in `internal_states`. # External State in YTsaurus Flow (Java) External State is a mechanism for working with external state stored in an external dynamic table in YTsaurus. You create the table to store the state yourself on the same cluster where the pipeline is deployed. For general information about stateful processing, see the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## Overview In the Java SDK Flow (Java and Kotlin), External State is represented by the `ExternalStateAccessor` class, which provides typed access to the rows of an external dynamic table. The state is bound to the message key (`group_by_schema`). For more details about `StateAccessor` and working with state, see [State Accessor](https://ytsaurus.tech/docs/en/state-accessor.md). {% note info %} If you need **read-only** access to the external state (a key-based join with TTL caching, without modification), the framework provides a separate mechanism: **External State Joiner**. It’s available in both Java/Kotlin (see [Read-only joiner](https://ytsaurus.tech/docs/en/#read-only-joiner)) and C++ ([External State Joiner](https://ytsaurus.tech/docs/cpp/state.md#external-state-joiner)) and is declared in the computation spec in the top-level `external_state_joiners` section (at the same level as `external_state_managers`). {% endnote %} ## Difference from Internal State | Characteristic | External State | Internal State | |---|---|---| | Storage | External dynamic table | Flow’s internal tables | | Table creation | You create it yourself | Automatic | | Data format | Typed `Payload` (table row) | Arbitrary (YSON, Protobuf, raw bytes) | | Access from other systems | Yes (sorted dynamic table) | No | | Schema | Defined by the table schema | Defined by you | For more details about Internal State, see [Internal State](https://ytsaurus.tech/docs/en/internal-state.md). Unlike internal state, external state is not tracked: a new value goes only through `set()`, and a joined external state is read-only. ## Getting ExternalStateAccessor External state is described by the `ExternalStateDescriptor` constant, which you create via `StateDescriptors.external(...)`. You usually declare the descriptor once per `RowFunction` class: ```java private static final ExternalStateDescriptor JOIN_STATE = StateDescriptors.external("/join-state"); ``` You get the `ExternalStateAccessor` for a specific [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) via `RuntimeContext` (which inherits `StatefulContext`): {% list tabs group=lang %} - Java ```java // For a message ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("state-name", message); // For a timer ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("state-name", timer); ``` - Kotlin ```kotlin // For a message val stateAccessor = ctx.getExternalStateAccessor("state-name", message) // For a timer val stateAccessor = ctx.getExternalStateAccessor("state-name", timer) ``` {% endlist %} Parameters: - Descriptor — `ExternalStateDescriptor`, whose name matches the key in the `external_state_managers` static spec and must start with `/`. - `message` / `timer` — the message or timer for whose [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) you need to get the state. {% note warning %} The external state name is validated: it must start with `/`, must not be empty, must not end with `/`, and must not contain two consecutive `/` characters. Calling `StateDescriptors.external("join-state")` (without the leading `/`) will throw an `IllegalArgumentException`. The same name must appear in the [spec](https://ytsaurus.tech/docs/en/#static-spec). {% endnote %} ### Declaring a proto external state {#proto-external-state} A profile state (`NYT::NFlow::NProfileState::TProfileStateManager`) stores a protobuf message. Describe it with `StateDescriptors.externalProto(name, TMessage.class)` — or `StateDescriptors.externalProtoReadOnly(...)` for a joiner — and access it through `ProtoStateAccessor`. Unlike a plain external state, the runner has to know the message to describe the state to the worker: it fills `descriptor_layout.descriptor_set` and `message_name` of the spec from the declared class. So declare the state to the pipeline as well — with `PipelineContext.registerState(...)` when you build the context yourself, or in Spring via `ComputationProvider.getStates()` or a `StateDescriptor` bean. The constant in the function alone is not enough for the runner. ## Main operations ### Reading the state {% list tabs group=lang %} - Java ```java ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("join-state", message); // Get the state: null when there is no value Payload maybeState = stateAccessor.get(); if (maybeState != null) { String value = maybeState.get("field_name", String.class); } // Get the state with a default value (an empty Payload with the state schema) Payload state = stateAccessor.getOrDefault(); ``` - Kotlin ```kotlin val stateAccessor = ctx.getExternalStateAccessor("join-state", message) // Get the state: null when there is no value val maybeState = stateAccessor.get() if (maybeState != null) { val value = maybeState.get("field_name", String::class.java) } // Get the state with a default value (an empty Payload with the state schema) val state = stateAccessor.getOrDefault() ``` {% endlist %} ### Writing the state {% list tabs group=lang %} - Java ```java ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("join-state", message); // Get the current state and modify it using PayloadBuilder PayloadBuilder builder = stateAccessor.getOrDefault().toBuilder(); builder.set("hit_payload", "some_value"); builder.set("show_time", 1234567890L); stateAccessor.set(builder.finish()); ``` - Kotlin ```kotlin val stateAccessor = ctx.getExternalStateAccessor("join-state", message) // Get the current state and modify it using PayloadBuilder val builder = stateAccessor.getOrDefault().toBuilder() builder.set("hit_payload", "some_value") builder.set("show_time", 1234567890L) stateAccessor.set(builder.finish()) ``` {% endlist %} ### Clearing the state {% list tabs group=lang %} - Java ```java ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("join-state", timer); // Clear the state (delete the row from the table) stateAccessor.clear(); ``` - Kotlin ```kotlin val stateAccessor = ctx.getExternalStateAccessor("join-state", timer) // Clear the state (delete the row from the table) stateAccessor.clear() ``` {% endlist %} {% note info %} An empty state corresponds to the absence of a row in the table. If there’s no row, `get()` returns `null`. Calling `clear()` deletes the row from the table. {% endnote %} ## Configuration in the static spec {#static-spec} To use External State, you must declare an external state manager in the `external_state_managers` section of the `Computation` in the static spec: ```yson "computations" = { "join" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(hit_id)"; "type" = "uint64"}; {"name" = "hit_id"; "type" = "string"}; {"name" = "hit_time"; "type" = "uint64"}; ]; "input_stream_ids" = ["action"; "hit"]; "output_stream_ids" = ["joined_action"]; "external_state_managers" = { "/join-state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state/table"; }; }; }; "parameters" = { "wait_for_actions" = "10s"; }; }; }; ``` Key fields: - `external_state_managers` — the section describing external states (a top-level field of the computation, at the same level as `parameters`, not nested in it). - The key inside `external_state_managers` (for example, `"/join-state"`) is the state name, which must start with `/`. You pass the same name to the descriptor `StateDescriptors.external("/join-state")`, through which the Computation gets the accessor: `ctx.getState(JOIN_STATE_DESCRIPTOR, message)`. - `external_state_manager_class_name` — the fully qualified name of the registered state manager class; the default is `"NYT::NFlow::TSimpleExternalStateManager"`. - `parameters/path` — the path to the YTsaurus dynamic table where the state is stored. ## Read-only joiner {#read-only-joiner} Use a read-only joiner when a computation needs only to read an external state that another computation owns (writes). The joiner isn’t the owner of the state table: it just joins its rows by the message key. This lets multiple computations read one table while keeping a single writer. You create the descriptor with `StateDescriptors.externalReadOnly(...)`, and the accessor with the same `ctx.getState(...)`: {% list tabs group=lang %} - Java ```java private static final JoinedExternalStateDescriptor RATING_SETTINGS = StateDescriptors.externalReadOnly("/rating-settings"); ReadOnlyExternalStateAccessor accessor = ctx.getState(RATING_SETTINGS, message); Payload settings = accessor.getOrDefault(); ``` - Kotlin ```kotlin private val ratingSettings = StateDescriptors.externalReadOnly("/rating-settings") val accessor = ctx.getState(ratingSettings, message) val settings = accessor.getOrDefault() ``` {% endlist %} `ReadOnlyExternalStateAccessor` supports `get()` and `getOrDefault()`. Calls to `set(...)` and `clear()` throw `UnsupportedOperationException` because the joiner doesn’t own the table. In the static spec, declare the joiner in the top-level `external_state_joiners` section (at the same level as `external_state_managers`). The name must match the argument to `externalReadOnly(...)`: ```yson "external_state_joiners" = { "/rating-settings" = { "external_state_joiner_class_name" = "NYT::NFlow::TSimpleExternalStateJoiner"; "parameters" = { "path" = "//path/to/state/table"; }; }; }; ``` {% note warning %} `getOrDefault()` on a joiner can build an empty `Payload` only if the schema for the requested keys arrives from the joined table. If no rows are found for the keys and the schema isn’t available, `getOrDefault()` throws `IllegalStateException`. In that case, use `get()` and handle `null`. {% endnote %} ## Creating a table for the state You must create the table for External State in advance. The schema of the table’s key columns must match the `group_by_schema` of the `Computation`. Here’s an example schema: | name | type | sort_order | expression | |------|------|:---:|------------| | `hash` | `uint64` | `ascending` | `farm_hash(hit_id)` | | `hit_id` | `string` | `ascending` | | | `hit_time` | `uint64` | `ascending` | | | `hit_payload` | `string` | | | | `show_time` | `uint64` | | | | `click_time` | `uint64` | | | ## Complete example Example from [wait_click_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/wait_click_join): {% list tabs group=lang %} - Java ```java public class JoinProcessFunction implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { var streamId = message.getStreamId(); // Get the ExternalStateAccessor for the current key ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("join-state", message); // Read the current state (or create an empty one) PayloadBuilder joinState = stateAccessor.getOrDefault().toBuilder(); if ("hit".equals(streamId)) { Hit hit = message.getPayload(); joinState.set("hit_payload", hit.getHitPayload()); } else if ("action".equals(streamId)) { Action action = message.getPayload(); if (Boolean.TRUE.equals(action.isClick())) { joinState.set("click_time", action.getActionTime()); } else { joinState.set("show_time", action.getActionTime()); } } // Save the updated state stateAccessor.set(joinState.finish()); // Set the timer that closes the key long maxTime = hitTime + waitTime; output.addTimer(maxTime, hitTime); } @Override public void onTimer(Timer timer, OutputCollector output, RuntimeContext ctx) { ExternalStateAccessor stateAccessor = ctx.getExternalStateAccessor("join-state", timer); Payload joinState = Objects.requireNonNull(stateAccessor.get()); // Build the output message from the state if (joinState.get("show_time", Long.class) != null && joinState.get("show_time", Long.class) != 0) { JoinedAction result = new JoinedAction(); // ... fill in the fields from the state ... output.addMessage(new Message("joined_action", result)); } // Clear the state after processing stateAccessor.clear(); } } ``` - Kotlin ```kotlin class JoinProcessFunction : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val streamId = message.getStreamId() // Get the ExternalStateAccessor for the current key val stateAccessor = ctx.getExternalStateAccessor("join-state", message) // Read the current state (or create an empty one) val joinState = stateAccessor.getOrDefault().toBuilder() if ("hit" == streamId) { val hit: Hit = message.getPayload() joinState.set("hit_payload", hit.getHitPayload()) } else if ("action" == streamId) { val action: Action = message.getPayload() if (Boolean.TRUE == action.isClick()) { joinState.set("click_time", action.getActionTime()) } else { joinState.set("show_time", action.getActionTime()) } } // Save the updated state stateAccessor.set(joinState.finish()) // Set the timer that closes the key val maxTime = hitTime + waitTime output.addTimer(maxTime, hitTime) } override fun onTimer(timer: Timer, output: OutputCollector, ctx: RuntimeContext) { val stateAccessor = ctx.getExternalStateAccessor("join-state", timer) val joinState = stateAccessor.get()!! // Build the output message from the state if (joinState.get("show_time", Long::class.java) != null && joinState.get("show_time", Long::class.java) != 0L) { val result = JoinedAction() // ... fill in the fields from the state ... output.addMessage(Message("joined_action", result)) } // Clear the state after processing stateAccessor.clear() } } ``` {% endlist %} # Spring Boot integration in YTsaurus Flow (Java) The Java SDK Flow (supports Kotlin) provides a [Spring Boot Starter](https://docs.spring.io/spring-boot/reference/using/build-systems.html#using.build-systems.starters) to simplify configuring and launching the pipeline. The Starter automatically creates the necessary beans and manages the gRPC server’s lifecycle. The same starter works from Kotlin code without changes thanks to `WITH_KOTLINC_ALLOPEN(preset=spring)`. One `@SpringBootApplication` class serves both roles: `YT_FLOW_MODE` selects the runner or companion, as described in [Entry point](https://ytsaurus.tech/docs/en/getting-started.md#entry-point). You do not need a separate runner class. [Source code for flow-spring-boot-starter](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter) ## Connecting To connect the Spring Boot Starter, you need to add the `flow-spring-boot-starter` dependency to your project. ## Quick start ### 1. Create a Spring Boot application Use one class with a `main` method as the pipeline entry point: {% list tabs group=lang %} - Java ```java @SpringBootApplication public class PipelineMain { public static void main(String[] args) throws Exception { new SpringApplicationBuilder(PipelineMain.class) .run(args); } } ``` - Kotlin ```kotlin @SpringBootApplication open class PipelineMain { companion object { @JvmStatic fun main(args: Array) { SpringApplicationBuilder(PipelineMain::class.java).run(*args) } } } ``` {% endlist %} Launch the pipeline through the same class: ```bash ./run.sh com.example.pipeline.PipelineMain --config pipeline.yson --flow-bin flow_server ``` Use the fully qualified class name: `run.sh` passes its first argument directly to `java`. ### 2. Register computations {#registration} Annotate the `ProcessFunction` class with `@FlowComputation` (for transformations) or `@FlowSourceComputation` (for sources), and specify the computation’s ID in the annotation. The annotations are meta-annotated with `@Component`, so the class automatically becomes a Spring bean — you don’t need a separate `@Component`. {% list tabs group=lang %} - Java ```java @FlowComputation(id = "join") public class JoinProcessFunction implements RowFunction { @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { // ... } } ``` - Kotlin ```kotlin @FlowComputation(id = "join") class JoinProcessFunction : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { // ... } } ``` {% endlist %} Declare streams as Spring beans `FlowStream` — they’re automatically registered in `PipelineContext`. Duplicate `streamId` values across any sources are rejected when building `PipelineContext`. {% list tabs group=lang %} - Java ```java @Configuration public class StreamConfiguration { @Bean public FlowStream hitStream() { return FlowStreams.typed("hit", Hit.class); } @Bean public FlowStream actionStream() { return FlowStreams.typed("action", Action.class); } @Bean public FlowStream joinedActionStream() { return FlowStreams.typed("joined_action", JoinedAction.class); } } ``` - Kotlin ```kotlin @Configuration class StreamConfiguration { @Bean fun hitStream(): FlowStream = FlowStreams.typed("hit", Hit::class.java) @Bean fun actionStream(): FlowStream = FlowStreams.typed("action", Action::class.java) @Bean fun joinedActionStream(): FlowStream = FlowStreams.typed("joined_action", JoinedAction::class.java) } ``` {% endlist %} As an alternative to separate beans, you can declare streams in one place by implementing the `ComputationProvider` interface (the `getStreams()` method) — see [ComputationProvider interface](https://ytsaurus.tech/docs/en/#computation-provider). The same interface declares the proto (profile) states of the pipeline (`getStates()`), so the runner can describe them to the worker; `StateDescriptor` beans are collected as well. That’s all you need to start. The Spring Boot Starter automatically: 1. Creates `PipelineContext` and registers `Computation` objects (from annotated classes), streams and states in it. 2. Creates and configures `GrpcServerExecution`. 3. Starts the gRPC server when the application launches. 4. Stops the server correctly when the application shuts down. ## Annotations `@FlowComputation` and `@FlowSourceComputation` {#annotations} [Source code for `@FlowComputation`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter/src/main/java/tech/ytsaurus/flow/spring/FlowComputation.java) [Source code for `@FlowSourceComputation`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter/src/main/java/tech/ytsaurus/flow/spring/FlowSourceComputation.java) The annotations mark the `ProcessFunction` class as a pipeline computation and set its ID: | Annotation | Computation type | Purpose | |-----------|-------------------|------------| | `@FlowComputation(id)` | `Transform` | Transformation — processes incoming messages. | | `@FlowSourceComputation(id)` | `Source` | Source — reads and parses incoming data. | Key points: - Both annotations are meta-annotated with `@Component`, so the annotated class automatically becomes a Spring bean during component scanning. You don’t need to specify a separate `@Component`. - The annotated class must implement `RowFunction` or `BatchFunction` (subclasses of `ProcessFunction`). Otherwise, the application won’t start and will show a clear error. - The ID must be unique among all computations and must match the computation’s ID in the pipeline specification. - Spring DI (`@Autowired`, constructor injection) is available in annotated classes — just like in any Spring bean. ## ComputationProvider interface {#computation-provider} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter/src/main/java/tech/ytsaurus/flow/spring/ComputationProvider.java) `ComputationProvider` lets you declare pipeline streams and states imperatively in one place — as an alternative to separate `FlowStream` and `StateDescriptor` beans. Implement it and register it as a Spring `@Configuration`. Computations aren’t registered through this interface — use `@FlowComputation` / `@FlowSourceComputation` annotations for them. {% list tabs group=lang %} - Java ```java public interface ComputationProvider { /** * Returns the list of streams to register in the pipeline. */ List> getStreams(); /** * Returns the states the pipeline declares; empty by default. */ default List> getStates() { return List.of(); } } ``` - Kotlin ```kotlin interface ComputationProvider { /** * Returns the list of streams to register in the pipeline. */ fun getStreams(): List> /** * Returns the states the pipeline declares; empty by default. */ fun getStates(): List> = emptyList() } ``` {% endlist %} `getStates()` declares the proto (profile) states of the pipeline: the runner fills their descriptor sets into the spec from the declared message classes, so the spec names no descriptors of its own. States of one name share a type — the owner of a state and the computations joining it declare it under one name — and states of different types need different names. ### Using Spring DI in ProcessFunction One of the main benefits of Spring integration is that you can use Dependency Injection in `ProcessFunction`. The annotated class is a regular Spring bean, so you can inject dependencies via the constructor or `@Autowired`: {% list tabs group=lang %} - Java ```java @FlowComputation(id = "my_computation") public class MyProcessFunction implements RowFunction { private final MyExternalService externalService; private final MyCache cache; @Autowired public MyProcessFunction(MyExternalService externalService, MyCache cache) { this.externalService = externalService; this.cache = cache; } @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { // externalService and cache are available here } } ``` - Kotlin ```kotlin @FlowComputation(id = "my_computation") class MyProcessFunction( private val externalService: MyExternalService, private val cache: MyCache, ) : RowFunction { override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { // externalService and cache are available here } } ``` {% endlist %} ## Configuration {#configuration} ### Application properties You set the settings in `application.yml` or `application.properties`: ```yaml flow: server: port: 8080 # Optional. By default, the value is taken from the port field in YT_FLOW_COMPANION_CONFIG ``` ### FlowProperties [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter/src/main/java/tech/ytsaurus/flow/spring/FlowProperties.java) | Property | Type | Default | Description | |----------|-----|:---:|----------| | `flow.server.port` | `Integer` | `null` | The gRPC server port. If not specified, the value is taken from the `port` field in `YT_FLOW_COMPANION_CONFIG` | {% note info %} In a production environment, the port is passed via the `YT_FLOW_COMPANION_CONFIG` environment variable (the `port` field), which Flow sets when starting the companion process. Explicitly specifying the port in the configuration is useful for local development and testing. {% endnote %} ## Auto-configuration {#auto-configuration} [Source code for FlowAutoConfiguration](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter/src/main/java/tech/ytsaurus/flow/spring/FlowAutoConfiguration.java) The Spring Boot Starter automatically creates these beans: | Bean | Creation condition | Description | |-----|-------------------|----------| | `PipelineContext` | An annotated bean (`@FlowComputation`/`@FlowSourceComputation`) or a `ComputationProvider` exists | Pipeline context with registered `Computation` objects and streams | | `FlowRunnerBootstrap` | Runner mode | Starts the pipeline and exits with the `flow_server` return code | | `CompanionExecutionConfig` | Companion mode with an annotated bean or `ComputationProvider` | gRPC server configuration (port) | | `GrpcServerExecution` | Companion mode with a `PipelineContext` | Manages the gRPC server | | `FlowCompanionLifecycle` | Companion mode with a `GrpcServerExecution` | Manages the server lifecycle | `PipelineContext` is built the same way in both modes, so unit tests that inject it through `@SpringBootTest` work without selecting a mode. `FlowRunnerBootstrap` itself does not depend on that bean: it collects only streams when an actual launch begins. It does not instantiate computation beans or their caches and clients just to submit a spec. Neither the gRPC nor the monitoring server starts in runner mode. Their configuration arrives from the worker through `YT_FLOW_COMPANION_CONFIG` and does not exist outside the companion. To avoid eagerly starting unrelated application beans, the starter defaults to `spring.main.web-application-type=none`, `spring.main.keep-alive=false`, and `spring.main.lazy-initialization=true` in runner mode. You can override these settings; test contexts keep normal Spring behavior. `OnFlowComponentsCondition` enables the starter when the context has a `ComputationProvider` bean or a bean annotated with `@FlowComputation` or `@FlowSourceComputation`. `YT_FLOW_MODE` chooses the role. For tests that cannot set an environment variable in their JVM, `flow.run-mode` accepts `Worker` or `runner`, ignoring case, only when `YT_FLOW_MODE` is unset. If both are set and disagree, context startup fails. `flow.run-mode` does not control `spring.main.*`: those settings are selected earlier from `YT_FLOW_MODE`. Streams and computations are collected only from the current context; beans in a `SpringApplicationBuilder.parent(...)` context do not enter the spec or companion. `FlowRunnerBootstrap` runs at the lowest `ApplicationRunner` priority and exits the JVM after launching. Give any application runner that must run before it an explicit `@Order` below `Ordered.LOWEST_PRECEDENCE`; runners with the same lowest priority have no guaranteed order. A test context does not launch a pipeline: `FlowRunnerBootstrap` detects common test frameworks, including JUnit, TestNG, Spring TestContext, and Cucumber. Set `flow.runner.enabled=false` for another framework. Outside tests, a launch without `--config` fails instead of silently doing nothing. All beans are created with the `@ConditionalOnMissingBean` annotation, which lets you override any of them if needed. ## Lifecycle {#lifecycle} [Source code for FlowCompanionLifecycle](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-spring-boot-starter/src/main/java/tech/ytsaurus/flow/spring/FlowCompanionLifecycle.java) `FlowCompanionLifecycle` implements `SmartLifecycle` and manages the gRPC server’s start and stop: - **Start**: the gRPC server starts automatically after all Spring beans are initialized (phase `Integer.MAX_VALUE`). - **Stop**: the gRPC server stops gracefully before the Spring beans are destroyed. ## Overriding beans {#custom-beans} If needed, you can override any auto-configured bean: {% list tabs group=lang %} - Java ```java @Configuration public class CustomFlowConfig { @Bean public PipelineContext pipelineContext( ObjectProvider computationProviders, ObjectProvider> flowStreams, ListableBeanFactory beanFactory ) { // FlowComponents collects computations from annotated beans, // and streams from ComputationProvider and FlowStream beans, just as // the default auto-configuration does. var context = FlowComponents.buildPipelineContext( computationProviders, flowStreams, beanFactory); // Additional configuration... return context; } @Bean public CompanionExecutionConfig companionExecutionConfig() { // Custom port configuration return new CompanionExecutionConfig(9090); } } ``` - Kotlin ```kotlin @Configuration class CustomFlowConfig { @Bean fun pipelineContext( computationProviders: ObjectProvider, flowStreams: ObjectProvider>, beanFactory: ListableBeanFactory ): PipelineContext { // FlowComponents collects computations from annotated beans, // and streams from ComputationProvider and FlowStream beans, just as // the default auto-configuration does. val context = FlowComponents.buildPipelineContext( computationProviders, flowStreams, beanFactory) // Additional configuration... return context } @Bean fun companionExecutionConfig(): CompanionExecutionConfig = // Custom port configuration CompanionExecutionConfig(9090) } ``` {% endlist %} ## Full application example {#full-example} Project structure: {% list tabs group=lang %} - Java ``` src/main/java/ ├── PipelineMain.java # @SpringBootApplication, only entry point ├── JoinProcessFunction.java # @FlowComputation(id = "join") implements RowFunction ├── StreamConfiguration.java # @Configuration with FlowStream beans └── model/ ├── Hit.java # @Entity POJO ├── Action.java # @Entity POJO └── JoinedAction.java # @Entity POJO src/main/resources/ └── log4j2.properties # Logging configuration ``` - Kotlin ``` src/main/kotlin/ ├── PipelineMain.kt # @SpringBootApplication, only entry point ├── JoinProcessFunction.kt # @FlowComputation(id = "join") : RowFunction ├── StreamConfiguration.kt # @Configuration with FlowStream beans └── model/ ├── Hit.kt # @Entity POJO ├── Action.kt # @Entity POJO └── JoinedAction.kt # @Entity POJO src/main/resources/ └── log4j2.properties # Logging configuration ``` {% endlist %} Example with the `@FlowComputation` annotation and streams via `ComputationProvider` — [wait_click_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/wait_click_join) (Java) and [wait_click_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/wait_click_join) (Kotlin). # Testing with TestComputationHarness in YTsaurus Flow (Java) The `flow-test-utils` module provides utilities for unit testing Flow pipeline components in Java and Kotlin without running a real gRPC server or C++ workers. The central class is [`TestComputationHarness`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/harness/TestComputationHarness.java), which emulates `doProcess` calls at the companion level. {% note info %} The `TestComputationHarness` class is designed for **unit testing** individual `ProcessFunction` instances without launching a real pipeline. For **integration testing** of the full pipeline (with real C++ workers, queues, and streams), use `FlowTestJavaBase` — see the [section below](https://ytsaurus.tech/docs/en/#integration-testing). {% endnote %} ## General testing architecture {#architecture} In production, the C++ worker sends gRPC requests to the companion, passing messages, timers, state, and watermarks. The `Companion` calls `Computation.doProcess()`, which delegates processing to the `ProcessFunction`. In tests, `TestComputationHarness` replaces the gRPC layer: it accepts [`TestDoProcessRequest`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/request/TestDoProcessRequest.java), converts it to protobuf format, calls `CompanionRequestProcessor.processBatch()`, and returns [`TestDoProcessResponse`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/request/TestDoProcessResponse.java) with deserialized results. ## Dependencies {#dependencies} To use the test utilities, you need to add a dependency on `flow-test-utils`: ``` PEERDIR( yt/java/flow/flow-test-utils ) ``` All examples below use JUnit version 5 or later. ## Setting up TestComputationHarness {#setup} To create a `TestComputationHarness`, you need: 1. **`PipelineContext`** — the pipeline context with registered `Computation` objects and streams. 2. **Pipeline spec** — the static pipeline specification in YSON format (the `pipeline.yson` file). 3. **External state schemas** (optional) — schemas for external states if the computation uses `ExternalStateAccessor`. ### Builder API {% list tabs group=lang %} - Java ```java TestComputationHarness harness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) // required .setPipelineSpec(txtSpec) // required: String, YTreeNode, or InputStream .addExternalStateSchema("state-name", schema) // optional .setJobContext(jobContext) // optional, default timeout=10 minutes .build(); ``` - Kotlin ```kotlin val harness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) // required .setPipelineSpec(txtSpec) // required: String, YTreeNode, or InputStream .addExternalStateSchema("state-name", schema) // optional .setJobContext(jobContext) // optional, default timeout=10 minutes .build() ``` {% endlist %} When you call `build()`, the harness automatically: - Extracts stream information from the pipeline spec. - Registers missing streams as untyped (via `FlowStreams.raw`) in the `PipelineContext`. - Creates an internal `CompanionRequestProcessor`. ## Building test requests {#test-requests} ### TestDoProcessRequest {#test-do-process-request} [`TestDoProcessRequest`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/request/TestDoProcessRequest.java) is a request to process a batch of messages and/or timers. You create it using a builder: {% list tabs group=lang %} - Java ```java var request = TestDoProcessRequest.builder("computation-id") .setMessages(messages) // List .setTimers(timers) // List .setWatermarks(watermarks) // Map .setExternalState("name", stateMap) // Map .setInternalState("name", stateMap) // Map .build(); ``` - Kotlin ```kotlin val request = TestDoProcessRequest.builder("computation-id") .setMessages(messages) // List .setTimers(timers) // List .setWatermarks(watermarks) // Map .setExternalState("name", stateMap) // Map .setInternalState("name", stateMap) // Map .build() ``` {% endlist %} Parameters: - **`computationId`** — the ID of the computation that the request targets. - **`messages`** — a list of input messages of type `ExtendedMessage`. - **`timers`** — a list of triggered timers of type `Timer`. - **`watermarks`** — a map of watermarks by stream (`streamId → timestamp`). - **`externalStates`** — pre-filled external state. - **`internalStates`** — pre-filled internal state. ### Building messages {#building-messages} #### With typed streams If you register streams as typed (via `FlowStreams.typed(...)`), you can pass the payload as a POJO object: {% list tabs group=lang %} - Java ```java ExtendedMessage msg = ExtendedMessage.builder() .setStreamId("hit") .setKey(key) .setPayload(new Hit("hit-1", 1000L, "payload-data")) .setEventTimestamp(1000L) .build(); ``` - Kotlin ```kotlin val msg = ExtendedMessage.builder() .setStreamId("hit") .setKey(key) .setPayload(Hit("hit-1", 1000L, "payload-data")) .setEventTimestamp(1000L) .build() ``` {% endlist %} #### With untyped streams (PayloadBuilder) If streams aren’t typed, you build the payload using `PayloadBuilder`: {% list tabs group=lang %} - Java ```java var hitSchema = testHarness.getStream("hit").getSchema(); var payload = new PayloadBuilder(hitSchema) .set("hit_id", "hit-1") .set("hit_time", 1000L) .set("hit_payload", "payload-data") .finish(); ExtendedMessage msg = ExtendedMessage.builder() .setStreamId("hit") .setKey(key) .setPayload(payload) .setEventTimestamp(1000L) .build(); ``` - Kotlin ```kotlin val hitSchema = testHarness.getStream("hit").getSchema() val payload = PayloadBuilder(hitSchema) .set("hit_id", "hit-1") .set("hit_time", 1000L) .set("hit_payload", "payload-data") .finish() val msg = ExtendedMessage.builder() .setStreamId("hit") .setKey(key) .setPayload(payload) .setEventTimestamp(1000L) .build() ``` {% endlist %} {% note info %} The `testHarness.getStream(streamId)` method returns a `FlowStream` with the schema extracted from the pipeline spec. This is convenient for getting the stream schema without defining it manually. {% endnote %} ### Building keys {#building-keys} You build the key (`Payload`) according to the `group_by_schema` of the `Computation` you’re testing, from the pipeline spec. {% note info %} The `testHarness.getGroupBySchema(computationId)` method returns a `TableSchema` based on the `group_by_schema` extracted from the pipeline spec for the `Computation` you’re testing. {% endnote %} {% list tabs group=lang %} - Java ```java TableSchema keySchema = testHarness.getGroupBySchema("join"); Payload key = new PayloadBuilder(keySchema) .set("hash", 0L) .set("hit_id", "hit-1") .set("hit_time", 1000L) .finish(); ``` - Kotlin ```kotlin val keySchema = testHarness.getGroupBySchema("join") val key = PayloadBuilder(keySchema) .set("hash", 0L) .set("hit_id", "hit-1") .set("hit_time", 1000L) .finish() ``` {% endlist %} {% note tip %} The `hash` field (farm_hash) is calculated on the C++ worker side. In tests, you can set it to `0L`. {% endnote %} ### Building timers {#building-timers} You create a timer using a builder: {% list tabs group=lang %} - Java ```java Timer timer = Timer.builder() .setStreamId("timer") .setKey(key) .setTriggerTimestamp(triggerTimestamp) .build(); ``` - Kotlin ```kotlin val timer = Timer.builder() .setStreamId("timer") .setKey(key) .setTriggerTimestamp(triggerTimestamp) .build() ``` {% endlist %} ### Configuring watermarks {#watermarks} Watermarks define which messages are considered late. A message with `eventTimestamp < watermark` will be discarded if the `ProcessFunction` implements the corresponding check via `ctx.getEpochInputEventWatermark()`. See an example of this check in [Wait Click Join in Java](https://ytsaurus.tech/docs/en/examples/wait_click_join.md#late-data-check). {% list tabs group=lang %} - Java ```java Map watermarks = Map.of( "hit", 0L, "action", 0L ); ``` - Kotlin ```kotlin val watermarks = mapOf( "hit" to 0L, "action" to 0L ) ``` {% endlist %} Setting the watermark to `0L` means that all messages with `eventTimestamp >= 0` won’t be considered late. ### Pre-populating state {#pre-populating-state} #### External State {% list tabs group=lang %} - Java ```java TableSchema stateSchema = TableSchema.builder() .addValue("hit_payload", ColumnValueType.STRING) .addValue("show_time", ColumnValueType.UINT64) .addValue("click_time", ColumnValueType.UINT64) .build(); ExternalState preState = new ExternalState( new PayloadBuilder(stateSchema) .set("hit_payload", "some-payload") .set("show_time", showTime) .set("click_time", clickTime) .finish() ); var request = TestDoProcessRequest.builder("join") .setTimers(List.of(timer)) .setExternalState("join-state", Map.of(key, preState)) .build(); ``` - Kotlin ```kotlin val stateSchema = TableSchema.builder() .addValue("hit_payload", ColumnValueType.STRING) .addValue("show_time", ColumnValueType.UINT64) .addValue("click_time", ColumnValueType.UINT64) .build() val preState = ExternalState( PayloadBuilder(stateSchema) .set("hit_payload", "some-payload") .set("show_time", showTime) .set("click_time", clickTime) .finish() ) val request = TestDoProcessRequest.builder("join") .setTimers(listOf(timer)) .setExternalState("join-state", mapOf(key to preState)) .build() ``` {% endlist %} #### Internal State (Proto) For internal state based on protobuf: {% list tabs group=lang %} - Java ```java TJoinState protoState = TJoinState.newBuilder() .setHitPayload("some-payload") .setShowTime(showTime) .setClickTime(clickTime) .build(); InternalState internalState = new InternalState(protoState.toByteArray()); var request = TestDoProcessRequest.builder("join") .setTimers(List.of(timer)) .setInternalState("join-state", Map.of(key, internalState)) .build(); ``` - Kotlin ```kotlin val protoState = TJoinState.newBuilder() .setHitPayload("some-payload") .setShowTime(showTime) .setClickTime(clickTime) .build() val internalState = InternalState(protoState.toByteArray()) val request = TestDoProcessRequest.builder("join") .setTimers(listOf(timer)) .setInternalState("join-state", mapOf(key to internalState)) .build() ``` {% endlist %} ## Analyzing the results {#analyzing-response} ### TestDoProcessResponse {#test-do-process-response} [`TestDoProcessResponse`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/request/TestDoProcessResponse.java) provides methods to verify processing results. | Method | Description | |-------|----------| | `getOutputMessagesFlatten()` | All output messages from all transform groups | | `getOutputTimersFlatten()` | All set timers from all transform groups | | `getTransformResults()` | A list of `TransformResult` grouped by parent IDs | {% note info %} `TestDoProcessResponse` provides two sets of methods to access the state: - **Full view** (`getExternalState(s)`, `getInternalState(s)`) — all states after processing: the keys passed in the request, with computation changes applied on top. Use this when you need to check the final state regardless of whether computation changed it. - **Only modified** (`getModifiedExternalState(s)`, `getModifiedInternalState(s)`) — only what computation actually changed. These are the states sent back to the worker and saved. Use this to verify that the state for a key hasn’t changed, or when you need to check only the state changes. All state access methods are null-safe: an unknown state name or key returns an empty map or `null`, not an exception. {% endnote %} Full view of the state (loaded states with computation changes applied): | Method | Description | |-------|----------| | `getExternalStateNames()` / `getInternalStateNames()` | Names of all states (loaded and/or modified) | | `getExternalStates()` / `getInternalStates()` | All states grouped by name | | `getExternalStates(name)` / `getInternalStates(name)` | All state records by name (empty map if none) | | `getExternalStateSize(name)` / `getInternalStateSize(name)` | Number of records in the state (0 if none) | | `getExternalState(name, key)` / `getInternalState(name, key)` | Specific state record (`null` if none) | Only states modified by computation: | Method | Description | |-------|----------| | `getModifiedExternalStateNames()` / `getModifiedInternalStateNames()` | Names of modified states | | `getModifiedExternalStates()` / `getModifiedInternalStates()` | Modified states grouped by name | | `getModifiedExternalStates(name)` / `getModifiedInternalStates(name)` | Modified state records by name (empty map if none) | | `getModifiedExternalStateSize(name)` / `getModifiedInternalStateSize(name)` | Number of modified records (0 if none) | | `getModifiedExternalState(name, key)` / `getModifiedInternalState(name, key)` | Specific modified record (`null` if not modified) | ### Checking output messages {% list tabs group=lang %} - Java ```java var response = testHarness.doProcess(request); // Check the number of output messages assertEquals(1, response.getOutputMessagesFlatten().size()); // Get the message and check the stream var msg = response.getOutputMessagesFlatten().get(0); assertEquals("joined_action", msg.getStreamId()); // For typed streams — cast payload to POJO JoinedAction result = (JoinedAction) msg.getPayload(); assertEquals("hit-1", result.getHitId()); // For untyped streams — read fields via get() byte[] data = msg.get("data", byte[].class); ``` - Kotlin ```kotlin val response = testHarness.doProcess(request) // Check the number of output messages assertEquals(1, response.getOutputMessagesFlatten().size) // Get the message and check the stream val msg = response.getOutputMessagesFlatten()[0] assertEquals("joined_action", msg.getStreamId()) // For typed streams — cast payload to POJO val result = msg.getPayload() as JoinedAction assertEquals("hit-1", result.getHitId()) // For untyped streams — read fields via get() val data = msg.get("data", ByteArray::class.java) ``` {% endlist %} ### Checking timers {% list tabs group=lang %} - Java ```java assertEquals(1, response.getOutputTimersFlatten().size()); var timer = response.getOutputTimersFlatten().get(0); assertEquals(expectedTriggerTimestamp, timer.getTriggerTimestamp()); ``` - Kotlin ```kotlin assertEquals(1, response.getOutputTimersFlatten().size) val timer = response.getOutputTimersFlatten()[0] assertEquals(expectedTriggerTimestamp, timer.getTriggerTimestamp()) ``` {% endlist %} ### Checking the state {% list tabs group=lang %} - Java ```java // External state assertEquals(1, response.getExternalStateSize("join-state")); var state = response.getExternalState("join-state", key); assertFalse(state.isReset()); assertEquals("payload-data", state.getValue().get("hit_payload", String.class)); // Check state reset var stateAfterTimer = response.getExternalState("join-state", key); assertTrue(stateAfterTimer.isReset()); ``` - Kotlin ```kotlin // External state assertEquals(1, response.getExternalStateSize("join-state")) val state = response.getExternalState("join-state", key)!! assertFalse(state.isReset) assertEquals("payload-data", state.value.get("hit_payload", String::class.java)) // Check state reset val stateAfterTimer = response.getExternalState("join-state", key)!! assertTrue(stateAfterTimer.isReset) ``` {% endlist %} {% list tabs group=lang %} - Java ```java // Internal state (Proto) var internalState = response.getInternalState("join-state", key); assertFalse(internalState.isReset()); TJoinState joinState = ProtoUtils.parseBytes(internalState.getValue(), TJoinState.class); assertEquals("payload-data", joinState.getHitPayload()); ``` - Kotlin ```kotlin // Internal state (Proto) val internalState = response.getInternalState("join-state", key)!! assertFalse(internalState.isReset) val joinState = ProtoUtils.parseBytes(internalState.value, TJoinState::class.java) assertEquals("payload-data", joinState.getHitPayload()) ``` {% endlist %} #### Full view vs. modified states {#all-vs-modified-state} The `getExternalState` / `getInternalState` methods return the **full view**: the pre-filled state is available even if computation didn’t change it. To check what computation actually changed, use the `getModified*` methods. {% list tabs group=lang %} - Java ```java // Full view: the loaded state is visible even if it wasn’t changed assertEquals(1, response.getExternalStateSize("join-state")); assertNotNull(response.getExternalState("join-state", key)); // Only modified states: computation wrote nothing for this key assertEquals(0, response.getModifiedExternalStateSize("join-state")); assertNull(response.getModifiedExternalState("join-state", key)); // Unknown name/key doesn’t throw an exception assertNull(response.getExternalState("unknown", key)); assertTrue(response.getExternalStates("unknown").isEmpty()); ``` - Kotlin ```kotlin // Full view: the loaded state is visible even if it wasn’t changed assertEquals(1, response.getExternalStateSize("join-state")) assertNotNull(response.getExternalState("join-state", key)) // Only modified states: computation wrote nothing for this key assertEquals(0, response.getModifiedExternalStateSize("join-state")) assertNull(response.getModifiedExternalState("join-state", key)) // Unknown name/key doesn’t throw an exception assertNull(response.getExternalState("unknown", key)) assertTrue(response.getExternalStates("unknown").isEmpty()) ``` {% endlist %} ## Example: a test without Spring {#example-without-spring} This is a complete unit test example for `JoinProcessFunction` from the [wait_click_join](https://ytsaurus.tech/docs/en/examples/wait_click_join.md) project. In this variant, you create `PipelineContext` manually, without a Spring container. {% list tabs group=lang %} - Java ```java public class JoinProcessFunctionTest { private static final long WAIT_SECONDS = 10L; private static final long BASE_HIT_TIME = 1000L; private static final long WATERMARK = 0L; private TestComputationHarness testHarness; private TableSchema keySchema; private TableSchema joinStateSchema; @BeforeEach void init() throws IOException { // 1. You create PipelineContext manually var pipelineContext = new PipelineContext(); // 2. Register the computation with the process function Computation join = Computation.builder() .setComputationId("join") .setProcessFunction(new JoinProcessFunction()) .build(); pipelineContext.registerComputation(join); // 3. Register typed streams pipelineContext.registerStream(FlowStreams.typed("hit", Hit.class)); pipelineContext.registerStream(FlowStreams.typed("action", Action.class)); pipelineContext.registerStream(FlowStreams.typed("joined_action", JoinedAction.class)); // 4. Define the external state schema this.joinStateSchema = TableSchema.builder() .addValue("hit_payload", ColumnValueType.STRING) .addValue("show_time", ColumnValueType.UINT64) .addValue("click_time", ColumnValueType.UINT64) .build(); // 5. Read the pipeline spec and create the harness var specPath = Paths.getSourcePath( "yt/yt/flow/examples/java/wait_click_join/test/pipeline.yson"); var txtSpec = Files.readString(Path.of(specPath)); this.testHarness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) .setPipelineSpec(txtSpec) .addExternalStateSchema("join-state", joinStateSchema) .build(); // 6. Get the key schema for the join computation (group_by_schema from pipeline.yson) this.keySchema = testHarness.getGroupBySchema("join"); } // — Helper methods — private Payload buildKey(String hitId, long hitTime) { return new PayloadBuilder(keySchema) .set("hash", 0L) .set("hit_id", hitId) .set("hit_time", hitTime) .finish(); } private ExtendedMessage buildHitMessage(String hitId, long hitTime, String hitPayload) { return ExtendedMessage.builder() .setStreamId("hit") .setKey(buildKey(hitId, hitTime)) .setPayload(new Hit(hitId, hitTime, hitPayload)) .setEventTimestamp(hitTime) .build(); } private Map defaultWatermarks() { return Map.of("hit", WATERMARK, "action", WATERMARK); } // — Tests — @Test void testHitMessageStoresHitPayloadInState() { var messages = List.of(buildHitMessage("hit-1", BASE_HIT_TIME, "payload-data")); var request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(defaultWatermarks()) .build(); var response = testHarness.doProcess(request); // There are no output messages — the result will appear when the timer fires assertTrue(response.getOutputMessagesFlatten().isEmpty()); // The timer must be set assertEquals(1, response.getOutputTimersFlatten().size()); assertEquals(BASE_HIT_TIME + WAIT_SECONDS, response.getOutputTimersFlatten().get(0).getTriggerTimestamp()); // The state must contain hit_payload Payload key = buildKey("hit-1", BASE_HIT_TIME); var state = response.getExternalState("join-state", key); assertFalse(state.isReset()); assertEquals("payload-data", state.getValue().get("hit_payload", String.class)); } @Test void testTimerEmitsJoinedAction() { Payload key = buildKey("hit-10", BASE_HIT_TIME); // Pre-populate the state ExternalState preState = new ExternalState( new PayloadBuilder(joinStateSchema) .set("hit_payload", "some-payload") .set("show_time", BASE_HIT_TIME + 3L) .set("click_time", BASE_HIT_TIME + 7L) .finish() ); Timer timer = Timer.builder() .setStreamId("timer") .setKey(key) .setTriggerTimestamp(BASE_HIT_TIME + WAIT_SECONDS) .build(); var request = TestDoProcessRequest.builder("join") .setTimers(List.of(timer)) .setExternalState("join-state", Map.of(key, preState)) .build(); var response = testHarness.doProcess(request); // There must be one output message assertEquals(1, response.getOutputMessagesFlatten().size()); var msg = response.getOutputMessagesFlatten().get(0); assertEquals("joined_action", msg.getStreamId()); JoinedAction result = (JoinedAction) msg.getPayload(); assertEquals("hit-10", result.getHitId()); assertTrue(result.getClick()); // The state must be reset assertTrue(response.getExternalState("join-state", key).isReset()); } @Test void testLateMessageIsDropped() { long watermark = BASE_HIT_TIME + 5L; long eventTimestamp = BASE_HIT_TIME + 3L; // < watermark → late var messages = List.of(buildActionMessage("hit-5", BASE_HIT_TIME, BASE_HIT_TIME + 2L, false, eventTimestamp)); var request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(Map.of("hit", watermark, "action", watermark)) .build(); var response = testHarness.doProcess(request); // The late message is dropped assertTrue(response.getOutputMessagesFlatten().isEmpty()); assertTrue(response.getOutputTimersFlatten().isEmpty()); assertEquals(0, response.getExternalStateSize("join-state")); } } ``` - Kotlin ```kotlin class JoinProcessFunctionTest { companion object { private const val WAIT_SECONDS = 10L private const val BASE_HIT_TIME = 1000L private const val WATERMARK = 0L } private lateinit var testHarness: TestComputationHarness private lateinit var keySchema: TableSchema private lateinit var joinStateSchema: TableSchema @BeforeEach fun init() { // 1. You create PipelineContext manually val pipelineContext = PipelineContext() // 2. Register the computation with the process function val join = Computation.builder() .setComputationId("join") .setProcessFunction(JoinProcessFunction()) .build() pipelineContext.registerComputation(join) // 3. Register typed streams pipelineContext.registerStream(FlowStreams.typed("hit", Hit::class.java)) pipelineContext.registerStream(FlowStreams.typed("action", Action::class.java)) pipelineContext.registerStream(FlowStreams.typed("joined_action", JoinedAction::class.java)) // 4. Define the external state schema joinStateSchema = TableSchema.builder() .addValue("hit_payload", ColumnValueType.STRING) .addValue("show_time", ColumnValueType.UINT64) .addValue("click_time", ColumnValueType.UINT64) .build() // 5. Read the pipeline spec and create the harness val specPath = Paths.getSourcePath( "yt/yt/flow/examples/java/wait_click_join/test/pipeline.yson") val txtSpec = Files.readString(Path.of(specPath)) testHarness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) .setPipelineSpec(txtSpec) .addExternalStateSchema("join-state", joinStateSchema) .build() // 6. Get the key schema for the join computation (group_by_schema from pipeline.yson) keySchema = testHarness.getGroupBySchema("join") } // — Helper methods — private fun buildKey(hitId: String, hitTime: Long): Payload = PayloadBuilder(keySchema) .set("hash", 0L) .set("hit_id", hitId) .set("hit_time", hitTime) .finish() private fun buildHitMessage(hitId: String, hitTime: Long, hitPayload: String): ExtendedMessage = ExtendedMessage.builder() .setStreamId("hit") .setKey(buildKey(hitId, hitTime)) .setPayload(Hit(hitId, hitTime, hitPayload)) .setEventTimestamp(hitTime) .build() private fun defaultWatermarks() = mapOf("hit" to WATERMARK, "action" to WATERMARK) // — Tests — @Test fun testHitMessageStoresHitPayloadInState() { val messages = listOf(buildHitMessage("hit-1", BASE_HIT_TIME, "payload-data")) val request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(defaultWatermarks()) .build() val response = testHarness.doProcess(request) // There are no output messages — the result will appear when the timer fires assertTrue(response.getOutputMessagesFlatten().isEmpty()) // The timer must be set assertEquals(1, response.getOutputTimersFlatten().size) assertEquals(BASE_HIT_TIME + WAIT_SECONDS, response.getOutputTimersFlatten()[0].getTriggerTimestamp()) // The state must contain hit_payload val key = buildKey("hit-1", BASE_HIT_TIME) val state = response.getExternalState("join-state", key)!! assertFalse(state.isReset) assertEquals("payload-data", state.value.get("hit_payload", String::class.java)) } @Test fun testTimerEmitsJoinedAction() { val key = buildKey("hit-10", BASE_HIT_TIME) // Pre-populate the state val preState = ExternalState( PayloadBuilder(joinStateSchema) .set("hit_payload", "some-payload") .set("show_time", BASE_HIT_TIME + 3L) .set("click_time", BASE_HIT_TIME + 7L) .finish() ) val timer = Timer.builder() .setStreamId("timer") .setKey(key) .setTriggerTimestamp(BASE_HIT_TIME + WAIT_SECONDS) .build() val request = TestDoProcessRequest.builder("join") .setTimers(listOf(timer)) .setExternalState("join-state", mapOf(key to preState)) .build() val response = testHarness.doProcess(request) // There must be one output message assertEquals(1, response.getOutputMessagesFlatten().size) val msg = response.getOutputMessagesFlatten()[0] assertEquals("joined_action", msg.getStreamId()) val result = msg.getPayload() as JoinedAction assertEquals("hit-10", result.getHitId()) assertTrue(result.getClick()) // The state must be reset assertTrue(response.getExternalState("join-state", key)!!.isReset) } @Test fun testLateMessageIsDropped() { val watermark = BASE_HIT_TIME + 5L val eventTimestamp = BASE_HIT_TIME + 3L // < watermark → late val messages = listOf(buildActionMessage("hit-5", BASE_HIT_TIME, BASE_HIT_TIME + 2L, false, eventTimestamp)) val request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(mapOf("hit" to watermark, "action" to watermark)) .build() val response = testHarness.doProcess(request) // The late message is dropped assertTrue(response.getOutputMessagesFlatten().isEmpty()) assertTrue(response.getOutputTimersFlatten().isEmpty()) assertEquals(0, response.getExternalStateSize("join-state")) } } ``` {% endlist %} ### Multi-step test {#multi-step-test} To test the full processing flow (hit → show → click → timer), you manually pass the state between steps: {% list tabs group=lang %} - Java ```java @Test void testFullJoinFlow() { String hitId = "hit-20"; long hitTime = BASE_HIT_TIME; // Step 1: process the hit message var hitRequest = TestDoProcessRequest.builder("join") .setMessages(List.of(buildHitMessage(hitId, hitTime, "full-payload"))) .setWatermarks(defaultWatermarks()) .build(); var hitResponse = testHarness.doProcess(hitRequest); Payload key = buildKey(hitId, hitTime); var stateAfterHit = hitResponse.getExternalState("join-state", key); // Step 2: process the show message, pass the state from step 1 var showRequest = TestDoProcessRequest.builder("join") .setMessages(List.of(buildActionMessage(hitId, hitTime, hitTime + 3L, false, hitTime + 1L))) .setExternalState("join-state", Map.of(key, stateAfterHit)) .setWatermarks(defaultWatermarks()) .build(); var showResponse = testHarness.doProcess(showRequest); var stateAfterShow = showResponse.getExternalState("join-state", key); // Step 3: process the click message var clickRequest = TestDoProcessRequest.builder("join") .setMessages(List.of(buildActionMessage(hitId, hitTime, hitTime + 7L, true, hitTime + 2L))) .setExternalState("join-state", Map.of(key, stateAfterShow)) .setWatermarks(defaultWatermarks()) .build(); var clickResponse = testHarness.doProcess(clickRequest); var stateAfterClick = clickResponse.getExternalState("join-state", key); // Step 4: trigger the timer Timer timer = Timer.builder() .setStreamId("timer") .setKey(key) .setTriggerTimestamp(hitTime + WAIT_SECONDS) .build(); var timerRequest = TestDoProcessRequest.builder("join") .setTimers(List.of(timer)) .setExternalState("join-state", Map.of(key, stateAfterClick)) .build(); var timerResponse = testHarness.doProcess(timerRequest); // Check the final result assertEquals(1, timerResponse.getOutputMessagesFlatten().size()); JoinedAction result = timerResponse.getOutputMessagesFlatten().get(0).getPayload(); assertEquals(hitId, result.getHitId()); assertTrue(result.getClick()); assertTrue(timerResponse.getExternalState("join-state", key).isReset()); } ``` - Kotlin ```kotlin @Test fun testFullJoinFlow() { val hitId = "hit-20" val hitTime = BASE_HIT_TIME // Step 1: process the hit message val hitRequest = TestDoProcessRequest.builder("join") .setMessages(listOf(buildHitMessage(hitId, hitTime, "full-payload"))) .setWatermarks(defaultWatermarks()) .build() val hitResponse = testHarness.doProcess(hitRequest) val key = buildKey(hitId, hitTime) val stateAfterHit = hitResponse.getExternalState("join-state", key)!! // Step 2: process the show message, pass the state from step 1 val showRequest = TestDoProcessRequest.builder("join") .setMessages(listOf(buildActionMessage(hitId, hitTime, hitTime + 3L, false, hitTime + 1L))) .setExternalState("join-state", mapOf(key to stateAfterHit)) .setWatermarks(defaultWatermarks()) .build() val showResponse = testHarness.doProcess(showRequest) val stateAfterShow = showResponse.getExternalState("join-state", key)!! // Step 3: process the click message val clickRequest = TestDoProcessRequest.builder("join") .setMessages(listOf(buildActionMessage(hitId, hitTime, hitTime + 7L, true, hitTime + 2L))) .setExternalState("join-state", mapOf(key to stateAfterShow)) .setWatermarks(defaultWatermarks()) .build() val clickResponse = testHarness.doProcess(clickRequest) val stateAfterClick = clickResponse.getExternalState("join-state", key)!! // Step 4: trigger the timer val timer = Timer.builder() .setStreamId("timer") .setKey(key) .setTriggerTimestamp(hitTime + WAIT_SECONDS) .build() val timerRequest = TestDoProcessRequest.builder("join") .setTimers(listOf(timer)) .setExternalState("join-state", mapOf(key to stateAfterClick)) .build() val timerResponse = testHarness.doProcess(timerRequest) // Check the final result assertEquals(1, timerResponse.getOutputMessagesFlatten().size) val result = timerResponse.getOutputMessagesFlatten()[0].getPayload() as JoinedAction assertEquals(hitId, result.getHitId()) assertTrue(result.getClick()) assertTrue(timerResponse.getExternalState("join-state", key)!!.isReset) } ``` {% endlist %} {% note warning %} `TestComputationHarness` doesn't store state between `doProcess()` calls. Each call is an independent batch. To emulate multi-step processing, you must manually pass the state from the previous step's response to the next step's request. {% endnote %} ## Example: a test with Spring {#example-with-spring} When you use Spring Boot Starter, testing becomes simpler: the `PipelineContext` is created automatically via `FlowAutoConfiguration` based on beans annotated with `@FlowComputation` or `@FlowSourceComputation`. ### Test configuration {#spring-test-config} For tests, you need to replace `GrpcServerExecution` with `NoServerTestExecution` so you don’t start a real gRPC server: {% list tabs group=lang %} - Java ```java @TestConfiguration public class MyTestConfiguration { @Bean public CompanionExecutionConfig companionExecutionConfig() { return new CompanionExecutionConfig(0, new MockEnvironmentReader().worker()); } @Bean public GrpcServerExecution grpcServerExecution( PipelineContext pipelineContext, CompanionExecutionConfig companionExecutionConfig ) { return new NoServerTestExecution(pipelineContext, companionExecutionConfig); } } ``` - Kotlin ```kotlin @TestConfiguration class MyTestConfiguration { @Bean fun companionExecutionConfig(): CompanionExecutionConfig = CompanionExecutionConfig(0, MockEnvironmentReader().worker()) @Bean fun grpcServerExecution( pipelineContext: PipelineContext, companionExecutionConfig: CompanionExecutionConfig ): GrpcServerExecution = NoServerTestExecution(pipelineContext, companionExecutionConfig) } ``` {% endlist %} Key points: - [`MockEnvironmentReader`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/config/MockEnvironmentReader.java) replaces environment variable reading. The `.worker()` method sets `YT_FLOW_MODE=Worker`. - [`NoServerTestExecution`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/java/flow/flow-test-utils/src/main/java/tech/ytsaurus/flow/execution/NoServerTestExecution.java) is a stub for `GrpcServerExecution` that doesn’t start a real gRPC server. - `CompanionExecutionConfig(0, ...)` — a port value of `0` means no real port is allocated. ### Test class {#spring-test-class} {% list tabs group=lang %} - Java ```java @SpringBootTest(classes = MyTestConfiguration.class) class JoinFunctionTest { @Autowired private PipelineContext pipelineContext; private TestComputationHarness testHarness; private TableSchema keySchema; @BeforeEach void init() throws IOException { this.keySchema = TableSchema.builder() .addValue("hash", ColumnValueType.UINT64) .addValue("hit_id", ColumnValueType.STRING) .addValue("hit_time", ColumnValueType.UINT64) .build(); var specPath = Paths.getSourcePath( "yt/yt/flow/examples/java/lb_wait_click_join/test/pipeline.yson"); var txtSpec = Files.readString(Path.of(specPath)); this.testHarness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) // injected via Spring .setPipelineSpec(txtSpec) .build(); } @Test void testHitMessageStoresState() { var messages = List.of(buildHitMessage("hit-1", 1000L, "payload-data")); var request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(Map.of("hit", 0L, "action", 0L)) .build(); var response = testHarness.doProcess(request); assertTrue(response.getOutputMessagesFlatten().isEmpty()); assertEquals(1, response.getOutputTimersFlatten().size()); assertEquals(1, response.getInternalStateSize("join-state")); } // ... helper methods and other tests } ``` - Kotlin ```kotlin @SpringBootTest(classes = [MyTestConfiguration::class]) class JoinFunctionTest { @Autowired private lateinit var pipelineContext: PipelineContext private lateinit var testHarness: TestComputationHarness private lateinit var keySchema: TableSchema @BeforeEach fun init() { keySchema = TableSchema.builder() .addValue("hash", ColumnValueType.UINT64) .addValue("hit_id", ColumnValueType.STRING) .addValue("hit_time", ColumnValueType.UINT64) .build() val specPath = Paths.getSourcePath( "yt/yt/flow/examples/java/lb_wait_click_join/test/pipeline.yson") val txtSpec = Files.readString(Path.of(specPath)) testHarness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) // injected via Spring .setPipelineSpec(txtSpec) .build() } @Test fun testHitMessageStoresState() { val messages = listOf(buildHitMessage("hit-1", 1000L, "payload-data")) val request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(mapOf("hit" to 0L, "action" to 0L)) .build() val response = testHarness.doProcess(request) assertTrue(response.getOutputMessagesFlatten().isEmpty()) assertEquals(1, response.getOutputTimersFlatten().size) assertEquals(1, response.getInternalStateSize("join-state")) } // ... helper methods and other tests } ``` {% endlist %} ### Differences from a test without Spring {#spring-vs-no-spring} | Aspect | Without Spring | With Spring | |--------|----------------|-------------| | Creating `PipelineContext` | Manually: `new PipelineContext()` | Automatically via `FlowAutoConfiguration` | | Registering Computation | `pipelineContext.registerComputation(...)` | Via the `@FlowComputation` or `@FlowSourceComputation` annotation | | Registering streams | `pipelineContext.registerStream(...)` | Via a `FlowStream` bean or `ComputationProvider.getStreams()` | | Injecting dependencies into ProcessFunction | Manually via the constructor | Automatically via `@Autowired` | | Replacing the gRPC server | Not needed (no server) | `NoServerTestExecution` + `MockEnvironmentReader` | ## Integration testing with FlowTestJavaBase {#integration-testing} To run full integration testing of the pipeline (with real C++ workers, queues, and streams), use the `FlowTestJavaBase` base class. ### Dependencies {#integration-dependencies} ``` PEERDIR( yt/yt/flow/library/python/integration_test_base ) ``` ### Setup {#java-test-setup} Your test inherits from `FlowTestJavaBase` and defines two required attributes: ```python from yt.yt.flow.library.python.integration_test_base.yt_flow_java_base import FlowTestJavaBase import yatest.common class TestWordCount(FlowTestJavaBase): JAVA_RUNNER_BINARY_DIR = yatest.common.binary_path( "yt/yt/flow/examples/java/word_count/wordcount/" ) JAVA_MAIN_CLASS = "tech.ytsaurus.flow.examples.wordcount.WordCountApplication" ``` | Attribute | Description | |-----------|-------------| | `JAVA_RUNNER_BINARY_DIR` | Path to the directory with the Java runner binary (contains `run.sh`) | | `JAVA_MAIN_CLASS` | Full name of the pipeline entry-point class: it starts the pipeline and serves as the companion | [Integration test examples (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java) {% note warning %} Integration tests require a deployed YTsaurus cluster and are run via `ya make -ttt`. For fast iteration, use unit tests with `TestComputationHarness` as described above. {% endnote %} Use these principles when writing integration tests for pipelines: * Test end-to-end scenarios. That is: * Write input/source data to the local YT. * In the pipeline spec, mark the sources as `finite=%true`. * For a [Key Visitor stream](https://ytsaurus.tech/docs/concepts/key_visitor.md) that should run during the test, set `finite=%false`. * Run the pipeline. * When the Key Visitor should finish, call the integration test base method [`self.ask_key_visitor_to_complete("", "")`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base/yt_flow_base.py); it switches that stream's [dynamic `finite` parameter](https://ytsaurus.tech/docs/concepts/key_visitor.md#dynamic-params) to `%true`. * Wait for the pipeline to finish. * Check the output data. * Prepare the environment with the same code you use in production: * Test the same pipeline binary that will run in production. * Generate pipeline specs with the same code that generates them for production. * If input/output data require non-trivial serialization/parsing, implement this logic with code shared with production. * Use the [shared test framework](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base) (a `README.md` is available). * Implement a failover test. * Many errors surface when workers fail and other workers take over their tasks. So, run a test with `problems=True` and more than one worker. * Write stable tests. * Remember that in CI, any part of a test might run unexpectedly long. * The ideal runtime for a single test is 20 seconds for any build type. * For sanitizer builds, reduce the amount of input data. * Set all local timeouts with a multiple safety margin. * The test execution logic shouldn’t depend significantly on the current time. For example, a test shouldn’t fail if it starts before midnight and finishes after. ## Debugging tests {#debug} ### Logs {#debug-logs} After a test finishes, you can find its logs in `test-results/py3test/testing_out_stuff` in the test directory. Key logs include: * `run.log` — Python test logs. * `//Controller_...` — Controller logs (`.err` is the process’s stderr; `.log` contains regular logs written via `YT_LOG_...`). * `//Worker_...` — Worker logs. * `//Runner...` — Runner logs. If something isn’t working, check errors in all these logs. The approach to reviewing them is the same as in production — see [Logs](https://ytsaurus.tech/docs/devops/vanilla/diagnostics/logs.md). You can also view logs before the test finishes. To do this, locate the temporary directory where the test runs. The simplest way is to run the test with the `--keep-temps` flag: `ya make --keep-temps -ttt `. In this case, `ya make` won’t delete the temporary directory after the test finishes and will print a link to it in the output. You can also use these bash aliases: ```bash alias curtestdir="ps -f -u $USER | python3 -c \"import sys, re; drs = set(e for e in re.findall(r'[\s=](https://ytsaurus.tech/docs/en//S*testing'%20+%20r'_out_stuff)\b', sys.stdin.read())); print('' if len(drs) == 1 else 'Select first from ' + repr(drs), file=sys.stderr); print(list(drs)[0])\"" alias cdcurtestdir='cd $(curtestdir)' # Get the link to the local YTsaurus UI from the logs. alias curlocalyt='cat $(curtestdir)/stderr 2>/dev/null | grep YT' ``` ### Test framework {#debug-test-framework} You can read about how to set up the environment for better test framework performance and how to influence test parameters in the framework’s [README.md](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base). You configure the behavior of integration tests using `--test-param NAME=VAL`: #| || **Parameter** | **Default** | **Values** | **Action** || || `RUNNER_LOG_LEVEL` | | `Error`, `Info`, `Debug`, … | Sets the logging level for the process that runs the pipeline (runner). || || `PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN` | `0` | `0`, `1` | Pauses the test before stopping Flow processes. Combined with `--test-disable-timeout`, this lets you keep the local YTsaurus and the Flow process federation running for a long time so you can study them at your own pace via the UI. || || `EXTERNAL_YT_CONFIG` | (not set) | YSON — see below | Runs the pipeline on real external YTsaurus clusters instead of the local recipe. || |# Examples: ```bash ya make -A --test-param RUNNER_LOG_LEVEL=Debug ya make -A --test-disable-timeout --test-param PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN=1 ``` ### `EXTERNAL_YT_CONFIG` {#external-yt-config} The local YTsaurus still starts via the recipe, but the test ignores it. Required fields that are common for all clusters: - `path` — base directory. - `tablet_cell_bundle` — bundle for the dynamic tables that are created. - `proxy_role` — RPC proxy role. Optional: `primary_medium` (default `"default"`). The `clusters` list — the first element is primary. Record fields: `cluster_name` (required), `proxy_url` (defaults to `cluster_name`). Authorization: when using an external YTsaurus, `YT_TOKEN`/`YT_USER` from the local recipe are cleared; yt-wrapper picks up the token from `~/.yt/token`. Isolation: `work_yt_path = path//`; the `path/` directory is deleted and recreated once per class in `setup_class`. Example: ```bash ya make -A --test-param 'EXTERNAL_YT_CONFIG={path="//tmp/yt_flow";tablet_cell_bundle=default;proxy_role=default;clusters=[{cluster_name=};];}' ``` ## See also - [Computation (Java)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (Java)](https://ytsaurus.tech/docs/en/state.md) - [Testing (Python)](https://ytsaurus.tech/docs/python/testing.md) # Java companion profiling in YTsaurus Flow (Java) Java companion includes continuous profiling based on [JDK Flight Recorder](https://wiki.openjdk.org/spaces/jmc/pages/37584926/Overview) (JFR). Use profiling to diagnose performance issues and analyze incidents. ## Capabilities {#capabilities} JFR files contain comprehensive telemetry for the Java application and JVM. This lets you: - Analyze performance — identify CPU/RAM-intensive methods and bottlenecks. - Diagnose memory and GC issues. - Investigate incidents after the fact using data from the failure period. JFR has low overhead and is a standard tool for monitoring Java applications in production. You can open and analyze the resulting JFR files with [Java Mission Control (JMC)](https://wiki.openjdk.org/spaces/jmc/overview), the standard tool for analyzing JFR recordings. ## How it works {#how-it-works} {#jfr-defaults} Profiling is enabled by default via the following JVM options: ``` -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -XX:StartFlightRecording=disk=true,settings=profile,maxage=24h,maxsize=1000m,dumponexit=true,filename=/dump.jfr -XX:FlightRecorderOptions=repository=/jfr,maxchunksize=30M ``` JFR is written to disk in 30 MB chunks to the directory specified by the `YT_FLOW_COMPANION_LOG_DIR` environment variable. Data is kept for up to 24 hours and is limited to a total size of 1 GB. You manage JFR via [environment variables](https://ytsaurus.tech/docs/en/env-variables.md): - `YT_FLOW_COMPANION_JFR_DISABLED` — if set to `1`, it completely disables JFR options. - `YT_FLOW_COMPANION_JFR_OPTS` — custom JFR options separated by spaces, which replace the default values (ignored if `YT_FLOW_COMPANION_JFR_DISABLED` is set). ## Other default diagnostic JVM options {#default-jvm-options} ### GC logging {#gc-defaults} Enabled by default: ``` -Xlog:gc:file=/gc.log:time,uptime:filecount=10,filesize=50m ``` Where `` is the value of the `YT_FLOW_COMPANION_LOG_DIR` environment variable. You can override this via `YT_FLOW_COMPANION_GC_LOG_OPTS` or disable it via `YT_FLOW_COMPANION_GC_LOG_DISABLED=1`. ### Crash dump and OOM handling {#crash-defaults} Always enabled: ``` -XX:+ExitOnOutOfMemoryError -XX:+CreateCoredumpOnCrash -XX:ErrorFile=/hs_err_%p.log ``` Where `` is the value of the `YT_FLOW_COMPANION_LOG_DIR` environment variable. ### Heap dump configuration (recommended) {#heap-dump} Heap dump options are **not** set by default and must be configured via the `YT_FLOW_COMPANION_JVM_EXTRA_OPTS` environment variable: ``` YT_FLOW_COMPANION_JVM_EXTRA_OPTS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=" ``` Where `` is the path to the directory for storing the heap dump. {% note warning %} A heap dump can be very large — up to the full size of the heap. We recommend allocating a separate persistent volume for the heap dump. {% endnote %} ## Getting profiling data {#get-profiling-data} ### Via SSH {#via-ssh} Connect to the worker via SSH and copy the JFR files to your development machine from the directory specified by the `YT_FLOW_COMPANION_LOG_DIR` environment variable. ### Via CLI {#via-cli} To download the latest complete JFR chunk from the companion, run this command: ```bash # - path to the pipeline directory in YT. # - IP address of the worker with rpc_port, for example "[IPv6]:port". ya run yt/yt/flow/tools/download_jfr --pipeline-path --worker ``` You can copy the ready-to-use command for downloading JFR from each companion in the YT web interface, on the pipeline page, in the **Workers** tab. # Environment variables in YTsaurus Flow (Java) The Java companion supports a set of environment variables to control the application and JVM behavior. You need to set the environment variables for the **worker** process. ## General variables {#general} #| || **Variable** | **Description** | **Default** || || `YT_FLOW_COMPANION_JOB_TTL` | TTL for the Job cache, format {number}{unit}. | `10m` || |# ## Variables for diagnostics and JVM {#diagnostics} For more details about default JVM options, profiling, and configuration, see the section [Java companion profiling](https://ytsaurus.tech/docs/en/profiling.md). #| || **Variable** | **Description** | **Default** || || `YT_FLOW_COMPANION_LOG_DIR` | Directory for companion logs and JVM diagnostic files. | `./logs` || || `YT_FLOW_COMPANION_JFR_DISABLED` | If set to `1`, disables Java Flight Recorder options. | JFR is enabled || || `YT_FLOW_COMPANION_JFR_OPTS` | Custom JFR options separated by a space, replacing the default values. Ignored if `YT_FLOW_COMPANION_JFR_DISABLED` is set. | [see profiling](https://ytsaurus.tech/docs/en/profiling.md#jfr-defaults) || || `YT_FLOW_COMPANION_GC_LOG_DISABLED` | If set to `1`, disables GC logging. | GC logging is enabled || || `YT_FLOW_COMPANION_GC_LOG_OPTS` | Custom GC logging options separated by a space, replacing the default values. Ignored if `YT_FLOW_COMPANION_GC_LOG_DISABLED` is set. | [see profiling](https://ytsaurus.tech/docs/en/profiling.md#gc-defaults) || || `YT_FLOW_COMPANION_JVM_EXTRA_OPTS` | Additional JVM options added after all default options. | — || |# # Word Count in YTsaurus Flow (Java) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) reads a [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) of words and counts how many times each word appears, using a YSON state. This example shows how to configure a [companion](https://ytsaurus.tech/concepts/glossary.md#companion) with Spring Boot. [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/word_count) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/word_count) ## Components ### WordCountApplication This single entry point starts the pipeline when `YT_FLOW_MODE` is unset and serves as its companion when the worker sets `YT_FLOW_MODE=Worker`: {% list tabs group=lang %} - Java ```java @SpringBootApplication public class WordCountApplication { public static void main(String[] args) { new SpringApplicationBuilder(WordCountApplication.class) .run(args); } } ``` - Kotlin ```kotlin @SpringBootApplication open class WordCountApplication { companion object { @JvmStatic fun main(args: Array) { SpringApplicationBuilder(WordCountApplication::class.java) .run(*args) } } } ``` {% endlist %} The gRPC server starts automatically through Spring Boot auto-config. ### Stream registration Typed streams are registered declaratively. The message POJO uses `@FlowMessage` to list its stream IDs and `@Entity` to define the schema. Spring Boot scans and registers these classes. The `mapper` computation is registered with `@FlowComputation` on `WordCountMapper`: {% list tabs group=lang %} - Java ```java @Entity @FlowMessage(streamIds = {"words"}) public class Word { private String word; public Word() { } public Word(String word) { this.word = word; } public String getWord() { return word; } public void setWord(String word) { this.word = word; } } ``` - Kotlin ```kotlin @Entity @FlowMessage(streamIds = ["words"]) class Word { var word: String? = null constructor() constructor(word: String?) { this.word = word } } ``` {% endlist %} - `reader` is a source computation without a process function; the C++ worker reads and parses its input. - `mapper` is implemented by `WordCountMapper` with `@FlowComputation(id = "mapper")`. - `@FlowMessage(streamIds = {"words"})` on `Word` registers the typed `words` stream. ### WordCountMapper This is the processing function that counts words using the [YsonStateAccessor](https://ytsaurus.tech/docs/state.md#yson-state): {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { Word input = message.getPayload(); StateAccessor stateAccessor = ctx.getState(WORD_STATE, message); var state = stateAccessor.getOrDefault(new WordCountState(input.getWord(), 0)); state.setCount(state.getCount() + 1); } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val input: Word = message.getPayload() val stateAccessor = ctx.getState(WORD_STATE, message) val state = stateAccessor.getOrDefault(WordCountState(input.word, 0)) state.count += 1 } ``` {% endlist %} The `@FlowComputation(id = "mapper")` annotation registers the class as a computation and makes it a Spring bean (it’s meta-annotated with `@Component`). ## Key patterns - **Spring Boot auto-config**: you don’t need to manually create `PipelineContext` and `GrpcServerExecution`. - **@FlowComputation**: the processing function becomes both a Spring bean and a computation; you can use dependency injection. - **@FlowMessage**: register typed streams from annotated message classes. - **FlowStreams.typed**: get typed access to messages via Java objects. ## Running Run the pipeline with the `WordCountApplication` class: ```bash ./run.sh tech.ytsaurus.flow.examples.wordcount.WordCountApplication --config pipeline.yson --flow-bin flow_server ``` The worker starts the same class with `YT_FLOW_MODE=Worker`; Spring Boot then runs the companion gRPC server. ## See also - [Quick start](https://ytsaurus.tech/docs/getting-started.md) - [Computation](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) # Shuffle in YTsaurus Flow (Java) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) reads the [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) of events, groups them by key, and counts the number of unique events using an external [state](https://ytsaurus.tech/concepts/glossary.md#state) (ExternalStateAccessor). This example shows how to configure a [companion](https://ytsaurus.tech/concepts/glossary.md#companion) with Spring Boot. [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/shuffle) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/shuffle) ## Companion components ### EventMapper This is a process function for the source-[computation](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) `reader`. It parses and transforms the input data. The version below is simplified. In the [actual code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/shuffle), you also parse the `data` field as JSON using Jackson’s `ObjectMapper`: {% list tabs group=lang %} - Java ```java @SuppressWarnings("unchecked") @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { var messageBuilder = ctx.createMessageBuilder("event"); String data = message.get("data", String.class); try { Map map = ysonMapper.readValue(data, Map.class); messageBuilder.set("value", map.get("value")); messageBuilder.set("key_a", map.get("key_a")); messageBuilder.set("key_b", map.get("key_b")); messageBuilder.set("key_c", map.get("key_c")); messageBuilder.set("key_d", map.get("key_d")); output.addMessage(messageBuilder.finish()); } catch (JsonProcessingException e) { log.error("Error parsing json from data filed", e); throw new RuntimeException(e); } } ``` - Kotlin ```kotlin @Suppress("UNCHECKED_CAST") override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val messageBuilder = ctx.createMessageBuilder("event") val data = message.get("data", String::class.java) try { val map = ysonMapper.readValue(data, Map::class.java) as Map messageBuilder.set("value", map["value"]) messageBuilder.set("key_a", map["key_a"]) messageBuilder.set("key_b", map["key_b"]) messageBuilder.set("key_c", map["key_c"]) messageBuilder.set("key_d", map["key_d"]) output.addMessage(messageBuilder.finish()) } catch (e: JsonProcessingException) { log.error("Error parsing json from data filed", e) throw RuntimeException(e) } } ``` {% endlist %} ### EventReducer This is a process function that uses [ExternalStateAccessor](https://ytsaurus.tech/docs/state.md#external-state) to count the number of events: {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { ExternalStateAccessor externalStateAccessor = ctx.getState(SHUFFLE_STATE, message); Payload state = externalStateAccessor.getOrDefault(); PayloadBuilder stateBuilder = state.toBuilder(); if (state.get("count", Long.class) == null) { stateBuilder.set("count", 1L); } else { stateBuilder.set("count", state.get("count", Long.class) + 1); } externalStateAccessor.set(stateBuilder.finish()); } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val externalStateAccessor = ctx.getState(SHUFFLE_STATE, message) val state = externalStateAccessor.getOrDefault() val stateBuilder = state.toBuilder() val count = state.get("count", Long::class.java) ?: 0L stateBuilder.set("count", count + 1) externalStateAccessor.set(stateBuilder.finish()) } ``` {% endlist %} How it works: - You get an `ExternalStateAccessor` for the state `"shuffle-state"`, which is bound to the key of the current message. - You extract the current state value. If there is no state, `getOrDefault()` returns an empty `Payload`. - You create a `PayloadBuilder` from the current state and increment the counter. - You save the updated state. ### Registering computations You register the source computation `reader` with the `@FlowSourceComputation` annotation, and the transformation `reducer` with the `@FlowComputation` annotation: {% list tabs group=lang %} - Java ```java @FlowSourceComputation(id = "reader") ``` ```java @FlowComputation(id = "reducer") ``` - Kotlin ```kotlin @FlowSourceComputation(id = "reader") ``` ```kotlin @FlowComputation(id = "reducer") ``` {% endlist %} ### PipelineMain This single entry point starts the pipeline or serves as its companion, depending on `YT_FLOW_MODE`. {% list tabs group=lang %} - Java ```java public static void main(String[] args) throws Exception { new SpringApplicationBuilder(PipelineMain.class).run(args); } ``` - Kotlin ```kotlin @SpringBootApplication open class PipelineMain { companion object { @JvmStatic fun main(args: Array) { SpringApplicationBuilder(PipelineMain::class.java) .run(*args) } } } ``` {% endlist %} ## Key patterns - **Configuration via Spring Boot**: you register computations with the `@FlowSourceComputation` / `@FlowComputation` annotations; `flow-spring-boot-starter` manages the gRPC server lifecycle. - **ExternalStateAccessor**: you work with the external state using `Payload` and `PayloadBuilder`. - **SourceComputation with ProcessFunction**: the `reader` uses `EventMapper` to transform input data on the companion side. ## See also - [Quick start (Java)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Java)](https://ytsaurus.tech/docs/computation.md) # Async Request in YTsaurus Flow (Java) Use this pipeline example to implement an event-driven request–response cycle. Incoming events trigger requests to the handler. Responses go back to the same computation and accumulate in the external state. This example shows a cyclic pipeline topology and shared use of external state. [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/async_request) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/async_request) ## Components ### StateKeeperFunction You handle two input streams — `event` and `response`. When you get an `event`, you generate a request with a unique `request_id` and emit it to the `request` stream. When you get a `response`, you accumulate the total length of responses in the `total_length` field of the external state: {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { String streamId = message.getStreamId(); if ("event".equals(streamId)) { handleEvent(message, output, ctx); } else if ("response".equals(streamId)) { handleResponse(message, ctx); } else { throw new IllegalArgumentException("Unknown streamId: " + streamId); } } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val streamId = message.getStreamId() if ("event" == streamId) { handleEvent(message, output, ctx) } else if ("response" == streamId) { handleResponse(message, ctx) } else { throw IllegalArgumentException("Unknown streamId: $streamId") } } ``` {% endlist %} ### RequestProcessorFunction This is a stateless computation: you get a request from the `request` stream, calculate the string length, and immediately send the response to the `response` stream: {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { long requestId = message.get("request_id", Long.class); long key = message.get("key", Long.class); String request = message.get("request", String.class); long length = request == null ? 0L : (long) request.length(); log.debug("Processing request (requestId={}, key={}, length={})", requestId, key, length); var builder = ctx.createMessageBuilder("response"); builder.set("request_id", requestId) .set("key", key) .set("length", length); output.addMessage(builder.finish()); } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val requestId = message.get("request_id", Long::class.java) val key = message.get("key", Long::class.java) val request = message.get("request", String::class.java) val length = if (request == null) 0L else request.length.toLong() log.debug("Processing request (requestId={}, key={}, length={})", requestId, key, length) val builder = ctx.createMessageBuilder("response") builder.set("request_id", requestId) .set("key", key) .set("length", length) output.addMessage(builder.finish()) } ``` {% endlist %} ### Registering computations You register the `state` and `processor` computations with the `@FlowComputation` annotation on their process function classes: {% list tabs group=lang %} - Java ```java @FlowComputation(id = "state") ``` ```java @FlowComputation(id = "processor") ``` - Kotlin ```kotlin @FlowComputation(id = "state") ``` ```kotlin @FlowComputation(id = "processor") ``` {% endlist %} ### PipelineMain This single entry point starts the pipeline or serves as its companion, depending on `YT_FLOW_MODE`. {% list tabs group=lang %} - Java ```java public static void main(String[] args) throws Exception { new SpringApplicationBuilder(PipelineMain.class).run(args); } ``` - Kotlin ```kotlin @JvmStatic fun main(args: Array) { SpringApplicationBuilder(PipelineMain::class.java).run(*args) } ``` {% endlist %} ## Key patterns - **Cyclic topology**: the `response` stream goes back to the `state` computation, closing the cycle `event → request → response → state`. Flow explicitly supports such graphs. - **Routing by `streamId`**: one function handles multiple input streams. You determine the message type using `message.getStreamId()`. - **ExternalStateAccessor with PayloadBuilder**: you update the `total_length` field selectively: `current.toBuilder()` → make changes → `stateAccessor.set(updated.finish())`. - **Configuration via Spring Boot**: you register computations with the `@FlowComputation` annotation; `flow-spring-boot-starter` manages the gRPC server lifecycle. ## See also - [Quick start (Java)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Java)](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/async_request.md) - [Extended version with retries](https://ytsaurus.tech/docs/en/retryable_async_request.md) # Retryable Async Request in YTsaurus Flow (Java) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) extends [AsyncRequest](https://ytsaurus.tech/docs/en/async_request.md): the request handler now supports retry attempts with a delay. If a failure occurs, `RequestProcessorFunction` saves the request state in the internal [state](https://ytsaurus.tech/concepts/glossary.md#state) and sets a timer for 5 seconds; success is determined by the predicate `(requestId + failedAttempts) % 3 == 0`. [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/retryable_async_request) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/retryable_async_request) ## Components ### RequestProcessorFunction This implements the retry logic using an internal YSON state and timers. When you receive a request, you save it to the state and call `tryRequest`. When the timer fires, you load the state and retry the attempt: {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { long requestId = message.get("request_id", Long.class); long key = message.get("key", Long.class); String request = message.get("request", String.class); log.debug("Received request message (requestId={}, key={})", requestId, key); StateAccessor accessor = ctx.getState(REQUEST_STATE, message); RequestState state = new RequestState(requestId, key, request, 0); accessor.set(state); tryRequest(state, accessor, output, ctx); } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val requestId = message.get("request_id", Long::class.java)!! val key = message.get("key", Long::class.java)!! val request = message.get("request", String::class.java) log.debug("Received request message (requestId={}, key={})", requestId, key) val accessor: StateAccessor = ctx.getState(REQUEST_STATE, message) val state = RequestState(requestId, key, request, 0) accessor.set(state) tryRequest(state, accessor, output, ctx) } ``` {% endlist %} {% list tabs group=lang %} - Java ```java @Override public void onTimer(Timer timer, OutputCollector output, RuntimeContext ctx) { log.debug("Timer fired, retrying request"); StateAccessor accessor = ctx.getState(REQUEST_STATE, timer); RequestState state = accessor.get(); if (state == null) { throw new IllegalStateException("No request state found on timer fire"); } tryRequest(state, accessor, output, ctx); } ``` - Kotlin ```kotlin override fun onTimer(timer: Timer, output: OutputCollector, ctx: RuntimeContext) { log.debug("Timer fired, retrying request") val accessor: StateAccessor = ctx.getState(REQUEST_STATE, timer) val state = accessor.get() ?: throw IllegalStateException("No request state found on timer fire") tryRequest(state, accessor, output, ctx) } ``` {% endlist %} The helper method `tryRequest` checks the success predicate. If the attempt fails, it increments the attempt counter, saves the state, and schedules the next attempt. ### RequestState This is a YSON-serializable model of the request state. It stores `requestId`, `key`, the request text `request`, and the counter `failedAttempts`: {% list tabs group=lang %} - Java ```java @Entity public class RequestState { @Column(name = "request_id") private long requestId; private long key; private String request; @Column(name = "failed_attempts") private int failedAttempts; public RequestState() { } public RequestState(long requestId, long key, String request, int failedAttempts) { this.requestId = requestId; this.key = key; this.request = request; this.failedAttempts = failedAttempts; } public long getRequestId() { return requestId; } public void setRequestId(long requestId) { this.requestId = requestId; } public long getKey() { return key; } public void setKey(long key) { this.key = key; } public String getRequest() { return request; } public void setRequest(String request) { this.request = request; } public int getFailedAttempts() { return failedAttempts; } public void setFailedAttempts(int failedAttempts) { this.failedAttempts = failedAttempts; } @Override public String toString() { return "RequestState{" + "requestId=" + requestId + ", key=" + key + ", request='" + request + '\'' + ", failedAttempts=" + failedAttempts + '}'; } } ``` - Kotlin ```kotlin @Entity class RequestState { @Column(name = "request_id") var requestId: Long = 0 var key: Long = 0 var request: String? = null @Column(name = "failed_attempts") var failedAttempts: Int = 0 constructor() constructor(requestId: Long, key: Long, request: String?, failedAttempts: Int) { this.requestId = requestId this.key = key this.request = request this.failedAttempts = failedAttempts } override fun toString(): String { return "RequestState{" + "requestId=" + requestId + ", key=" + key + ", request='" + request + '\'' + ", failedAttempts=" + failedAttempts + '}' } } ``` {% endlist %} ### StateKeeperFunction This is identical to the `StateKeeperFunction` from [AsyncRequest](https://ytsaurus.tech/docs/en/async_request.md): it processes the `event` and `response` streams and accumulates `total_length` in the external state. The retry logic is fully encapsulated in `RequestProcessorFunction`. {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { String streamId = message.getStreamId(); if ("event".equals(streamId)) { handleEvent(message, output, ctx); } else if ("response".equals(streamId)) { handleResponse(message, ctx); } else { throw new IllegalArgumentException("Unknown streamId: " + streamId); } } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val streamId = message.streamId when (streamId) { "event" -> handleEvent(message, output, ctx) "response" -> handleResponse(message, ctx) else -> throw IllegalArgumentException("Unknown streamId: $streamId") } } ``` {% endlist %} ### Registering computations You register the `state` and `processor` computations with the `@FlowComputation` annotation on the classes of their process functions: {% list tabs group=lang %} - Java ```java @FlowComputation(id = "state") ``` ```java @FlowComputation(id = "processor") ``` - Kotlin ```kotlin @FlowComputation(id = "state") ``` ```kotlin @FlowComputation(id = "processor") ``` {% endlist %} ### PipelineMain This single entry point starts the pipeline or serves as its companion, depending on `YT_FLOW_MODE`. {% list tabs group=lang %} - Java ```java public static void main(String[] args) throws Exception { new SpringApplicationBuilder(PipelineMain.class).run(args); } ``` - Kotlin ```kotlin @SpringBootApplication open class PipelineMain { companion object { @JvmStatic fun main(args: Array) { SpringApplicationBuilder(PipelineMain::class.java) .run(*args) } } } ``` {% endlist %} ## Key patterns - **Retries via timers**: if a failure occurs, `output.addTimer(now + 5, 0L)` schedules a repeated call to `onTimer`; the state between attempts is stored in `YsonStateAccessor`. - **Success predicate**: `(requestId + failedAttempts) % 3 == 0` simulates an unstable external service; in real tasks, you replace this with a check of the HTTP status or another indicator. - **Clearing the state after success**: `accessor.clear()` is called only on a successful response, preventing reprocessing. - **Separation of concerns**: the session state logic (`StateKeeperFunction`) is separated from the retry logic (`RequestProcessorFunction`), which makes it easier to test each part independently. ## Differences from AsyncRequest | Aspect | AsyncRequest | RetryableAsyncRequest | |--------|--------------|-----------------------| | `RequestProcessorFunction` | Stateless, responds immediately | Has internal state and timers | | Failure handling | Not provided | Retry with a 5-second delay | | Request state | Absent | `RequestState` in YSON | ## See also - [Quick start (Java)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Java)](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) - [Basic version without retries](https://ytsaurus.tech/docs/en/async_request.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/retryable_async_request.md) # Wait Click Join in YTsaurus Flow (Java) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) performs a join of [streams](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) of hits and actions within a time window. For each hit, the system waits for related actions (show, click) within a specified time interval, then generates a merged event. [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/wait_click_join) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/wait_click_join) For a detailed description of the task and join logic, see the [C++ version description](https://ytsaurus.tech/cpp/examples/wait_click_join.md). ## Problem statement {#problem-statement} You have a system with three types of events: - **hit** — a user request event. It contains `hit_id`, `hit_time`, and `hit_payload`. - **action show** — an event indicating that the user saw the response. It contains `hit_id`, `hit_time`, `action_time`, and `is_click = false`. - **action click** — an event indicating that the user clicked the response. It contains `hit_id`, `hit_time`, `action_time`, and `is_click = true`. Data arrives in two streams: `hit` and `action`. The output produces a join: for each show, you attach information about whether a click occurred and the `hit_payload` from the hit. Hits without shows are ignored. The system waits for action events for `wait_for_actions` seconds after `hit_time`. ## Data flow diagram {#data-flow} ```mermaid flowchart TD hit_queue["Hit Queue
(QYT)"] action_queue["Action Queue
(QYT)"] joined_action_queue["Joined Action Queue
(QYT)"] hit_reader["hit_reader
(passthrough computation)"] action_reader["action_reader
(passthrough computation)"] join_computation["join computation
(with companion)"] state["external state
(dynamic table)"] join_computation --> state state --> join_computation hit_queue --> hit_reader action_queue --> action_reader action_reader --> join_computation hit_reader --> join_computation join_computation --> joined_action_queue ``` ## JoinProcessFunction The core pipeline logic is implemented in `JoinProcessFunction`, which processes two streams — `hit` and `action`: {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { var streamId = message.getStreamId(); String hitId; Long hitTime; Hit hit = null; Action action = null; if ("hit".equals(streamId)) { hit = message.getPayload(); hitId = hit.getHitId(); hitTime = hit.getHitTime(); } else if ("action".equals(streamId)) { action = message.getPayload(); hitId = action.getHitId(); hitTime = action.getHitTime(); } else { throw new IllegalArgumentException("Unknown streamId: %s".formatted(streamId)); } log.debug("Process message (Key: {}, HitId: {}, HitTime: {})", message.getKey(), hitId, hitTime); long waitTime = parseDuration(ctx.getComputationParameters().get("wait_for_actions").stringValue()).toSeconds(); // [BEGIN timer_setup] long maxTime = hitTime + waitTime; // All messages out of wait range would be dropped. if (message.getEventTimestamp() >= maxTime) { log.warn("Message is out of wait range, skip (HitId: {}, HitTime: {}, MaxTime: {})", hitId, hitTime, maxTime); return; } // Drop late data. // [BEGIN late_data_check] if (message.getEventTimestamp() < ctx.getEpochInputEventWatermark()) { log.warn("Late message, skip (HitId: {}, EventTimestamp: {}, InputEventWatermark: {})", hitId, message.getEventTimestamp(), ctx.getEpochInputEventWatermark()); return; } // [END late_data_check] // [BEGIN state_update] ExternalStateAccessor stateAccessor = ctx.getState(JOINED_ACTION_STATE, message); PayloadBuilder joinState = stateAccessor.getOrDefault().toBuilder(); if ("hit".equals(streamId)) { log.debug("Process hit message (HitId: {}, HitPayload: {}, Hit: {})", hitId, hit.getHitPayload(), hit); joinState.set("hit_payload", hit.getHitPayload()); } else { log.debug("Process action message (HitId: {}, ActionTime: {}, Action: {})", hitId, action.getActionTime(), action); if (Boolean.TRUE.equals(action.isClick())) { joinState.set("click_time", action.getActionTime()); } else { joinState.set("show_time", action.getActionTime()); } } log.debug("Set state (KeyHitId: {}, State: {})", hitId, joinState); stateAccessor.set(joinState.finish()); // [END state_update] log.debug("Add timer for Key (MaxTime: {}, HitTime: {})", maxTime, hitTime); output.addTimer(maxTime, hitTime); // [END timer_setup] } // [END on_message] // [BEGIN on_timer] @Override public void onTimer(Timer timer, OutputCollector output, RuntimeContext ctx) { // Key to pojo conversion is not supported yet. String hitId = timer.getKey().get("hit_id", String.class); log.debug("Process timer (KeyHitId: {}, Timer: {})", hitId, timer); ExternalStateAccessor stateAccessor = ctx.getState(JOINED_ACTION_STATE, timer); Payload joinState = stateAccessor.get(); if (joinState == null) { throw new IllegalStateException("Empty state for timer: " + timer); } log.debug("Process state (KeyHitId: {}, State: {})", hitId, joinState); if ("timer".equals(timer.getStreamId())) { // ExternalState to pojo conversion is not supported yet. if (joinState.get("show_time", Long.class) != null && joinState.get("hit_payload", String.class) != null && joinState.get("show_time", Long.class) != 0) { log.debug("Click join: {}", joinState); JoinedAction joinedAction = new JoinedAction(); long hitTime = timer.getKey().get("hit_time", Long.class); joinedAction.setHitId(hitId); joinedAction.setHitTime(hitTime); joinedAction.setShowTime(joinState.get("show_time", Long.class)); if (joinState.get("click_time", Long.class) != null && joinState.get("click_time", Long.class) != 0) { joinedAction.setClick(true); joinedAction.setClickTime(joinState.get("click_time", Long.class)); } else { joinedAction.setClick(false); joinedAction.setClickTime(0L); } joinedAction.setHitPayload(joinState.get("hit_payload", String.class)); output.addMessage(Message.builder().setStreamId("joined_action").setPayload(joinedAction).build()); } stateAccessor.clear(); } else { throw new IllegalArgumentException("Unknown streamId: %s".formatted(timer.getStreamId())); } } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val streamId = message.streamId val hitId: String val hitTime: Long var hit: Hit? = null var action: Action? = null if ("hit" == streamId) { hit = message.getPayload() hitId = hit.hitId!! hitTime = hit.hitTime!! } else if ("action" == streamId) { action = message.getPayload() hitId = action.hitId!! hitTime = action.hitTime!! } else { throw IllegalArgumentException("Unknown streamId: $streamId") } log.debug("Process message (Key: {}, HitId: {}, HitTime: {})", message.key, hitId, hitTime) val waitTime = parseDuration( ctx.computationParameters.get("wait_for_actions")!!.stringValue() ).toSeconds() // [BEGIN timer_setup] val maxTime = hitTime + waitTime // All messages out of wait range would be dropped. if (message.eventTimestamp >= maxTime) { log.warn( "Message is out of wait range, skip (HitId: {}, HitTime: {}, MaxTime: {})", hitId, hitTime, maxTime ) return } // Drop late data. // [BEGIN late_data_check] if (message.eventTimestamp < ctx.epochInputEventWatermark) { log.warn( "Late message, skip (HitId: {}, EventTimestamp: {}, InputEventWatermark: {})", hitId, message.eventTimestamp, ctx.epochInputEventWatermark ) return } // [END late_data_check] // [BEGIN state_update] val stateAccessor = ctx.getState(JOINED_ACTION_STATE, message) val joinState = stateAccessor.orDefault.toBuilder() if ("hit" == streamId) { log.debug( "Process hit message (HitId: {}, HitPayload: {}, Hit: {})", hitId, hit!!.hitPayload, hit ) joinState.set("hit_payload", hit.hitPayload) } else { log.debug( "Process action message (HitId: {}, ActionTime: {}, Action: {})", hitId, action!!.actionTime, action ) if (action.isClick() == true) { joinState.set("click_time", action.actionTime) } else { joinState.set("show_time", action.actionTime) } } log.debug("Set state (KeyHitId: {}, State: {})", hitId, joinState) stateAccessor.set(joinState.finish()) // [END state_update] log.debug("Add timer for Key (MaxTime: {}, HitTime: {})", maxTime, hitTime) output.addTimer(maxTime, hitTime) // [END timer_setup] } // [END on_message] // [BEGIN on_timer] override fun onTimer(timer: Timer, output: OutputCollector, ctx: RuntimeContext) { // Key to pojo conversion is not supported yet. val hitId = timer.key.get("hit_id", String::class.java) log.debug("Process timer (KeyHitId: {}, Timer: {})", hitId, timer) val stateAccessor = ctx.getState(JOINED_ACTION_STATE, timer) val joinState = stateAccessor.get() ?: throw IllegalStateException("Empty state for timer: $timer") log.debug("Process state (KeyHitId: {}, State: {})", hitId, joinState) if ("timer" == timer.streamId) { // ExternalState to pojo conversion is not supported yet. if (joinState.get("show_time", Long::class.java) != null && joinState.get("hit_payload", String::class.java) != null && joinState.get("show_time", Long::class.java) != 0L ) { log.debug("Click join: {}", joinState) val joinedAction = JoinedAction() val hitTime = timer.key.get("hit_time", Long::class.java) joinedAction.hitId = hitId joinedAction.hitTime = hitTime joinedAction.showTime = joinState.get("show_time", Long::class.java) if (joinState.get("click_time", Long::class.java) != null && joinState.get("click_time", Long::class.java) != 0L ) { joinedAction.isClick = true joinedAction.clickTime = joinState.get("click_time", Long::class.java) } else { joinedAction.isClick = false joinedAction.clickTime = 0L } joinedAction.hitPayload = joinState.get("hit_payload", String::class.java) output.addMessage( Message.builder().setStreamId("joined_action").setPayload(joinedAction).build() ) } stateAccessor.clear() } else { throw IllegalArgumentException("Unknown streamId: ${timer.streamId}") } } ``` {% endlist %} ## Key patterns ### Late data filtering {#late-data-check} {% list tabs group=lang %} - Java ```java if (message.getEventTimestamp() < ctx.getEpochInputEventWatermark()) { log.warn("Late message, skip (HitId: {}, EventTimestamp: {}, InputEventWatermark: {})", hitId, message.getEventTimestamp(), ctx.getEpochInputEventWatermark()); return; } ``` - Kotlin ```kotlin if (message.eventTimestamp < ctx.epochInputEventWatermark) { log.warn( "Late message, skip (HitId: {}, EventTimestamp: {}, InputEventWatermark: {})", hitId, message.eventTimestamp, ctx.epochInputEventWatermark ) return } ``` {% endlist %} Messages that arrive with an event time less than the current [watermark](https://ytsaurus.tech/concepts/glossary.md#timestamps-and-watermarks) are considered late and are ignored. For more details about watermarks, see the [Watermarks & Timers](https://ytsaurus.tech/concepts/watermarks.md) section. ### [Timers](https://ytsaurus.tech/concepts/glossary.md#timer) for closing the window {% list tabs group=lang %} - Java ```java long maxTime = hitTime + waitTime; // All messages out of wait range would be dropped. if (message.getEventTimestamp() >= maxTime) { log.warn("Message is out of wait range, skip (HitId: {}, HitTime: {}, MaxTime: {})", hitId, hitTime, maxTime); return; } // Drop late data. // [BEGIN late_data_check] if (message.getEventTimestamp() < ctx.getEpochInputEventWatermark()) { log.warn("Late message, skip (HitId: {}, EventTimestamp: {}, InputEventWatermark: {})", hitId, message.getEventTimestamp(), ctx.getEpochInputEventWatermark()); return; } // [END late_data_check] // [BEGIN state_update] ExternalStateAccessor stateAccessor = ctx.getState(JOINED_ACTION_STATE, message); PayloadBuilder joinState = stateAccessor.getOrDefault().toBuilder(); if ("hit".equals(streamId)) { log.debug("Process hit message (HitId: {}, HitPayload: {}, Hit: {})", hitId, hit.getHitPayload(), hit); joinState.set("hit_payload", hit.getHitPayload()); } else { log.debug("Process action message (HitId: {}, ActionTime: {}, Action: {})", hitId, action.getActionTime(), action); if (Boolean.TRUE.equals(action.isClick())) { joinState.set("click_time", action.getActionTime()); } else { joinState.set("show_time", action.getActionTime()); } } log.debug("Set state (KeyHitId: {}, State: {})", hitId, joinState); stateAccessor.set(joinState.finish()); // [END state_update] log.debug("Add timer for Key (MaxTime: {}, HitTime: {})", maxTime, hitTime); output.addTimer(maxTime, hitTime); ``` - Kotlin ```kotlin val maxTime = hitTime + waitTime // All messages out of wait range would be dropped. if (message.eventTimestamp >= maxTime) { log.warn( "Message is out of wait range, skip (HitId: {}, HitTime: {}, MaxTime: {})", hitId, hitTime, maxTime ) return } // Drop late data. // [BEGIN late_data_check] if (message.eventTimestamp < ctx.epochInputEventWatermark) { log.warn( "Late message, skip (HitId: {}, EventTimestamp: {}, InputEventWatermark: {})", hitId, message.eventTimestamp, ctx.epochInputEventWatermark ) return } // [END late_data_check] // [BEGIN state_update] val stateAccessor = ctx.getState(JOINED_ACTION_STATE, message) val joinState = stateAccessor.orDefault.toBuilder() if ("hit" == streamId) { log.debug( "Process hit message (HitId: {}, HitPayload: {}, Hit: {})", hitId, hit!!.hitPayload, hit ) joinState.set("hit_payload", hit.hitPayload) } else { log.debug( "Process action message (HitId: {}, ActionTime: {}, Action: {})", hitId, action!!.actionTime, action ) if (action.isClick() == true) { joinState.set("click_time", action.actionTime) } else { joinState.set("show_time", action.actionTime) } } log.debug("Set state (KeyHitId: {}, State: {})", hitId, joinState) stateAccessor.set(joinState.finish()) // [END state_update] log.debug("Add timer for Key (MaxTime: {}, HitTime: {})", maxTime, hitTime) output.addTimer(maxTime, hitTime) ``` {% endlist %} When you receive a hit, you set a timer for `hit_time + wait_for_actions`. The timer triggers when the watermark reaches `maxTime`, meaning the system is sure that all events with `event_time < maxTime` have been processed. ### ExternalStateAccessor with PayloadBuilder The [state](https://ytsaurus.tech/concepts/glossary.md#state) is stored in an external dynamic table. `PayloadBuilder` lets you update individual state fields without full re-serialization: {% list tabs group=lang %} - Java ```java ExternalStateAccessor stateAccessor = ctx.getState(JOINED_ACTION_STATE, message); PayloadBuilder joinState = stateAccessor.getOrDefault().toBuilder(); if ("hit".equals(streamId)) { log.debug("Process hit message (HitId: {}, HitPayload: {}, Hit: {})", hitId, hit.getHitPayload(), hit); joinState.set("hit_payload", hit.getHitPayload()); } else { log.debug("Process action message (HitId: {}, ActionTime: {}, Action: {})", hitId, action.getActionTime(), action); if (Boolean.TRUE.equals(action.isClick())) { joinState.set("click_time", action.getActionTime()); } else { joinState.set("show_time", action.getActionTime()); } } log.debug("Set state (KeyHitId: {}, State: {})", hitId, joinState); stateAccessor.set(joinState.finish()); ``` - Kotlin ```kotlin val stateAccessor = ctx.getState(JOINED_ACTION_STATE, message) val joinState = stateAccessor.orDefault.toBuilder() if ("hit" == streamId) { log.debug( "Process hit message (HitId: {}, HitPayload: {}, Hit: {})", hitId, hit!!.hitPayload, hit ) joinState.set("hit_payload", hit.hitPayload) } else { log.debug( "Process action message (HitId: {}, ActionTime: {}, Action: {})", hitId, action!!.actionTime, action ) if (action.isClick() == true) { joinState.set("click_time", action.actionTime) } else { joinState.set("show_time", action.actionTime) } } log.debug("Set state (KeyHitId: {}, State: {})", hitId, joinState) stateAccessor.set(joinState.finish()) ``` {% endlist %} ### State cleanup After the timer fires and the output event is generated, you clear the state using `stateAccessor.clear()` to avoid accumulating outdated profiles. ## Data models {#models} POJO classes with JPA annotations are defined for typed message handling. {% note warning %} You must ensure that the order of fields in the typed data model matches the order of columns in the stream definition in the static spec. If you break the field order, it can lead to hard-to-diagnose errors. If the stream is not described in the static spec, the typed model sets the column order. Keep that order compatible with the output queues and tables. {% endnote %} ### Hit {% list tabs group=lang %} - Java ```java // The stream schema is derived from this class: the runner writes it into the pipeline spec, // so "hit" needs no schema of its own there. Field order defines column order, and // columnDefinition pins a column type where the default for the field type differs. @Entity public class Hit { @Column(name = "hit_id", columnDefinition = "string") private String hitId; @Column(name = "hit_time", columnDefinition = "uint64") private Long hitTime; @Column(name = "hit_payload", columnDefinition = "string") private String hitPayload; public Hit() { } public Hit(String hitId, Long hitTime, String hitPayload) { this.hitId = hitId; this.hitTime = hitTime; this.hitPayload = hitPayload; } public String getHitId() { return hitId; } public void setHitId(String hitId) { this.hitId = hitId; } public Long getHitTime() { return hitTime; } public void setHitTime(Long hitTime) { this.hitTime = hitTime; } public String getHitPayload() { return hitPayload; } public void setHitPayload(String hitPayload) { this.hitPayload = hitPayload; } @Override public String toString() { return "Hit{" + "hitId='" + hitId + '\'' + ", hitTime=" + hitTime + ", hitPayload='" + hitPayload + '\'' + '}'; } } ``` - Kotlin ```kotlin // The stream schema is derived from this class: the runner writes it into the pipeline spec, // so "hit" needs no schema of its own there. Field order defines column order, and // columnDefinition pins a column type where the default for the field type differs. @Entity class Hit { @Column(name = "hit_id", columnDefinition = "string") var hitId: String? = null @Column(name = "hit_time", columnDefinition = "uint64") var hitTime: Long? = null @Column(name = "hit_payload", columnDefinition = "string") var hitPayload: String? = null constructor() constructor(hitId: String, hitTime: Long, hitPayload: String) { this.hitId = hitId this.hitTime = hitTime this.hitPayload = hitPayload } override fun toString(): String { return "Hit{" + "hitId='$hitId'" + ", hitTime=$hitTime" + ", hitPayload='$hitPayload'" + '}' } } ``` {% endlist %} ### Action {% list tabs group=lang %} - Java ```java // The stream schema is derived from this class: the runner writes it into the pipeline spec, // so "action" needs no schema of its own there. Field order defines column order, and // columnDefinition pins a column type where the default for the field type differs. @Entity public class Action { @Column(name = "hit_id", columnDefinition = "string") private String hitId; @Column(name = "hit_time", columnDefinition = "uint64") private Long hitTime; @Column(name = "is_click") private Boolean click; @Column(name = "action_time", columnDefinition = "uint64") private Long actionTime; public Action() { } public Action(String hitId, Long hitTime, Long actionTime, Boolean isClick) { this.hitId = hitId; this.hitTime = hitTime; this.actionTime = actionTime; this.click = isClick; } public String getHitId() { return hitId; } public void setHitId(String hitId) { this.hitId = hitId; } public Long getHitTime() { return hitTime; } public void setHitTime(Long hitTime) { this.hitTime = hitTime; } public Long getActionTime() { return actionTime; } public void setActionTime(Long actionTime) { this.actionTime = actionTime; } public Boolean isClick() { return click; } public void setIsClick(Boolean click) { this.click = click; } @Override public String toString() { return "Action{" + "hitId='" + hitId + '\'' + ", hitTime=" + hitTime + ", actionTime=" + actionTime + ", click=" + click + '}'; } } ``` - Kotlin ```kotlin // The stream schema is derived from this class: the runner writes it into the pipeline spec, // so "action" needs no schema of its own there. Field order defines column order, and // columnDefinition pins a column type where the default for the field type differs. @Entity class Action { @Column(name = "hit_id", columnDefinition = "string") var hitId: String? = null @Column(name = "hit_time", columnDefinition = "uint64") var hitTime: Long? = null @Column(name = "is_click") var click: Boolean? = null @Column(name = "action_time", columnDefinition = "uint64") var actionTime: Long? = null constructor() constructor(hitId: String, hitTime: Long, actionTime: Long, isClick: Boolean) { this.hitId = hitId this.hitTime = hitTime this.actionTime = actionTime this.click = isClick } fun isClick(): Boolean? = click fun setIsClick(click: Boolean?) { this.click = click } override fun toString(): String { return "Action{" + "hitId='$hitId'" + ", hitTime=$hitTime" + ", actionTime=$actionTime" + ", click=$click" + '}' } } ``` {% endlist %} ### JoinedAction {% list tabs group=lang %} - Java ```java // The stream schema is derived from this class: the runner writes it into the pipeline spec, // so "joined_action" needs no schema of its own there. Field order defines column order, and // columnDefinition pins a column type where the default for the field type differs. @Entity public class JoinedAction { @Column(name = "hit_id", columnDefinition = "string") private String hitId; @Column(name = "hit_time", columnDefinition = "uint64") private Long hitTime; @Column(name = "is_click") private Boolean isClick; @Column(name = "show_time", columnDefinition = "uint64") private Long showTime; @Column(name = "click_time", columnDefinition = "uint64") private Long clickTime; @Column(name = "hit_payload", columnDefinition = "string") private String hitPayload; public JoinedAction() { } public JoinedAction(String hitId, Long hitTime, Boolean isClick, Long showTime, Long clickTime, String hitPayload) { this.hitId = hitId; this.hitTime = hitTime; this.isClick = isClick; this.showTime = showTime; this.clickTime = clickTime; this.hitPayload = hitPayload; } public String getHitId() { return hitId; } public void setHitId(String hitId) { this.hitId = hitId; } public Long getHitTime() { return hitTime; } public void setHitTime(Long hitTime) { this.hitTime = hitTime; } public Boolean getClick() { return isClick; } public void setClick(Boolean click) { isClick = click; } public Long getShowTime() { return showTime; } public void setShowTime(Long showTime) { this.showTime = showTime; } public Long getClickTime() { return clickTime; } public void setClickTime(Long clickTime) { this.clickTime = clickTime; } public String getHitPayload() { return hitPayload; } public void setHitPayload(String hitPayload) { this.hitPayload = hitPayload; } } ``` - Kotlin ```kotlin // The stream schema is derived from this class: the runner writes it into the pipeline spec, // so "joined_action" needs no schema of its own there. Field order defines column order, and // columnDefinition pins a column type where the default for the field type differs. @Entity class JoinedAction { @Column(name = "hit_id", columnDefinition = "string") var hitId: String? = null @Column(name = "hit_time", columnDefinition = "uint64") var hitTime: Long? = null @get:JvmName("getClick") @set:JvmName("setClick") @Column(name = "is_click") var isClick: Boolean? = null @Column(name = "show_time", columnDefinition = "uint64") var showTime: Long? = null @Column(name = "click_time", columnDefinition = "uint64") var clickTime: Long? = null @Column(name = "hit_payload", columnDefinition = "string") var hitPayload: String? = null constructor() constructor(hitId: String, hitTime: Long, isClick: Boolean, showTime: Long, clickTime: Long, hitPayload: String) { this.hitId = hitId this.hitTime = hitTime this.isClick = isClick this.showTime = showTime this.clickTime = clickTime this.hitPayload = hitPayload } } ``` {% endlist %} ## Pipeline configuration (Spring) {#pipeline-configuration} [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/wait_click_join/wait_click_join/src/main/java/tech/ytsaurus/flow/examples/waitclickjoin/PipelineConfiguration.java) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/wait_click_join/wait_click_join/src/main/kotlin/tech/ytsaurus/flow/examples/waitclickjoin/PipelineConfiguration.kt) The example uses the [Spring Boot integration](https://ytsaurus.tech/docs/spring.md) for configuration. The `join` computation is registered with the `@FlowComputation` annotation on the `JoinProcessFunction` class: {% list tabs group=lang %} - Java ```java @FlowComputation(id = "join") ``` - Kotlin ```kotlin @FlowComputation(id = "join") ``` {% endlist %} Typed streams are declared via `ComputationProvider` (the `getStreams()` method): {% list tabs group=lang %} - Java ```java @Configuration public class PipelineConfiguration implements ComputationProvider { @Override public List> getStreams() { return List.of( FlowStreams.typed("hit", Hit.class), FlowStreams.typed("action", Action.class), FlowStreams.typed("joined_action", JoinedAction.class) ); } } ``` - Kotlin ```kotlin @Configuration open class PipelineConfiguration : ComputationProvider { override fun getStreams(): List> { return listOf( FlowStreams.typed("hit", Hit::class.java), FlowStreams.typed("action", Action::class.java), FlowStreams.typed("joined_action", JoinedAction::class.java) ) } } ``` {% endlist %} Key points: - The `join` computation (`JoinProcessFunction`) is registered with the `@FlowComputation(id = "join")` annotation. - You register three typed streams: `hit`, `action`, and `joined_action`. - Typed streams let you use `message.getPayload()` to retrieve POJO objects. ## Entry points {#entry-points} ### PipelineMain This single entry point starts the pipeline or serves as its companion, depending on `YT_FLOW_MODE`. {% list tabs group=lang %} - Java ```java public static void main(String[] args) throws Exception { new SpringApplicationBuilder(PipelineMain.class) .run(args); } ``` - Kotlin ```kotlin @JvmStatic fun main(args: Array) { SpringApplicationBuilder(PipelineMain::class.java) .run(*args) } ``` {% endlist %} ## Static spec {#static-spec} ### Computation join ```yson "join" = { "computation_class_name" = "TJoin"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(hit_id)"; "type" = "uint64"}; {"name" = "hit_id"; "type" = "string"}; {"name" = "hit_time"; "type" = "uint64"}; ]; "input_stream_ids" = ["action"; "hit"]; "output_stream_ids" = ["joined_action"]; "external_state_managers" = { "/join-state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state"; }; }; }; "parameters" = { "wait_for_actions" = "10s"; }; "timers" = { "timer" = {}; }; }; ``` - `group_by_schema` is the partitioning key: `(hash, hit_id, hit_time)`. - `input_stream_ids` are two input streams: `action` and `hit`. - `output_stream_ids` is one output stream: `joined_action`. - `external_state_managers` is a top-level section that describes [External State](https://ytsaurus.tech/docs/external-state.md) (on the same level as `parameters`). The key (`"/join-state"`) is the state name that starts with `/`; you pass the same name to the `StateDescriptors.external("/join-state")` descriptor. `external_state_manager_class_name` is the registered manager class (`NYT::NFlow::TSimpleExternalStateManager` for the standard option). `parameters/path` is the path to the YTsaurus dynamic table; the key column schema of the table must match `group_by_schema`. - `parameters.wait_for_actions` is the time you wait for action events after a hit. You can access it in Java via `RuntimeContext.getComputationParameters()`. - `timers.timer` declares the timer stream for closing hits. ## Testing {#testing} [Test source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/wait_click_join/wait_click_join/src/test/java/tech/ytsaurus/flow/examples/waitclickjoin/JoinProcessFunctionTest.java) [Test source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/wait_click_join/wait_click_join/src/test/kotlin/tech/ytsaurus/flow/examples/waitclickjoin/JoinProcessFunctionTest.kt) You use `TestComputationHarness` for testing. It’s a test harness that lets you call `doProcess` without running the full pipeline. ### Set up the test environment {% list tabs group=lang %} - Java ```java @BeforeEach void init() throws IOException { var pipelineContext = new PipelineContext(); Computation join = Computation.builder() .setComputationId("join") .setProcessFunction(new JoinProcessFunction()) .build(); pipelineContext.registerComputation(join); pipelineContext.registerStream(FlowStreams.typed("hit", Hit.class)); pipelineContext.registerStream(FlowStreams.typed("action", Action.class)); pipelineContext.registerStream(FlowStreams.typed("joined_action", JoinedAction.class)); // External state schema for "/join-state" this.joinStateSchema = TableSchema.builder() .addValue("hit_payload", ColumnValueType.STRING) .addValue("show_time", ColumnValueType.UINT64) .addValue("click_time", ColumnValueType.UINT64) .build(); // The pipeline spec is packaged as a classpath resource in the app module's // src/main/resources, so the test reads the single source of truth directly. try (var specStream = getClass().getResourceAsStream("/pipeline.yson")) { YTreeNode spec = YTreeTextSerializer.deserialize(specStream); this.testHarness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) .setPipelineSpec(spec) .addExternalStateSchema("/join-state", joinStateSchema) .build(); } } ``` - Kotlin ```kotlin @BeforeEach fun init() { val pipelineContext = PipelineContext() val join = Computation.builder() .setComputationId("join") .setProcessFunction(JoinProcessFunction()) .build() pipelineContext.registerComputation(join) pipelineContext.registerStream(FlowStreams.typed("hit", Hit::class.java)) pipelineContext.registerStream(FlowStreams.typed("action", Action::class.java)) pipelineContext.registerStream(FlowStreams.typed("joined_action", JoinedAction::class.java)) // External state schema for "/join-state" this.joinStateSchema = TableSchema.builder() .addValue("hit_payload", ColumnValueType.STRING) .addValue("show_time", ColumnValueType.UINT64) .addValue("click_time", ColumnValueType.UINT64) .build() // The pipeline spec is packaged as a classpath resource in the app module's // src/main/resources, so the test reads the single source of truth directly. javaClass.getResourceAsStream("/pipeline.yson")!!.use { specStream -> val spec = YTreeTextSerializer.deserialize(specStream) this.testHarness = TestComputationHarness.builder() .setPipelineContext(pipelineContext) .setPipelineSpec(spec) .addExternalStateSchema("/join-state", joinStateSchema) .build() } } ``` {% endlist %} ### Test hit message processing {% list tabs group=lang %} - Java ```java @Test void testHitMessageStoresHitPayloadInState() { String hitId = "hit-1"; long hitTime = BASE_HIT_TIME; var messages = List.of(buildHitMessage(hitId, hitTime, "payload-data")); var request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(defaultWatermarks()) .build(); var response = testHarness.doProcess(request); // No output messages yet — timer is scheduled, output comes on timer fire assertTrue(response.getOutputMessagesFlatten().isEmpty()); // A timer must be scheduled assertEquals(1, response.getOutputTimersFlatten().size()); var timer = response.getOutputTimersFlatten().getFirst(); assertEquals(hitTime + WAIT_SECONDS, timer.getTriggerTimestamp()); // External state must be updated with hit_payload assertEquals(1, response.allStates().externalSize("/join-state")); Payload key = buildKey(hitId, hitTime); var state = response.allStates().get(JoinProcessFunction.JOINED_ACTION_STATE, key).get(); assertEquals("payload-data", state.get("hit_payload", String.class)); } ``` - Kotlin ```kotlin @Test fun testHitMessageStoresHitPayloadInState() { val hitId = "hit-1" val hitTime = BASE_HIT_TIME val messages = listOf(buildHitMessage(hitId, hitTime, "payload-data")) val request = TestDoProcessRequest.builder("join") .setMessages(messages) .setWatermarks(defaultWatermarks()) .build() val response = testHarness.doProcess(request) // No output messages yet — timer is scheduled, output comes on timer fire assertTrue(response.outputMessagesFlatten.isEmpty()) // A timer must be scheduled assertEquals(1, response.outputTimersFlatten.size) val timer = response.outputTimersFlatten.first() assertEquals(hitTime + WAIT_SECONDS, timer.triggerTimestamp) // External state must be updated with hit_payload assertEquals(1, response.allStates().externalSize("/join-state")) val key = buildKey(hitId, hitTime) val state = response.allStates().get(JoinProcessFunction.JOINED_ACTION_STATE, key).get()!! assertEquals("payload-data", state.get("hit_payload", String::class.java)) } ``` {% endlist %} ### Test the full join flow {% list tabs group=lang %} - Java ```java @Test void testFullJoinFlow_HitThenShowThenClickThenTimer() { String hitId = "hit-20"; long hitTime = BASE_HIT_TIME; long showTime = hitTime + 3L; long clickTime = hitTime + 7L; // Step 1: process hit message var hitMessages = List.of(buildHitMessage(hitId, hitTime, "full-payload")); var hitRequest = TestDoProcessRequest.builder("join") .setMessages(hitMessages) .setWatermarks(defaultWatermarks()) .build(); var hitResponse = testHarness.doProcess(hitRequest); assertTrue(hitResponse.getOutputMessagesFlatten().isEmpty()); assertEquals(1, hitResponse.getOutputTimersFlatten().size()); assertEquals(1, hitResponse.allStates().externalSize("/join-state")); Payload key = buildKey(hitId, hitTime); Payload stateAfterHit = hitResponse.allStates() .get(JoinProcessFunction.JOINED_ACTION_STATE, key).get(); assertEquals("full-payload", stateAfterHit.get("hit_payload", String.class)); // Step 2: process show action message, passing the state from step 1 var showMessages = List.of(buildActionMessage(hitId, hitTime, showTime, false, hitTime + 1L)); var showRequest = TestDoProcessRequest.builder("join") .setMessages(showMessages) .setState(JoinProcessFunction.JOINED_ACTION_STATE, key, stateAfterHit) .setWatermarks(defaultWatermarks()) .build(); var showResponse = testHarness.doProcess(showRequest); assertTrue(showResponse.getOutputMessagesFlatten().isEmpty()); Payload stateAfterShow = showResponse.allStates().get(JoinProcessFunction.JOINED_ACTION_STATE, key) .get(); assertEquals("full-payload", stateAfterShow.get("hit_payload", String.class)); assertEquals(showTime, stateAfterShow.get("show_time", Long.class)); // Step 3: process click action message, passing the state from step 2 var clickMessages = List.of(buildActionMessage(hitId, hitTime, clickTime, true, hitTime + 2L)); var clickRequest = TestDoProcessRequest.builder("join") .setMessages(clickMessages) .setState(JoinProcessFunction.JOINED_ACTION_STATE, key, stateAfterShow) .setWatermarks(defaultWatermarks()) .build(); var clickResponse = testHarness.doProcess(clickRequest); assertTrue(clickResponse.getOutputMessagesFlatten().isEmpty()); Payload stateAfterClick = clickResponse.allStates().get(JoinProcessFunction.JOINED_ACTION_STATE, key) .get(); assertEquals(clickTime, stateAfterClick.get("click_time", Long.class)); // Step 4: fire the timer with the accumulated state Timer timer = buildTimer(hitId, hitTime); var timerRequest = TestDoProcessRequest.builder("join") .setTimers(List.of(timer)) .setState(JoinProcessFunction.JOINED_ACTION_STATE, key, stateAfterClick) .build(); var timerResponse = testHarness.doProcess(timerRequest); assertEquals(1, timerResponse.getOutputMessagesFlatten().size()); JoinedAction joinedAction = timerResponse.getOutputMessagesFlatten().getFirst().getPayload(); assertEquals(hitId, joinedAction.getHitId()); assertEquals(hitTime, joinedAction.getHitTime()); assertEquals("full-payload", joinedAction.getHitPayload()); assertEquals(showTime, joinedAction.getShowTime()); assertTrue(joinedAction.getClick()); assertEquals(clickTime, joinedAction.getClickTime()); assertNull(timerResponse.allStates().get(JoinProcessFunction.JOINED_ACTION_STATE, key).get()); } ``` - Kotlin ```kotlin @Test fun testFullJoinFlow_HitThenShowThenClickThenTimer() { val hitId = "hit-20" val hitTime = BASE_HIT_TIME val showTime = hitTime + 3L val clickTime = hitTime + 7L // Step 1: process hit message val hitMessages = listOf(buildHitMessage(hitId, hitTime, "full-payload")) val hitRequest = TestDoProcessRequest.builder("join") .setMessages(hitMessages) .setWatermarks(defaultWatermarks()) .build() val hitResponse = testHarness.doProcess(hitRequest) assertTrue(hitResponse.outputMessagesFlatten.isEmpty()) assertEquals(1, hitResponse.outputTimersFlatten.size) assertEquals(1, hitResponse.allStates().externalSize("/join-state")) val key = buildKey(hitId, hitTime) val stateAfterHit = hitResponse.allStates() .get(JoinProcessFunction.JOINED_ACTION_STATE, key).get()!! assertEquals("full-payload", stateAfterHit.get("hit_payload", String::class.java)) // Step 2: process show action message, passing the state from step 1 val showMessages = listOf(buildActionMessage(hitId, hitTime, showTime, false, hitTime + 1L)) val showRequest = TestDoProcessRequest.builder("join") .setMessages(showMessages) .setState(JoinProcessFunction.JOINED_ACTION_STATE, key, stateAfterHit) .setWatermarks(defaultWatermarks()) .build() val showResponse = testHarness.doProcess(showRequest) assertTrue(showResponse.outputMessagesFlatten.isEmpty()) val stateAfterShow = showResponse.allStates() .get(JoinProcessFunction.JOINED_ACTION_STATE, key).get()!! assertEquals("full-payload", stateAfterShow.get("hit_payload", String::class.java)) assertEquals(showTime, stateAfterShow.get("show_time", Long::class.java)) // Step 3: process click action message, passing the state from step 2 val clickMessages = listOf(buildActionMessage(hitId, hitTime, clickTime, true, hitTime + 2L)) val clickRequest = TestDoProcessRequest.builder("join") .setMessages(clickMessages) .setState(JoinProcessFunction.JOINED_ACTION_STATE, key, stateAfterShow) .setWatermarks(defaultWatermarks()) .build() val clickResponse = testHarness.doProcess(clickRequest) assertTrue(clickResponse.outputMessagesFlatten.isEmpty()) val stateAfterClick = clickResponse.allStates() .get(JoinProcessFunction.JOINED_ACTION_STATE, key).get()!! assertEquals(clickTime, stateAfterClick.get("click_time", Long::class.java)) // Step 4: fire the timer with the accumulated state val timer = buildTimer(hitId, hitTime) val timerRequest = TestDoProcessRequest.builder("join") .setTimers(listOf(timer)) .setState(JoinProcessFunction.JOINED_ACTION_STATE, key, stateAfterClick) .build() val timerResponse = testHarness.doProcess(timerRequest) assertEquals(1, timerResponse.outputMessagesFlatten.size) val joinedAction = timerResponse.outputMessagesFlatten.first().getPayload() assertEquals(hitId, joinedAction.hitId) assertEquals(hitTime, joinedAction.hitTime) assertEquals("full-payload", joinedAction.hitPayload) assertEquals(showTime, joinedAction.showTime) assertTrue(joinedAction.isClick!!) assertEquals(clickTime, joinedAction.clickTime) assertNull(timerResponse.allStates().get(JoinProcessFunction.JOINED_ACTION_STATE, key).get()) } ``` {% endlist %} The full test suite (9 scenarios) is in the [source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/wait_click_join/wait_click_join/src/test/java/tech/ytsaurus/flow/examples/waitclickjoin/JoinProcessFunctionTest.java). ## Integration testing {#integration-testing} You run integration testing for Java pipelines the same way as for C++ pipelines: by launching the full pipeline, including C++ workers, queues, and streams. For details, see [Integration tests](https://ytsaurus.tech/docs/testing.md). ## Key ideas {#key-ideas} 1. **Time window**: The system waits for action events for `wait_for_actions` seconds after `hit_time`. You discard all events outside this window. 2. **Watermark for late data**: Events with `event_timestamp < watermark` are considered late and are discarded. This inevitably leads to some data loss, but it ensures watermark correctness. 3. **External State for data accumulation**: You store intermediate data (hit_payload, show_time, click_time) in External State, which is bound to the key `(hit_id, hit_time)`. In this case, you’re not limited to using only External State; an implementation with Internal State is also possible. 4. **Timer for closing the window**: A timer with `triggerTimestamp = hit_time + wait_for_actions` ensures result generation after the wait window closes. 5. **Idempotency of timers**: You set the timer on every message, but with the same `triggerTimestamp`. Flow deduplicates timers with the same key and `triggerTimestamp`. 6. **State cleanup**: After you generate the result, you clear the state via `stateAccessor.clear()`, which removes the row from the table. ## See also - [Quick start (Java)](https://ytsaurus.tech/docs/getting-started.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) # URL Downloader in YTsaurus Flow (Java) The [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) groups incoming URLs by host, accumulates them in the internal [state](https://ytsaurus.tech/concepts/glossary.md#state), and processes them in batches based on a timer. The timer triggers 5 seconds after a URL is added to the queue and emits the results to the output [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). [Source code (Java)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/java/url_downloader) [Source code (Kotlin)](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/kotlin/url_downloader) ## Components ### UrlDownloadFunction This is the main process function that implements the logic for accumulating and processing URLs. The `onMessage` method adds a URL to the host’s internal state and sets a timer. The `onTimer` method processes the accumulated URLs and emits the results. {% list tabs group=lang %} - Java ```java @Override public void onMessage(ExtendedMessage message, OutputCollector output, RuntimeContext ctx) { UrlMessage urlMessage = message.getPayload(); String host = urlMessage.getHost(); String url = urlMessage.getUrl(); StateAccessor accessor = ctx.getState(HOST_STATE, message); HostState state = accessor.getOrDefault(new HostState(host)); state.getPendingUrls().add(url); output.addTimer(System.currentTimeMillis() / 1000 + 5, 0L); log.debug("Queued url (Host: {}, Url: {})", host, url); } ``` - Kotlin ```kotlin override fun onMessage(message: ExtendedMessage, output: OutputCollector, ctx: RuntimeContext) { val urlMessage: UrlMessage = message.getPayload() val host = urlMessage.host val url = urlMessage.url val accessor = ctx.getState(HOST_STATE, message) val state = accessor.getOrDefault(HostState(host)) state.pendingUrls!!.add(url) output.addTimer(System.currentTimeMillis() / 1000 + 5, 0L) log.debug("Queued url (Host: {}, Url: {})", host, url) } ``` {% endlist %} {% list tabs group=lang %} - Java ```java @Override public void onTimer(Timer timer, OutputCollector output, RuntimeContext ctx) { StateAccessor accessor = ctx.getState(HOST_STATE, timer); HostState state = accessor.get(); if (state == null || state.getPendingUrls().isEmpty()) { accessor.clear(); return; } String host = state.getHost(); for (String url : new ArrayList<>(state.getPendingUrls())) { String data = processUrl(url); ProcessedUrl processedUrl = new ProcessedUrl(host, url, data); output.addMessage(new Message("processed_urls", processedUrl)); log.debug("Processed url (Host: {}, Url: {})", host, url); } accessor.clear(); } ``` - Kotlin ```kotlin override fun onTimer(timer: Timer, output: OutputCollector, ctx: RuntimeContext) { val accessor = ctx.getState(HOST_STATE, timer) val state = accessor.get() if (state == null || state.pendingUrls.isNullOrEmpty()) { accessor.clear() return } val host = state.host for (url in ArrayList(state.pendingUrls)) { val data = processUrl(url!!) val processedUrl = ProcessedUrl(host, url, data) output.addMessage(Message("processed_urls", processedUrl)) log.debug("Processed url (Host: {}, Url: {})", host, url) } accessor.clear() } ``` {% endlist %} ### HostState This is the internal state model that is serialized to YSON. It stores the host name and the list of URLs that are waiting to be processed. {% list tabs group=lang %} - Java ```java @Entity public class HostState { private String host; @Column(name = "pending_urls") private List pendingUrls = new ArrayList<>(); public HostState() { } public HostState(String host) { this.host = host; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public List getPendingUrls() { return pendingUrls; } public void setPendingUrls(List urls) { this.pendingUrls = urls; } } ``` - Kotlin ```kotlin @Entity class HostState { var host: String? = null @Column(name = "pending_urls") var pendingUrls: MutableList? = ArrayList() constructor() constructor(host: String?) { this.host = host } } ``` {% endlist %} ### Registering the computation and streams You register the `url_downloader` computation with the `@FlowComputation` annotation on the process function class. {% list tabs group=lang %} - Java ```java @FlowComputation(id = "url_downloader") ``` - Kotlin ```kotlin @FlowComputation(id = "url_downloader") ``` {% endlist %} You declare typed streams via `ComputationProvider` (the `getStreams()` method). {% list tabs group=lang %} - Java ```java @Configuration public class UrlDownloaderComputationContext implements ComputationProvider { @Override public List> getStreams() { return List.of( FlowStreams.typed("urls", UrlMessage.class), FlowStreams.typed("processed_urls", ProcessedUrl.class) ); } } ``` - Kotlin ```kotlin @Configuration open class UrlDownloaderComputationContext : ComputationProvider { override fun getStreams(): List> { return listOf( FlowStreams.typed("urls", UrlMessage::class.java), FlowStreams.typed("processed_urls", ProcessedUrl::class.java), ) } } ``` {% endlist %} ### PipelineMain This single entry point starts the pipeline or serves as its companion, depending on `YT_FLOW_MODE`. {% list tabs group=lang %} - Java ```java public static void main(String[] args) throws Exception { new SpringApplicationBuilder(PipelineMain.class).run(args); } ``` - Kotlin ```kotlin @JvmStatic fun main(args: Array) { SpringApplicationBuilder(PipelineMain::class.java) .run(*args) } ``` {% endlist %} ## Key patterns - **Grouping by key**: you create a separate state for each host. Flow automatically routes messages with the same key to a single computation instance. - **Wall-clock time-based timer**: `output.addTimer(System.currentTimeMillis() / 1000 + 5, 0L)` starts processing 5 seconds after a URL is added to the queue. Multiple `addTimer` calls with the same `triggerTimestamp` are deduplicated. - **Batch processing in `onTimer`**: all accumulated URLs are processed at once when the timer triggers. This reduces the number of calls to downstream services. - **State cleanup**: after processing, you delete the state via `accessor.clear()` to prevent memory leaks. - **YsonStateAccessor**: the internal state is serialized to YSON and stored on the C++ worker side. You get the Java object via `getOrDefault`. ## See also - [Quick start (Java)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Java)](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/url_downloader.md) # Quick start in YTsaurus Flow (Python) You implement Python computations in Flow through the [companion](https://ytsaurus.tech/docs/concepts/glossary.md#companion) mechanism. Python code runs in a separate gRPC process that interacts with the C++ [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker). [Python SDK source code for Flow](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion) [Examples](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python) ## Application architecture {#architecture} Any Flow [pipeline](https://ytsaurus.tech/docs/concepts/glossary.md#pipeline) consists of three components: - `Runner` — starts the pipeline and sets a new version of the [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec). - `Controller` — manages the pipeline’s operation. - `Worker` — performs the actual data processing. ## Pipeline API {#pipeline-api} The Python SDK provides a unified approach to configure a companion: the `Pipeline` class. It lets you register [computations](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) and start the companion’s gRPC server: ```python from yt.yt.flow.library.python.companion import Pipeline pipeline = Pipeline() pipeline.add("mapper", WordCountMapper()) pipeline.run() ``` The `pipeline.add(computation_id, function)` method registers a processing function for the computation with the specified ID. The ID must match the `computation_id` in the pipeline’s [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec). ## Computation and SourceComputation {#computation-and-source} To create a [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) in Python, choose the appropriate registration mode that matches the [C++ Computation type](https://ytsaurus.tech/docs/concepts/companion.md#computation-types): - `pipeline.add(id, fn)` — for `TTransformCompanionComputation` and `TSwiftMapCompanionComputation`. - `pipeline.add(id, fn, source=True)` — for `TSwiftOrderedSourceCompanionComputation` and `TTransformOrderedSourceCompanionComputation`. ```python # SourceComputation to read data from a source pipeline.add("reader", EventMapper(), source=True) # Computation to process data pipeline.add("mapper", WordCountMapper()) ``` `pipeline.add()` has two required parameters: - **computation_id** — this is used to map requests between the [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker) and the companion. - **fn** — a function with the logic for processing [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message). It can be an instance of `RowFunction`, `BatchFunction`, a regular function, or a class with the `on_message`/`on_messages` method. You filter messages in source computations by using the [distribute](https://ytsaurus.tech/docs/en/distribute.md) flag when emitting a message from a Process Function. ## Process Function {#process-function} There are two types of ProcessFunction: - `RowFunction` — receives [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message) and [timers](https://ytsaurus.tech/docs/concepts/glossary.md#timer) one at a time and provides the `on_message` and `on_timer` methods. - `BatchFunction` — receives the entire batch of messages and timers and provides the `on_messages` and `on_timers` methods. For more details, see the [Computation (Python)](https://ytsaurus.tech/docs/en/computation.md) section. ## Message filtering {#message-filtering} To filter a message in SourceComputation, emit it with `output.add_message(message, distribute=False)`. The message is not published further along the graph, but it is still accounted for when the watermark is evaluated. For more details, see the [distribute flag (Python)](https://ytsaurus.tech/docs/en/distribute.md) section. ## Node companion {#node-companion} The entry point to the Python companion is the `__main__.py` file. In it, you must configure the computations via `Pipeline` and call `pipeline.run()`. The `main` function from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md) (in the file itself, it’s called from the standard `if __name__ == "__main__":`): ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("mapper", WordCountMapper()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` If your custom functions need additional resources (a Map, cache, etc.), `main` is a good place to create them. `pipeline.run()` has two modes, which are selected automatically based on the `YT_FLOW_COMPANION_CONFIG` environment variable: - The variable isn’t set — this is a host-based launch. `run()` enriches the pipeline’s spec (see [Launching the pipeline](https://ytsaurus.tech/docs/en/#launch)) and passes control to `flow_server`. - The variable is set — `flow_server` has already launched the same binary in a Job as a companion. `run()` starts the companion’s gRPC server. The same binary thus both launches the pipeline and acts as a companion inside the Job — you don’t need to deploy the companion separately. You can also use the `@pipeline.computation` decorator for quick registration: ```python pipeline = Pipeline() @pipeline.computation("mapper") def mapper(message, output, ctx): word = message.payload["word"] state = ctx.state("word-state", message) data = state.get_or_default({"word": word, "count": 0}) data["count"] += 1 state.set(data) ``` ## Companion CPU parallelism {#companion-process-count} The Python companion can scale across multiple CPU cores: at startup, it forks into `N` interpreter processes that listen on the **same** gRPC port with the `SO_REUSEPORT` option. The Linux kernel distributes incoming RPC calls among them, and each process handles its own batches under its own GIL — this gives an almost linear performance gain across N cores. By default (`companion_process_count = 0`), the fan-out is enabled only if a finite cgroup CPU quota exists; without an explicit limit (`unlimited` — a typical situation in a dev/CI container), the companion remains single-process. `N` comes from `companion_process_count` in the companion’s config: - `0` (default) — automatic selection based on the cgroup CPU quota: `ceil(quota)`, capped at 16. Without a finite quota — `1` (no forks). - `>0` — an explicit value (also capped at 16). `1` returns to single-process behavior (no forks, no `SO_REUSEPORT`). {% note warning %} Memory consumption may grow proportionally to the number of interpreters: each process holds its own copy of the loaded pipeline and any objects used in it. If you increase `companion_process_count`, increase the companion’s `memory_limit` accordingly. {% endnote %} ## Building with ya make {#build} You build a project with a Python companion using `ya make`. In the `ya.make` file, you must specify dependencies on the Python SDK: ``` PEERDIR( yt/yt/flow/library/python/companion ) ``` ## Launching the pipeline {#launch} You launch the built binary with the command: ```bash ./my_pipeline --config pipeline.yson --flow-bin ``` Here’s what happens: - The Python binary (it links `library/python/companion`) reads `pipeline.yson`, enriches the spec — it writes *itself* into it as a Python companion, which `flow_server` will deliver to the Job — and writes the expanded config. - Then it passes control to the specified `flow_server` via `execv` (`flow_server --config `). `flow_server` is not embedded in the Python binary. This keeps the pipeline lightweight and allows whoever runs the pipeline to select the `flow_server` version. If `--flow-bin` is not specified, the runner uses the path from the `YT_FLOW_BIN` environment variable. You can build both binaries with a single command: ```bash cd yt/yt/flow ya make yandex/dev/pipelines/python_vanilla_shuffle bin/flow_server ``` `flow_server` handles the entire launch: it validates the spec, creates a vanilla Operation if needed, **sets the pipeline’s spec** (`set-pipeline-specs`), and starts the pipeline. The Python side only *builds* and enriches the spec and never sets it directly. ### The `vanilla` block {#vanilla} If the `pipeline.yson` file includes a `vanilla` block with `enable = %true`, `flow_server` launches the pipeline as a single YT vanilla Operation (Controller + Workers) and delivers the Python binary to the Job as a companion. This is a one-click launch — you don’t need a separately started `flow_server`. ```yson { "cluster_url" = ""; "path" = "//home/flow-dev/python-vanilla-shuffle/pipeline"; "spec" = { ... }; "vanilla" = { "enable" = %true; "pool" = "yt-dev"; "controller" = { "count" = 1; "cpu_limit" = 4; "memory_limit" = 12884901888; }; "worker" = { "count" = 5; "cpu_limit" = 4; "memory_limit" = 12884901888; }; }; } ``` Required parameters: `pool` and `worker.count`. For the other fields (`cpu_limit`, `memory_limit`, number of Controllers, etc.), reasonable default values exist — see the full list of fields and their descriptions in [TVanillaConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs.md#NYT_NFlow_TVanillaConfig) and [TVanillaTaskConfig](https://ytsaurus.tech/docs/generated_docs/all_yson_structs.md#NYT_NFlow_TVanillaTaskConfig). ### Updating the spec of a running pipeline {#release} `flow_server` is the only component that sets the pipeline’s spec; the Python side only builds the spec. Therefore, the process for rolling out changes to an already running pipeline is: 1. Rebuild the Python binary (`ya make ...`). 2. Run `./my_pipeline --config pipeline.yson --flow-bin ` again. `flow_server` will set the spec again and start the pipeline. For a vanilla launch, the make-before-break strategy is used: the new Operation is prepared (the binary is loaded into the YT cache) while the old Operation continues to run, after which a switch occurs — the old Operation ends, and the prepared new one starts. The way the old Operation ends is controlled by the `YT_FLOW_GRACEFUL_UPDATE` environment variable: `1` (default) — the pipeline stops (`stop`), `0` — it’s paused (`pause`). ## See also - [Computation (Python)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (Python)](https://ytsaurus.tech/docs/en/state.md) - [Examples](https://ytsaurus.tech/docs/en/examples/wordcount.md) - [Companion](https://ytsaurus.tech/docs/concepts/companion.md) # Computation in YTsaurus Flow (Python) {% note info %} This page describes Python-specific details for working with computations. For general concepts, see the [Computation](https://ytsaurus.tech/docs/concepts/computation.md) section. {% endnote %} ## Computation Types {#computation-types} In Flow, there are two types of `Computation`: [`Swift`](https://ytsaurus.tech/docs/concepts/glossary.md#swift) and `Transform`. Your choice determines how exactly-once guarantees are provided and what transformations you can implement. | Type | Guarantee Approach | Use Case | |------|--------------------|----------| | `Swift` | The transformation code is deterministic and will be called again if needed | Stateless transformations | | `Transform` | The result is always stored in YT, so no determinism requirements apply to the transformations | Stateful transformations [Learn more](https://ytsaurus.tech/docs/concepts/stateful.md) | What determinism means and what breaks if you violate it is explained in the [Determinism requirement](https://ytsaurus.tech/docs/concepts/swift.md#determinism) section. When using a [companion](https://ytsaurus.tech/docs/concepts/glossary.md#companion), you select `Swift` or `Transform` by specifying `computation_class_name` in the static [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec): - `NYT::NFlow::NCompanion::TTransformCompanionComputation` — for `Transform`. - `NYT::NFlow::NCompanion::TSwiftMapCompanionComputation` — for `Swift`. - `NYT::NFlow::NCompanion::TTransformOrderedSourceCompanionComputation` — for a `Transform` source. - `NYT::NFlow::NCompanion::TSwiftOrderedSourceCompanionComputation` — for a `Swift` source. ## Creating a Computation {#computation} In Python, you create a computation with `Pipeline.add()`, and it registers automatically in `PipelineContext`. See an example from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md): ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("mapper", WordCountMapper()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` {% note warning %} `process_function=None` is not allowed: computations without business logic aren’t registered in Python. If you need [passthrough](https://ytsaurus.tech/docs/concepts/glossary.md#passthrough), don’t register the computation in Python at all. Instead, specify the C++ passthrough class in `computation_class_name` in the static spec (see [Passthrough Computation](https://ytsaurus.tech/docs/concepts/computation.md#passthrough)). {% endnote %} In the static spec, you create a Computation with the same `id` (in this example, `mapper`): ```yson "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ ... ]; "input_stream_ids" = [...]; "output_stream_ids" = [...]; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = { ... }; }; ``` For more on specs, see the [Spec, DynamicSpec, and Config](https://ytsaurus.tech/docs/concepts/spec.md) section. ## SourceComputation {#sourcecomputation} `SourceComputation` is the top node in the [pipeline](https://ytsaurus.tech/docs/concepts/glossary.md#pipeline) graph that reads data from external sources. On the worker side, it corresponds to [TSwiftOrderedSourceComputation](https://ytsaurus.tech/docs/concepts/computation.md#tswiftorderedsourcecomputation) or [TTransformOrderedSourceComputation](https://ytsaurus.tech/docs/concepts/computation.md#ttransformorderedsourcecomputation). In Python, you create a `SourceComputation` by passing `source=True` to `Pipeline.add()`. You filter [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message) inside the Process Function using the [distribute](https://ytsaurus.tech/docs/en/distribute.md) flag. For deterministic processing without user state, set `TSwiftOrderedSourceCompanionComputation` in the static spec. If the source uses internal state or nondeterministic logic, use `TTransformOrderedSourceCompanionComputation`: the worker materializes its output and commits that output atomically with state and the source offset. The internal-state key is the source partition key. ### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `computation_id` | Yes | Unique identifier | | `fn` (process function) | Yes | Function for processing messages | ### Creating a SourceComputation ```python pipeline.add("reader", MyParsingFunction(), source=True) ``` For a passthrough Source, don’t use Python. Instead, specify `NYT::NFlow::TSwiftPassthroughOrderedSourceComputation` in `computation_class_name` in the spec, and leave the computation unregistered in the Python companion. See [Passthrough Computation](https://ytsaurus.tech/docs/concepts/computation.md#passthrough) for more details. ### Interaction with Worker {#companion-info} When initializing, the [Worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker) requests information about registered `Computation` and `SourceComputation` objects from the Python companion. The source computation on the worker side sends input messages to the Python companion, which applies the `ProcessFunction` and returns the result. ## Process Function {#process-function} You implement the business logic for data processing in a Process Function. To do this, choose one of two base classes: [RowFunction](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/computation.py) or [BatchFunction](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/computation.py). {% note info %} Choosing between `RowFunction` and `BatchFunction` depends entirely on your business logic. `RowFunction` doesn’t add extra processing overhead compared to `BatchFunction` because Flow internally transfers data in batches. {% endnote %} ### RowFunction {#row-function} `RowFunction` receives [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message) and [timers](https://ytsaurus.tech/docs/concepts/glossary.md#timer) one at a time. The class provides two methods: - `on_message(message, output, ctx)` — called for each input message. - `on_timer(timer, output, ctx)` — called when a timer fires (optional). #### Example of a stateless function ```python from yt.yt.flow.library.python.companion.computation import RowFunction class X2Mapper(RowFunction): def on_message(self, message, output, ctx): builder = ctx.message_builder("x2_numbers") # 1 number = message.payload["number"] # 2 builder.set("number_x2", number * 2) # 3 output.add_message(builder.finish()) # 4 ``` Let’s go line by line: 1. `ctx.message_builder("x2_numbers")` — creates a `MessageBuilder` for the output [stream](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) with id = `x2_numbers`. A stream with this identifier must be present in the `output_stream_ids` list in the static [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec) of the computation. 2. `message.payload["number"]` — gets the value of the `number` field from the incoming message. The payload supports dict-like access to fields. 3. `builder.set("number_x2", number * 2)` — writes the value to the `number_x2` field. This field must exist in the schema of the `x2_numbers` stream in the static spec. 4. `output.add_message(builder.finish())` — the `finish()` method returns the completed message and resets the builder for reuse. The message is added to the `OutputCollector`. ### BatchFunction {#batch-function} `BatchFunction` receives the entire list of messages and timers that came from the [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker). The class provides two methods: - `on_messages(messages, output, ctx)` — called for a batch of messages. - `on_timers(timers, output, ctx)` — called for a batch of timers (optional). {% note warning %} A batch corresponds to one worker request and may contain messages with **different [keys](https://ytsaurus.tech/docs/concepts/glossary.md#key)** (see [Companion](https://ytsaurus.tech/docs/concepts/companion.md#schema)). By default, all messages of the batch become the parents of every output message. `output.set_parent_ids(ids)` replaces this default: the method takes the `message_id` (a string or a list of strings) of input messages of the current batch and returns a new collector — all output added through it gets exactly these messages as parents. Parents are message ids and are unrelated to keys; pass the `message_id` of the input messages the given output was actually derived from. When the call is mandatory (see [When to set lineage explicitly](https://ytsaurus.tech/docs/concepts/lineage.md#explicit-lineage)): - **Swift**: mandatory when the batch has more than one message — every output message must have exactly one parent (`out = output.set_parent_ids(message.message_id)`), otherwise processing fails with an error. Multiple parents are allowed only with [`allow_batching_with_relaxed_guarantees`](https://ytsaurus.tech/docs/concepts/guarantees.md#swift-allow-batching-with-relaxed-guarantees). - **Transform**: optional, but with the "whole batch" default the event timestamp of every output message equals the minimum over the whole batch. - In `RowFunction` there is no need to call `set_parent_ids`: the SDK automatically sets the current input message as the parent. If you need per-key processing (in Transform, or in Swift with `allow_batching_with_relaxed_guarantees`), group the batch in user code and set the parents of each group: ```python def on_messages(self, messages, output, ctx): groups = {} for message in messages: groups.setdefault(message.key["user_id"], []).append(message) for key, group in groups.items(): out = output.set_parent_ids([m.message_id for m in group]) ... # Process the group, emit via out. ``` In a Swift computation the grouping and the parent assignment must be deterministic, including the order of groups. A Python `dict` preserves insertion order, so the example above is reproducible; unordered structures (such as `set`) must not be used. For details, see [Determinism requirement](https://ytsaurus.tech/docs/concepts/swift.md#determinism). For a complete example, see [batch_compaction](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/batch_compaction): a Swift computation with `allow_batching_with_relaxed_guarantees` that collapses same-key messages into one. {% endnote %} #### Example of a batch function ```python from yt.yt.flow.library.python.companion.computation import BatchFunction class X2BatchMapper(BatchFunction): def on_messages(self, messages, output, ctx): builder = ctx.message_builder("x2_numbers") # 1 for message in messages: # 2 number = message.payload["number"] # 3 builder.set("number_x2", number * 2) # 4 output.add_message(builder.finish()) # 5 ``` The key difference from `RowFunction` is: - You create the `MessageBuilder` once for the entire batch (line 1). - The `finish()` method returns the completed message and resets the `MessageBuilder` to its initial state, so you can reuse it for the next message (line 5). ## Message Filtering {#message-filtering} To filter messages in source computations, use the per-message [distribute](https://ytsaurus.tech/docs/en/distribute.md) flag: you emit the message from the Process Function with `distribute=False`, and it isn’t published further along the graph, but it’s still considered when evaluating the [watermark](https://ytsaurus.tech/docs/concepts/watermarks.md). ## Registration in Pipeline {#pipeline-registration} You register all computations with `Pipeline.add()`. Inside, `Pipeline` uses `PipelineContext` to store and manage registered objects. ```python from yt.yt.flow.library.python.companion import Pipeline pipeline = Pipeline() # Transform computation pipeline.add("computation_id", my_function) # Source computation pipeline.add("reader", my_function, source=True) ``` You can also use the `@pipeline.computation` decorator: ```python pipeline = Pipeline() @pipeline.computation("mapper") def mapper(message, output, ctx): word = message.payload["word"] state = ctx.state("word-state", message) data = state.get_or_default({"word": word, "count": 0}) data["count"] += 1 state.set(data) ``` {% note warning %} Each Computation must have a unique identifier that matches the identifiers in the static spec. Trying to register a Computation with an existing identifier will cause an error and prevent the companion from starting. {% endnote %} ## RuntimeContext {#runtime-context} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/context.py) `RuntimeContext` (`ctx`) gives you access to the computation’s runtime context. Key methods: | Method | Description | |--------|-------------| | `ctx.message_builder(stream_id)` | Create a `MessageBuilder` for the specified output [stream](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) | | `ctx.parameters` | Map of the computation’s parameters from the spec | | `ctx.min_watermark` | Minimum [watermark](https://ytsaurus.tech/docs/concepts/glossary.md#timestamps-and-watermarks) across all input streams | | `ctx.watermark(stream_id)` | [Watermark](https://ytsaurus.tech/docs/concepts/glossary.md#timestamps-and-watermarks) of a specific stream (`int` or `None`) | | `ctx.state(name, message)` | Get the YSON [state](https://ytsaurus.tech/docs/concepts/glossary.md#state) tied to the message key | | `ctx.raw_state(name, message)` | Get the state as raw bytes | | `ctx.proto_state(name, message, ProtoClass)` | Get the Protobuf state | | `ctx.external_state(name, message)` | Get the external state | For more on working with states, see the [Working with States (Python)](https://ytsaurus.tech/docs/en/state.md) section. ### MessageBuilder {#message-builder} You use `MessageBuilder` to create output messages: ```python builder = ctx.message_builder("stream_id") builder.set("field_name", value) message = builder.finish() output.add_message(message) ``` The `finish()` method returns the completed `Message` object and resets the builder for reuse. The `stream_id` field must be present in the `output_stream_ids` list in the static [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec) of the computation. ### Computation Parameters {#parameters} ```python wait_for_actions = ctx.parameters["wait_for_actions"] ``` The `ctx.parameters` map contains the parameters specified in the static spec of the computation. ### Watermarks {#watermarks} ```python # Minimum watermark across all input streams min_wm = ctx.min_watermark # Watermark of a specific stream (int or None) stream_wm = ctx.watermark("stream_id") ``` ## OutputCollector {#output-collector} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/computation.py) Use `OutputCollector` to send processing results: | Method | Description | | --- | --- | | `output.add_message(message)` | Add an output message (a `Message` object obtained via `builder.finish()`) | | `output.add_message(message, options)` | Add a message with `AddMessageOptions` controlling distribution and the Swift message ID suffix | | `output.add_timer(trigger_timestamp, event_timestamp, stream_id)` | Add a [timer](https://ytsaurus.tech/docs/concepts/glossary.md#timer) with the specified trigger time | | `output.set_parent_ids(parent_ids)` | Set parent IDs to track the [lineage](https://ytsaurus.tech/docs/concepts/lineage.md) of messages. Returns a new `OutputCollector` | For a Swift computation, `MessageIdSuffix.payload_hash()` or `MessageIdSuffix.user_defined(value)` makes message identity independent of emission order: ```python output.add_message( message, AddMessageOptions(message_id_suffix=MessageIdSuffix.payload_hash()), ) ``` Default options use the message sequence number. Payload-hash and user-defined suffixes are supported only by Swift computations. Example of creating an output message and a timer: ```python def on_message(self, message, output, ctx): # Create an output message builder = ctx.message_builder("output_stream") builder.set("field", value) output.add_message(builder.finish()) # Create a timer output.add_timer(trigger_timestamp=1000, event_timestamp=500) ``` ## ExtendedMessage {#extended-message} An incoming [message](https://ytsaurus.tech/docs/concepts/glossary.md#message) (`ExtendedMessage`) contains: - `message.payload` — Payload with dict-like access to fields: `message.payload["field"]`. - `message.stream_id` — The ID of the input [stream](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) (`str`). - `message.key` — The [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) of the message (Payload) from `group_by_schema`: `message.key["field"]`. - `message.event_timestamp` — The event timestamp of the message (`int`). ## Timer {#timer} A [timer](https://ytsaurus.tech/docs/concepts/glossary.md#timer) object (`Timer`) contains: - `timer.key` — The [key](https://ytsaurus.tech/docs/concepts/glossary.md#key) of the timer (Payload): `timer.key["field"]`. - `timer.stream_id` — The ID of the timer’s stream (`str`). - `timer.trigger_timestamp` — The trigger time (`int`). - `timer.event_timestamp` — The event timestamp (`int`). ## CompanionManager resource configuration {#companion-manager} To run a Python companion, you need to declare the `CompanionManager` resource in a static spec: ```yson "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { "entrypoint" = { "executable" = "./py_companion"; }; }; "dependencies" = {}; }; ``` The `resource_class_name` parameter specifies the resource class that will launch the companion. For a Python companion, `resource_class_name` must always be `NYT::NFlow::NCompanion::TCompanionManager`. The companion process is described by the `entrypoint` parameter (`executable`, `args`, `env`). When you [launch a pipeline from a host](https://ytsaurus.tech/docs/en/getting-started.md#launch) via `pipeline.run()`, you don’t need to fill in `entrypoint` manually: the Python binary sets `entrypoint = {"executable" = "./py_companion"}` for every `TCompanionManager` resource in the spec, and `flow_server` delivers the binary to the job under that name. The key difference from the Java configuration is that Java has a separate resource class `NYT::NFlow::NCompanion::TJavaCompanionManager` with parameters `jdk_bin_path`, `classpath`, and `main_class`, while the Python companion uses the shared `TCompanionManager` with the `entrypoint` parameter. For more details about the spec, see the section [Spec, DynamicSpec and Config](https://ytsaurus.tech/docs/concepts/spec.md). ## See also - [Computation (concept)](https://ytsaurus.tech/docs/concepts/computation.md) - [Working with states (Python)](https://ytsaurus.tech/docs/en/state.md) - [Quick start (Python)](https://ytsaurus.tech/docs/en/getting-started.md) - [Companion](https://ytsaurus.tech/docs/concepts/companion.md) # The distribute flag in YTsaurus Flow (Python) The `distribute` flag is a per-message flag that you set when you add an output [message](https://ytsaurus.tech/docs/concepts/glossary.md#message) to [SourceComputation](https://ytsaurus.tech/docs/en/computation.md#sourcecomputation). It controls whether the message is published further along the processing graph. The `distribute` flag ensures: - Correct [watermark](https://ytsaurus.tech/docs/concepts/watermarks.md) evaluation: messages with `distribute=False` are still accounted for by the watermark generator (unlike filtering in `on_message`, which can break the watermark). - Assignment of deterministic identifiers to messages. {% note warning %} To filter a message in `SourceComputation`, don’t skip it in `on_message`. Instead, emit it with `distribute=False`. This way, the message isn’t published further, but it remains accounted for in watermark evaluation. {% endnote %} ## When to use distribute=False Use the `distribute=False` flag when: - You need to filter some output messages at the source-computation stage. - Correct watermark evaluation is important. If you don’t set the flag, it defaults to `True`, and the message is published further. ## Usage {#usage} You move the filtering logic to the processing function: instead of a separate filtering step, you emit the message with the required flag. ```python from yt.yt.flow.library.python.companion.computation import AddMessageOptions, RowFunction class HitParsingFunction(RowFunction): def on_message(self, message, output, ctx): builder = ctx.message_builder("hit") builder.set("hit_id", message.payload["hit_id"]) builder.set("hit_payload", message.payload["hit_payload"]) # Duplicates are emitted but not published further. is_duplicate = message.payload["hit_payload"] == "duplicate_payload" output.add_message( builder.finish(), AddMessageOptions(distribute=not is_duplicate), ) ``` ## Registering a source-computation {#registration} You register a source-computation via `Pipeline.add()` with `source=True`. You no longer need a separate filtering parameter — the decision about publishing is made in the processing function. ```python from yt.yt.flow.library.python.companion import Pipeline pipeline = Pipeline() pipeline.add("hit_reader", HitParsingFunction(), source=True) ``` ## See also - [Computation (Python)](https://ytsaurus.tech/docs/en/computation.md) - [Watermarks](https://ytsaurus.tech/docs/concepts/watermarks.md) - [The distribute flag (Java)](https://ytsaurus.tech/docs/java/distribute.md) # Working with states in YTsaurus Flow (Python) {% note info %} This page describes the Python API for working with states. For general state concepts, see the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. {% endnote %} ## YSON State {#yson-state} The simplest way to work with a state is using the YSON format. You store the state as a Python Map, which is automatically serialized to YSON: ```python state = ctx.state("state-name", message) ``` This returns a `YsonStateAccessor` with the following methods: - `get()` — get the current value (`dict` or `None`). - `set(dict)` — save the value. - `clear()` — delete the state. - `get_or_default(dict)` — get the current value or return the default value. Example from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md): ```python class WordCountMapper(RowFunction): """Processes Word messages and maintains word count state.""" def on_message(self, message, output, ctx): state = ctx.state("word-state", message) data = state.get_or_default({"word": message.payload["word"], "count": 0}) data["count"] += 1 state.set(data) ``` Here, the state is tied to the message key, which is defined via `group_by_schema` in the [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec). Each unique key has its own independent state. ## Raw State {#raw-state} To store a state as raw bytes: ```python state = ctx.raw_state("state-name", message) ``` This returns a `RawStateAccessor` with the following methods: - `get()` — get the value (`bytes` or `None`). - `set(bytes)` — save the value. - `clear()` — delete the state. - `get_or_default(bytes)` — get the value or return the default value. ## Proto State {#proto-state} To store a state as a Protobuf message: ```python state_accessor = ctx.proto_state("state-name", message, TJoinState) ``` This returns a `ProtoStateAccessor` with the following methods: - `get()` — deserialize and return the Protobuf object (or `None`). - `set(proto_message)` — serialize and save the value. - `clear()` — delete the state. - `get_or_default(default=None)` — get the value or return the default. If you don’t specify a default, it returns an empty instance of the Proto class. ## External State {#external-state} An external state works like a Payload — it gives you dict-like access to fields: ```python state = ctx.external_state("/state-name", message) ``` The state name must start with `/` and match the key in `external_state_managers` in the static spec. If you call `ctx.external_state("state-name", message)` without the leading `/`, it raises a `ValueError`. This returns an `ExternalStateAccessor`, which is also a `Payload`: - `state.get("field")` — read a field. - `state["field"]` — dict-like read of a field. - `state.to_builder()` — get a `PayloadBuilder` with the current values. - `state.set(payload)` — save a new value (it accepts a Payload from `builder.finish()`). - `state.clear()` — delete the state. Example from [Shuffle](https://ytsaurus.tech/docs/en/examples/shuffle.md) (EventReducer): ```python class EventReducer(RowFunction): """Counts events per key using external state.""" def on_message(self, message, output, ctx): state = ctx.external_state("/shuffle-state", message) builder = state.to_builder() builder.set("count", (state.get("count") or 0) + 1) state.set(builder.finish()) ``` The pattern for working with an external state is: 1. Get the current state using `ctx.external_state(...)`. 2. Create a builder with `state.to_builder()`. 3. Update the required fields with `builder.set(...)`. 4. Save the changes with `state.set(builder.finish())`. ## State in timers {#state-in-timers} The API for working with a state in a timer handler is the same — you pass the `timer` object instead of `message`: ```python def on_timer(self, timer, output, ctx): state = ctx.external_state("/join-state", timer) # Read the state show_time = state.get("show_time") hit_payload = state.get("hit_payload") # Clear the state after processing state.clear() ``` Example from [WaitClickJoin](https://ytsaurus.tech/docs/en/examples/wait_click_join.md) (JoinProcessFunction): ```python def on_timer(self, timer, output, ctx): hit_id = timer.key["hit_id"] log.debug("Process timer (KeyHitId: %s, Timer: %s)", hit_id, timer) state = ctx.external_state("/join-state", timer) log.debug("Process state (KeyHitId: %s, State: %s)", hit_id, state) if timer.stream_id == "timer": show_time = state.get("show_time") hit_payload = state.get("hit_payload") if show_time is not None and hit_payload is not None and show_time != 0: log.debug("Click join: %s", state) hit_time = timer.key["hit_time"] click_time = state.get("click_time") builder = ctx.message_builder("joined_action") builder.set("hit_id", hit_id) builder.set("hit_time", hit_time) builder.set("show_time", show_time) builder.set("hit_payload", hit_payload) if click_time is not None and click_time != 0: builder.set("is_click", True) builder.set("click_time", click_time) else: builder.set("is_click", False) builder.set("click_time", 0) output.add_message(builder.finish()) state.clear() else: raise ValueError(f"Unknown streamId: {timer.stream_id}") ``` ## Binding a state to a key {#group-by-schema} In `TTransformCompanionComputation`, state binds to the [message key](https://ytsaurus.tech/docs/concepts/glossary.md#key) defined by `group_by_schema` in the computation spec. Messages with the same key share one state. `TTransformOrderedSourceCompanionComputation` does not support `group_by_schema`. Its internal state uses the source partition key, so messages from one partition share that state. See [Computation (Python)](https://ytsaurus.tech/docs/en/computation.md#sourcecomputation) for choosing a SourceComputation class, and [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) for key configuration. ## Configuring states in the spec {#spec-configuration} Internal states (YSON, Raw, Proto) must be declared in the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) parameters in the `internal_states` section. The state name in your code (the first argument of `ctx.state(...)`) must match the name declared in the spec. External states (External) are configured via the `external_state_managers` section in the computation spec and have their own schema that describes the available fields. The key inside `external_state_managers` (for example, `"/shuffle-state"`) sets the state name, which must start with `/`. In the `external_state_manager_class_name` field, specify the registered manager class (for a typical scenario, use `"NYT::NFlow::TSimpleExternalStateManager"`). For more details on the spec and available managers, see [External State](https://ytsaurus.tech/docs/en/external-state.md#static-spec) and the [C++ documentation](https://ytsaurus.tech/docs/cpp/state.md#external-state). ## See also - [Stateful processing (concept)](https://ytsaurus.tech/docs/concepts/stateful.md) - [Computation (Python)](https://ytsaurus.tech/docs/en/computation.md) - [Quick start (Python)](https://ytsaurus.tech/docs/en/getting-started.md) # StateAccessor in YTsaurus Flow (Python) Use StateAccessor to read, modify, and delete state values. For general information about stateful processing, see the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## How it works {#how-it-works} In Flow, the [state](https://ytsaurus.tech/docs/concepts/glossary.md#state) is stored in [sorted dynamic tables](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md). If you’re using [external state](https://ytsaurus.tech/docs/en/external-state.md), you create this table yourself. If you’re using [internal state](https://ytsaurus.tech/docs/en/internal-state.md), Flow automatically creates and manages the tables. For `TTransformCompanionComputation`, the key columns in the state table match the `group_by_schema` of the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation). For the internal state of `TTransformOrderedSourceCompanionComputation`, the source partition key is used instead; this SourceComputation does not support `group_by_schema`. In both cases, messages with the same key share one state. ## Reading and writing data {#reading-and-writing-data} The [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker) directly handles table operations (reading, writing, and deleting data). When the worker receives a new batch of messages, it loads the state values for all keys in the batch and sends them to the [companion](https://ytsaurus.tech/docs/concepts/companion.md) along with the messages and timers. For more details, see the [interaction schema](https://ytsaurus.tech/docs/concepts/companion.md#schema). You write new values to the state table transactionally within an [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch). ## Four accessor types {#accessor-types} The Python SDK provides four accessor types for working with state: | Accessor | Format | Retrieval | Description | |----------|--------|-----------|-------------| | [YsonStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#yson-state-accessor) | YSON (dict) | `ctx.state(name, msg)` | Serializes a Python dict to YSON | | [RawStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#raw-state-accessor) | `bytes` | `ctx.raw_state(name, msg)` | Raw bytes without serialization | | [ProtoStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#proto-state-accessor) | Protobuf | `ctx.proto_state(name, msg, ProtoClass)` | Serializes using Protobuf | | [ExternalStateAccessor](https://ytsaurus.tech/docs/en/external-state.md) | Payload (table row) | `ctx.external_state("/name", msg)` | Typed access to an external table | The first three accessors work with [internal state](https://ytsaurus.tech/docs/en/internal-state.md) (tables are automatically managed by Flow). `ExternalStateAccessor` works with [external state](https://ytsaurus.tech/docs/en/external-state.md) (you create the table yourself). ## Common API {#common-api} All internal accessors (`YsonStateAccessor`, `RawStateAccessor`, `ProtoStateAccessor`) provide the same set of methods: | Method | Description | |--------|-------------| | `get()` | Get the state value (or `None` if the state doesn’t exist) | | `set(value)` | Set the state value | | `clear()` | Delete the state for the current key | | `get_or_default(default)` | Get the value or return `default` | ## Getting an accessor {#getting-accessor} You get an accessor through `RuntimeContext` (`ctx`) inside `on_message` or `on_timer`: ```python class MyFunction(RowFunction): def on_message(self, message, output, ctx): # YSON (dict) yson_state = ctx.state("state-name", message) # Raw bytes raw_state = ctx.raw_state("state-name", message) # Protobuf proto_state = ctx.proto_state("state-name", message, MyProtoClass) # External (the name must start with "/") ext_state = ctx.external_state("/state-name", message) def on_timer(self, timer, output, ctx): # Similarly, but pass timer instead of message state = ctx.state("state-name", timer) ``` Parameters: - `name` — a string with the state name declared in the [static spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec). For internal states, this is an arbitrary string from `internal_states`; for external states, it’s a key from `external_state_managers` that must start with `/`. - `message` / `timer` — the message or timer for which you need to get the state for the [key](https://ytsaurus.tech/docs/concepts/glossary.md#key). - `ProtoClass` (only for `proto_state`) — the Protobuf message class for deserialization. ## When to use which type {#choosing-type} | Situation | Recommended accessor | |-----------|----------------------| | A simple dictionary with several fields | `ctx.state()` (YSON) | | Arbitrary binary data | `ctx.raw_state()` (Raw) | | Structured data with a fixed schema | `ctx.proto_state()` (Protobuf) | | Data that needs to be accessed from other systems | `ctx.external_state("/name", msg)` (External) | | Data that requires a custom table | `ctx.external_state("/name", msg)` (External) | ## See also - [Internal State (Python)](https://ytsaurus.tech/docs/en/internal-state.md) - [External State (Python)](https://ytsaurus.tech/docs/en/external-state.md) - [Working with states (Python)](https://ytsaurus.tech/docs/en/state.md) — a brief overview of all types - [Stateful processing (concept)](https://ytsaurus.tech/docs/concepts/stateful.md) - [StateAccessor (Java)](https://ytsaurus.tech/docs/java/state-accessor.md) # Internal State in YTsaurus Flow (Python) Internal State is a mechanism for working with internal [state](https://ytsaurus.tech/docs/concepts/glossary.md#state) stored in Flow’s internal tables. Unlike [External State](https://ytsaurus.tech/docs/en/external-state.md), you don’t need to create tables manually—Flow manages them automatically. Learn more about `StateAccessor` and general principles for working with state: [StateAccessor](https://ytsaurus.tech/docs/en/state-accessor.md). General information about stateful processing is in the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## Overview The Python SDK provides three types of accessors for working with Internal State, which differ by serialization format. To work with external state, use a separate accessor: [ExternalStateAccessor](https://ytsaurus.tech/docs/en/external-state.md). | Accessor | Format | Description | |----------|--------|----------| | [YsonStateAccessor](https://ytsaurus.tech/docs/en/#yson-state-accessor) | YSON (dict) | Serializes a Python dict using YSON | | [RawStateAccessor](https://ytsaurus.tech/docs/en/#raw-state-accessor) | `bytes` | No serialization (raw bytes) | | [ProtoStateAccessor](https://ytsaurus.tech/docs/en/#proto-state-accessor) | Protobuf | Serializes using Protobuf | All accessors provide the same set of methods: `get()`, `set(value)`, `clear()`, `get_or_default(default)`, `read_only()`. ## Changing the value in place {#in-place} The value returned by `get()` and `get_or_default()` is live: it is decoded once per key and batch, every accessor for that key returns the same object, and the changes made to it are written to the state at the end of the batch without a `set()` call. Nothing is written when the value did not change. The default from `get_or_default()` is attached to the key but not written: it can be changed right away, and it becomes the state value only once it is changed, so an untouched default creates no row in the state table: ```python ctx.state("word-state", message).get_or_default({"count": 0})["count"] += 1 ``` `set()` still replaces the whole value, and `clear()` removes the state. A `None` default is not a value and is not attached. A value that encodes to no bytes — an all-default protobuf message, for instance — is not a value either: setting it removes the state. A `RawStateAccessor` default is attached like any other, and a later `get()` returns it. But a `bytes` object is immutable, so an attached default cannot be changed in place, and a non-empty raw value reaches YTsaurus only through `set()`. To detect the changes, a value that was read is re-encoded at the end of the batch. When the computation only reads a state, use `read_only()`: that accessor returns the same object but does not track it, its `get_or_default()` does not create the state, and `set()` and `clear()` raise `ReadOnlyStateError`: ```python count = ctx.state("word-state", message).read_only().get_or_default({"count": 0})["count"] ``` The value is shared, not a copy: if a writable accessor read the same key earlier in the batch, a change made through the read-only view still reaches the state. ## YsonStateAccessor {#yson-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/context.py) `YsonStateAccessor` uses YSON serialization. The state is stored as a Python dict, which is automatically serialized to YSON and deserialized back. ### Get the accessor ```python # For a message state = ctx.state("state-name", message) # For a timer state = ctx.state("state-name", timer) ``` ### Methods | Method | Return type | Description | |-------|-----------------|----------| | `get()` | `dict` or `None` | Deserialize and return the current value | | `set(value)` | — | Serialize and save the value (dict or bytes) | | `clear()` | — | Delete the state for the current key | | `get_or_default(default)` | `dict` | Return the current value, or `default` (written only once you change it) | ### Example from WordCount ```python class WordCountMapper(RowFunction): """Processes Word messages and maintains word count state.""" def on_message(self, message, output, ctx): state = ctx.state("word-state", message) data = state.get_or_default({"word": message.payload["word"], "count": 0}) data["count"] += 1 state.set(data) ``` [Full source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/word_count/word_count_mapper.py) Here, the state is tied to the [message](https://ytsaurus.tech/docs/concepts/glossary.md#message) key (defined via `group_by_schema` in the [spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec)). Each unique word has its own independent counter. ## RawStateAccessor {#raw-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/context.py) `RawStateAccessor` works with raw bytes without serialization or deserialization. ### Get the accessor ```python # For a message state = ctx.raw_state("state-name", message) # For a timer state = ctx.raw_state("state-name", timer) ``` ### Methods | Method | Return type | Description | |-------|-----------------|----------| | `get()` | `bytes` or `None` | Get the raw bytes | | `set(value: bytes)` | — | Save the raw bytes | | `clear()` | — | Delete the state for the current key | | `get_or_default(default: bytes)` | `bytes` | Return the current value, or attach and return `default` (reaches YTsaurus only through `set()`) | ### Usage example ```python state = ctx.raw_state("raw-state", message) data = state.get() if data is not None: # Process raw data... pass # Write raw data state.set(b"\x01\x02\x03") # Clear state.clear() ``` ## ProtoStateAccessor {#proto-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/context.py) `ProtoStateAccessor` uses Protobuf serialization. The state is deserialized into an instance of the specified Protobuf class. ### Get the accessor ```python # For a message state = ctx.proto_state("state-name", message, TJoinState) # For a timer state = ctx.proto_state("state-name", timer, TJoinState) ``` The third argument is the Protobuf message class used for deserialization. ### Methods | Method | Return type | Description | |-------|-----------------|----------| | `get()` | Proto object or `None` | Deserialize and return the value | | `set(value)` | — | Serialize and save the Proto object | | `clear()` | — | Delete the state for the current key | | `get_or_default(default=None)` | Proto object | Return the value, `default`, or an empty instance of the Proto class (written only once you change it) | {% note info %} The `get_or_default()` method with no arguments returns an empty instance of the Proto class (equivalent to `ProtoClass()`). This is convenient for initializing the state on the first access; the state itself appears once you change that message. {% endnote %} ## Configuration in the static spec {#static-spec} Internal State doesn’t require you to create external tables. States are automatically stored in Flow’s internal tables. You must declare the names of internal states in the `internal_states` section of the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) parameters in the static spec: ```yson { "spec" = { "computations" = { "reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "output_stream_ids" = ["words"]; "source_streams" = { "queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { "queue_path" = "//path/to/queue"; "consumer_path" = "//path/to/consumer"; "finite" = false; }; }; }; "parameters" = {}; }; "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(word)"; "type" = "uint64"; required = %true;}; {"name" = "word"; "type" = "string";}; ]; "input_stream_ids" = ["words"]; "output_stream_ids" = []; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = { "internal_states" = ["word-state"]; }; }; }; "streams" = { "words" = { "schema" = [ {name = "word"; type = "string";}; ]; }; }; "resources" = { "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { }; "dependencies" = {}; }; }; }; "dynamic_spec" = { "computations" = { "reader" = { "parameters" = { }; }; "mapper" = { "parameters" = { }; }; }; }; } ``` The state name in your code (the first argument of `ctx.state(...)`, `ctx.raw_state(...)`, or `ctx.proto_state(...)`) must match the name declared in `internal_states`. {% note warning %} If the state name isn’t declared in `internal_states`, calling it via `ctx.state(...)` will raise a `ValueError` exception. {% endnote %} ## See also - [StateAccessor (Python)](https://ytsaurus.tech/docs/en/state-accessor.md) - [External State (Python)](https://ytsaurus.tech/docs/en/external-state.md) - [Working with states (Python)](https://ytsaurus.tech/docs/en/state.md) — brief overview - [Stateful processing (concept)](https://ytsaurus.tech/docs/concepts/stateful.md) - [Internal State (Java)](https://ytsaurus.tech/docs/java/internal-state.md) - [WordCount example](https://ytsaurus.tech/docs/en/examples/wordcount.md) # External State in YTsaurus Flow (Python) Use External State to work with external state stored in an external dynamic table in YTsaurus. You create the table to store the state yourself, on the same cluster where the pipeline is deployed. For general information about stateful processing, see the [Stateful processing](https://ytsaurus.tech/docs/concepts/stateful.md) section. ## Overview In the Python SDK, External State is represented by the `ExternalStateAccessor` class. This class is a subclass of `Payload` and gives you dict-like access to the columns of the external dynamic table. The state is bound to the message key (`group_by_schema`). For more details about `StateAccessor` and working with state, see [StateAccessor](https://ytsaurus.tech/docs/en/state-accessor.md). {% note info %} If you need only read-only access to the external state (a key-based join with TTL caching, without modification), the framework provides a separate mechanism: **External State Joiner**. Currently, the joiner is available only in C++ ([External State Joiner](https://ytsaurus.tech/docs/cpp/state.md#external-state-joiner)) and is declared in the computation spec, in the top-level `external_state_joiners` section (at the same level as `external_state_managers`). {% endnote %} ## Difference from Internal State | Characteristic | External State | Internal State | |---|---|---| | Storage | External dynamic table | Flow internal tables | | Table creation | You create it yourself | Automatic | | Data format | Typed `Payload` (table row) | Arbitrary (YSON, Protobuf, raw bytes) | | Access from other systems | Yes (sorted dynamic table) | No | | Schema | Defined by the table schema | Defined by you | For more details about Internal State, see [Internal State](https://ytsaurus.tech/docs/en/internal-state.md). ## Getting ExternalStateAccessor {#getting-accessor} You get `ExternalStateAccessor` through `RuntimeContext` (`ctx`): ```python # For a message state = ctx.external_state("/state-name", message) # For a timer state = ctx.external_state("/state-name", timer) ``` Parameters: - `"/state-name"` — a string with the state name from the `external_state_managers` section of the [static spec](https://ytsaurus.tech/docs/concepts/glossary.md#spec-and-dynamic-spec). The name must start with `/` and match the key in the spec. - `message` / `timer` — the message or timer for which you need to get the state for the [key](https://ytsaurus.tech/docs/concepts/glossary.md#key). {% note warning %} The external state name is validated: it must start with `/`, must not be empty, must not end with `/`, and must not contain two consecutive `/` characters. Calling `ctx.external_state("state-name", message)` (without the leading `/`) will raise a `ValueError`. {% endnote %} ## ExternalStateAccessor as Payload {#accessor-as-payload} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/context.py) `ExternalStateAccessor` inherits from the `Payload` class, which lets you read column values directly: ```python state = ctx.external_state("/shuffle-state", message) # Dict-like access value = state["count"] # Raises KeyError if the column doesn't exist value = state.get("count") # Returns None if the column doesn't exist exists = "count" in state # Checks existence ``` The `Payload` class also provides: - `keys()` — a list of column names with non-empty values. - `to_dict()` — converts to a standard Python dict. ## Main operations {#operations} ### Reading state ```python state = ctx.external_state("/join-state", message) # Read a column value hit_payload = state.get("hit_payload", str) show_time = state.get("show_time") # Dict-like access try: value = state["hit_payload"] except KeyError: value = None ``` ### Writing state with PayloadBuilder To modify the state, use the `to_builder()` / `set()` / `finish()` pattern: ```python state = ctx.external_state("/join-state", message) # Get a builder with current values builder = state.to_builder() # Update the required fields builder.set("hit_payload", "some_value") builder.set("show_time", 1234567890) # Save the updated state state.set(builder.finish()) ``` The `to_builder()` method returns a `PayloadBuilder` pre-filled with the current state values. The `builder.set(column, value)` method returns the builder itself (it supports chaining calls). The `builder.finish()` method creates a new `Payload` and resets the builder. ### Clearing state ```python state = ctx.external_state("/join-state", timer) # Delete the row from the table state.clear() ``` {% note info %} An empty state corresponds to the absence of a row in the table. If the row doesn't exist, `state.get("column")` returns `None`. Calling `clear()` removes the row from the table. {% endnote %} ## Configuration in the static spec {#static-spec} To use External State, you must declare an external state manager in the `external_state_managers` section of the [computation](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) in the static spec: ```yson "computations" = { "reducer" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"}; {"name" = "key"; "type" = "string"}; ]; "input_stream_ids" = ["input"]; "output_stream_ids" = ["output"]; "external_state_managers" = { "/shuffle-state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state/table"; }; }; }; "parameters" = {}; }; }; ``` Key fields: - `external_state_managers` — a top-level section inside `Computation` that describes external state managers (previously `parameters/external_states`). - The key inside `external_state_managers` (for example, `"/shuffle-state"`) — the state name you use in Python code when calling `ctx.external_state("/shuffle-state", message)`. The name must start with `/`. - `external_state_manager_class_name` — the name of the registered external state manager class. For a typical scenario, use `"NYT::NFlow::TSimpleExternalStateManager"`; For more details, see the [C++ documentation](https://ytsaurus.tech/docs/cpp/state.md#external-state). - `parameters.path` — the path to the YTsaurus dynamic table where the state is stored. ## Creating a table for the state You must create the table for External State in advance. The schema of the table's key columns must match the computation's `group_by_schema`. ## Complete example — EventReducer from Shuffle {#example} Example from [shuffle](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/shuffle): ```python from yt.yt.flow.library.python.companion.computation import RowFunction class EventReducer(RowFunction): """Counts the number of events for each key using external state.""" def on_message(self, message, output, ctx): state = ctx.external_state("/shuffle-state", message) builder = state.to_builder() builder.set("count", (state.get("count") or 0) + 1) state.set(builder.finish()) ``` [Full source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/shuffle/event_reducer.py) Work pattern: 1. Get the current state with `ctx.external_state(...)`. 2. Create a builder with `state.to_builder()`. 3. Update the required fields with `builder.set(...)`. 4. Save with `state.set(builder.finish())`. ## See also - [StateAccessor (Python)](https://ytsaurus.tech/docs/en/state-accessor.md) - [Internal State (Python)](https://ytsaurus.tech/docs/en/internal-state.md) - [Working with states (Python)](https://ytsaurus.tech/docs/en/state.md) — a brief overview - [Stateful processing (concept)](https://ytsaurus.tech/docs/concepts/stateful.md) - [External State (Java)](https://ytsaurus.tech/docs/java/external-state.md) - [Shuffle example](https://ytsaurus.tech/docs/en/examples/shuffle.md) # Testing in YTsaurus Flow (Python) {% note info %} This page describes **unit testing** of Python-[pipeline](https://ytsaurus.tech/docs/concepts/glossary.md#pipeline) components, as well as **integration testing** of the full pipeline via `FlowTestPythonBase`. {% endnote %} ## General testing architecture {#architecture} In production, the C++ [worker](https://ytsaurus.tech/docs/concepts/glossary.md#worker) sends gRPC requests to the [companion](https://ytsaurus.tech/docs/concepts/companion.md), passing [messages](https://ytsaurus.tech/docs/concepts/glossary.md#message), timers, [states](https://ytsaurus.tech/docs/concepts/glossary.md#state), and [watermarks](https://ytsaurus.tech/docs/concepts/watermarks.md). The companion calls `Computation.do_process()`, which delegates processing to `ProcessFunction`. In unit tests, pipeline components are tested by directly calling `do_process()` on the `Computation` object with a prepared `RequestContext`. All tests use the standard pytest. ## Dependencies {#dependencies} To unit test the companion library, you need to add dependencies to `ya.make`: ``` PEERDIR( yt/yt/flow/library/python/companion ) ``` If you’re using Protobuf states, also add a dependency on the proto library. ## Testing ProcessFunction {#testing-process} [Test source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/test/test_computation.py) To test `RowFunction` / `BatchFunction`, you create a `Computation` object and call `do_process()` with a prepared `RequestContext`. ### Creating RequestContext `RequestContext` is a dataclass that contains the input data for processing: ```python from yt.yt.flow.library.python.companion.context import RequestContext from yt.yt.flow.library.python.companion.job import Job from yt.yt.flow.library.python.companion.stream import ( StreamIdsMapping, StreamSpecs, RawStream, ) from yt.yt.flow.library.python.companion.row import ExtendedMessage def make_request_ctx(messages=None, timers=None): """Create a minimal RequestContext for tests.""" mapping = StreamIdsMapping({"input": 0, "output": 1}) specs = StreamSpecs(mapping, [RawStream("input"), RawStream("output")]) job = Job( job_id="test-job", computation_id="test-comp", stream_specs=specs, static_spec={}, ) return RequestContext( job_id="test-job", request_id="test-req", computation_id="test-comp", messages=messages or [], timers=timers or [], stream_specs=specs, job=job, ) ``` ### Testing RowFunction ```python from yt.yt.flow.library.python.companion.computation import ( Computation, RowFunction, ) from yt.yt.flow.library.python.companion.row import ( ExtendedMessage, Message, ) class PassthroughFunction(RowFunction): def on_message(self, message, output, ctx): output.add_message( Message(message_id=message.message_id, stream_id="output") ) def test_passthrough(): comp = Computation( computation_id="test", process_function=PassthroughFunction(), ) messages = [ ExtendedMessage(message_id="m1", stream_id="input"), ExtendedMessage(message_id="m2", stream_id="input"), ] ctx = make_request_ctx(messages=messages) response = comp.do_process(ctx) assert len(response.transform_results) == 2 assert response.transform_results[0].messages[0].message_id == "m1" assert response.transform_results[1].messages[0].message_id == "m2" ``` ### Testing timers ```python from yt.yt.flow.library.python.companion.row import Timer class TimerFunction(RowFunction): def on_message(self, message, output, ctx): output.add_timer(trigger_timestamp=1000, event_timestamp=500) def on_timer(self, timer, output, ctx): output.add_message( Message(message_id="from-timer", stream_id="output") ) def test_timer_roundtrip(): comp = Computation( computation_id="test", process_function=TimerFunction(), ) messages = [ExtendedMessage(message_id="m1")] timers = [Timer(message_id="t1")] ctx = make_request_ctx(messages=messages, timers=timers) response = comp.do_process(ctx) results = response.transform_results # The message creates a timer, the timer creates a message assert any(r.timers for r in results) assert any(r.messages for r in results) ``` ## Testing states {#testing-states} [Test source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/companion/test/test_context.py) ### YSON State ```python import yt.type_info as ti from yt.yt.flow.library.python.companion.context import DefaultRuntimeContext from yt.yt.flow.library.python.companion.row import ( ColumnSchema, ExtendedMessage, Payload, TableSchema, ) from yt.yt.flow.library.python.companion.stream import ( StreamIdsMapping, StreamSpecs, RawStream, ) from yt.yt.flow.library.python.companion.wire_protocol import ( ColumnValueType, UnversionedRow, UnversionedValue, ) KEY_SCHEMA = TableSchema([ColumnSchema("id", ti.String)]) def make_key_payload(): row = UnversionedRow(values=[ UnversionedValue(column_id=0, type=ColumnValueType.STRING, value=b"test-key"), ]) return Payload(row, KEY_SCHEMA) def make_ctx(internal_state_names=None, **kwargs): streams = [RawStream("input"), RawStream("output")] mapping = StreamIdsMapping({s.stream_id: i for i, s in enumerate(streams)}) specs = StreamSpecs(mapping, streams) return DefaultRuntimeContext( internal_state_names=internal_state_names or set(), stream_specs=specs, internal_states=kwargs.get("internal_states", {}), external_states=kwargs.get("external_states", {}), watermarks={}, min_watermark=0, computation_parameters={}, key_schema=KEY_SCHEMA, ) def test_yson_state_roundtrip(): ctx = make_ctx(internal_state_names={"word-state"}) message = ExtendedMessage(message_id="m1", key=make_key_payload()) accessor = ctx.state("word-state", message) accessor.set({"word": "hello", "count": 1}) accessor2 = ctx.state("word-state", message) result = accessor2.get() assert result["word"] == "hello" assert result["count"] == 1 ``` ### Proto State ```python from google.protobuf.wrappers_pb2 import Int64Value def test_proto_state_roundtrip(): ctx = make_ctx(internal_state_names={"join-state"}) message = ExtendedMessage(message_id="m1", key=make_key_payload()) accessor = ctx.proto_state("join-state", message, Int64Value) state = Int64Value(value=42) accessor.set(state) accessor2 = ctx.proto_state("join-state", message, Int64Value) assert accessor2.get().value == 42 ``` ### External State ```python from yt.yt.flow.library.python.companion.state import StatesHolder from yt.yt.flow.library.python.companion.row import PayloadBuilder STATE_SCHEMA = TableSchema([ ColumnSchema("count", ti.Int64), ColumnSchema("name", ti.String), ]) def test_external_state_roundtrip(): ext_holder = StatesHolder("ext", KEY_SCHEMA, STATE_SCHEMA) ctx = make_ctx(external_states={"/shuffle-state": ext_holder}) message = ExtendedMessage(message_id="m1", key=make_key_payload()) state = ctx.external_state("/shuffle-state", message) builder = state.to_builder() builder.set("count", 99) state.set(builder.finish()) state2 = ctx.external_state("/shuffle-state", message) assert state2.get("count") == 99 ``` ## Analyzing the response {#analyzing-response} The `ResponseContext` object returned by `do_process()` contains: | Field | Type | Description | |------|------|-------------| | `transform_results` | `List[TransformResult]` | List of processing results | | `internal_states` | `Dict[str, StatesHolder]` | Internal states after processing | | `external_states` | `Dict[str, StatesHolder]` | External states after processing | Each `TransformResult` contains: | Field | Type | Description | |------|------|-------------| | `parent_ids` | `List[str]` | IDs of parent messages | | `messages` | `List[Message]` | Output messages | | `timers` | `List[NewTimer]` | Created timers | ## End-to-end testing with FlowTestPythonBase {#e2e-tests} Use the `FlowTestPythonBase` base class for full end-to-end pipeline testing (with real C++ workers, queues, and streams). ### Dependencies {#integration-dependencies} In addition to `PEERDIR` for `integration_test_base`, the integration test needs a cluster recipe, `DEPENDS` for the pipeline binary and `flow_server`, and `DATA` with the spec. The complete `ya.make` for the test from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md): ```text PY3TEST() INCLUDE(${ARCADIA_ROOT}/yt/yt/flow/library/python/integration_test_base/recipe.inc) TEST_SRCS( test_wordcount.py yt_sync.py ) PEERDIR( yt/yt/flow/library/python/queue ) DEPENDS( ${MODDIR}/.. yt/yt/flow/bin/flow_server ) DATA(arcadia/${MODDIR}/pipeline.yson) REQUIREMENTS( cpu:4 ram:32 ) TAG(ya:huge_logs) SIZE(MEDIUM) END() ``` ### Setup {#python-test-setup} The test inherits from `FlowTestPythonBase` and sets the `PYTHON_COMPANION_BINARY` attribute: ```python class Test(FlowTestPythonBase): PYTHON_COMPANION_BINARY = yatest.common.binary_path("yt/yt/flow/examples/python/word_count/word_count") ``` | Attribute | Description | |----------|-------------| | `PYTHON_COMPANION_BINARY` | Path to the Python companion binary | [Example of an E2E WordCount test](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/word_count/test/test_wordcount.py) {% note warning %} Integration tests require a deployed YTsaurus cluster and are run with `ya make -tt`. For fast iteration, use the unit tests described above. {% endnote %} Use these principles when writing integration tests for pipelines: * Test end-to-end scenarios. That is: * Write input/source data to the local YT. * In the pipeline spec, mark the sources as `finite=%true`. * For a [Key Visitor stream](https://ytsaurus.tech/docs/concepts/key_visitor.md) that should run during the test, set `finite=%false`. * Run the pipeline. * When the Key Visitor should finish, call the integration test base method [`self.ask_key_visitor_to_complete("", "")`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base/yt_flow_base.py); it switches that stream's [dynamic `finite` parameter](https://ytsaurus.tech/docs/concepts/key_visitor.md#dynamic-params) to `%true`. * Wait for the pipeline to finish. * Check the output data. * Prepare the environment with the same code you use in production: * Test the same pipeline binary that will run in production. * Generate pipeline specs with the same code that generates them for production. * If input/output data require non-trivial serialization/parsing, implement this logic with code shared with production. * Use the [shared test framework](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base) (a `README.md` is available). * Implement a failover test. * Many errors surface when workers fail and other workers take over their tasks. So, run a test with `problems=True` and more than one worker. * Write stable tests. * Remember that in CI, any part of a test might run unexpectedly long. * The ideal runtime for a single test is 20 seconds for any build type. * For sanitizer builds, reduce the amount of input data. * Set all local timeouts with a multiple safety margin. * The test execution logic shouldn’t depend significantly on the current time. For example, a test shouldn’t fail if it starts before midnight and finishes after. ## Debugging tests {#debug} ### Logs {#debug-logs} After a test finishes, you can find its logs in `test-results/py3test/testing_out_stuff` in the test directory. Key logs include: * `run.log` — Python test logs. * `//Controller_...` — Controller logs (`.err` is the process’s stderr; `.log` contains regular logs written via `YT_LOG_...`). * `//Worker_...` — Worker logs. * `//Runner...` — Runner logs. If something isn’t working, check errors in all these logs. The approach to reviewing them is the same as in production — see [Logs](https://ytsaurus.tech/docs/devops/vanilla/diagnostics/logs.md). You can also view logs before the test finishes. To do this, locate the temporary directory where the test runs. The simplest way is to run the test with the `--keep-temps` flag: `ya make --keep-temps -ttt `. In this case, `ya make` won’t delete the temporary directory after the test finishes and will print a link to it in the output. You can also use these bash aliases: ```bash alias curtestdir="ps -f -u $USER | python3 -c \"import sys, re; drs = set(e for e in re.findall(r'[\s=](https://ytsaurus.tech/docs/en//S*testing'%20+%20r'_out_stuff)\b', sys.stdin.read())); print('' if len(drs) == 1 else 'Select first from ' + repr(drs), file=sys.stderr); print(list(drs)[0])\"" alias cdcurtestdir='cd $(curtestdir)' # Get the link to the local YTsaurus UI from the logs. alias curlocalyt='cat $(curtestdir)/stderr 2>/dev/null | grep YT' ``` ### Test framework {#debug-test-framework} You can read about how to set up the environment for better test framework performance and how to influence test parameters in the framework’s [README.md](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base). You configure the behavior of integration tests using `--test-param NAME=VAL`: #| || **Parameter** | **Default** | **Values** | **Action** || || `RUNNER_LOG_LEVEL` | | `Error`, `Info`, `Debug`, … | Sets the logging level for the process that runs the pipeline (runner). || || `PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN` | `0` | `0`, `1` | Pauses the test before stopping Flow processes. Combined with `--test-disable-timeout`, this lets you keep the local YTsaurus and the Flow process federation running for a long time so you can study them at your own pace via the UI. || || `EXTERNAL_YT_CONFIG` | (not set) | YSON — see below | Runs the pipeline on real external YTsaurus clusters instead of the local recipe. || |# Examples: ```bash ya make -A --test-param RUNNER_LOG_LEVEL=Debug ya make -A --test-disable-timeout --test-param PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN=1 ``` ### `EXTERNAL_YT_CONFIG` {#external-yt-config} The local YTsaurus still starts via the recipe, but the test ignores it. Required fields that are common for all clusters: - `path` — base directory. - `tablet_cell_bundle` — bundle for the dynamic tables that are created. - `proxy_role` — RPC proxy role. Optional: `primary_medium` (default `"default"`). The `clusters` list — the first element is primary. Record fields: `cluster_name` (required), `proxy_url` (defaults to `cluster_name`). Authorization: when using an external YTsaurus, `YT_TOKEN`/`YT_USER` from the local recipe are cleared; yt-wrapper picks up the token from `~/.yt/token`. Isolation: `work_yt_path = path//`; the `path/` directory is deleted and recreated once per class in `setup_class`. Example: ```bash ya make -A --test-param 'EXTERNAL_YT_CONFIG={path="//tmp/yt_flow";tablet_cell_bundle=default;proxy_role=default;clusters=[{cluster_name=};];}' ``` ## Speeding up iteration: `--ext-py` {#ext-py} The `--ext-py` flag avoids linking the binary on every `ya make` run if the changes from the previous run affected only `*.py` files: ```bash ya make -A --ext-py ``` ## See also - [Computation (Python)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (Python)](https://ytsaurus.tech/docs/en/state.md) - [Distribute flag (Python)](https://ytsaurus.tech/docs/en/distribute.md) - [Testing (Java)](https://ytsaurus.tech/docs/java/testing.md) - If you're extending Flow itself — [Pipeline testing framework](https://ytsaurus.tech/docs/contributor/testing-framework.md). # Word Count in YTsaurus Flow (Python) Use this simple stateful-pipeline example in Python to count how many times each word appears. You’ll use an internal YSON-state for the counts. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/word_count) ## Structure The pipeline includes a single transform-computation called `mapper`. You use it to read words from the input stream and update the counter in the state. ## `__main__.py` This is the entry point. You create the pipeline and register the only computation. ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("mapper", WordCountMapper()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` ## `word_count_mapper.py` This `RowFunction` uses `ctx.state()` to work with the YSON-state. For each message key, you store a Map with the `word` and `count` fields. ```python class WordCountMapper(RowFunction): """Processes Word messages and maintains word count state.""" def on_message(self, message, output, ctx): state = ctx.state("word-state", message) data = state.get_or_default({"word": message.payload["word"], "count": 0}) data["count"] += 1 state.set(data) ``` ## Key patterns - A simple stateful pipeline with a single computation. - Internal YSON-state via `ctx.state()` with `get_or_default` and `set`. - The state key is defined by `group_by_schema` from the spec (in this case, by the `word` field). ## See also - [Quick start (Python)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Python)](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) # Shuffle in YTsaurus Flow (Python) Use this pipeline example with two computations: the source computation parses JSON data and sends typed messages, and the transform computation counts events in an external state. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/shuffle) ## Structure - `reader` (source) -- `EventMapper`: parses JSON from the `data` field and sends typed messages to the `event` stream. - `reducer` (transform) -- `EventReducer`: counts the number of events by key using external state. ## `__main__.py` ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("reader", EventMapper(), source=True) pipeline.add("reducer", EventReducer()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` ## `event_mapper.py` The source function parses JSON from the `data` field of the input message and creates a typed message using `ctx.message_builder()`: ```python class EventMapper(RowFunction): """Parses JSON from data field and emits event messages with key fields.""" def on_message(self, message, output, ctx): data = message.payload["data"] try: parsed = json.loads(data) except (json.JSONDecodeError, TypeError) as e: log.error("Error parsing json from data field: %s", e) raise builder = ctx.message_builder("event") builder.set("value", parsed.get("value")) builder.set("key_a", parsed.get("key_a")) builder.set("key_b", parsed.get("key_b")) builder.set("key_c", parsed.get("key_c")) builder.set("key_d", parsed.get("key_d")) output.add_message(builder.finish()) ``` ## `event_reducer.py` The transform function uses external state to count events: ```python class EventReducer(RowFunction): """Counts events per key using external state.""" def on_message(self, message, output, ctx): state = ctx.external_state("/shuffle-state", message) builder = state.to_builder() builder.set("count", (state.get("count") or 0) + 1) state.set(builder.finish()) ``` ## Key patterns - A pipeline with multiple computations: source + transform. - A [source](https://ytsaurus.tech/concepts/glossary.md#source) computation with `source=True` to read from an external source. - Parsing JSON and creating typed messages using `ctx.message_builder()`. - External state with the `to_builder()` / `set()` / `finish()` pattern. ## See also - [Quick start (Python)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Python)](https://ytsaurus.tech/docs/computation.md) # Async Request in YTsaurus Flow (Python) This example shows a two-component [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that implements asynchronous request processing. One [computation](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) routes events into requests and accumulates responses. The other performs computation without [state](https://ytsaurus.tech/concepts/glossary.md#state). This is a Python implementation of a similar [C++ example](https://ytsaurus.tech/cpp/examples/async_request.md). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/async_request) ## Structure The pipeline includes two computations: 1. **`state`** (`StateKeeperFunction`) — a stateful computation that: - Accepts events from the `event` stream and generates requests to the `request` stream with a unique `request_id`. - Accepts responses from the `response` stream and accumulates the total length (`total_length`) in the external [state](https://ytsaurus.tech/concepts/glossary.md#state). 2. **`processor`** (`RequestProcessorFunction`) — a stateless computation that accepts requests from the `request` stream and immediately returns a response (the length of the request string) to the `response` stream. The `event → request → response → state` cycle closes between the two computations. ## `state_keeper_function.py` This file handles routing of incoming streams (`event` / `response`) and working with the external state. ```python class StateKeeperFunction(RowFunction): """Sends requests for events; accumulates total_length for responses.""" def on_message(self, message, output, ctx): stream_id = message.stream_id if stream_id == "event": key = message.payload["key"] data = message.payload["data"] request_id = random.getrandbits(64) builder = ctx.message_builder("request") builder.set("request_id", request_id) builder.set("key", key) builder.set("request", data) output.add_message(builder.finish()) log.debug("Send request (RequestId: %s)", request_id) elif stream_id == "response": length = message.payload["length"] request_id = message.payload["request_id"] log.debug("Received response (RequestId: %s)", request_id) state = ctx.external_state("/state", message) current = state.get("total_length") total = (current if current is not None else 0) + length builder = state.to_builder() builder.set("total_length", total) state.set(builder.finish()) else: raise ValueError(f"Unknown stream_id: {stream_id}") ``` ## `request_processor_function.py` This is a stateless request handler: it calculates the length of the request string and returns the response. ```python class RequestProcessorFunction(RowFunction): """Stateless: returns length of request string as response.""" def on_message(self, message, output, ctx): request_id = message.payload["request_id"] key = message.payload["key"] request = message.payload["request"] length = len(request) builder = ctx.message_builder("response") builder.set("request_id", request_id) builder.set("key", key) builder.set("length", length) output.add_message(builder.finish()) log.debug("Processed request (RequestId: %s)", request_id) ``` ## `__main__.py` This is the entry point: it creates the pipeline and registers both computations. ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("state", StateKeeperFunction()) pipeline.add("processor", RequestProcessorFunction()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` ## Key patterns - **Routing by `stream_id`**: the `if stream_id == "event" / "response"` branching lets a single computation handle multiple input streams with different logic. - **Generating a unique `request_id`**: `random.getrandbits(64)` ensures correlation between the request and the response in the asynchronous cycle. - **External state** via `ctx.external_state("/state", message)`: the `to_builder()` / `set()` pattern supports cumulative updates to the external state. - **Stateless computation**: `RequestProcessorFunction` doesn’t use state — it’s a pure transformation of a request into a response, which lets you scale it independently. - **Two-component pipeline**: `pipeline.add("state", ...)` and `pipeline.add("processor", ...)` register the computations. The streams between them are described in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec). ## See also - [Quick start (Python)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Python)](https://ytsaurus.tech/docs/computation.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/async_request.md) - [Similar example in Java](https://ytsaurus.tech/java/examples/async_request.md) # Retryable Async Request in YTsaurus Flow (Python) This extends the [AsyncRequest](https://ytsaurus.tech/docs/en/async_request.md) example: the request handler supports automatic retries using [timers](https://ytsaurus.tech/concepts/glossary.md#timer). If a request fails, it’s saved in the internal [state](https://ytsaurus.tech/concepts/glossary.md#state) and retried after a fixed interval, up to `MAX_RETRIES` attempts. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/retryable_async_request) ## Structure The pipeline includes two computations: 1. **`state`** (`StateKeeperFunction`) — a stateful computation: it routes events into requests and accumulates the total response length in the external state (similar to `async_request`). 2. **`processor`** (`RequestProcessorFunction`) — a stateful computation with retry logic: - On the first attempt, it saves the request in the internal state and tries to process it. - If processing fails, it increments the failure counter, saves the state, and sets a timer (`DELAY_SECONDS = 5`). - It retries the attempt after the timer fires (up to `MAX_RETRIES = 3`). - If successful, it sends the response and clears the state. ## `request_processor_function.py` This is the key example file: it implements retry logic via `on_timer` and the helper method `_try_or_retry`. ```python class RequestProcessorFunction(RowFunction): """Retries requests up to MAX_RETRIES times; emits response on success.""" def on_message(self, message, output, ctx): request_id = message.payload["request_id"] data = { "request_id": request_id, "key": message.payload["key"], "request": message.payload["request"], "failed_attempts": 0, } state = ctx.state("request-state", message) self._try_or_retry(state, data, output, ctx) def on_timer(self, timer, output, ctx): state = ctx.state("request-state", timer) data = state.get_or_default(None) if data is not None: self._try_or_retry(state, data, output, ctx) def _try_or_retry(self, state, data, output, ctx): request_id = data["request_id"] failed_attempts = data["failed_attempts"] if not _is_request_successful(request_id, failed_attempts): data["failed_attempts"] += 1 state.set(data) output.add_timer(int(time.time()) + DELAY_SECONDS) log.debug( "Failed request (RequestId: %s, FailedAttempts: %s)", request_id, data["failed_attempts"], ) else: key = data["key"] length = len(data["request"]) builder = ctx.message_builder("response") builder.set("request_id", request_id) builder.set("key", key) builder.set("length", length) output.add_message(builder.finish()) state.clear() log.debug( "Processed request (RequestId: %s, FailedAttempts: %s)", request_id, failed_attempts, ) ``` ## `state_keeper_function.py` It handles stream routing and accumulates results in the external state (the same as in `async_request`). ```python class StateKeeperFunction(RowFunction): """Routes event messages to requests and accumulates response lengths in external state.""" def on_message(self, message, output, ctx): stream_id = message.stream_id if stream_id == "event": key = message.payload["key"] data = message.payload["data"] request_id = random.getrandbits(64) builder = ctx.message_builder("request") builder.set("request_id", request_id) builder.set("key", key) builder.set("request", data) output.add_message(builder.finish()) log.debug("Send request (RequestId: %s)", request_id) elif stream_id == "response": request_id = message.payload["request_id"] length = message.payload["length"] log.debug("Received response (RequestId: %s)", request_id) state = ctx.external_state("/state", message) current = state.get("total_length") total_length = current if current is not None else 0 total_length += length builder = state.to_builder() builder.set("total_length", total_length) state.set(builder.finish()) else: raise ValueError(f"Unexpected stream_id: {stream_id}") ``` ## `__main__.py` This is the entry point: its structure matches `async_request`, but it imports different classes. ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("state", StateKeeperFunction()) pipeline.add("processor", RequestProcessorFunction()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` ## Key patterns - **Retry via timers**: `output.add_timer(int(time.time()) + DELAY_SECONDS)` delays the next retry; `on_timer` reads the state and runs `_try_or_retry` again. This is the standard pattern for implementing retries in Flow without external queues. - **Retry counter in state**: the `failed_attempts` field is stored with the request data in `ctx.state("request-state", message)`, which ensures correctness after a restart. - **Deterministic failure simulation**: `_is_request_succeed(request_id, failed_attempts)` simulates an unstable external service; in real code, you replace this with an HTTP client call. - **Clearing state on success**: `state.clear()` after successful processing prevents the timer from triggering again on outdated data. - **Separation of concerns**: `StateKeeperFunction` doesn’t know about retries — all retry logic is encapsulated in `RequestProcessorFunction`, which makes it easier to swap retry strategies. ## See also - [Quick start (Python)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Python)](https://ytsaurus.tech/docs/computation.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) - [AsyncRequest (Python)](https://ytsaurus.tech/docs/en/async_request.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/retryable_async_request.md) - [Similar example in Java](https://ytsaurus.tech/java/examples/retryable_async_request.md) # Wait Click Join in YTsaurus Flow (Python) This is an example of a join-pipeline that merges hit and action streams by key, using timers and external state. It’s a Python implementation of a similar C++ example. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/wait_click_join) ## Structure The pipeline includes a single transform computation called `join`, which: - Receives messages from two input streams: `hit` and `action`. - Accumulates data in the external state. - Sets a timer for the end of the waiting window. - When the timer triggers, it generates a join result or clears the state. ## `__main__.py` ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("join", JoinProcessFunction()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` ## `join_process_function.py` This file holds the core join logic. The function processes two streams and uses timers for windowed aggregation: ### `on_message` ```python def on_message(self, message, output, ctx): stream_id = message.stream_id if stream_id == "hit": hit_id = message.payload["hit_id"] hit_time = message.payload["hit_time"] hit_payload = message.payload["hit_payload"] elif stream_id == "action": hit_id = message.payload["hit_id"] hit_time = message.payload["hit_time"] else: raise ValueError(f"Unknown streamId: {stream_id}") log.debug("Process message (Key: %s, HitId: %s, HitTime: %s)", message.key, hit_id, hit_time) wait_time = _parse_duration_seconds(ctx.parameters["wait_for_actions"]) max_time = hit_time + wait_time # All messages out of wait range would be dropped. if message.event_timestamp >= max_time: log.warning( "Message is out of wait range, skip (HitId: %s, HitTime: %s, MaxTime: %s)", hit_id, hit_time, max_time, ) return # Drop late data. if message.event_timestamp < ctx.min_watermark: log.warning( "Late message, skip (HitId: %s, EventTimestamp: %s, InputEventWatermark: %s)", hit_id, message.event_timestamp, ctx.min_watermark, ) return state = ctx.external_state("/join-state", message) builder = state.to_builder() if stream_id == "hit": log.debug("Process hit message (HitId: %s, HitPayload: %s)", hit_id, hit_payload) builder.set("hit_payload", hit_payload) else: action_time = message.payload["action_time"] is_click = message.payload["is_click"] log.debug("Process action message (HitId: %s, ActionTime: %s)", hit_id, action_time) if is_click: builder.set("click_time", action_time) else: builder.set("show_time", action_time) state.set(builder.finish()) log.debug("Add timer for Key (MaxTime: %s, HitTime: %s)", max_time, hit_time) output.add_timer(max_time, hit_time) ``` ### `on_timer` ```python def on_timer(self, timer, output, ctx): hit_id = timer.key["hit_id"] log.debug("Process timer (KeyHitId: %s, Timer: %s)", hit_id, timer) state = ctx.external_state("/join-state", timer) log.debug("Process state (KeyHitId: %s, State: %s)", hit_id, state) if timer.stream_id == "timer": show_time = state.get("show_time") hit_payload = state.get("hit_payload") if show_time is not None and hit_payload is not None and show_time != 0: log.debug("Click join: %s", state) hit_time = timer.key["hit_time"] click_time = state.get("click_time") builder = ctx.message_builder("joined_action") builder.set("hit_id", hit_id) builder.set("hit_time", hit_time) builder.set("show_time", show_time) builder.set("hit_payload", hit_payload) if click_time is not None and click_time != 0: builder.set("is_click", True) builder.set("click_time", click_time) else: builder.set("is_click", False) builder.set("click_time", 0) output.add_message(builder.finish()) state.clear() else: raise ValueError(f"Unknown streamId: {timer.stream_id}") ``` ## Key patterns - **External state** with `ctx.external_state("/join-state", message)`: use the `to_builder()` / `set()` / `clear()` pattern to accumulate data from multiple streams. - **Timers** via `output.add_timer(max_time, hit_time)`: set a timer with `trigger_timestamp` (when it fires) and `event_timestamp` (linked to the event). - **Late data filtering**: use `message.event_timestamp < ctx.min_watermark` to discard late data (learn more about watermarks). - **Computation parameters**: read configuration from the spec with `ctx.parameters["wait_for_actions"]`. - **MessageBuilder**: create output messages with a defined schema using `ctx.message_builder("joined_action")`. - **Processing multiple streams**: branch on `message.stream_id` to apply different logic for handling hit and action streams. ## See also - [Quick start (Python)](https://ytsaurus.tech/docs/getting-started.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) # URL Downloader in YTsaurus Flow (Python) This example shows a [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that groups incoming URLs by host and processes them in batches using [timers](https://ytsaurus.tech/concepts/glossary.md#timer). It demonstrates the typical pattern: “accumulate in state → process by timer → clear state.” [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/python/url_downloader) ## Structure The pipeline includes a single transform-[computation](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) called `url_downloader`, which: - Accepts messages with `host` and `url` fields from the input [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). - Accumulates URLs for each host in an internal YSON [state](https://ytsaurus.tech/concepts/glossary.md#state). - Sets a timer for 5 seconds ahead each time a new URL arrives. - When the timer fires, it processes all accumulated URLs and publishes the results to the output stream `processed_urls`. ## `url_download_function.py` This file contains the core processing logic. The function implements both `RowFunction` methods: `on_message` to accumulate URLs in the state and `on_timer` to process them in batches. ```python class UrlDownloadFunction(RowFunction): """Groups URLs by host, processes them with a timer-driven loop.""" def on_message(self, message, output, ctx): host = message.payload["host"] url = message.payload["url"] state = ctx.state("host-state", message) data = state.get_or_default({"host": host, "pending_urls": []}) data["pending_urls"].append(url) state.set(data) output.add_timer(int(time.time()) + 5) def on_timer(self, timer, output, ctx): state = ctx.state("host-state", timer) data = state.get_or_default(None) if not data or not data.get("pending_urls"): state.clear() return host = data["host"] for url in list(data["pending_urls"]): result = _process_url(url) builder = ctx.message_builder("processed_urls") builder.set("host", host) builder.set("url", url) builder.set("data", result) output.add_message(builder.finish()) log.debug("Processed url (Host: %s, Url: %s)", host, url) state.clear() ``` ## `__main__.py` This is the entry point: it creates the pipeline and registers the single `url_downloader` computation. ```python def main(): log.info("Starting companion execution") pipeline = Pipeline() pipeline.add("url_downloader", UrlDownloadFunction()) log.info("Starting pipeline...") pipeline.run() log.info("Pipeline completed") ``` ## Key patterns - **Timer-driven batch processing**: `output.add_timer(int(time.time()) + 5)` in `on_message` and the processing logic in `on_timer` provide a standard way to batch events by a time window. - **Internal YSON state** via `ctx.state("host-state", message)`: the `get_or_default` / `set` / `clear` pattern helps you accumulate and clear data. - **State key by host**: you group URLs by host using `group_by_schema` in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec), so each computation instance processes URLs for a single host in isolation. - **MessageBuilder**: `ctx.message_builder("processed_urls")` builds output messages with an explicit stream schema. - **Safety check in `on_timer`**: `if not data or not data.get("pending_urls")` prevents reprocessing data after the state is cleared. ## See also - [Quick start (Python)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Python)](https://ytsaurus.tech/docs/computation.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/url_downloader.md) - [Similar example in Java](https://ytsaurus.tech/java/examples/url_downloader.md) # Getting started with YTsaurus Flow (Go) Go computation support in Flow is built on the [companion](https://ytsaurus.tech/flow/concepts/glossary.md#companion) mechanism. Go code runs in a separate gRPC process that interacts with the C++ [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker). [Go SDK source code for Flow](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow) [Examples](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go) Import the SDK as `go.ytsaurus.tech/yt/go/flow`. ## Application architecture {#architecture} Every Flow [pipeline](https://ytsaurus.tech/flow/concepts/glossary.md#pipeline) consists of three parts: - `Runner` — starts the pipeline and installs a new version of the [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec). - `Controller` — manages the pipeline. - `Worker` — performs the data processing itself. The Go pipeline binary plays two roles: started from a host, it acts as the runner; started by the worker inside a job, it acts as the companion. The environment determines which role is needed, see [Node companion](https://ytsaurus.tech/docs/en/#node-companion). ## Pipeline API {#pipeline-api} The Go SDK provides a single way to configure the companion — the `flow.Pipeline` type. It lets you register [computations](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) and start the companion gRPC server: ```go pipeline := flow.NewPipeline() pipeline.Add(flow.NewRowComputation("mapper", &wordCountMapper{})) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "word_count: %v\n", err) os.Exit(1) } ``` Constructors such as `flow.NewRowComputation(computationID, fn)` bind a processing function to the computation with the given identifier, and `pipeline.Add(computations ...*flow.Computation)` registers them in the pipeline. The identifier must match `computation_id` in the pipeline [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec). Assemble the `Pipeline` from a single goroutine and only then pass it to `Run`. The set of computations is fixed when the server starts: the worker requests it once, so a computation added after the start is no longer visible to the worker. Typed YSON streams are registered with `pipeline.AddStreams(flow.NewYSONStream[T](https://ytsaurus.tech/docs/en/id))`. The `T` structure embeds `flow.YSONMessage`, and the schema columns are derived from its `yson` tags. When started from a host, `Run` adds these schemas to `spec.streams`; you don’t need to duplicate them manually in `pipeline.yson`. For details, see [Typed YSON messages](https://ytsaurus.tech/docs/en/computation.md#typed-yson-messages). ## Computation and SourceComputation {#computation-and-source} To create a [computation](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) in Go, choose the constructor that matches the [C++ Computation type](https://ytsaurus.tech/flow/concepts/companion.md#computation-types): - `flow.NewRowComputation(id, fn)` and `flow.NewBatchComputation(id, fn)` — for `TTransformCompanionComputation` and `TSwiftMapCompanionComputation`. - `flow.NewRowSourceComputation(id, fn)` and `flow.NewBatchSourceComputation(id, fn)` — for `TSwiftOrderedSourceCompanionComputation` and `TTransformOrderedSourceCompanionComputation`. ```go // SourceComputation for reading data from a source pipeline.Add(flow.NewRowSourceComputation("reader", &eventMapper{})) // Computation for processing data pipeline.Add(flow.NewRowComputation("reducer", &eventReducer{})) ``` The constructors take two required parameters: - **id** — used to map requests between the [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker) and the companion. - **fn** — the value that holds the [message](https://ytsaurus.tech/flow/concepts/glossary.md#message) processing logic. It implements the `flow.RowFunction` or `flow.BatchFunction` interface; a plain function can be passed through the `flow.RowFunc` and `flow.BatchFunc` adapters. The type of a computation is whatever created it: a source differs from a transform only in how it is declared to the worker. A computation without a processing function is rejected on the spot — the constructor panics, because there is no longer any way to report such an error over the protocol. Messages in source computations are filtered through the [distribute](https://ytsaurus.tech/docs/en/distribute.md) flag when a message is emitted from the Process Function. ## Process Function {#process-function} There are two kinds of ProcessFunction: - `flow.RowFunction` — receives [messages](https://ytsaurus.tech/flow/concepts/glossary.md#message) one at a time, through the `OnMessage` method. [Timers](https://ytsaurus.tech/flow/concepts/glossary.md#timer) and visits are handled by implementing the `flow.RowTimerFunction` (`OnTimer`) and `flow.RowVisitFunction` (`OnVisit`) interfaces on the same type. - `flow.BatchFunction` — receives the whole batch of messages at once, through the `OnMessages` method; timers and visits are handled by `flow.BatchTimerFunction` (`OnTimers`) and `flow.BatchVisitFunction` (`OnVisits`). A computation declares only the handlers it needs. The worker delivers inputs according to the computation’s spec, and the Go SDK skips timers and visits whose handler isn’t implemented. Every handler receives four arguments: ```go func (*wordCountMapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error ``` - `ctx` — the context of the request the input arrived in: it has a deadline and is canceled when the worker abandons the batch. - `rt` — `flow.Runtime`, which gives access to [states](https://ytsaurus.tech/docs/en/state.md), computation parameters, [watermarks](https://ytsaurus.tech/flow/concepts/watermarks.md), and stream schemas. - `msg` — the input message together with the key it is grouped by. - `out` — `flow.OutputCollector` for emitting messages and setting timers. An error returned by a handler stops the processing of the rest of the batch: the worker retries the whole request, so a partial response would cause already processed inputs to be counted twice. For details, see [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md). ## Message filtering {#message-filtering} To filter a message in a SourceComputation, emit it with `out.AddUndistributedMessage(msg)` — it isn’t published further along the graph but is still accounted for in watermark evaluation. For details, see [The distribute flag (Go)](https://ytsaurus.tech/docs/en/distribute.md). ## Node companion {#node-companion} The entry point of a Go companion is the `main` function. In it, configure the computations through `flow.Pipeline` and call `pipeline.Run()`. The `main` function from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md): [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/word_count/main.go) If your functions need additional resources (a dictionary, a cache, an HTTP client, and so on), `main` is the place to create them: put them into the fields of the value that is bound to the computation. `pipeline.Run()` has two modes, selected automatically by the pair of environment variables `YT_FLOW_MODE` and `YT_FLOW_COMPANION_CONFIG`: - Neither is set — nobody told the process what to serve, so this is a start from a host. `Run()` enriches the pipeline spec (see [Starting a pipeline](https://ytsaurus.tech/docs/en/#launch)) and hands control over to `flow_server`, so `Run()` never returns. - At least one is set — `flow_server` has already started this same binary in a job as a companion. `Run()` brings up the companion gRPC server and serves the registered computations until the worker stops it. The decision is made by the pair, not by a single config: a process for which the worker set `YT_FLOW_MODE` but passed no config is an underconfigured companion, and it must refuse to serve rather than take the runner branch and fail on a command line it was never given. The same binary therefore both starts the pipeline and works as a companion inside a job — the companion doesn’t need a separate deployment. If you need to manage the server lifecycle yourself (in tests, for example), use `pipeline.Server(opts...)` instead of `Run()`: it builds a `flow.Server` from the config in the environment. By default, the server writes request errors to `stderr`; the `flow.WithLogger` option replaces that logger. ## Companion parallelism {#companion-process-count} A Go companion serves requests concurrently: the worker processes computation partitions in parallel, and every request is served by its own goroutine. That is why a Go companion doesn’t need the pre-fork that Python has (there it exists to work around the GIL): the `companion_process_count` parameter of the companion config is accepted and validated, but sets nothing. If a handler starts child goroutines itself, use [`flow.Go`](https://ytsaurus.tech/docs/en/computation.md#goroutines) so that their CPU and memory are accounted to the same job. The Go SDK doesn’t yet expose its own HTTP monitoring endpoint. For diagnostics, use the worker and controller metrics and the companion logs. {% note warning %} A single `Computation` value — and therefore a single function bound to it — serves all requests for that identifier. A function that keeps state between calls synchronizes that state itself. {% endnote %} ## Building with ya make {#build} A project with a Go companion is built with `ya make`. The pipeline binary is described by the `GO_PROGRAM` module; SDK dependencies are derived from the imports, so no separate `PEERDIR` is needed for them: ``` GO_PROGRAM() SRCS( main.go word_count_mapper.go ) GO_TEST_SRCS( word_count_mapper_test.go ) END() ``` You can build the pipeline binary and `flow_server` with a single command: ```bash cd yt/yt/flow ya make examples/go/word_count bin/flow_server ``` ## Using the SDK as a Go module {#go-module} Outside the repository build, the SDK is the Go module `go.ytsaurus.tech/yt/go/flow`. It is released together with the other Flow components, at the version of the Flow release, so pin the version of the release you run: ```bash go get go.ytsaurus.tech/yt/go/flow@vX.Y.Z ``` The module names the `go.ytsaurus.tech/yt/go` release it needs, and `go get` picks it up. Don't pin `go.ytsaurus.tech/yt/go` below that release: earlier releases still contain the `flow` packages themselves, and the build fails with `ambiguous import`. Build the pipeline binary with `go build` and start it as described below, passing a `flow_server` of the same release through `--flow-bin`. To launch it from the released Flow image instead, see [Running in a docker environment](https://ytsaurus.tech/docs/devops/docker-environment.md#languages). ## Starting a pipeline {#launch} Run the built binary with: ```bash ./word_count --config pipeline.yson --flow-bin ``` Here is what happens: - The Go binary reads `pipeline.yson`, enriches the spec — writing *itself* into it as the Go companion that `flow_server` will deliver to the job — and writes the extended config to a temporary file. - It then hands control over to the specified `flow_server` through `execve` (`flow_server --config `). Replacing the process image instead of starting a child leaves the exit code and the startup signals to the caller. Unknown command-line flags are skipped rather than rejected: the pipeline binary is your own program, and it is free to declare its own flags. `flow_server` is not embedded in the Go binary: this keeps the pipeline lightweight and allows whoever starts the pipeline to select the `flow_server` version. If `--flow-bin` is not specified, the runner uses the path from the `YT_FLOW_BIN` environment variable. The entire startup is performed by `flow_server`: it validates the spec, creates a vanilla operation if needed, **installs the pipeline spec** (`set-pipeline-specs`), and starts the pipeline. The Go side only *builds* and enriches the spec and never installs it directly. ### The `vanilla` block {#vanilla} If `pipeline.yson` contains a `vanilla` block with `enable = %true`, `flow_server` starts the pipeline as a single YT vanilla operation (controller plus workers) and delivers the Go binary to the job as a companion. This is a one-button start — a separately deployed `flow_server` isn’t needed. ```yson { "cluster_url" = ""; "path" = "//home/flow-dev/go-word-count/pipeline"; "spec" = { ... }; "vanilla" = { "enable" = %true; "pool" = "yt-dev"; "controller" = { "count" = 1; "cpu_limit" = 4; "memory_limit" = 12884901888; }; "worker" = { "count" = 5; "cpu_limit" = 4; "memory_limit" = 12884901888; }; }; } ``` The required parameters are `pool` and `worker.count`. The remaining fields (`cpu_limit`, `memory_limit`, the number of controllers, and so on) have reasonable defaults — for the full list of fields and their descriptions, see [TVanillaConfig](https://ytsaurus.tech/flow/generated_docs/all_yson_structs.md#NYT_NFlow_TVanillaConfig) and [TVanillaTaskConfig](https://ytsaurus.tech/flow/generated_docs/all_yson_structs.md#NYT_NFlow_TVanillaTaskConfig). The Go runner adds registered stream schemas to `spec.streams` when they are not already present. With Vanilla enabled, it also makes these changes: - The pipeline binary is added to `vanilla.worker.local_files` under the name `go_companion` — `flow_server` delivers it to the job sandbox under that name. - Every resource with `resource_class_name = "NYT::NFlow::NCompanion::TCompanionManager"` without a declared `parameters.entrypoint.executable` (or with `./go_companion`) gets `parameters.entrypoint.executable = "./go_companion"`, so the worker starts the companion from the sandbox itself. A declared one is kept, and when every such resource declares one, the binary is not shipped. - `vanilla.worker.port_count` is raised to at least `3` for the companion. {% note info %} Without Vanilla, the runner still adds registered stream schemas. It leaves the companion host path already written in the spec unchanged. {% endnote %} ### Updating the spec of a running pipeline {#release} `flow_server` is the only component that installs the pipeline spec; the Go side only builds it. So the process of rolling out changes to an already running pipeline is: 1. Rebuild the Go binary (`ya make ...`). 2. Run `./word_count --config pipeline.yson --flow-bin ` again. `flow_server` installs the spec anew and starts the pipeline. A vanilla start uses the make-before-break strategy: the new operation is prepared (the binary is uploaded to the YT cache) while the old operation keeps running, and then the switch happens — the old operation finishes and the prepared new one starts. The way the old operation is finished is controlled by the `YT_FLOW_GRACEFUL_UPDATE` environment variable: `1` (the default) stops the pipeline (`stop`), `0` pauses it (`pause`). ## See also - [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) - [Testing (Go)](https://ytsaurus.tech/docs/en/testing.md) - [Examples: Word Count (Go)](https://ytsaurus.tech/docs/en/examples/wordcount.md) - [Companion](https://ytsaurus.tech/flow/concepts/companion.md) # Computation in YTsaurus Flow (Go) {% note info %} This page describes Go-specific details of working with computations. For general concepts, see the [Computation](https://ytsaurus.tech/flow/concepts/computation.md) section. {% endnote %} ## Computation types {#computation-types} In Flow, there are two kinds of `Computation`: [`Swift`](https://ytsaurus.tech/flow/concepts/glossary.md#swift) and `Transform`. Your choice determines how exactly-once guarantees are provided and what transformations you can implement with them. | Type | Guarantee approach | Use case | |-----|-----------------------------|------------| | `Swift`| The transformation code is deterministic and will be called again if needed | Stateless transformations | | `Transform` | The result is always stored in YT, so no determinism requirements apply to the transformations | Stateful transformations [Learn more](https://ytsaurus.tech/flow/concepts/stateful.md) | When using a [companion](https://ytsaurus.tech/flow/concepts/glossary.md#companion), you select `Swift` or `Transform` by specifying `computation_class_name` in the static [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec): - `NYT::NFlow::NCompanion::TTransformCompanionComputation` — for `Transform`. - `NYT::NFlow::NCompanion::TSwiftMapCompanionComputation` — for `Swift`. - `NYT::NFlow::NCompanion::TSwiftOrderedSourceCompanionComputation` — for a `Swift` source. - `NYT::NFlow::NCompanion::TTransformOrderedSourceCompanionComputation` — for a `Transform` source. On the Go side, the choice of constructor doesn’t select `Swift` versus `Transform`: it selects what the computation is declared as to the [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker), a source or a transform. `Swift` and `Transform` computations are created by the same constructors, and it is `computation_class_name` in the spec that tells them apart. | Constructor | Type reported to the worker | `computation_class_name` in the spec | |-------------|-------------------------|----------------------------------| | `flow.NewRowComputation(id, fn)` | `Transform` | `TTransformCompanionComputation` or `TSwiftMapCompanionComputation` | | `flow.NewBatchComputation(id, fn)` | `Transform` | `TTransformCompanionComputation` or `TSwiftMapCompanionComputation` | | `flow.NewRowSourceComputation(id, fn)` | `Source` | `TSwiftOrderedSourceCompanionComputation` or `TTransformOrderedSourceCompanionComputation` | | `flow.NewBatchSourceComputation(id, fn)` | `Source` | `TSwiftOrderedSourceCompanionComputation` or `TTransformOrderedSourceCompanionComputation` | For a source, `TSwiftOrderedSourceCompanionComputation` is suitable only for deterministic processing without user state. If a SourceComputation uses [internal state](https://ytsaurus.tech/docs/en/state.md) or non-deterministic logic, specify `TTransformOrderedSourceCompanionComputation` in the spec: the worker materializes the output and commits it together with the state and the source offset. The internal state key in such a computation is the source partition key. ## Creating a Computation {#computation} A computation is created by a constructor and registered in `flow.Pipeline` through `pipeline.Add`. Here is an example from [Shuffle](https://ytsaurus.tech/docs/en/examples/shuffle.md), where the companion serves both ends of the pipeline — the source and the transform: [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/shuffle/main.go) The constructors take two required parameters: | Parameter | Required | Description | |----------|:---:|----------| | `id` | Yes | The computation identifier, matching the key in `computations` of the static spec | | `fn` | Yes | The value that holds the processing logic: `flow.RowFunction` or `flow.BatchFunction` | {% note warning %} `fn == nil` is not allowed: the constructor panics on the spot. A computation without a processing function would fail every batch sent to it, and there is no longer any way to report such an error over the protocol. If you need [passthrough](https://ytsaurus.tech/flow/concepts/glossary.md#passthrough), don’t register the computation in Go at all. Instead, specify the C++ passthrough class in `computation_class_name` in the static spec (see [Passthrough Computation](https://ytsaurus.tech/flow/concepts/computation.md#passthrough)). {% endnote %} In the static spec, you create a Computation with the same `id` (in this example, `mapper`): ```yson "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ ... ]; "input_stream_ids" = [...]; "output_stream_ids" = [...]; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = { ... }; }; ``` For more on specs, see the [Spec, DynamicSpec, and Config](https://ytsaurus.tech/flow/concepts/spec.md) section. ## SourceComputation {#sourcecomputation} `SourceComputation` is the node in the [pipeline](https://ytsaurus.tech/flow/concepts/glossary.md#pipeline) graph that reads data from external sources. On the worker side it corresponds to [TSwiftOrderedSourceComputation](https://ytsaurus.tech/flow/concepts/computation.md#tswiftorderedsourcecomputation) or [TTransformOrderedSourceComputation](https://ytsaurus.tech/flow/concepts/computation.md#ttransformorderedsourcecomputation). In Go, a source is created by the `flow.NewRowSourceComputation` and `flow.NewBatchSourceComputation` constructors. The processing function interface of a source is the same as that of a transform: a source differs from a transform only in how it is declared to the worker. ### Creating a SourceComputation {#creating-sourcecomputation} ```go pipeline.Add(flow.NewRowSourceComputation("reader", &eventMapper{})) ``` For a passthrough source, don’t use Go. Instead, specify `NYT::NFlow::TSwiftPassthroughOrderedSourceComputation` in `computation_class_name` in the spec, and leave the computation unregistered in the Go companion. For details, see [Passthrough Computation](https://ytsaurus.tech/flow/concepts/computation.md#passthrough). ### Interaction with Worker {#companion-info} When initializing, the [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker) requests from the Go companion the list of registered computations together with their type (`Source` or `Transform`). The list is fixed when the gRPC server starts: a computation added to the `Pipeline` after the start is no longer visible to the worker. After that, the worker addresses the computation by its `id` and sends batches of inputs. The worker reports the streams a source supplies messages to not once per job but on every request, so `rt.StreamSpecs()` and `rt.MessageBuilder(...)` in a source always describe the current request. ## Process Function {#process-function} Data processing logic is implemented in a Process Function. Choose one of two interfaces: [`flow.RowFunction`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/computation.go) or [`flow.BatchFunction`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/computation.go). {% note info %} Choosing `RowFunction` or `BatchFunction` is purely a matter of business logic. `RowFunction` adds no data processing overhead compared to `BatchFunction`, because Flow passes data in batches internally. {% endnote %} Every handler receives four arguments: | Argument | Description | | --- | --- | | `ctx context.Context` | The context of the request the input arrived in. It has a deadline and is canceled when the worker abandons the batch | | `rt flow.Runtime` | Access to [states](https://ytsaurus.tech/docs/en/state.md), computation parameters, [watermarks](https://ytsaurus.tech/flow/concepts/watermarks.md), and stream schemas | | `msg` / `timer` / `visit` | The input being processed, together with the [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) it is grouped by | | `out flow.OutputCollector` | Collecting output messages and setting [timers](https://ytsaurus.tech/flow/concepts/glossary.md#timer) | An error returned by a handler stops the processing of the rest of the batch: the worker retries the whole request, so a partial response would cause already processed inputs to be counted twice. The companion returns the `INTERNAL` gRPC error to the worker with the original text and by default writes it to `stderr`; the logger can be replaced with the `flow.WithLogger` option (see [Node companion](https://ytsaurus.tech/docs/en/getting-started.md#node-companion)). ### RowFunction {#row-function} `flow.RowFunction` receives [messages](https://ytsaurus.tech/flow/concepts/glossary.md#message) one at a time. The interface declares a single method, `OnMessage`. #### Typed YSON messages {#typed-yson-messages} The Go counterpart of C++ `TYsonMessage` is a Go structure with an embedded `flow.YSONMessage`. Payload fields are described by `yson` tags: ```go type numberMessage struct { flow.YSONMessage Number int64 `yson:"number"` } type doubledMessage struct { flow.YSONMessage NumberX2 int64 `yson:"number_x2"` } ``` These structures define both the handler API and the stream schemas. The runner adds the derived schemas to the spec before starting `flow_server`: ```go pipeline.AddStreams( flow.NewYSONStream[numberMessage](https://ytsaurus.tech/docs/en/%22numbers%22), flow.NewYSONStream[doubledMessage](https://ytsaurus.tech/docs/en/%22x2_numbers%22), ) ``` In the handler, the input is decoded into a structure, and the output is created and encoded without working with `Payload` and `MessageBuilder` directly: ```go type x2Mapper struct{} var _ flow.RowFunction = (*x2Mapper)(nil) func (*x2Mapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input numberMessage if err := msg.ConvertTo(&input); err != nil { return err } output := flow.NewYSONMessage[doubledMessage](https://ytsaurus.tech/docs/en/%22x2_numbers%22) output.NumberX2 = input.Number * 2 encoded, err := flow.ConvertFrom(rt, output) if err != nil { return err } out.AddMessage(encoded) return nil } ``` `msg.ConvertTo(&input)` transfers the stream identifier, the timestamps, and the input ID into `input.Meta`. The key of a message, timer, or visit is converted by the same method: `msg.Key.ConvertTo(&key)`. `flow.NewYSONMessage` sets the output stream; the event and system timestamps can be changed through `output.Meta` before the `flow.ConvertFrom` call if needed. The low-level `flow.Payload`, `flow.PayloadBuilder`, and `rt.MessageBuilder` remain available for dynamic schemas and column-wise processing. #### Stateful function example {#stateful-example} The function from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md) counts the occurrences of every word in the key’s [state](https://ytsaurus.tech/flow/concepts/glossary.md#state): ```go type wordCountMapper struct{} var _ flow.RowFunction = (*wordCountMapper)(nil) func (*wordCountMapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input wordMessage if err := msg.ConvertTo(&input); err != nil { return err } state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20wordStateName,%20msg) if err != nil { return err } fresh := state.Empty() counter := state.Value() if fresh { counter.Word = input.Word } counter.Count++ return nil } ``` ### Optional handlers {#optional-handlers} [Timers](https://ytsaurus.tech/flow/concepts/glossary.md#timer) and visits from [key visitor streams](https://ytsaurus.tech/flow/concepts/key_visitor.md) are handled by separate interfaces declared on the same type: | Interface | Method | Input | | --- | --- | --- | | `flow.RowTimerFunction` | `OnTimer(ctx, rt, timer, out)` | `flow.Timer` | | `flow.RowVisitFunction` | `OnVisit(ctx, rt, visit, out)` | `flow.Visit` | This is how handler optionality is expressed in Go: a computation declares only the methods it needs. The worker delivers timers and visits according to the computation’s spec, and the Go SDK skips those whose handler isn’t implemented. User structures implement these interfaces on a pointer, so that the value isn’t copied when the methods are called; the `var _ flow.RowFunction = (*myFunction)(nil)` check fixes the contract at compile time. ```go type urlDownloadFunction struct{} var ( _ flow.RowFunction = (*urlDownloadFunction)(nil) _ flow.RowTimerFunction = (*urlDownloadFunction)(nil) ) // The required handler: the type implements flow.RowFunction. func (*urlDownloadFunction) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { // ... out.AddTimer(flow.TimerRequest{TriggerTimestamp: uint64(time.Now().Add(flushDelay).Unix())}) return nil } // Declaring OnTimer on the same type adds timer handling. func (*urlDownloadFunction) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { // ... return nil } ``` For complete examples, see [URL Downloader](https://ytsaurus.tech/docs/en/examples/url_downloader.md) and [Wait Click Join](https://ytsaurus.tech/docs/en/examples/wait_click_join.md). ### BatchFunction {#batch-function} `flow.BatchFunction` receives the whole batch of messages that arrived from the [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker) in a single call of the `OnMessages` method. Timers and visits are handled by the `flow.BatchTimerFunction` (`OnTimers`) and `flow.BatchVisitFunction` (`OnVisits`) interfaces. A batch corresponds to one worker request and may contain messages with different [keys](https://ytsaurus.tech/flow/concepts/glossary.md#key); per-key grouping, if needed, is done in user code (see [Companion](https://ytsaurus.tech/flow/concepts/companion.md#schema)). #### Batch function example {#batch-example} ```go type x2BatchMapper struct{} var _ flow.BatchFunction = (*x2BatchMapper)(nil) func (*x2BatchMapper) OnMessages( ctx context.Context, rt flow.Runtime, msgs []flow.ExtendedMessage, out flow.OutputCollector, ) error { for _, msg := range msgs { var input numberMessage if err := msg.ConvertTo(&input); err != nil { return err } output := flow.NewYSONMessage[doubledMessage](https://ytsaurus.tech/docs/en/%22x2_numbers%22) output.NumberX2 = input.Number * 2 encoded, err := flow.ConvertFrom(rt, output) if err != nil { return err } out.AddMessage(encoded) } return nil } ``` Unlike in `RowFunction`, the output of a batch function relates to the batch as a whole: the [lineage](https://ytsaurus.tech/flow/concepts/lineage.md) of the output messages consists of the identifiers of all the batch inputs, not of one. A row function is called per input, and its output relates to exactly that input. ### Functions without their own type {#function-adapters} A computation that needs neither its own fields nor timer and visit handlers doesn’t have to declare a type: a plain function is passed through the `flow.RowFunc` and `flow.BatchFunc` adapters. ```go pipeline.Add(flow.NewRowComputation("mapper", flow.RowFunc( func( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { return nil }, ))) ``` ## Message filtering {#message-filtering} Messages in source computations are filtered with the per-message [distribute](https://ytsaurus.tech/docs/en/distribute.md) flag: the message is emitted from the Process Function by calling `out.AddUndistributedMessage(msg)` and isn’t published further along the graph, but is accounted for in [watermark](https://ytsaurus.tech/flow/concepts/watermarks.md) evaluation. The worker reads the flag only on the source path. A transform publishes a message regardless of the flag, so filtering in a transform simply means not calling `out.AddMessage`. ## Registering in a Pipeline {#pipeline-registration} All computations are registered through `pipeline.Add`, which takes them as a variadic argument: ```go pipeline := flow.NewPipeline() pipeline.Add( // A Transform computation flow.NewRowComputation("reducer", &eventReducer{}), // A Source computation flow.NewRowSourceComputation("reader", &eventMapper{}), ) ``` Assemble the `Pipeline` from a single goroutine and only then pass it to `pipeline.Run()`. {% note warning %} Every Computation must have a unique identifier matching the identifiers in the static spec. Registering two computations with the same `id` results in the `flow.ErrDuplicateComputation` error while the server is being built, and the companion cannot start. {% endnote %} A single `Computation` value — and therefore a single function bound to it — serves all requests for that identifier. The worker processes computation partitions in parallel, and every request is served by its own goroutine, so a function that keeps state between calls synchronizes that state itself. ## Goroutines in a handler {#goroutines} The handler already runs in its own goroutine. If you need additional parallelism inside it, start child goroutines through `flow.Go(ctx, fn)` rather than with the `go` statement: this way the companion keeps the consumed CPU and memory attributed to the current job. `flow.Go` only starts the function. The handler waits for all child goroutines itself, collects their errors, and finishes them before it returns. Fire-and-forget work is not allowed: after the return, the request context is canceled, and the result can no longer be added to the response to the worker. ```go results := make(chan result, len(requests)) var wg sync.WaitGroup for index, request := range requests { wg.Add(1) flow.Go(ctx, func(ctx context.Context) { defer wg.Done() value, err := callService(ctx, request) results <- result{index: index, value: value, err: err} }) } wg.Wait() close(results) ``` `flow.Runtime`, the state accessors, and `OutputCollector` are not designed for concurrent use. In child goroutines, perform only independent business logic or I/O; read and change state and collect output messages in the original handler goroutine after `wg.Wait()`. ## Runtime {#runtime} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/context.go) `flow.Runtime` (`rt`) gives access to the computation execution context: | Method | Description | | --- | --- | | `rt.MessageBuilder(streamID)` | Create a `MessageBuilder` for the given output [stream](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) | | `rt.Parameters()` | Computation parameters from the static spec | | `rt.DynamicParameters()` | Computation parameters from the dynamic spec | | `rt.KeySchema()` | The schema of the [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) the batch is grouped by | | `rt.StreamSpecs()` | The computation streams and their schemas | | `rt.MinWatermark()` | The minimum [watermark](https://ytsaurus.tech/flow/concepts/glossary.md#timestamps-and-watermarks) across all input streams | | `rt.Watermark(streamID)` | The [watermark](https://ytsaurus.tech/flow/concepts/glossary.md#timestamps-and-watermarks) of a specific stream | | `rt.InternalState(name)` | The holder of an internal [state](https://ytsaurus.tech/flow/concepts/glossary.md#state) | | `rt.ExternalState(name)` | The holder of an external state owned by the computation | | `rt.JoinedExternalState(name)` | The holder of a joined external state (read-only) | Holders are a low-level interface: in user code, the key’s state is opened by the `flow.OpenYSONState`, `flow.OpenProtoState`, `flow.OpenRawState`, and `flow.OpenExternalState` accessors. For details, see the [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) and [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md) sections. ### The low-level MessageBuilder {#message-builder} For dynamic schemas, an output message can be created through `MessageBuilder`: ```go builder, err := rt.MessageBuilder("stream_id") if err != nil { return err } msg, err := builder.Set("field_name", value).Finish() if err != nil { return err } out.AddMessage(msg) ``` The `Finish()` method returns a ready `flow.Message` without modifying the builder. The `stream_id` identifier must be present in the `output_stream_ids` list of the computation’s static [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec); otherwise `rt.MessageBuilder` returns `flow.ErrUnknownStream`. The builder is typed by the stream schema: `Set` converts the given value to the wire type of the column. A row is assembled one column at a time, so `Set` returns the builder itself rather than an error: the first rejected value is remembered, the subsequent `Set` calls do nothing, and the error is returned from `Finish()` — `flow.ErrTypeMismatch` if the value doesn’t fit into the column, and `flow.ErrColumnNotFound` if the schema has no such column. Values of `any` and composite columns are serialized to YSON, and a `[]byte` written into such a column is treated as already serialized YSON. A whole row is written with a single `builder.SetStruct(v)` call: the columns are taken from the yson tags of the `v` structure — the same tags [states](https://ytsaurus.tech/docs/en/state.md) are serialized by. A column that isn’t in the stream schema is rejected as an error. The reverse operation is `payload.ConvertTo(&v)`: it fills the structure fields from the columns of the same name, leaving as is those that aren’t in the row. `builder.SetEventTimestamp(ts)` and `builder.SetSystemTimestamp(ts)` are also available. The worker fills both fields by default; `SetSystemTimestamp` is usually not needed in user code. ### Computation parameters {#parameters} `flow.Parameters` are the parameters from the spec, left unserialized: only the computation itself knows what its configuration looks like. ```go var waitForActions bool if err := rt.Parameters().Get("wait_for_actions", &waitForActions); err != nil { return err } ``` `Get(name, dst)` deserializes the parameter from YSON into `dst` and returns `flow.ErrParameterNotFound` if the parameter is absent. Use the `Has(name)` method to check whether a parameter is present, and `Names()` to get the list of the names that are set. ### Watermarks {#watermarks} ```go // The minimum watermark across all input streams minWatermark := rt.MinWatermark() // The watermark of a specific stream watermark, ok := rt.Watermark("stream_id") ``` The second value returned by `rt.Watermark` tells whether the request reported the watermark of that stream. `rt.MinWatermark()` is zero if the request reported no watermarks at all: event time hasn’t moved yet. ## OutputCollector {#output-collector} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/output.go) `flow.OutputCollector` is used to send processing results: | Method | Description | | --- | --- | | `out.AddMessage(msg)` | Add an output message (a `flow.Message` value obtained from `builder.Finish()`) | | `out.AddMessage(msg, options)` | Add a message with `flow.AddMessageOptions` controlling distribution and the Swift message ID suffix | | `out.AddUndistributedMessage(msg)` | Add a source message with `distribute = false` | | `out.AddTimer(timer)` | Set a [timer](https://ytsaurus.tech/flow/concepts/glossary.md#timer) on the key being processed | | `out.WithParentIDs(parentIDs...)` | Return a collector that writes into a separate group with the given [lineage](https://ytsaurus.tech/flow/concepts/lineage.md) | An example of creating an output message and a timer: ```go func (*myFunction) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { output := flow.NewYSONMessage[outputMessage](https://ytsaurus.tech/docs/en/%22output_stream%22) output.Field = value encoded, err := flow.ConvertFrom(rt, output) if err != nil { return err } out.AddMessage(encoded) // Creating a timer out.AddTimer(flow.TimerRequest{TriggerTimestamp: 1000, EventTimestamp: 500}) return nil } ``` ### Message ID suffixes in Swift {#message-id-suffixes} `out.AddMessage` accepts zero or one `flow.AddMessageOptions`. Its `Distribute` field controls publication: `flow.DistributeDefault` and `flow.DistributeMessage` publish the message, while `flow.DoNotDistributeMessage` has the same source-filtering semantics as `out.AddUndistributedMessage`. In a transform, the flag has no effect; omit the message to filter it. For a Swift computation, `MessageIDSuffix` selects the derived message ID suffix: ```go out.AddMessage(hashedMessage, flow.AddMessageOptions{ MessageIDSuffix: flow.PayloadHashMessageIDSuffix(), }) suffix, err := flow.UserDefinedMessageIDSuffix(semanticKey) if err != nil { return err } out.AddMessage(keyedMessage, flow.AddMessageOptions{ Distribute: flow.DoNotDistributeMessage, MessageIDSuffix: suffix, }) ``` - With zero-value options, or with `flow.SequenceNumberMessageIDSuffix()`, Flow uses the current message sequence number for the parent message ID and output stream pair. - `flow.PayloadHashMessageIDSuffix()` uses a 128-bit CityHash of the canonical payload wire representation. Equal payloads with the same parent and stream get the same message ID; a hash collision is theoretically possible. - `flow.UserDefinedMessageIDSuffix(value)` rejects an empty value. The caller is responsible for keeping the value stable and unique among distinct logical messages with the same parent and stream. Payload hashes and user-defined suffixes are supported only by Swift computations. They make message identity independent of emission order, but don’t remove the [Swift determinism requirement](https://ytsaurus.tech/flow/concepts/swift.md#determinism): the same message ID must still denote the same logical output. The `StreamID` field of `flow.TimerRequest` selects the timer stream; an empty value means the pipeline’s only timer stream. `OutputCollector` is not designed for concurrent use: the collector belongs to the goroutine serving the request. ## ExtendedMessage {#extended-message} An incoming [message](https://ytsaurus.tech/flow/concepts/glossary.md#message) (`flow.ExtendedMessage`) contains: - `msg.ConvertTo(&value)` — converts the message payload into a structure with an embedded `flow.YSONMessage`. - `msg.Key` — the message [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) from `group_by_schema`; the key structure is filled through `msg.Key.ConvertTo(&key)`. - `msg.StreamID` — the identifier of the input [stream](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) (`string`). - `msg.EventTimestamp` — the event timestamp of the message (`uint64`). - `msg.SystemTimestamp` — the time the message was created (`uint64`). - `msg.ID` — the message identifier assigned by the worker (`string`). For dynamic schemas, you can work with `msg.Payload` directly. This low-level API provides the `Int64`, `Uint64`, `Float64`, `Bool`, `String`, `Bytes`, `Any(column, dst)`, `Has(column)`, and `Columns()` accessors. ## Timer {#timer} A [timer](https://ytsaurus.tech/flow/concepts/glossary.md#timer) value (`flow.Timer`) contains: - `timer.Key` — the timer [key](https://ytsaurus.tech/flow/concepts/glossary.md#key): `timer.Key.String("host")`. - `timer.StreamID` — the identifier of the timer stream (`string`). - `timer.TriggerTimestamp` — the trigger time (`uint64`). - `timer.EventTimestamp` — the event timestamp (`uint64`). A visit, `flow.Visit`, is arranged the same way but without a trigger time: it carries `Key`, `StreamID`, and the timestamps. For details, see [Key Visitor Streams](https://ytsaurus.tech/flow/concepts/key_visitor.md). ## Configuring the CompanionManager resource {#companion-manager} To start a Go companion, declare the `CompanionManager` resource in the static spec: ```yson "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { "entrypoint" = { "executable" = "./go_companion"; }; }; "dependencies" = {}; }; ``` The `resource_class_name` parameter points to the resource class that will start the companion. For a Go companion, `resource_class_name` must always be `NYT::NFlow::NCompanion::TCompanionManager`. The companion process is described by the `entrypoint` parameter (`executable`, `args`, `env`); the worker starts the companion itself and monitors its lifecycle. When [starting a pipeline from a host](https://ytsaurus.tech/docs/en/getting-started.md#launch) through `pipeline.Run()`, you don’t need to fill in `entrypoint` manually: the Go binary writes `entrypoint = {"executable" = "./go_companion"}` itself, and `flow_server` delivers the binary to the job under that name. The `companion_process_count` parameter is accepted and validated by a Go companion but sets nothing: pre-forking is needed by Python because of the GIL, whereas a Go companion serves requests concurrently with goroutines. For details, see [Companion parallelism](https://ytsaurus.tech/docs/en/getting-started.md#companion-process-count). For more on specs, see the [Spec, DynamicSpec, and Config](https://ytsaurus.tech/flow/concepts/spec.md) section. ## See also - [Computation (concept)](https://ytsaurus.tech/flow/concepts/computation.md) - [Getting started (Go)](https://ytsaurus.tech/docs/en/getting-started.md) - [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) - [The distribute flag (Go)](https://ytsaurus.tech/docs/en/distribute.md) - [Companion](https://ytsaurus.tech/flow/concepts/companion.md) # The distribute flag in YTsaurus Flow (Go) The `distribute` flag is a per-message flag that you set when you add an output [message](https://ytsaurus.tech/flow/concepts/glossary.md#message) in a [source computation](https://ytsaurus.tech/docs/en/getting-started.md#computation-and-source). It controls whether the message is published further along the processing graph. The `distribute` flag ensures: - Correct [watermark](https://ytsaurus.tech/flow/concepts/watermarks.md) evaluation: messages with `distribute=false` are still accounted for by the watermark generator (unlike simply skipping a message in the handler, which breaks watermark evaluation). - Assignment of deterministic identifiers to messages. {% note warning %} To filter a message in a source, don’t skip it in `OnMessage` — add it with `distribute=false` instead. This way, the message isn’t published further, but it remains accounted for in watermark evaluation. {% endnote %} ## When to use distribute=false {#when-to-use} Use the `distribute=false` flag when: - You need to filter some of the output messages at the source computation stage. - Correct watermark evaluation is important. The `out.AddMessage(msg)` method publishes the message further. `out.AddUndistributedMessage(msg)` keeps it accounted for but doesn’t publish it. ## Usage {#usage} The filtering logic moves into the processing function: a regular message is added with `AddMessage`, a filtered one with `AddUndistributedMessage`. ```go type hitMessage struct { flow.YSONMessage HitID uint64 `yson:"hit_id"` HitPayload string `yson:"hit_payload"` } // hitParsingFunction parses the input row and drops duplicates. type hitParsingFunction struct{} var _ flow.RowFunction = (*hitParsingFunction)(nil) func (*hitParsingFunction) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input hitMessage if err := msg.ConvertTo(&input); err != nil { return err } hit := flow.NewYSONMessage[hitMessage](https://ytsaurus.tech/docs/en/%22hit%22) hit.HitID = input.HitID hit.HitPayload = input.HitPayload encoded, err := flow.ConvertFrom(rt, hit) if err != nil { return err } // Duplicates are added but not published further. isDuplicate := input.HitPayload == "duplicate_payload" if isDuplicate { out.AddUndistributedMessage(encoded) } else { out.AddMessage(encoded) } return nil } ``` The flag is set separately for every message, so within a single handler call some messages can be published and others not. The order of messages is preserved: a dropped message stays in the output group and takes part in the [lineage](https://ytsaurus.tech/flow/concepts/lineage.md), it just doesn’t go further along the graph. ## The flag is read only on the source path {#source-path-only} The [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker) reads the `distribute` flag only on the source path. That is why the Go SDK rejects `AddUndistributedMessage` in a transform with the `flow.ErrDistributeOnTransform` error. {% note info %} A transform filters a message by simply not collecting it: don’t call `AddMessage` for it. The watermark isn’t affected — it is advanced by the input messages of the source, not by the output of a transform. {% endnote %} ## Registering a source computation {#registration} A source computation is created by the `flow.NewRowSourceComputation` constructor (or `flow.NewBatchSourceComputation`) and registered in the pipeline through `pipeline.Add`. No separate filtering parameter is needed — the decision about publishing is made in the processing function: [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/shuffle/main.go) The type of a computation is whatever created it: a source differs from a transform only in how it is declared to the worker. The `distribute` flag is therefore taken into account for exactly those computations that were created by the source constructors. ## Checking in tests {#testing} The flag values are visible in offline tests: the `Distribute()` method of the run result returns the flags in the same order as `Messages()` and `Rows()`. ```go r := h.Process( h.Message("hits", flowtest.Row{"hit_id": uint64(1), "hit_payload": "payload"}), h.Message("hits", flowtest.Row{"hit_id": uint64(2), "hit_payload": "duplicate_payload"}), ) require.Equal(t, []bool{true, false}, r.Distribute()) ``` For details, see [Testing (Go)](https://ytsaurus.tech/docs/en/testing.md). ## See also - [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md) - [Testing (Go)](https://ytsaurus.tech/docs/en/testing.md) - [Examples: Shuffle (Go)](https://ytsaurus.tech/docs/en/examples/shuffle.md) - [Watermarks](https://ytsaurus.tech/flow/concepts/watermarks.md) - [The distribute flag (Python)](https://ytsaurus.tech/flow/python/distribute.md) # Working with states in YTsaurus Flow (Go) {% note info %} This page describes the Go API for working with states. The general state concepts are described in the [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) section. {% endnote %} A [state](https://ytsaurus.tech/flow/concepts/glossary.md#state) accessor is opened by the free function `flow.OpenXxxState(rt, name, input)` with three arguments: - `rt` — `flow.Runtime`, the second argument of any handler (see [Process Function](https://ytsaurus.tech/docs/en/getting-started.md#process-function)). - `name` — the state name declared in the computation [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec). - `input` — the input to whose [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) the accessor is bound: `flow.ExtendedMessage`, `flow.Timer`, or `flow.Visit`. All three implement the `flow.Input` interface, so working with state looks the same in a message, timer, and visit handler. The opening functions are free functions rather than methods of `Runtime`: the YSON and Proto accessors are parameterized by the state type, and methods in Go have no type parameters of their own. An accessor addresses the state of exactly one key and lives within a single request. Only the records the computation wrote to travel to the worker: reading a state doesn’t send it back. ## YSON State {#yson-state} The simplest way to work with state is the YSON format. The state is described by an ordinary Go structure with `yson` tags: ```go state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20%22word-state%22,%20msg) if err != nil { return err } if state.Empty() { state.Value().Word = word } state.Value().Count++ return nil ``` `flow.YSONState[T]` provides `Empty()`, `Get() (*T, bool)`, `Value() *T`, `Clear()`, and `ReadOnly()`. `Value()` returns a mutable value and creates a zero value for a missing state. A separate `Set` isn’t needed: after the batch is processed successfully, the SDK serializes the changes automatically; if the handler fails, they are discarded. ```go type wordCountState struct { Word string `yson:"word"` Count int64 `yson:"count"` } ``` ```go type wordCountMapper struct{} var _ flow.RowFunction = (*wordCountMapper)(nil) func (*wordCountMapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input wordMessage if err := msg.ConvertTo(&input); err != nil { return err } state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20wordStateName,%20msg) if err != nil { return err } fresh := state.Empty() counter := state.Value() if fresh { counter.Word = input.Word } counter.Count++ return nil } ``` The state is bound to the message key, which is defined through `group_by_schema` in the computation spec. An independent value is stored for every unique key. ## Raw State {#raw-state} To store the state as raw bytes: ```go state, err := flow.OpenRawState(rt, "raw-state", msg) ``` Returns `flow.RawStateAccessor` with the methods: - `Get() ([]byte, bool)` — get the value and the indication of its presence. - `Or(fallback []byte) []byte` — get the value or `fallback`. - `Set(data []byte) error` — save the value. - `Clear() error` — delete the state. The YSON and Proto states are wrappers over the raw accessor: `RawStateAccessor` itself is needed when the computation performs the serialization on its own. ## Proto State {#proto-state} To store the state as a Protobuf message: ```go state, err := flow.OpenProtoState[TJoinState](https://ytsaurus.tech/docs/en/rt,%20%22join-state%22,%20msg) ``` The state type is named by its value form (`TJoinState`), and the state works with a pointer to it (`*TJoinState`) — it is on the pointer that the generated code implements `proto.Message`. Returns `*flow.ProtoState[T, PT]` with the methods: - `Empty() bool` — check whether the value is missing. - `Get() (PT, bool)` — return the mutable message. - `Or(fallback PT) PT` — return the saved message, or write and return `fallback`. - `Set(value PT) error` — replace the whole value. - `Clear()` — delete the state. - `ReadOnly() flow.ReadOnlyProtoState[T, PT]` — a read-only view. The message is changed in place: the changes are written at the end of the batch without a `Set`, see [Changing the value in place](https://ytsaurus.tech/docs/en/internal-state.md#in-place). ```go state, err := flow.OpenProtoState[TJoinState](https://ytsaurus.tech/docs/en/rt,%20%22join-state%22,%20msg) if err != nil { return err } window := state.Or(&TJoinState{}) window.HitPayload = payload return nil ``` ## External State {#external-state} An external state is a row of your own dynamic table. In user code it is represented by an ordinary Go structure with `yson` tags: ```go state, err := flow.OpenExternalState(rt, "/shuffle-state", msg) ``` The state name is an absolute path matching the key in the `external_state_managers` section of the static spec. A name without a leading `/` is rejected with the `flow.ErrInvalidStateName` error, an undeclared name with the `flow.ErrUnknownState` error. Returns `flow.ExternalStateAccessor`. The main operations: - `ConvertTo(&value) (bool, error)` — fill the structure with the saved row; `bool` distinguishes a missing row. - `ConvertFrom(&value) error` — save the fields of the structure into the state row. - `Clear() error` — delete the row. The low-level `Get`, `Or`, `Builder`, `Set`, and `Schema` are needed only for dynamic schemas and column-wise processing. An example from [Shuffle](https://ytsaurus.tech/docs/en/examples/shuffle.md): ```go type shuffleState struct { Count int64 `yson:"count"` } type eventReducer struct{} var _ flow.RowFunction = (*eventReducer)(nil) func (*eventReducer) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { state, err := flow.OpenExternalState(rt, shuffleStateName, msg) if err != nil { return err } var counter shuffleState if _, err := state.ConvertTo(&counter); err != nil { return err } counter.Count++ return state.ConvertFrom(&counter) } ``` The pattern for working with external state: 1. Open the state through `flow.OpenExternalState(...)`. 2. Convert the row into a structure through `state.ConvertTo(&value)`. 3. Change the fields of the structure and save it through `state.ConvertFrom(&value)`. For more details, see the [External State (Go)](https://ytsaurus.tech/docs/en/external-state.md) section. ## Joined External State {#joined-external-state} If an external state is needed for reading only — for example, to enrich messages with a reference dataset — the computation declares it in the `external_state_joiners` section and opens it with a separate function: ```go reference, err := flow.OpenJoinedExternalState(rt, "/reference", msg) ``` Returns `flow.JoinedExternalStateAccessor`. The row is read into a structure through `ConvertTo(&value)`; the accessor has no writing by design, because a joined state is filled from the request and never travels back. The low-level `Get`, `Or`, and `Schema` are available for dynamic schemas. The namespaces don’t intersect: a state the computation owns isn’t available through `flow.OpenJoinedExternalState`, and vice versa. ```go type referenceState struct { Name *string `yson:"name"` } type lookupJoin struct{} var _ flow.RowFunction = (*lookupJoin)(nil) func (*lookupJoin) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var event eventMessage if err := msg.ConvertTo(&event); err != nil { return err } name, ok, err := referenceName(rt, msg) if err != nil || !ok { return err } enriched := flow.NewYSONMessage[enrichedMessage](https://ytsaurus.tech/docs/en/enrichedStreamID) enriched.Key = event.Key enriched.Name = name encoded, err := flow.ConvertFrom(rt, enriched) if err != nil { return err } out.AddMessage(encoded) return nil } func referenceName(rt flow.Runtime, msg flow.ExtendedMessage) (string, bool, error) { reference, err := flow.OpenJoinedExternalState(rt, referenceStateName, msg) if errors.Is(err, flow.ErrStateNotRead) { return "", false, nil } if err != nil { return "", false, err } var state referenceState exists, err := reference.ConvertTo(&state) if err != nil || !exists || state.Name == nil { return "", false, err } return *state.Name, true, nil } ``` [Source code of the example](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/external_state_join) {% note warning %} The worker joins only those keys for which it found rows. A batch that didn’t match any key of the reference dataset arrives without the joined state at all, and opening returns `flow.ErrStateNotRead` — the request simply didn’t bring a state with that name. This isn’t a processing error: distinguish it with `errors.Is` and treat it as an absence of data. {% endnote %} ## State in timers {#state-in-timers} The API for working with state in a [timer](https://ytsaurus.tech/flow/concepts/glossary.md#timer) handler is identical — a timer is passed instead of a message: ```go state, err := flow.OpenExternalState(rt, "/join-state", timer) ``` Here is an example from [WaitClickJoin](https://ytsaurus.tech/docs/en/examples/wait_click_join.md) — the window is closed by a timer, and the state is cleared right after the result is published: ```go func (*joinFunction) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { if timer.StreamID != timerStream { return fmt.Errorf("unhandled timer stream %q", timer.StreamID) } state, err := flow.OpenExternalState(rt, joinStateName, timer) if err != nil { return err } var window joinState if _, err := state.ConvertTo(&window); err != nil { return err } if window.ShowTime != nil && *window.ShowTime != 0 && window.HitPayload != nil { var key joinKey if err := timer.Key.ConvertTo(&key); err != nil { return err } joined, err := joinedAction(rt, key, window) if err != nil { return err } out.AddMessage(joined) } return state.Clear() } ``` A state cleared in this request is read as missing from then on: the computation sees the state as it will be after the response to the worker. ## Binding the state to the key {#group-by-schema} In `TTransformCompanionComputation`, the state is bound to the message [key](https://ytsaurus.tech/flow/concepts/glossary.md#key), which is defined through `group_by_schema` in the computation spec. All the messages with the same key share one state. The key the accessor is bound to is taken from the input — that is why one handler can’t accidentally write the state of a foreign key. In `TTransformOrderedSourceCompanionComputation`, the `group_by_schema` field isn’t supported. The key of the internal state is the source partition key, so all the messages of one partition share the state. For more on choosing the class for a SourceComputation, see the [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md#sourcecomputation) section. For more on configuring keys, see [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md). The key schema is available in the handler through `rt.KeySchema()`, and the key itself through the `Key` field of the input (`msg.Key`, `timer.Key`, `visit.Key`). ## Configuring states in the spec {#spec-configuration} A state that isn’t declared in the spec can’t be opened: the opening function returns `flow.ErrUnknownState` with the list of the declared names. States are declared in three different places of the [computation](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) description: - Internal states (YSON, Raw, Proto) — as a list of names in `parameters.internal_states`. The names are arbitrary, without a leading `/`. - External states the computation owns — in the `external_state_managers` section. The section key sets the state name (an absolute path), and the `external_state_manager_class_name` field sets the registered manager class; for the typical scenario it is `"NYT::NFlow::TSimpleExternalStateManager"`. - External states the computation only reads — in the `external_state_joiners` section, next to `external_state_managers`. ```yson "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "external_state_managers" = { "/shuffle-state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state"; }; }; }; "external_state_joiners" = { "/reference" = { "external_state_joiner_class_name" = "NYT::NFlow::TSimpleExternalStateJoiner"; "parameters" = { "path" = "//path/to/current"; }; }; }; "parameters" = { "internal_states" = ["word-state"]; }; }; ``` The tables of internal states are created and maintained by Flow, while the external state table is created by you. For more details, see the [Internal State (Go)](https://ytsaurus.tech/docs/en/internal-state.md) and [External State (Go)](https://ytsaurus.tech/docs/en/external-state.md) sections. ## See also - [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md) - [Internal State (Go)](https://ytsaurus.tech/docs/en/internal-state.md) - [External State (Go)](https://ytsaurus.tech/docs/en/external-state.md) - [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md) - [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) # StateAccessor in YTsaurus Flow (Go) StateAccessor is the interface for reading, modifying, and deleting [state](https://ytsaurus.tech/flow/concepts/glossary.md#state) values. General information about stateful processing is available in the [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) section. ## How it works {#how-it-works} [State](https://ytsaurus.tech/flow/concepts/glossary.md#state) in Flow is stored in [sorted dynamic tables](https://ytsaurus.tech/user-guide/dynamic-tables/sorted-dynamic-tables.md). For [external state](https://ytsaurus.tech/docs/en/external-state.md), you create this table yourself; for [internal state](https://ytsaurus.tech/docs/en/internal-state.md), the tables are created and managed by Flow automatically. For `TTransformCompanionComputation`, the key columns of the state table match the `group_by_schema` of the [computation](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation). For the internal state of `TTransformOrderedSourceCompanionComputation`, the key is the source partition key: `group_by_schema` isn’t supported in such a SourceComputation. In every case, messages with the same key share one state. In Go, an accessor is a value that binds two things together: the holder of the state with a given name and the key of a specific input. The holders live in `flow.Runtime` and are available directly (`rt.InternalState(name)`, `rt.ExternalState(name)`, `rt.JoinedExternalState(name)`), but an ordinary computation doesn’t need them: the accessor is exactly the convenient view of the state of one key. ## Reading and writing data {#reading-and-writing-data} Working with the table directly (reading, writing, and deleting data) is done by the [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker). When the next batch of messages arrives, the worker loads the state values for all the keys in the batch and sends them to the [companion](https://ytsaurus.tech/flow/concepts/companion.md) together with the messages and timers. For more details, see the [interaction schema](https://ytsaurus.tech/flow/concepts/companion.md#schema). New values are written to the state table transactionally within an [epoch](https://ytsaurus.tech/flow/concepts/glossary.md#epoch). What travels back to the worker isn’t the whole state but a delta: only the changed records. For Raw and External states, the write is performed via `Set` or `Clear`; YSON and Proto states are written by [changing the value in place](https://ytsaurus.tech/docs/en/internal-state.md#in-place). A plain read sends nothing. `Clear` doesn’t erase the record from the accessor, it marks the record as deleted, and the deletion reaches the worker in exactly that form. For the computation there is no difference: a state the request didn’t bring and a state cleared in this request are both read as missing — the computation sees the state as it will be after the response. {% note warning %} `flow.Runtime` and all the accessors opened from it belong to the goroutine serving the request and aren’t designed for concurrent use. If a handler parallelizes its work, read and write the state in the same goroutine the handler was called in. The rules for starting child goroutines are described in the [Goroutines in a handler](https://ytsaurus.tech/docs/en/computation.md#goroutines) section. {% endnote %} ## Accessor types {#accessor-types} The Go SDK provides five types of accessors: | Accessor | Format | Opening | Description | |----------|--------|----------|----------| | [RawStateAccessor](https://ytsaurus.tech/docs/en/internal-state.md#raw-state-accessor) | `[]byte` | `flow.OpenRawState(rt, name, input)` | Raw bytes without serialization | | [YSONState](https://ytsaurus.tech/docs/en/internal-state.md#yson-state) | YSON | `flow.OpenYSONState[T](https://ytsaurus.tech/docs/en/rt,%20name,%20input)` | Serialization of a Go value into YSON | | [ProtoState](https://ytsaurus.tech/docs/en/internal-state.md#proto-state) | Protobuf | `flow.OpenProtoState[T](https://ytsaurus.tech/docs/en/rt,%20name,%20input)` | Serialization via Protobuf | | [ExternalStateAccessor](https://ytsaurus.tech/docs/en/external-state.md) | Go structure (table row) | `flow.OpenExternalState(rt, "/name", input)` | Reading and writing a row of an external table | | [JoinedExternalStateAccessor](https://ytsaurus.tech/docs/en/external-state.md) | Go structure | `flow.OpenJoinedExternalState(rt, "/name", input)` | Read-only access to another computation’s state table | The first three work with [internal state](https://ytsaurus.tech/docs/en/internal-state.md), whose tables are managed by Flow. `RawStateAccessor` serializes explicit writes; `YSONState` and `ProtoState` hold a mutable value and save it automatically after a successful batch. `ExternalStateAccessor` and `JoinedExternalStateAccessor` work with [external state](https://ytsaurus.tech/docs/en/external-state.md) — a dynamic table that you create yourself. They differ in rights: the first is available to the computation that owns the state, the second to the computation that only reads it. ## Internal state API {#common-api} The Raw accessor uses explicit read and write operations: | Method | Result type | Description | |-------|----------------|----------| | `Get()` | `([]byte, bool)` | A copy of the saved bytes | | `Or(fallback []byte)` | `[]byte` | The current value or `fallback` | | `Set(data []byte)` | `error` | Save the bytes; empty bytes delete the state | | `Clear()` | `error` | Delete the state | YSON and Proto states are changed in place: | Method | `YSONState[T]` | `ProtoState[T, PT]` | |-------|----------------|---------------------| | `Empty()` | `bool` | `bool` | | `Get()` | `(*T, bool)` | `(PT, bool)` | | `Value()` / `Or(fallback)` | `*T` | `PT` | | `Set(value)` | no method | `error` | | `Clear()` | no result | no result | | `ReadOnly()` | `ReadOnlyYSONState[T]` | `ReadOnlyProtoState[T, PT]` | Deserialization is performed on opening. Changes to the value are saved automatically only after all the batch handlers have completed successfully — see [Changing the value in place](https://ytsaurus.tech/docs/en/internal-state.md#in-place). `ExternalStateAccessor` converts a table row into a Go structure: | Method | Result type | Description | |-------|----------------|----------| | `ConvertTo(&value)` | `(bool, error)` | Read the row into the structure; `bool` distinguishes a missing row | | `ConvertFrom(&value)` | `error` | Save the fields of the structure into the row | | `Clear()` | `error` | Delete the row | `JoinedExternalStateAccessor` provides `ConvertTo(&value)`, but neither writing nor clearing. For dynamic schemas, both accessors keep the low-level `Get`, `Or`, and `Schema`, and the owner also keeps `Builder` and `Set`. ## Getting an accessor {#getting-accessor} An accessor is opened by a free function inside `OnMessage`, `OnTimer`, or `OnVisit`: ```go func (*myFunction) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { // YSON ysonState, err := flow.OpenYSONState[myState](https://ytsaurus.tech/docs/en/rt,%20%22state-name%22,%20msg) // Raw bytes rawState, err := flow.OpenRawState(rt, "state-name", msg) // Protobuf protoState, err := flow.OpenProtoState[TMyState](https://ytsaurus.tech/docs/en/rt,%20%22state-name%22,%20msg) // External state: the name must start with "/" extState, err := flow.OpenExternalState(rt, "/state-name", msg) // Read-only external state joined, err := flow.OpenJoinedExternalState(rt, "/reference", msg) } func (*myFunction) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { // The same thing, but a timer is passed instead of a message state, err := flow.OpenYSONState[myState](https://ytsaurus.tech/docs/en/rt,%20%22state-name%22,%20timer) } ``` Parameters: - `rt` — `flow.Runtime`, the second argument of the handler. - `name` — the name of the state declared in the [static spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec). For internal states, it is an arbitrary string from `internal_states`; for external ones, a key from `external_state_managers` or `external_state_joiners` that must start with `/`. - `input` — the input to whose [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) the accessor is bound. Any value implementing `flow.Input` will do: `flow.ExtendedMessage`, `flow.Timer`, and `flow.Visit`. The type parameter is specified only for the YSON and Proto accessors and sets the type of the state: `flow.OpenProtoState[TMyState]` produces an accessor over `*TMyState`. This is exactly why the opening functions are free functions rather than methods of `Runtime`: methods in Go have no type parameters of their own. Opening returns an error if the state name isn’t suitable: | Error | Reason | |--------|---------| | `flow.ErrUnknownState` | The name isn’t declared in the computation spec | | `flow.ErrInvalidStateName` | The external state name isn’t an absolute path | | `flow.ErrStateNotRead` | The request didn’t bring an external state with this name | | `flow.ErrNoStateSchema` | The external state arrived without the schema of its rows | {% note info %} `flow.ErrStateNotRead` when opening a joined state is a normal situation, not a failure: the worker joins only those keys for which it found rows, so a batch that didn’t match any row of the reference dataset arrives without such a state. Distinguish this error with `errors.Is` and treat it as an absence of data. {% endnote %} ## When to use each type {#choosing-type} | Situation | Recommended accessor | |----------|------------------------| | A structure with several fields whose schema changes together with the code | `flow.OpenYSONState[T]` (YSON) | | Arbitrary binary data or your own serialization | `flow.OpenRawState` (Raw) | | Structured data with a fixed schema shared with other languages | `flow.OpenProtoState[T]` (Protobuf) | | Data that must be accessible from other systems | `flow.OpenExternalState` (External) | | Data that requires a user table | `flow.OpenExternalState` (External) | | A reference dataset that the computation only reads | `flow.OpenJoinedExternalState` (Joined) | ## See also - [Internal State (Go)](https://ytsaurus.tech/docs/en/internal-state.md) - [External State (Go)](https://ytsaurus.tech/docs/en/external-state.md) - [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) — a brief overview of all the types - [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md) - [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) # Internal State in YTsaurus Flow (Go) Internal State is the mechanism for working with internal [state](https://ytsaurus.tech/flow/concepts/glossary.md#state) stored in the internal tables of Flow. Unlike [External State](https://ytsaurus.tech/docs/en/external-state.md), you don’t need to create the tables yourself — Flow manages them automatically. For more on accessors and the general principles of working with state, see [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md). General information about stateful processing is available in the [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) section. ## Overview {#overview} The Go SDK provides three kinds of accessors for working with Internal State, differing in the serialization format. Separate accessors are used for external state — see [External State (Go)](https://ytsaurus.tech/docs/en/external-state.md). | Accessor | Format | Opened by the function | |---|---|---| | [YSONState](https://ytsaurus.tech/docs/en/#yson-state) | YSON | `flow.OpenYSONState[T]` | | [RawStateAccessor](https://ytsaurus.tech/docs/en/#raw-state-accessor) | `[]byte` | `flow.OpenRawState` | | [ProtoState](https://ytsaurus.tech/docs/en/#proto-state) | Protobuf | `flow.OpenProtoState[T]` | `RawStateAccessor` reads and writes raw bytes explicitly through `Get`, `Set`, and `Clear`. `YSONState` and `ProtoState` provide a mutable value: the changes made to it are serialized automatically after the batch handlers complete successfully — see [Changing the value in place](https://ytsaurus.tech/docs/en/#in-place). Each of the `flow.OpenXxxState` functions takes three arguments: - `rt` — the `flow.Runtime` of the handler. - `name` — the state name. Internal state names don’t start with `/` and must be declared in `parameters.internal_states` of the [computation](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec), see [Configuration in the static spec](https://ytsaurus.tech/docs/en/#static-spec). - `input` — the input to whose [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) the state is bound. Any value implementing `flow.Input` will do: `flow.ExtendedMessage`, `flow.Timer`, `flow.Visit`. {% note info %} The accessor shows the state as it will be after the response to the worker: a state that wasn’t in the incoming request and a state cleared in the same call through `Clear` are read the same way — as missing. {% endnote %} ## Changing the value in place {#in-place} The value returned by `Value()`, `Get()`, and `Or()` is live: it is decoded once per key and batch, every accessor for that key returns the same value, and the changes made to it are written to the state at the end of the batch without a `Set()` call. Nothing is written when the value did not change: a state that serializes to the bytes it arrived with does not go back. The default from `Value()` and `Or()` becomes the state value and is written as after `Set()`, so it can be changed right away: ```go state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20%22word-state%22,%20msg) if err != nil { return err } state.Value().Count++ ``` `ProtoState.Set()` still replaces the whole message, and `Clear()` removes the state. An empty Protobuf message serializes to zero bytes, and zero bytes are the absence of a state: an `Or(&T{})` default is not written, while a message whose fields were all unset and a `Set(&T{})` both delete the state. `RawStateAccessor` hands out no mutable value: its `Get()` returns a copy of the bytes, and they are only written through `Set()`. [External State](https://ytsaurus.tech/docs/en/external-state.md) states are not tracked in place either. A key has a single decoded value per batch, so opening the same state and key with another Go type returns an error. If a handler returned an error, the in-place changes don’t make it into the response to the worker. A state that Go serializes to bytes other than the ones it arrived with is rewritten canonically on the first read — once per key. To detect the changes, a value that was read is re-encoded at the end of the batch. When the computation only reads a state, use `ReadOnly()`: that view returns the same value but does not track it, and `ReadOnlyYSONState.Value()` and `ReadOnlyProtoState.Or()` do not create the state. It has no write methods, so writing through it does not compile. The value is shared with the mutable state of the same key: if that state is also opened for writing somewhere, the changes made through the view still reach the worker. ```go count := state.ReadOnly().Value().Count ``` ## YSONState {#yson-state} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/context.go) `YSONState[T]` stores the state as a YSON-serialized value of type `T`. The type can be any structure with `yson` tags, as well as a map, a slice, or a scalar — anything `yson.Marshal` understands. ### Getting the state {#getting-yson-state} ```go // For a message state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20%22word-state%22,%20msg) // For a timer state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20%22word-state%22,%20timer) ``` Deserialization is performed on opening. Opening the same state and key again within the request returns the same mutable value. ### Methods {#yson-methods} | Method | Result type | Description | |---|---|---| | `Empty()` | `bool` | Check whether the value is missing | | `Get()` | `(*T, bool)` | Get the mutable value. The second result distinguishes a saved state from a missing one | | `Value()` | `*T` | Get the mutable value; a zero value is created if the state is missing | | `Clear()` | — | Delete the value | | `ReadOnly()` | `ReadOnlyYSONState[T]` | A read-only view | The changes to the value are serialized automatically after all the batch handlers complete successfully. If a handler returned an error, the changes to the YSON state don’t make it into the response to the worker. ### Example from WordCount {#yson-example} The type the pipeline stores for one word: ```go type wordCountState struct { Word string `yson:"word"` Count int64 `yson:"count"` } ``` The message handler: ```go type wordCountMapper struct{} var _ flow.RowFunction = (*wordCountMapper)(nil) func (*wordCountMapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input wordMessage if err := msg.ConvertTo(&input); err != nil { return err } state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20wordStateName,%20msg) if err != nil { return err } fresh := state.Empty() counter := state.Value() if fresh { counter.Word = input.Word } counter.Count++ return nil } ``` [Full source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/word_count/word_count_mapper.go) Here the state is bound to the key of the [message](https://ytsaurus.tech/flow/concepts/glossary.md#message). For a new key, `Empty()` returns `true`, and `Value()` creates an empty `wordCountState`. Assignments to the fields are saved without a separate `Set`. The same state is also opened in the [timer](https://ytsaurus.tech/flow/concepts/glossary.md#timer) handler. This is how the [URL Downloader](https://ytsaurus.tech/docs/en/examples/url_downloader.md) works: `OnMessage` accumulates a batch, and `OnTimer` reads it and clears it through `Clear`. ## RawStateAccessor {#raw-state-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/context.go) `RawStateAccessor` works with raw bytes without serialization and deserialization. It is the accessor the other two are built on top of — take it when you define the state format yourself. ### Getting the accessor {#getting-raw-accessor} ```go // For a message state, err := flow.OpenRawState(rt, "raw-state", msg) // For a timer state, err := flow.OpenRawState(rt, "raw-state", timer) ``` ### Methods {#raw-methods} | Method | Result type | Description | |---|---|---| | `Get()` | `([]byte, bool)` | Get the raw bytes. The second result distinguishes a saved state from a missing one | | `Or(fallback []byte)` | `[]byte` | Return the current value, or `fallback` if there is no state | | `Set(data []byte)` | `error` | Save the raw bytes; empty bytes delete the state | | `Clear()` | `error` | Delete the state for the current key | The `Get` and `Or` methods don’t return an error: there is nothing to deserialize here. ### Usage example {#raw-example} ```go state, err := flow.OpenRawState(rt, "raw-state", msg) if err != nil { return err } // Reading the raw data if data, ok := state.Get(); ok { // Processing the raw data... _ = data } // Writing the raw data if err := state.Set([]byte{0x01, 0x02, 0x03}); err != nil { return err } // Clearing return state.Clear() ``` ## ProtoState {#proto-state} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/context.go) `ProtoState` serializes the state through Protobuf. The type of the Protobuf message is given in its value form, and the state returns a pointer to it: `flow.OpenProtoState[TJoinState]` returns a state over `*TJoinState`. ### Getting the state {#getting-proto-accessor} ```go // For a message state, err := flow.OpenProtoState[TJoinState](https://ytsaurus.tech/docs/en/rt,%20%22join-state%22,%20msg) // For a timer state, err := flow.OpenProtoState[TJoinState](https://ytsaurus.tech/docs/en/rt,%20%22join-state%22,%20timer) ``` Deserialization is performed on opening. Opening the same state and key again within the request returns the same mutable message. ### Methods {#proto-methods} | Method | Result type | Description | |---|---|---| | `Empty()` | `bool` | Check whether the value is missing | | `Get()` | `(*T, bool)` | Get the mutable message. The second result distinguishes a saved state from a missing one | | `Or(fallback *T)` | `*T` | Return the current message, or write and return `fallback` if there is no state | | `Set(value *T)` | `error` | Replace the whole state value; an empty message deletes the state | | `Clear()` | — | Delete the state for the current key | | `ReadOnly()` | `ReadOnlyProtoState[T, PT]` | A read-only view | {% note info %} Unlike in Python, where `get_or_default()` without arguments returns an empty instance of the Proto class, in Go the default value is set explicitly — pass `&T{}` if you want to start with an empty message. An empty message serializes to zero bytes, and zero bytes mean the state is absent: such a message is not written for a key that has no state, and deletes the state of a key that has one. Note that a message whose every field holds its zero value is empty in exactly this sense. {% endnote %} ### Usage example {#proto-example} ```go state, err := flow.OpenProtoState[TJoinState](https://ytsaurus.tech/docs/en/rt,%20%22join-state%22,%20msg) if err != nil { return err } window := state.Or(&TJoinState{}) window.ShowTime = showTime return nil ``` ## Configuration in the static spec {#static-spec} Internal State doesn’t require creating external tables. The states are stored automatically in the internal tables of Flow. The names of internal states must be declared in the `internal_states` section of the [computation](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) parameters in the static spec: ```yson { "spec" = { "computations" = { "reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "output_stream_ids" = ["words"]; "source_streams" = { "queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { "queue_path" = "//path/to/queue"; "consumer_path" = "//path/to/consumer"; "finite" = false; }; }; }; "parameters" = {}; }; "mapper" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(word)"; "type" = "uint64"; required = %true;}; {"name" = "word"; "type" = "string";}; ]; "input_stream_ids" = ["words"]; "output_stream_ids" = []; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = { "internal_states" = ["word-state"]; }; }; }; "resources" = { "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { }; "dependencies" = {}; }; }; }; "dynamic_spec" = { "computations" = { "reader" = { "parameters" = { }; }; "mapper" = { "parameters" = { }; }; }; }; } ``` The state name in the code (the second argument of `flow.OpenYSONState`, `flow.OpenRawState`, or `flow.OpenProtoState`) must match the name declared in `internal_states`. {% note warning %} If the state name isn’t declared in `internal_states`, the opening function returns an error wrapping `flow.ErrUnknownState`; the error text lists the declared names. An error returned from a handler stops the processing of the whole batch — the worker retries the entire request. {% endnote %} ## See also - [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md) - [External State (Go)](https://ytsaurus.tech/docs/en/external-state.md) - [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) — a brief overview - [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) - [Examples: Word Count (Go)](https://ytsaurus.tech/docs/en/examples/wordcount.md) # External State in YTsaurus Flow (Go) External State is the mechanism for working with external [state](https://ytsaurus.tech/flow/concepts/glossary.md#state) stored in an external YTsaurus dynamic table. You create the table for storing the state yourself, on the same cluster the [pipeline](https://ytsaurus.tech/flow/concepts/glossary.md#pipeline) is deployed on. General information about stateful processing is available in the [Stateful processing](https://ytsaurus.tech/flow/concepts/stateful.md) section. ## Overview {#overview} External State in the Go SDK is represented by two accessors: `flow.ExternalStateAccessor` for the state the computation **owns**, and `flow.JoinedExternalStateAccessor` for the state the computation only **reads**. Both convert a row of an external dynamic table into a Go structure with `yson` tags. The state is bound to the [key](https://ytsaurus.tech/flow/concepts/glossary.md#key) of the message (`group_by_schema`). | Accessor | Spec section | Opened by the function | Writing | |---|---|---|---| | [ExternalStateAccessor](https://ytsaurus.tech/docs/en/#getting-accessor) | `external_state_managers` | `flow.OpenExternalState` | Yes | | [JoinedExternalStateAccessor](https://ytsaurus.tech/docs/en/#joined-external-state) | `external_state_joiners` | `flow.OpenJoinedExternalState` | No | Ownership and reading live in different namespaces: a state declared in `external_state_managers` isn’t available through `flow.OpenJoinedExternalState`, and vice versa. For more on accessors and the general principles of working with state, see [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md). ## Difference from Internal State {#vs-internal-state} | Characteristic | External State | Internal State | |---|---|---| | Storage | External dynamic table | Internal tables of Flow | | Table creation | You create it yourself | Automatic | | Data format | A Go structure over a table row | Arbitrary (YSON, Protobuf, raw bytes) | | Access from other systems | Yes (a sorted dynamic table) | No | | Schema | Defined by the table schema | Defined by you | | State name | An absolute path starting with `/` | An ordinary name without `/` | For more on internal state, see [Internal State (Go)](https://ytsaurus.tech/docs/en/internal-state.md). ## Getting the accessor {#getting-accessor} [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/context.go) `flow.ExternalStateAccessor` is opened through `flow.Runtime`: ```go // For a message state, err := flow.OpenExternalState(rt, "/shuffle-state", msg) // For a timer state, err := flow.OpenExternalState(rt, "/shuffle-state", timer) ``` The arguments are the same as for the internal state accessors: - `rt` — the `flow.Runtime` of the handler. - `name` — the state name from the `external_state_managers` section of the [static spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec) (`"/shuffle-state"` in the example). The name must start with `/` and match the key in the spec. - `input` — the input to whose key the state is bound: `flow.ExtendedMessage`, `flow.Timer`, or `flow.Visit` (anything implementing `flow.Input`). {% note warning %} The name of an external state is validated: it must start with `/`, must not be empty, must not be the root `/` itself, must not end with `/`, and must not contain two consecutive `/`. A malformed name gives an error wrapping `flow.ErrInvalidStateName`; a well-formed name that isn’t declared in the spec gives an error wrapping `flow.ErrUnknownState`. {% endnote %} Unlike internal state, external state can’t be created out of nothing: the row schema is known from the state table rather than from the spec, so it arrives together with the request. If the request didn’t bring a state with this name, opening returns an error wrapping `flow.ErrStateNotRead`. For a state the computation owns, the worker supplies a row for every key of the batch (an empty one for keys nothing has been saved for yet), so in practice this error occurs with [joined state](https://ytsaurus.tech/docs/en/#joined-external-state). ## Main operations {#operations} ### Reading and writing typed state {#read} Describe the columns you use with a structure. Pointers let you distinguish a missing column from its zero value: ```go type joinState struct { ShowTime *uint64 `yson:"show_time"` ClickTime *uint64 `yson:"click_time"` } ``` `ConvertTo` reads the row into the structure, and the second result reports whether the row existed: ```go state, err := flow.OpenExternalState(rt, "/join-state", msg) if err != nil { return err } var window joinState _, err = state.ConvertTo(&window) if err != nil { return err } ``` After the change, the structure is saved by the reverse conversion: ```go window.ShowTime = &showTime return state.ConvertFrom(&window) ``` `ConvertFrom` updates the fields of the structure on top of the current row, so the columns not present in the structure are preserved. For dynamic schemas, the low-level `Get`, `Or`, `Builder`, `Set`, and `Schema` remain available. ### Clearing the state {#clear} ```go state, err := flow.OpenExternalState(rt, "/join-state", timer) if err != nil { return err } // Deleting the row from the table return state.Clear() ``` {% note info %} An empty state corresponds to the absence of a row in the table: `ConvertTo` for such a key returns `false` as its first result. Only the rows the computation changed through `Set` or `Clear` travel back to the worker — the state protocol transfers a delta, not the whole state. {% endnote %} ## Configuration in the static spec {#static-spec} To use External State, declare an external state manager in the `external_state_managers` section of the [computation](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) in the static spec. Here is an example from [static_table_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join), where the `reference_loader` computation owns the reference dataset: ```yson { "spec" = { "computations" = { "reference_reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "output_stream_ids" = ["reference"]; "source_streams" = { "reference_table" = { "source_class_name" = "NYT::NFlow::NStaticTableConnector::TSource"; "parameters" = { }; }; }; "parameters" = {}; }; "event_reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "output_stream_ids" = ["event"]; "source_streams" = { "event_queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { }; }; }; "parameters" = {}; }; "reference_loader" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"; required = %true;}; {"name" = "key"; "type" = "uint64";}; ]; "input_stream_ids" = ["reference"]; "output_stream_ids" = []; "external_state_managers" = { "/reference_state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state"; }; }; }; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = {}; }; "enricher" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"; required = %true;}; {"name" = "key"; "type" = "uint64";}; ]; "input_stream_ids" = ["event"]; "output_stream_ids" = ["enriched"]; "external_state_joiners" = { "/reference_state" = { "external_state_joiner_class_name" = "NYT::NFlow::TSimpleExternalStateJoiner"; "parameters" = { "path" = "//path/to/state"; }; }; }; "sinks" = { "queue" = { "sink_class_name" = "NYT::NFlow::TAsyncQueueSink"; "input_stream_ids" = ["enriched"]; "parameters" = { }; }; }; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = {}; }; }; "resources" = { "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { }; "dependencies" = {}; }; }; }; "dynamic_spec" = { "computations" = { "reference_reader" = { "parameters" = {}; "source_streams" = {"reference_table" = {"parameters" = {}}}; }; "event_reader" = { "parameters" = {}; }; "reference_loader" = { "parameters" = {}; }; "enricher" = { "parameters" = {}; }; }; }; } ``` The key fields: - `external_state_managers` — the top-level section inside the computation describing the external state managers. - The key inside `external_state_managers` (here `"/reference_state"`) — the state name used in the Go code when calling `flow.OpenExternalState(rt, "/reference_state", msg)`. The name must start with `/`. - `external_state_manager_class_name` — the name of the registered external state manager class. For the typical scenario, it is `"NYT::NFlow::TSimpleExternalStateManager"`; For more details, see the [C++ documentation](https://ytsaurus.tech/flow/cpp/state.md#external-state). - `parameters.path` — the path to the YTsaurus dynamic table the state is stored in. ## Creating the table for the state {#state-table} The table for External State must be created in advance. The key columns of the table must match the `group_by_schema` of the computation: the worker finds the state row exactly by the message key. You create the table with standard commands, for example `yt create table ... --attributes '{dynamic=true; schema=...}'` and `yt mount-table` — see the [Create command](https://ytsaurus.tech/user-guide/storage/cypress-example.md#create) section. The [Shuffle](https://ytsaurus.tech/docs/en/examples/shuffle.md) example's `reducer` computation groups messages by `farm_hash(value), value`; create the state table with matching key columns `hash` (`uint64`) and `value` (`string`), plus a `count` (`int64`) value column. ## Complete example — eventReducer from Shuffle {#example} ```go type shuffleState struct { Count int64 `yson:"count"` } type eventReducer struct{} var _ flow.RowFunction = (*eventReducer)(nil) func (*eventReducer) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { state, err := flow.OpenExternalState(rt, shuffleStateName, msg) if err != nil { return err } var counter shuffleState if _, err := state.ConvertTo(&counter); err != nil { return err } counter.Count++ return state.ConvertFrom(&counter) } ``` [Full source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/shuffle/event_reducer.go) The working pattern: 1. Open the state through `flow.OpenExternalState(...)`. 2. Read it into a structure through `state.ConvertTo(&value)`. 3. Change the fields and save the structure through `state.ConvertFrom(&value)`. ## Joined External State {#joined-external-state} If an external state is needed **for reading only** — for example, to enrich events with a reference dataset that another computation populates — joined state is used. On the framework side it is served by the [External State Joiner](https://ytsaurus.tech/flow/cpp/state.md#external-state-joiner), which reads the table with TTL-based caching; in the spec it is declared in the `external_state_joiners` section (at the same level as `external_state_managers`). In Go, such a state is represented by the separate `flow.JoinedExternalStateAccessor` type, which has neither `Set` nor `Clear`: the joiner never writes back, and a response claiming otherwise would be rejected by the [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker). ### Getting the accessor {#getting-joined-accessor} ```go reference, err := flow.OpenJoinedExternalState(rt, "/reference_state", msg) ``` | Method | Result type | Description | |---|---|---| | `ConvertTo(&value)` | `(bool, error)` | Fill the structure with the joined row; `bool` reports whether it was found | For dynamic schemas, the low-level `Get`, `Or`, and `Schema` are available. {% note warning %} The worker joins only those keys for which it found rows. Therefore a batch that didn’t match on any key arrives without the joined state at all, and `flow.OpenJoinedExternalState` returns an error wrapping `flow.ErrStateNotRead`. This isn’t a failure: handle it as “there is nothing to enrich with” rather than as an error of the computation. {% endnote %} ### Configuration in the static spec {#joined-static-spec} The `enricher` computation from the [static_table_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join) example reads the very reference dataset owned by `reference_loader` from the [section above](https://ytsaurus.tech/docs/en/#static-spec): ```yson { "spec" = { "computations" = { "reference_reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "output_stream_ids" = ["reference"]; "source_streams" = { "reference_table" = { "source_class_name" = "NYT::NFlow::NStaticTableConnector::TSource"; "parameters" = { }; }; }; "parameters" = {}; }; "event_reader" = { "computation_class_name" = "NYT::NFlow::TSwiftPassthroughOrderedSourceComputation"; "output_stream_ids" = ["event"]; "source_streams" = { "event_queue" = { "source_class_name" = "NYT::NFlow::TQueueSource"; "parameters" = { }; }; }; "parameters" = {}; }; "reference_loader" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"; required = %true;}; {"name" = "key"; "type" = "uint64";}; ]; "input_stream_ids" = ["reference"]; "output_stream_ids" = []; "external_state_managers" = { "/reference_state" = { "external_state_manager_class_name" = "NYT::NFlow::TSimpleExternalStateManager"; "parameters" = { "path" = "//path/to/state"; }; }; }; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = {}; }; "enricher" = { "computation_class_name" = "NYT::NFlow::NCompanion::TTransformCompanionComputation"; "group_by_schema" = [ {"name" = "hash"; "expression" = "farm_hash(key)"; "type" = "uint64"; required = %true;}; {"name" = "key"; "type" = "uint64";}; ]; "input_stream_ids" = ["event"]; "output_stream_ids" = ["enriched"]; "external_state_joiners" = { "/reference_state" = { "external_state_joiner_class_name" = "NYT::NFlow::TSimpleExternalStateJoiner"; "parameters" = { "path" = "//path/to/state"; }; }; }; "sinks" = { "queue" = { "sink_class_name" = "NYT::NFlow::TAsyncQueueSink"; "input_stream_ids" = ["enriched"]; "parameters" = { }; }; }; "required_resource_ids" = { "CompanionManager" = { "worker" = true; "controller" = false; }; }; "parameters" = {}; }; }; "resources" = { "CompanionManager" = { "resource_class_name" = "NYT::NFlow::NCompanion::TCompanionManager"; "parameters" = { }; "dependencies" = {}; }; }; }; "dynamic_spec" = { "computations" = { "reference_reader" = { "parameters" = {}; "source_streams" = {"reference_table" = {"parameters" = {}}}; }; "event_reader" = { "parameters" = {}; }; "reference_loader" = { "parameters" = {}; }; "enricher" = { "parameters" = {}; }; }; }; } ``` The fields of `external_state_joiners` repeat the fields of `external_state_managers` up to the class name: `external_state_joiner_class_name` instead of `external_state_manager_class_name`. The `parameters.path` path is resolved on every access, so if you point it at a symlink, switching the symlink replaces the whole reference dataset under a running pipeline, without a restart. ### Example {#joined-example} ```go type enricher struct{} var _ flow.RowFunction = (*enricher)(nil) func (*enricher) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var event eventMessage if err := msg.ConvertTo(&event); err != nil { return err } name, joined, err := joinedName(rt, msg) if err != nil || !joined { return err } enriched := flow.NewYSONMessage[enrichedMessage](https://ytsaurus.tech/docs/en/enrichedStreamID) enriched.Key = event.Key enriched.Name = name encoded, err := flow.ConvertFrom(rt, enriched) if err != nil { return err } out.AddMessage(encoded) return nil } func joinedName(rt flow.Runtime, msg flow.ExtendedMessage) (string, bool, error) { state, err := flow.OpenJoinedExternalState(rt, referenceStateName, msg) if errors.Is(err, flow.ErrStateNotRead) { return "", false, nil } if err != nil { return "", false, err } var reference referenceState exists, err := state.ConvertTo(&reference) if err != nil || !exists || reference.NormalizedName == nil { return "", false, err } return *reference.NormalizedName, true, nil } ``` [Full source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join/enricher.go) The same technique, but with a reference dataset populated by an external process rather than by the pipeline, is covered in the [external_state_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/external_state_join) example: there `parameters.path` points at a symlink, and replacing the symlink changes the reference dataset entirely. ## See also - [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md) - [Internal State (Go)](https://ytsaurus.tech/docs/en/internal-state.md) - [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) — a brief overview - [Stateful processing (concept)](https://ytsaurus.tech/flow/concepts/stateful.md) - [Examples: Shuffle (Go)](https://ytsaurus.tech/docs/en/examples/shuffle.md) # Testing in YTsaurus Flow (Go) {% note info %} This page describes **unit testing** of the [computations](https://ytsaurus.tech/flow/concepts/glossary.md#stream-and-computation) of a Go [pipeline](https://ytsaurus.tech/flow/concepts/glossary.md#pipeline) through the `flowtest` harness, as well as **integration testing** of the full pipeline through `FlowTestGoBase`. {% endnote %} ## General testing architecture {#architecture} In production, the C++ [worker](https://ytsaurus.tech/flow/concepts/glossary.md#worker) sends gRPC requests to the [companion](https://ytsaurus.tech/flow/concepts/companion.md), passing [messages](https://ytsaurus.tech/flow/concepts/glossary.md#message), [timers](https://ytsaurus.tech/flow/concepts/glossary.md#timer), visits, [states](https://ytsaurus.tech/docs/en/state.md), and [watermarks](https://ytsaurus.tech/flow/concepts/watermarks.md). The companion parses the request, builds a `flow.Job` and a `flow.Runtime` from it, and calls the Process Function of the registered computation. In unit tests, the worker’s place is taken by `flowtest.Harness` from the [`flowtest`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/go/flow/flowtest) package. The harness stores what the worker reports to the companion — the streams, the key schema, the declared states, and the parameters — and runs the computation through the same job, the same runtime, and the same dispatching as the companion server does, down to rendering the response into the wire format. As a result, a message to an undeclared stream or a key that cannot be encoded fails in the unit test rather than in the job. The value under test is the same `*flow.Computation` that is registered in the pipeline: the harness is given the result of `flow.NewRowComputation` or a related constructor, and a source differs from a transform only in what created it — you don’t need to tell the harness about that separately. Unit tests need no cluster, no gRPC connection, and no `flow_server`. Tests are written with the standard `testing` package; the examples use [testify](https://github.com/stretchr/testify) (`require`) for assertions. ## Dependencies {#dependencies} The harness needs no separate `PEERDIR`: the dependencies of a Go module are derived from the imports. It is enough to list the test files in `GO_TEST_SRCS` of the pipeline module: ```text GO_PROGRAM() SRCS( main.go word_count_mapper.go ) GO_TEST_SRCS( word_count_mapper_test.go ) END() RECURSE_FOR_TESTS( gotest test ) ``` And to add a `gotest` directory next to it with a `GO_TEST_FOR` module, through which the tests are run: ```text GO_TEST_FOR(yt/yt/flow/examples/go/word_count) SIZE(SMALL) END() ``` ## Testing a Process Function {#testing-process} ### Creating a harness {#harness} The harness is created by the `flowtest.New(tb, computation, opts)` function. The first argument is `*testing.T` (`*testing.B` and `*testing.F` also work): the harness reports every usage error through it, so only what the test asserts remains in the test. ```go h := flowtest.New(t, flow.NewRowComputation("mapper", &wordCountMapper{}), flowtest.Options{ Streams: map[string]flow.Schema{"words": flowtest.Schema("word:string")}, KeySchema: flowtest.Schema("word:string"), InternalStates: []string{wordStateName}, }) ``` The fields of `flowtest.Options`: | Field | Description | |------|----------| | `Streams` | The streams the computation exchanges messages over, by stream identifier. The computation can read and write only the streams listed here. | | `KeySchema` | The schema of the key the inputs are grouped by. A computation without grouping leaves the field empty. | | `InternalStates` | The names of the [internal states](https://ytsaurus.tech/docs/en/internal-state.md) the computation declares — they reach it as `parameters.internal_states`. | | `ExternalStates` | The schemas of the [external states](https://ytsaurus.tech/docs/en/external-state.md) the computation owns, by state name. The names are absolute paths, as the worker requires. | | `JoinedExternalStates` | The schemas of the external states the computation reads without owning them. | | `Parameters` | The `parameters` map of the static [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec) — what the computation reads through `rt.Parameters()`. | | `DynamicParameters` | The `parameters` map of the dynamic spec. | A column schema is assembled by the `flowtest.Schema("word:string", "count:int64")` helper — the type names are the same as in YTsaurus. Build a schema that cannot be described this way through `flow.NewSchema` from `schema.Schema`. For a typed YSON stream, use the same schema the pipeline registers: `flow.YSONMessageSchema[event]()`. This way, a structure with an embedded `flow.YSONMessage` describes both the spec columns and the test input rows. For a plain structure without `flow.YSONMessage`, `flowtest.SchemaOf(event{})` remains. It follows the common `schema.Infer`: in particular, a Go string becomes `utf8`. If the schema must match an existing spec exactly, use `flowtest.Schema`. ### Inputs {#inputs} The inputs of one batch are built by harness methods and passed to `Process` in a single call: | Method | What it builds | |-------|------------| | `h.Key(flowtest.Row{...})` | A key according to the `KeySchema` schema. | | `h.Message(streamID, row)` | A message without a key — what a computation without grouping receives. | | `h.KeyedMessage(streamID, key, row)` | A message together with the key it is grouped by. | | `h.Timer(key, triggerTimestamp)` | A fired timer of the key. | | `h.Visit(key)` | A visit of a key from a key visitor stream. | | `h.SetWatermark(streamID, watermark)` | The watermark of a stream; it holds until it is set again. | Every message is given its own identifier, just as the worker does. The timestamps stay zero: a test that needs them can simply set them on the result. ```go msg := h.KeyedMessage("hits", key, flowtest.Row{"hit_id": "h1"}) msg.EventTimestamp = 1000 ``` `h.Process(inputs ...flow.Input)` runs the computation over the batch and returns `*flowtest.Response`; if processing returned an error, the test fails. The state survives a run: what the computation wrote is applied to the state of the next run — exactly the way the worker applies the response delta before sending the next batch. A test that needs a clean slate should build a new harness. ### A complete example {#unit-test-example} The unit tests of the mapper from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md) — the harness, a batch of messages, and a check of the internal state: ```go func newHarness(t *testing.T) *flowtest.Harness { return flowtest.New(t, flow.NewRowComputation("mapper", &wordCountMapper{}), flowtest.Options{ Streams: map[string]flow.Schema{"words": flowtest.Schema("word:string")}, KeySchema: flowtest.Schema("word:string"), InternalStates: []string{wordStateName}, }) } func TestRepeatedWordAccumulates(t *testing.T) { h := newHarness(t) key := h.Key(flowtest.Row{"word": "hello"}) var batch []flow.Input for range 3 { batch = append(batch, h.KeyedMessage("words", key, flowtest.Row{"word": "hello"})) } r := h.Process(batch...) require.EqualValues(t, 3, counterOf(t, r, key).Count) } func TestCounterSurvivesTheBatch(t *testing.T) { h := newHarness(t) key := h.Key(flowtest.Row{"word": "hello"}) h.Process(h.KeyedMessage("words", key, flowtest.Row{"word": "hello"})) r := h.Process(h.KeyedMessage("words", key, flowtest.Row{"word": "hello"})) require.EqualValues(t, 2, counterOf(t, r, key).Count) } func TestWordsAreCountedApart(t *testing.T) { h := newHarness(t) hello := h.Key(flowtest.Row{"word": "hello"}) world := h.Key(flowtest.Row{"word": "world"}) r := h.Process( h.KeyedMessage("words", hello, flowtest.Row{"word": "hello"}), h.KeyedMessage("words", world, flowtest.Row{"word": "world"}), h.KeyedMessage("words", hello, flowtest.Row{"word": "hello"}), ) require.EqualValues(t, 2, counterOf(t, r, hello).Count) require.EqualValues(t, 1, counterOf(t, r, world).Count) } func counterOf(t *testing.T, r *flowtest.Response, key flow.Payload) wordCountState { t.Helper() var counter wordCountState require.True(t, r.InternalStateYSON(wordStateName, key, &counter), "no counter stored for the key") return counter } ``` ### Processing errors {#errors} An error returned by a handler stops the processing of the whole batch: the worker retries the request, so there is no such thing as a partial response. Such a run is checked with the `h.ProcessError` method, which returns the error and fails if processing succeeded instead: ```go err := h.ProcessError(h.Message("queue", flowtest.Row{"data": "}not json{"})) require.ErrorContains(t, err, "parsing the data column") ``` A run that ended with an error produces no output and doesn’t change the state — that is why `ProcessError` doesn’t return a `Response`. ### Timers and watermarks {#timers-and-watermarks} A timer is built from a key and a trigger time. In a pipeline with several timer streams, the one you need is selected by the `StreamID` field — an empty value means the pipeline’s only timer stream: ```go timer := h.Timer(key, closeTime) timer.StreamID = timerStream r := h.Process(timer) ``` The watermark of a stream is set by `h.SetWatermark` and holds for all subsequent runs. This is how dropping of late data is checked: the computation reads `rt.MinWatermark()`, the minimum across the input streams, so a stream that hasn’t advanced keeps the window open for the rest. ```go h.SetWatermark(hitStream, hitTime+3) h.SetWatermark(actionStream, 0) ``` For a complete set of tests of a window with a timer and watermarks, see [Wait Click Join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/wait_click_join/join_function_test.go). ## Testing states {#testing-states} The state a computation starts a run with is put into the harness before the `Process` call, and the result is read from `Response`. For details on the accessors themselves, see the [State Accessor](https://ytsaurus.tech/docs/en/state-accessor.md) section. ### Internal state {#internal-state} The name of an internal state must be declared in `InternalStates`; otherwise the harness reports exactly the same error as the runtime in a job does. | Method | What it puts | |-------|------------| | `h.PutInternalState(name, key, data)` | Raw bytes, which `flow.OpenRawState` reads. | | `h.PutInternalStateYSON(name, key, value)` | A value serialized to YSON — what `flow.OpenYSONState` reads. | | `h.PutInternalStateProto(name, key, value)` | A serialized protobuf message for `flow.OpenProtoState`. | The state is read back with `Response` methods: ```go var counter wordCountState require.True(t, r.InternalStateYSON(wordStateName, key, &counter)) require.EqualValues(t, 1, counter.Count) ``` ### External state {#external-state} An external state owned by the computation is put with `h.PutExternalState(name, key, row)` and is read as a row: `r.ExternalState` returns a `flow.Payload`, and `r.ExternalStateRow` an already decoded `flowtest.Row`. {% note info %} An internal state and a joined external state reach a run only for the keys they store something for. An external state owned by the computation reaches it for every key of the batch, empty where nothing is stored: the worker resolves the computation’s own state for every key it passes — which is exactly what makes it possible to write state for a key seen for the first time. {% endnote %} The unit tests of the reducer from [Shuffle](https://ytsaurus.tech/docs/en/examples/shuffle.md), which counts events in an external state: ```go var shuffleStreams = []string{"event_a", "event_b", "event_c", "event_d"} func newReducerHarness(t *testing.T) *flowtest.Harness { streams := make(map[string]flow.Schema, len(shuffleStreams)) for _, streamID := range shuffleStreams { streams[streamID] = eventSchema } return flowtest.New(t, flow.NewRowComputation("reducer", &eventReducer{}), flowtest.Options{ Streams: streams, KeySchema: flowtest.Schema("value:string"), ExternalStates: map[string]flow.Schema{shuffleStateName: flowtest.Schema("count:int64")}, }) } func TestAValueIsCountedOncePerShuffleStream(t *testing.T) { h := newReducerHarness(t) key := h.Key(flowtest.Row{"value": "v"}) var batch []flow.Input for _, streamID := range shuffleStreams { batch = append(batch, h.KeyedMessage(streamID, key, flowtest.Row{"value": "v"})) } r := h.Process(batch...) require.EqualValues(t, 4, countOf(t, r, key)) require.Empty(t, r.Messages()) require.Empty(t, r.Timers()) } func TestValuesAreCountedApart(t *testing.T) { h := newReducerHarness(t) first := h.Key(flowtest.Row{"value": "v1"}) second := h.Key(flowtest.Row{"value": "v2"}) r := h.Process( h.KeyedMessage("event_a", first, flowtest.Row{"value": "v1"}), h.KeyedMessage("event_b", second, flowtest.Row{"value": "v2"}), h.KeyedMessage("event_c", first, flowtest.Row{"value": "v1"}), ) require.EqualValues(t, 2, countOf(t, r, first)) require.EqualValues(t, 1, countOf(t, r, second)) } func TestCounterSurvivesTheBatch(t *testing.T) { h := newReducerHarness(t) key := h.Key(flowtest.Row{"value": "v"}) h.Process(h.KeyedMessage("event_a", key, flowtest.Row{"value": "v"})) r := h.Process(h.KeyedMessage("event_b", key, flowtest.Row{"value": "v"})) require.EqualValues(t, 2, countOf(t, r, key)) } func countOf(t *testing.T, r *flowtest.Response, key flow.Payload) int64 { t.Helper() row, ok := r.ExternalState(shuffleStateName, key) require.True(t, ok, "no counter stored for the key") count, err := row.Int64(countColumn) require.NoError(t, err) return count } ``` ### Joined external state {#joined-external-state} A joined external state — a state the computation reads without owning it — is put with `h.PutJoinedExternalState(name, key, row)` and read through `r.JoinedExternalState` / `r.JoinedExternalStateRow`. It cannot be written to: nothing written into a read-only state leaves the response. ```go h := flowtest.New(t, flow.NewRowComputation("lookup_join", &lookupJoin{}), flowtest.Options{ Streams: map[string]flow.Schema{"event": flowtest.Schema("key:uint64")}, KeySchema: flowtest.Schema("hash:uint64", "key:uint64"), JoinedExternalStates: map[string]flow.Schema{ referenceStateName: flowtest.Schema("hash:uint64", "key:uint64", "name:string"), }, }) h.PutJoinedExternalState(referenceStateName, key, flowtest.Row{"key": uint64(1), "name": "alice"}) ``` A key no row was put for doesn’t reach the computation — just as in production, where the worker joins what it found and nothing beyond that. For a complete set of tests, see [external_state_join](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/external_state_join/lookup_join_test.go). ## Analyzing the results {#analyzing-response} The `*flowtest.Response` returned by `Process` is what the run produced: the collected output and the states in the form they will be saved in. | Method | What it returns | |-------|----------------| | `Groups()` | `[]flow.OutputGroup` — the output groups in the order they appeared. | | `Messages()` | The output messages of all groups, in order. | | `MessagesOn(streamID)` | The output messages of a single stream. | | `Rows()` | The payloads of the output messages, decoded into `flowtest.Row` and aligned with `Messages()`. | | `Distribute()` | The [distribute](https://ytsaurus.tech/docs/en/distribute.md) flag of every message, aligned with `Messages()`. | | `Timers()` | `[]flow.TimerRequest` — the timers the computation asked the worker to set. | An output group is the lineage of the output, not the shape of the input: `RowFunction` opens one group per input, `BatchFunction` one per batch, and groups nothing was written into are dropped. The states are read like this: | Method | What it returns | |-------|----------------| | `InternalStateRaw(name, key)` | The bytes the internal state stores for the key. | | `InternalStateYSON(name, key, dst)` | Deserializes the YSON of the internal state into `dst`. | | `InternalStateProto(name, key, dst)` | Deserializes the protobuf message of the internal state into `dst`. | | `InternalStateReset(name, key)` | The run cleared the state of the key. | | `InternalStateWritten(name)` | The run wrote into the state: only what was written reaches the worker. | | `InternalStateLen(name)` | The number of keys the state was read or written for. | | `ExternalState(name, key)`, `ExternalStateRow(name, key)` | The row of the external state — as a `flow.Payload` and as a `flowtest.Row`. | | `ExternalStateReset(name, key)`, `ExternalStateWritten(name)`, `ExternalStateLen(name)` | The same for the external state. | | `JoinedExternalState(name, key)`, `JoinedExternalStateRow(name, key)` | The row of the joined external state. | The state is reported as it will be saved: a record cleared by the run reads as absent, and `*Reset` is what tells it apart from one that never existed. ## Running unit tests {#running-unit-tests} Unit tests are `SMALL` tests and need no cluster. ```bash cd yt/yt/flow/examples/go/word_count go test ./... ``` You can filter a single test by name: ```bash go test ./... -run 'TestCounterSurvivesTheBatch' ``` ## Integration testing with FlowTestGoBase {#e2e-tests} For full integration testing of a pipeline (with real C++ workers, queues, and streams), use the `FlowTestGoBase` base class — a Python test that runs the same Go binary that goes to production. In such a test, the pipeline is started by the runner, not by the test itself: the Go binary starts as `./word_count --config pipeline.yson --flow-bin flow_server`, enriches the [spec](https://ytsaurus.tech/flow/concepts/glossary.md#spec-and-dynamic-spec), and hands control over to `flow_server`, which installs it. The companion in the job is brought up by the worker — exactly as in production. ### Dependencies {#integration-dependencies} An integration test needs a cluster recipe, `DEPENDS` on the pipeline binary and `flow_server`, and `DATA` with the spec. The full `ya.make` of the test from [WordCount](https://ytsaurus.tech/docs/en/examples/wordcount.md): ```text PY3TEST() INCLUDE(${ARCADIA_ROOT}/yt/yt/flow/library/python/integration_test_base/recipe.inc) TEST_SRCS( test_wordcount.py yt_sync.py ) PEERDIR( yt/yt/flow/library/python/queue ) DEPENDS( ${MODDIR}/.. yt/yt/flow/bin/flow_server ) DATA(arcadia/${MODDIR}/pipeline.yson) REQUIREMENTS( cpu:4 ram:32 ) TAG(ya:huge_logs) SIZE(MEDIUM) END() ``` ### Setup {#go-test-setup} The test inherits from `FlowTestGoBase` and sets the `GO_COMPANION_BINARY` attribute: ```python class Test(FlowTestGoBase): GO_COMPANION_BINARY = yatest.common.binary_path("yt/yt/flow/examples/go/word_count/word_count") ``` | Attribute | Description | |---------|----------| | `GO_COMPANION_BINARY` | The path to the Go pipeline binary: the same binary is both the runner and the companion. | | `VANILLA_WORKER_PORT_COUNT` | The number of ports per worker; `3` by default — rpc, monitoring, and the port the worker brings the companion up on. | The pipeline is started by the `start_flow_process_federation` method, which is given the spec through the `--config` argument; the base class sets `--flow-bin` itself. For a local federation, it also writes the path to the built binary into the companion resources, so that the worker starts it from disk. [An example of the WordCount E2E test](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/word_count/test/test_wordcount.py) {% note warning %} Integration tests require a deployed YTsaurus cluster and are of the `MEDIUM` size, so they are run with `ya test -tt`. For fast iteration, use the unit tests described above. {% endnote %} Use these principles when writing integration tests for pipelines: * Test end-to-end scenarios. That is: * Write input/source data to the local YT. * In the pipeline spec, mark the sources as `finite=%true`. * For a [Key Visitor stream](https://ytsaurus.tech/docs/concepts/key_visitor.md) that should run during the test, set `finite=%false`. * Run the pipeline. * When the Key Visitor should finish, call the integration test base method [`self.ask_key_visitor_to_complete("", "")`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base/yt_flow_base.py); it switches that stream's [dynamic `finite` parameter](https://ytsaurus.tech/docs/concepts/key_visitor.md#dynamic-params) to `%true`. * Wait for the pipeline to finish. * Check the output data. * Prepare the environment with the same code you use in production: * Test the same pipeline binary that will run in production. * Generate pipeline specs with the same code that generates them for production. * If input/output data require non-trivial serialization/parsing, implement this logic with code shared with production. * Use the [shared test framework](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base) (a `README.md` is available). * Implement a failover test. * Many errors surface when workers fail and other workers take over their tasks. So, run a test with `problems=True` and more than one worker. * Write stable tests. * Remember that in CI, any part of a test might run unexpectedly long. * The ideal runtime for a single test is 20 seconds for any build type. * For sanitizer builds, reduce the amount of input data. * Set all local timeouts with a multiple safety margin. * The test execution logic shouldn’t depend significantly on the current time. For example, a test shouldn’t fail if it starts before midnight and finishes after. ## Debugging tests {#debug} ### Logs {#debug-logs} After a test finishes, you can find its logs in `test-results/py3test/testing_out_stuff` in the test directory. Key logs include: * `run.log` — Python test logs. * `//Controller_...` — Controller logs (`.err` is the process’s stderr; `.log` contains regular logs written via `YT_LOG_...`). * `//Worker_...` — Worker logs. * `//Runner...` — Runner logs. If something isn’t working, check errors in all these logs. The approach to reviewing them is the same as in production — see [Logs](https://ytsaurus.tech/docs/devops/vanilla/diagnostics/logs.md). You can also view logs before the test finishes. To do this, locate the temporary directory where the test runs. The simplest way is to run the test with the `--keep-temps` flag: `ya make --keep-temps -ttt `. In this case, `ya make` won’t delete the temporary directory after the test finishes and will print a link to it in the output. You can also use these bash aliases: ```bash alias curtestdir="ps -f -u $USER | python3 -c \"import sys, re; drs = set(e for e in re.findall(r'[\s=](https://ytsaurus.tech/docs/en//S*testing'%20+%20r'_out_stuff)\b', sys.stdin.read())); print('' if len(drs) == 1 else 'Select first from ' + repr(drs), file=sys.stderr); print(list(drs)[0])\"" alias cdcurtestdir='cd $(curtestdir)' # Get the link to the local YTsaurus UI from the logs. alias curlocalyt='cat $(curtestdir)/stderr 2>/dev/null | grep YT' ``` ### Test framework {#debug-test-framework} You can read about how to set up the environment for better test framework performance and how to influence test parameters in the framework’s [README.md](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/python/integration_test_base). You configure the behavior of integration tests using `--test-param NAME=VAL`: #| || **Parameter** | **Default** | **Values** | **Action** || || `RUNNER_LOG_LEVEL` | | `Error`, `Info`, `Debug`, … | Sets the logging level for the process that runs the pipeline (runner). || || `PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN` | `0` | `0`, `1` | Pauses the test before stopping Flow processes. Combined with `--test-disable-timeout`, this lets you keep the local YTsaurus and the Flow process federation running for a long time so you can study them at your own pace via the UI. || || `EXTERNAL_YT_CONFIG` | (not set) | YSON — see below | Runs the pipeline on real external YTsaurus clusters instead of the local recipe. || |# Examples: ```bash ya make -A --test-param RUNNER_LOG_LEVEL=Debug ya make -A --test-disable-timeout --test-param PAUSE_BEFORE_FLOW_PROCESS_FEDERATION_TEARDOWN=1 ``` ### `EXTERNAL_YT_CONFIG` {#external-yt-config} The local YTsaurus still starts via the recipe, but the test ignores it. Required fields that are common for all clusters: - `path` — base directory. - `tablet_cell_bundle` — bundle for the dynamic tables that are created. - `proxy_role` — RPC proxy role. Optional: `primary_medium` (default `"default"`). The `clusters` list — the first element is primary. Record fields: `cluster_name` (required), `proxy_url` (defaults to `cluster_name`). Authorization: when using an external YTsaurus, `YT_TOKEN`/`YT_USER` from the local recipe are cleared; yt-wrapper picks up the token from `~/.yt/token`. Isolation: `work_yt_path = path//`; the `path/` directory is deleted and recreated once per class in `setup_class`. Example: ```bash ya make -A --test-param 'EXTERNAL_YT_CONFIG={path="//tmp/yt_flow";tablet_cell_bundle=default;proxy_role=default;clusters=[{cluster_name=};];}' ``` ## See also - [Computation (Go)](https://ytsaurus.tech/docs/en/computation.md) - [Working with states (Go)](https://ytsaurus.tech/docs/en/state.md) - [State Accessor (Go)](https://ytsaurus.tech/docs/en/state-accessor.md) - [Examples: Word Count (Go)](https://ytsaurus.tech/docs/en/examples/wordcount.md) - If you are working on Flow itself — [Pipeline testing framework](https://ytsaurus.tech/flow/contributor/testing-framework.md). # YTsaurus Flow Go examples Start with [Word Count](https://ytsaurus.tech/docs/en/wordcount.md) for a basic pipeline. Then explore [Shuffle](https://ytsaurus.tech/docs/en/shuffle.md), [Async Request](https://ytsaurus.tech/docs/en/async_request.md), and [Wait Click Join](https://ytsaurus.tech/docs/en/wait_click_join.md) for processing and joining streams. Read the [Go getting started guide](https://ytsaurus.tech/docs/getting-started.md) before running an example. # Word Count in YTsaurus Flow (Go) The simplest example of a stateful [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) in Go: counting the occurrences of every word in an internal YSON [state](https://ytsaurus.tech/concepts/glossary.md#state). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/word_count) ## Structure {#structure} The pipeline consists of two [computations](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation): - `reader` — a native source (`TSwiftPassthroughOrderedSourceComputation`) declared directly in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec): it reads the queue and publishes the rows to the `words` [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). It has no Go code. - `mapper` — a transform computation (`TTransformCompanionComputation`) served by the companion: it reads the `words` stream and updates the counter of the word in the internal state. The messages are grouped by word (`group_by_schema` with `farm_hash(word)` and `word`), so the state of the key being processed is the counter of exactly one word. The result of the pipeline lies in the internal state table: nothing is sent further along the graph. ## `main.go` {#main-go} The entry point: creating the pipeline, registering the only computation, and starting it. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/word_count/main.go) ```go type wordMessage struct { flow.YSONMessage Word string `yson:"word"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams(flow.NewYSONStream[wordMessage](https://ytsaurus.tech/docs/en/%22words%22)) pipeline.Add(flow.NewRowComputation("mapper", &wordCountMapper{})) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "word_count: %v\n", err) os.Exit(1) } } ``` ## `word_count_mapper.go` {#word-count-mapper-go} The state value is an ordinary Go structure with YSON tags: it is stored in the internal state table exactly in this form. ```go type wordCountState struct { Word string `yson:"word"` Count int64 `yson:"count"` } ``` The `flow.RowFunction` that opens the internal state by the name `word-state` through `flow.OpenYSONState` and increments the counter: ```go type wordCountMapper struct{} var _ flow.RowFunction = (*wordCountMapper)(nil) func (*wordCountMapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input wordMessage if err := msg.ConvertTo(&input); err != nil { return err } state, err := flow.OpenYSONState[wordCountState](https://ytsaurus.tech/docs/en/rt,%20wordStateName,%20msg) if err != nil { return err } fresh := state.Empty() counter := state.Value() if fresh { counter.Word = input.Word } counter.Count++ return nil } ``` ## Key patterns {#key-patterns} - The simplest stateful pipeline with a single computation on the companion side: the source stays native, and no Go code is needed for it. - Internal YSON state through `flow.OpenYSONState[T](https://ytsaurus.tech/docs/en/rt,%20name,%20msg)`: `Value()` returns a mutable structure that the SDK saves after a successful batch. - The state name (`word-state`) matches the name from `parameters.internal_states` of the computation in the spec. - The state key is defined by `group_by_schema` from the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec) — in this case, by the `word` field. - The input is converted into `wordMessage` once through `msg.ConvertTo(&input)`, after which the handler works with the fields of the structure. ## See also - [Getting started (Go)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Go)](https://ytsaurus.tech/docs/computation.md) - [Internal State (Go)](https://ytsaurus.tech/docs/internal-state.md) - [Testing (Go)](https://ytsaurus.tech/docs/testing.md) - [Stateful processing](https://ytsaurus.tech/concepts/stateful.md) # Shuffle in YTsaurus Flow (Go) An example of a [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) built from two Go [computations](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation): the source computation parses JSON and sends typed [messages](https://ytsaurus.tech/concepts/glossary.md#message), and the transform computation counts the events in an external [state](https://ytsaurus.tech/concepts/glossary.md#state). Between them stand native passthrough computations that regroup the event by four different keys. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/shuffle) ## Structure {#structure} - `reader` (source, `TSwiftOrderedSourceCompanionComputation`) — `eventMapper`: parses the JSON from the `data` column and sends a typed message to the `event` [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). - `shuffle_a` … `shuffle_d` — native `TSwiftPassthroughComputation` declared in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec). Each of them regroups the flow by its own key (`key_a` … `key_d`) and publishes it to a separate stream, `event_a` … `event_d`. They have no Go code. - `reducer` (transform, `TTransformCompanionComputation`) — `eventReducer`: it is subscribed to all four streams and counts how many times a value arrives, in the external state `/shuffle-state`. An event written to the queue once reaches the reducer along four different paths, so it is counted four times. The companion serves only the ends of the pipeline — `reader` and `reducer`. ## `main.go` {#main-go} The entry point: registering the source and transform computations with a single `pipeline.Add` call. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/shuffle/main.go) ```go func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[event](https://ytsaurus.tech/docs/en/eventStreamID), flow.NewYSONStream[event](https://ytsaurus.tech/docs/en/%22event_a%22), flow.NewYSONStream[event](https://ytsaurus.tech/docs/en/%22event_b%22), flow.NewYSONStream[event](https://ytsaurus.tech/docs/en/%22event_c%22), flow.NewYSONStream[event](https://ytsaurus.tech/docs/en/%22event_d%22), ) pipeline.Add( flow.NewRowSourceComputation("reader", &eventMapper{}), flow.NewRowComputation("reducer", &eventReducer{}), ) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "shuffle: %v\n", err) os.Exit(1) } } ``` ## `event_mapper.go` {#event-mapper-go} The JSON structure that the input queue carries in the `data` column. The four keys are independent of each other — that is exactly what gives the shuffle stages something to regroup by: ```go type sourceMessage struct { flow.YSONMessage Data []byte `yson:"data"` } type event struct { flow.YSONMessage KeyA uint64 `json:"key_a" yson:"key_a"` KeyB uint64 `json:"key_b" yson:"key_b"` KeyC uint64 `json:"key_c" yson:"key_c"` KeyD uint64 `json:"key_d" yson:"key_d"` Value string `json:"value" yson:"value"` } ``` The source function converts the input row into `sourceMessage`, parses the JSON into `event`, and publishes a typed message: a flow can only be regrouped by a stream column, so the parsed keys are moved into separate fields. ```go type eventMapper struct{} var _ flow.RowFunction = (*eventMapper)(nil) func (*eventMapper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input sourceMessage if err := msg.ConvertTo(&input); err != nil { return err } parsed := flow.NewYSONMessage[event](https://ytsaurus.tech/docs/en/eventStreamID) if err := json.Unmarshal(input.Data, parsed); err != nil { return fmt.Errorf("parsing the data column: %w", err) } parsed.Meta.StreamID = eventStreamID message, err := flow.ConvertFrom(rt, parsed) if err != nil { return err } out.AddMessage(message) return nil } ``` ## `event_reducer.go` {#event-reducer-go} The transform function with an external state for counting the events: ```go type shuffleState struct { Count int64 `yson:"count"` } type eventReducer struct{} var _ flow.RowFunction = (*eventReducer)(nil) func (*eventReducer) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { state, err := flow.OpenExternalState(rt, shuffleStateName, msg) if err != nil { return err } var counter shuffleState if _, err := state.ConvertTo(&counter); err != nil { return err } counter.Count++ return state.ConvertFrom(&counter) } ``` ## Key patterns {#key-patterns} - A pipeline of several computations: a [source](https://ytsaurus.tech/concepts/glossary.md#source) built on `flow.NewRowSourceComputation` plus a transform built on `flow.NewRowComputation`. Both are registered by a single `pipeline.Add`. - Parsing JSON and creating a typed message: `msg.ConvertTo(&input)` → `json.Unmarshal` → `flow.ConvertFrom(rt, event)` → `out.AddMessage(msg)`. - External state through `flow.OpenExternalState(rt, "/shuffle-state", msg)`: `ConvertTo` reads the counter into a structure, `ConvertFrom` saves the update. The name of an external state is an absolute path that matches the key in `external_state_managers` of the spec. - `state.ConvertTo` returns whether the row exists: for a key the state has never been written for, the counter starts from zero. - Regrouping the flow is the job of the native passthrough computations: you can count the same value across several cuts without writing a single line of Go code for the shuffle stages themselves. ## See also - [Getting started (Go)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Go)](https://ytsaurus.tech/docs/computation.md) - [External State (Go)](https://ytsaurus.tech/docs/external-state.md) - [Testing (Go)](https://ytsaurus.tech/docs/testing.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/shuffle.md) # Static Table Join in YTsaurus Flow (Go) An example of a [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that loads a static reference table into an external [state](https://ytsaurus.tech/concepts/glossary.md#state) and then enriches events from a queue by the shared key. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join) ## Structure {#structure} - `reference_reader` — a native static-table source that publishes the reference rows to the `reference` stream. - `reference_loader` (`referenceLoader`) — a Go transform computation that normalizes the value and writes it to the external state `/reference_state`. - `event_reader` — a native queue source that publishes the `event` stream. - `enricher` (`enricher`) — a Go transform computation that reads `/reference_state` through `external_state_joiners` and publishes the result to the `enriched` stream. The manager and the joiner refer to the same state table: the first one fills it from the static table, the second one performs a read-only lookup for the incoming events. ## `main.go` {#main-go} The entry point registers three typed streams and both Go computations. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join/main.go) ```go const referenceStateName = "/reference_state" type referenceMessage struct { flow.YSONMessage Key uint64 `yson:"key"` Name string `yson:"name"` } type eventMessage struct { flow.YSONMessage Key uint64 `yson:"key"` } type enrichedMessage struct { flow.YSONMessage Key uint64 `yson:"key"` Name string `yson:"name"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[referenceMessage](https://ytsaurus.tech/docs/en/%22reference%22), flow.NewYSONStream[eventMessage](https://ytsaurus.tech/docs/en/%22event%22), flow.NewYSONStream[enrichedMessage](https://ytsaurus.tech/docs/en/enrichedStreamID), ) pipeline.Add( flow.NewRowComputation("reference_loader", &referenceLoader{}), flow.NewRowComputation("enricher", &enricher{}), ) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "static_table_join: %v\n", err) os.Exit(1) } } ``` ## `reference_loader.go` {#reference-loader-go} The loader decodes a reference row, opens the external state for its key, and saves the normalized value. [Source code: `reference_loader.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join/reference_loader.go) ```go type referenceState struct { NormalizedName *string `yson:"normalized_name"` } type referenceLoader struct{} var _ flow.RowFunction = (*referenceLoader)(nil) func (*referenceLoader) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var reference referenceMessage if err := msg.ConvertTo(&reference); err != nil { return err } state, err := flow.OpenExternalState(rt, referenceStateName, msg) if err != nil { return err } normalized := normalize(reference.Name) return state.ConvertFrom(&referenceState{NormalizedName: &normalized}) } func normalize(name string) string { return strings.ToLower(strings.TrimSpace(name)) } ``` ## `enricher.go` {#enricher-go} The enricher reads the joined state row and creates an output message only for a key that was found. [Source code: `enricher.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/static_table_join/enricher.go) ```go type enricher struct{} var _ flow.RowFunction = (*enricher)(nil) func (*enricher) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var event eventMessage if err := msg.ConvertTo(&event); err != nil { return err } name, joined, err := joinedName(rt, msg) if err != nil || !joined { return err } enriched := flow.NewYSONMessage[enrichedMessage](https://ytsaurus.tech/docs/en/enrichedStreamID) enriched.Key = event.Key enriched.Name = name encoded, err := flow.ConvertFrom(rt, enriched) if err != nil { return err } out.AddMessage(encoded) return nil } func joinedName(rt flow.Runtime, msg flow.ExtendedMessage) (string, bool, error) { state, err := flow.OpenJoinedExternalState(rt, referenceStateName, msg) if errors.Is(err, flow.ErrStateNotRead) { return "", false, nil } if err != nil { return "", false, err } var reference referenceState exists, err := state.ConvertTo(&reference) if err != nil || !exists || reference.NormalizedName == nil { return "", false, err } return *reference.NormalizedName, true, nil } ``` ## Key patterns {#key-patterns} - `flow.OpenExternalState` and `ConvertFrom` are used at the loading stage to write the reference data. - `flow.OpenJoinedExternalState` and `ConvertTo` are used at the enrichment stage for the read-only lookup. - Both computations work with typed structures; the payload is converted only at the input and the output of the handler. - The stream schemas are derived from the `yson` tags and registered through `flow.NewYSONStream`. ## See also - [External State (Go)](https://ytsaurus.tech/docs/external-state.md) - [State Accessor (Go)](https://ytsaurus.tech/docs/state-accessor.md) - [Static table](https://ytsaurus.tech/connectors/static-table.md) # External State Join in YTsaurus Flow (Go) An example of a [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that enriches events from a queue with data from a read-only external [state](https://ytsaurus.tech/concepts/glossary.md#state). The reference table is attached through `TSimpleExternalStateJoiner`, and the Go computation looks a row up by the event key. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/external_state_join) ## Structure {#structure} - `event_reader` — a native queue source that publishes the `event` stream. - `lookup_join` (`lookupJoin`) — a Go transform computation grouped by `key`. It reads `/reference` through `external_state_joiners` and publishes the enriched message to the `enriched` stream. - A queue sink stores the messages from `enriched`. The joiner path in the spec may point to a Cypress link. This lets you switch the reference data to a new version of the table atomically, without restarting the pipeline. ## `main.go` {#main-go} The entry point registers the schemas of the input and output streams, adds the computation, and starts the pipeline. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/external_state_join/main.go) ```go type eventMessage struct { flow.YSONMessage Key uint64 `yson:"key"` } type enrichedMessage struct { flow.YSONMessage Key uint64 `yson:"key"` Name string `yson:"name"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[eventMessage](https://ytsaurus.tech/docs/en/%22event%22), flow.NewYSONStream[enrichedMessage](https://ytsaurus.tech/docs/en/enrichedStreamID), ) pipeline.Add(flow.NewRowComputation("lookup_join", &lookupJoin{})) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "external_state_join: %v\n", err) os.Exit(1) } } ``` ## `lookup_join.go` {#lookup-join-go} The computation decodes the typed input message, reads the joined state row, and creates a typed output message. [Source code: `lookup_join.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/external_state_join/lookup_join.go) ```go type referenceState struct { Name *string `yson:"name"` } type lookupJoin struct{} var _ flow.RowFunction = (*lookupJoin)(nil) func (*lookupJoin) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var event eventMessage if err := msg.ConvertTo(&event); err != nil { return err } name, ok, err := referenceName(rt, msg) if err != nil || !ok { return err } enriched := flow.NewYSONMessage[enrichedMessage](https://ytsaurus.tech/docs/en/enrichedStreamID) enriched.Key = event.Key enriched.Name = name encoded, err := flow.ConvertFrom(rt, enriched) if err != nil { return err } out.AddMessage(encoded) return nil } func referenceName(rt flow.Runtime, msg flow.ExtendedMessage) (string, bool, error) { reference, err := flow.OpenJoinedExternalState(rt, referenceStateName, msg) if errors.Is(err, flow.ErrStateNotRead) { return "", false, nil } if err != nil { return "", false, err } var state referenceState exists, err := reference.ConvertTo(&state) if err != nil || !exists || state.Name == nil { return "", false, err } return *state.Name, true, nil } ``` ## Key patterns {#key-patterns} - `flow.OpenJoinedExternalState(rt, referenceStateName, msg)` opens the read-only state for the key of the current message. - `flow.ErrStateNotRead` means that there is no joined row for the key; in this case the example publishes no result. - `ConvertTo` stays at the SDK boundary: the business logic works with `eventMessage`, `referenceState`, and `enrichedMessage` rather than with raw wire-protocol rows. - The joined external state isn’t changed from the computation: the `ConvertFrom` method isn’t used for it. ## See also - [External State (Go)](https://ytsaurus.tech/docs/external-state.md) - [State Accessor (Go)](https://ytsaurus.tech/docs/state-accessor.md) - [Static table](https://ytsaurus.tech/connectors/static-table.md) # Async Request in YTsaurus Flow (Go) An example of a [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that makes an asynchronous call to an external service: one [computation](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) turns events into requests and accumulates the responses in an external [state](https://ytsaurus.tech/concepts/glossary.md#state), and another one serves the requests without any state. This is a Go implementation of the same scenario as the [C++ example](https://ytsaurus.tech/cpp/examples/async_request.md). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/async_request) ## Structure {#structure} The companion serves two computations; `injector` stays a native source declared in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec): 1. **`state`** (`stateKeeper`) — a stateful computation grouped by `key`, which: - takes events from the `event` stream and produces a request in the `request` stream with a random `request_id`; - takes responses from the `response` stream and adds up their total length (`total_length`) in the external state `/state`. 2. **`processor`** (`requestProcessor`) — a stateless computation grouped by `request_id`: it takes requests from the `request` stream and immediately answers with the length of the request string in the `response` stream. The `event → request → response → state` cycle closes between the two computations. An event is answered with a request rather than with the result right away, so the serving side never holds up processing: the response arrives later, as a separate [message](https://ytsaurus.tech/concepts/glossary.md#message), and only then does the state of the key move on. ## `main.go` {#main-go} The entry point: creating the pipeline and registering both computations. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/async_request/main.go) ```go type eventMessage struct { flow.YSONMessage Key uint64 `yson:"key"` Data string `yson:"data"` } type requestMessage struct { flow.YSONMessage RequestID uint64 `yson:"request_id"` Key uint64 `yson:"key"` Request string `yson:"request"` } type responseMessage struct { flow.YSONMessage RequestID uint64 `yson:"request_id"` Key uint64 `yson:"key"` Length int64 `yson:"length"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[eventMessage](https://ytsaurus.tech/docs/en/eventStreamID), flow.NewYSONStream[requestMessage](https://ytsaurus.tech/docs/en/requestStreamID), flow.NewYSONStream[responseMessage](https://ytsaurus.tech/docs/en/responseStreamID), ) pipeline.Add( flow.NewRowComputation("state", &stateKeeper{}), flow.NewRowComputation("processor", &requestProcessor{}), ) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "async_request: %v\n", err) os.Exit(1) } } ``` ## `state_keeper.go` {#state-keeper-go} Routing of the input streams (`event` / `response`) and work with the external state. ```go type totalLengthState struct { TotalLength int64 `yson:"total_length"` } type stateKeeper struct{} var _ flow.RowFunction = (*stateKeeper)(nil) func (k *stateKeeper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { switch msg.StreamID { case eventStreamID: return k.sendRequest(rt, msg, out) case responseStreamID: return k.accumulate(rt, msg) default: return fmt.Errorf("unhandled stream %q", msg.StreamID) } } func (*stateKeeper) sendRequest(rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector) error { var event eventMessage if err := msg.ConvertTo(&event); err != nil { return err } request := flow.NewYSONMessage[requestMessage](https://ytsaurus.tech/docs/en/requestStreamID) request.RequestID = rand.Uint64() request.Key = event.Key request.Request = event.Data encoded, err := flow.ConvertFrom(rt, request) if err != nil { return err } out.AddMessage(encoded) return nil } func (*stateKeeper) accumulate(rt flow.Runtime, msg flow.ExtendedMessage) error { var response responseMessage if err := msg.ConvertTo(&response); err != nil { return err } state, err := flow.OpenExternalState(rt, totalLengthStateName, msg) if err != nil { return err } var total totalLengthState if _, err := state.ConvertTo(&total); err != nil { return err } total.TotalLength += response.Length return state.ConvertFrom(&total) } ``` ## `request_processor.go` {#request-processor-go} The stateless request handler: it computes the length of the request string and returns a response. ```go type requestProcessor struct{} var _ flow.RowFunction = (*requestProcessor)(nil) func (*requestProcessor) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var request requestMessage if err := msg.ConvertTo(&request); err != nil { return err } response := flow.NewYSONMessage[responseMessage](https://ytsaurus.tech/docs/en/responseStreamID) response.RequestID = request.RequestID response.Key = request.Key response.Length = int64(len(request.Request)) encoded, err := flow.ConvertFrom(rt, response) if err != nil { return err } out.AddMessage(encoded) return nil } ``` ## Key patterns {#key-patterns} - **Routing by `msg.StreamID`**: a `switch` on the input stream identifier lets one computation handle several inputs with different logic. An unknown stream is an error rather than a silent skip. - **A random `request_id`**: `rand.Uint64()` links the request to the response. The request carries the key of the original event, so the response, partitioned by `request_id`, comes back to the state the call belongs to. - **External state** through `flow.OpenExternalState(rt, "/state", msg)`: the row is turned into `totalLengthState` by `ConvertTo`, changed as a structure, and saved through `ConvertFrom`. - **A stateless computation**: `requestProcessor` uses no state and is grouped by `request_id` rather than by the event key, so the requests of one key spread over all [partitions](https://ytsaurus.tech/concepts/glossary.md#partition) and scale independently. - **Stream dependency**: `streams_dependency` in the spec declares that `request` is produced from `event` — the worker takes this into account when advancing [watermarks](https://ytsaurus.tech/concepts/watermarks.md). ## See also - [Getting started (Go)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Go)](https://ytsaurus.tech/docs/computation.md) - [External State (Go)](https://ytsaurus.tech/docs/external-state.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/async_request.md) - [Similar example in Java](https://ytsaurus.tech/java/examples/async_request.md) # Retryable Async Request in YTsaurus Flow (Go) An extension of the [Async Request](https://ytsaurus.tech/docs/en/async_request.md) example: the request handler retries every failed attempt through [timers](https://ytsaurus.tech/concepts/glossary.md#timer). The whole request is stored in the internal [state](https://ytsaurus.tech/concepts/glossary.md#state) and retried with a fixed delay until the external service answers it. [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/retryable_async_request) ## Structure {#structure} The pipeline consists of three [computations](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation): - `injector` — a native source (`TSwiftPassthroughOrderedSourceComputation`) declared directly in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec): it reads the queue and publishes the events to the `event` [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). It has no Go code. - `state` (`stateKeeper`) — a transform computation grouped by `key`: for every event it opens a request in the `request` stream, and it adds the response that arrives in the `response` stream to the total response length of the key in the external state `/state`. - `processor` (`requestProcessor`) — a transform computation grouped by `request_id`: it makes the attempts to call the external service and publishes the response to the `response` stream. The two computations are grouped by different keys for a reason: `processor` is partitioned by request identifier, so the requests of one key are retried independently of each other. The `delay` timer stream is declared in the spec with `allow_timer_self_dependency = %true` — the computation sets timers for itself. ## `main.go` {#main-go} The entry point: creating the pipeline and registering both computations of the companion. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/retryable_async_request/main.go) ```go type eventMessage struct { flow.YSONMessage Key uint64 `yson:"key"` Data string `yson:"data"` } type requestMessage struct { flow.YSONMessage RequestID uint64 `yson:"request_id"` Key uint64 `yson:"key"` Request string `yson:"request"` } type responseMessage struct { flow.YSONMessage RequestID uint64 `yson:"request_id"` Key uint64 `yson:"key"` Length int64 `yson:"length"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[eventMessage](https://ytsaurus.tech/docs/en/eventStreamID), flow.NewYSONStream[requestMessage](https://ytsaurus.tech/docs/en/requestStreamID), flow.NewYSONStream[responseMessage](https://ytsaurus.tech/docs/en/responseStreamID), ) pipeline.Add( flow.NewRowComputation("state", &stateKeeper{}), flow.NewRowComputation("processor", &requestProcessor{}), ) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "retryable_async_request: %v\n", err) os.Exit(1) } } ``` ## `state_keeper.go` {#state-keeper-go} Stream routing and accumulation of the results in the external state: the branch on `msg.StreamID` sends the event and the response to two different handlers. ```go type totalLengthState struct { TotalLength int64 `yson:"total_length"` } type stateKeeper struct{} var _ flow.RowFunction = (*stateKeeper)(nil) func (s *stateKeeper) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { switch msg.StreamID { case eventStreamID: return s.sendRequest(rt, msg, out) case responseStreamID: return s.bookResponse(rt, msg) default: return fmt.Errorf("unhandled stream %q", msg.StreamID) } } func (s *stateKeeper) sendRequest(rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector) error { var event eventMessage if err := msg.ConvertTo(&event); err != nil { return err } request := flow.NewYSONMessage[requestMessage](https://ytsaurus.tech/docs/en/requestStreamID) request.RequestID = rand.Uint64() request.Key = event.Key request.Request = event.Data encoded, err := flow.ConvertFrom(rt, request) if err != nil { return err } out.AddMessage(encoded) return nil } func (s *stateKeeper) bookResponse(rt flow.Runtime, msg flow.ExtendedMessage) error { var response responseMessage if err := msg.ConvertTo(&response); err != nil { return err } state, err := flow.OpenExternalState(rt, totalStateName, msg) if err != nil { return err } var total totalLengthState if _, err := state.ConvertTo(&total); err != nil { return err } total.TotalLength += response.Length return state.ConvertFrom(&total) } ``` ## `request_processor.go` {#request-processor-go} The state value is an ordinary Go structure with YSON tags. It holds everything a retry needs, including the counter of failed attempts, so the retry no longer needs the original message: ```go type requestState struct { RequestID uint64 `yson:"request_id"` Key uint64 `yson:"key"` Request string `yson:"request"` FailedAttempts int64 `yson:"failed_attempts"` } ``` The retry logic: `OnMessage` opens the request and makes the first attempt, `OnTimer` reads the request from the state and repeats it. The `attempt` method shared by both handlers either sets a timer for the next attempt or publishes the response and clears the state: ```go type requestProcessor struct{} var ( _ flow.RowFunction = (*requestProcessor)(nil) _ flow.RowTimerFunction = (*requestProcessor)(nil) ) func (p *requestProcessor) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { state, err := flow.OpenYSONState[requestState](https://ytsaurus.tech/docs/en/rt,%20requestStateName,%20msg) if err != nil { return err } var input requestMessage if err := msg.ConvertTo(&input); err != nil { return err } request := requestState{ RequestID: input.RequestID, Key: input.Key, Request: input.Request, } return p.attempt(rt, state, request, out) } func (p *requestProcessor) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { state, err := flow.OpenYSONState[requestState](https://ytsaurus.tech/docs/en/rt,%20requestStateName,%20timer) if err != nil { return err } if state.Empty() { return nil } return p.attempt(rt, state, *state.Value(), out) } func (p *requestProcessor) attempt( rt flow.Runtime, state *flow.YSONState[requestState], request requestState, out flow.OutputCollector, ) error { if !succeeds(request) { request.FailedAttempts++ *state.Value() = request out.AddTimer(flow.TimerRequest{TriggerTimestamp: uint64(time.Now().Add(retryDelay).Unix())}) return nil } response := flow.NewYSONMessage[responseMessage](https://ytsaurus.tech/docs/en/responseStreamID) response.RequestID = request.RequestID response.Key = request.Key response.Length = int64(len(request.Request)) encoded, err := flow.ConvertFrom(rt, response) if err != nil { return err } out.AddMessage(encoded) state.Clear() return nil } func succeeds(request requestState) bool { return (request.RequestID+uint64(request.FailedAttempts))%attemptsToSucceed == 0 } ``` ## Key patterns {#key-patterns} - Retries through timers: `out.AddTimer(flow.TimerRequest{TriggerTimestamp: ...})` postpones the next attempt by `retryDelay`, and `OnTimer` performs it. This is the standard way to implement retries in Flow without external queues. An empty `StreamID` in `flow.TimerRequest` means the only timer stream of the computation. - The attempt counter lives in the state together with the request data, so it survives a [worker](https://ytsaurus.tech/concepts/glossary.md#worker) restart: after a restart the retries continue from the same place. - The input is converted into `requestMessage` once through `msg.ConvertTo(&input)`, while the retry logic works with a separate `requestState` structure that holds the counter of failed attempts. - The same state is opened both by a message and by a timer: `flow.OpenYSONState[requestState](https://ytsaurus.tech/docs/en/rt,%20requestStateName,%20msg)` and `flow.OpenYSONState[requestState](https://ytsaurus.tech/docs/en/rt,%20requestStateName,%20timer)` — both inputs carry the grouping key that the state is addressed by. - Clearing the state on success: after `state.Clear()`, a timer that fires later finds `state.Empty()` and returns without doing anything — a stale firing is harmless. - Separation of responsibilities: `stateKeeper` knows nothing about retries, the whole logic is encapsulated in `requestProcessor`, so you can change the retry strategy without touching the accounting of the results. - Deterministic failure simulation: `succeeds(request)` stands in for a real client of the external service — in production code there would be an HTTP call here. ## See also - [Getting started (Go)](https://ytsaurus.tech/docs/getting-started.md) - [Async Request (Go)](https://ytsaurus.tech/docs/en/async_request.md) - [Working with states (Go)](https://ytsaurus.tech/docs/state.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/retryable_async_request.md) # Wait Click Join in YTsaurus Flow (Go) An example of a join [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that joins the [streams](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation) of impressions (`hit`) and actions (`action`) by a shared key, using [timers](https://ytsaurus.tech/concepts/glossary.md#timer) and an external [state](https://ytsaurus.tech/concepts/glossary.md#state). This is a Go implementation of the similar [C++ example](https://ytsaurus.tech/cpp/examples/wait_click_join.md). [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/wait_click_join) ## Structure {#structure} The pipeline consists of three [computations](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation): - `hit_reader` and `action_reader` — native sources (`TSwiftPassthroughOrderedSourceComputation`) declared directly in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec): they read two queues and publish the `hit` and `action` streams. They have no Go code. Each source declares a `watermark_strategy`, so the [watermark](https://ytsaurus.tech/concepts/glossary.md#timestamps-and-watermarks) of the pipeline is computed from the event time. - `join` (`joinFunction`) — a transform computation served by the companion. The `join` computation works as follows: 1. It receives messages from two input streams: `hit` and `action`. 2. It accumulates the window of one impression in the external state `/join-state`. 3. It sets a timer for the moment the waiting window closes. 4. When the timer fires, it publishes the join result to the `joined_action` stream or simply clears the state. Both input streams are grouped by `hit_id` and `hit_time`, so everything that belongs to one impression falls into one key. The length of the window is set by the `wait_for_actions` computation parameter from the spec. ## `main.go` {#main-go} The entry point: creating the pipeline, registering the only computation, and starting it. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/wait_click_join/main.go) ```go type actionMessage struct { flow.YSONMessage HitID string `yson:"hit_id"` HitTime uint64 `yson:"hit_time"` IsClick bool `yson:"is_click"` ActionTime uint64 `yson:"action_time"` } type hitMessage struct { flow.YSONMessage HitID string `yson:"hit_id"` HitTime uint64 `yson:"hit_time"` HitPayload string `yson:"hit_payload"` } type joinedActionMessage struct { flow.YSONMessage HitID string `yson:"hit_id"` HitTime uint64 `yson:"hit_time"` IsClick bool `yson:"is_click"` ShowTime uint64 `yson:"show_time"` ClickTime uint64 `yson:"click_time"` HitPayload string `yson:"hit_payload"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[actionMessage](https://ytsaurus.tech/docs/en/actionStream), flow.NewYSONStream[hitMessage](https://ytsaurus.tech/docs/en/hitStream), flow.NewYSONStream[joinedActionMessage](https://ytsaurus.tech/docs/en/joinedStream), ) pipeline.Add(flow.NewRowComputation("join", &joinFunction{})) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "wait_click_join: %v\n", err) os.Exit(1) } } ``` ## `join_function.go` {#join-function-go} The main logic of the join: the function handles two streams and uses timers for window aggregation. ### `OnMessage` {#on-message} It drops late messages, appends its own field to the impression window, and sets the timer for closing the window: ```go type joinFunction struct{} var ( _ flow.RowFunction = (*joinFunction)(nil) _ flow.RowTimerFunction = (*joinFunction)(nil) ) func (*joinFunction) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var hit *hitMessage var action *actionMessage var hitTime uint64 switch msg.StreamID { case hitStream: hit = &hitMessage{} if err := msg.ConvertTo(hit); err != nil { return err } hitTime = hit.HitTime case actionStream: action = &actionMessage{} if err := msg.ConvertTo(action); err != nil { return err } hitTime = action.HitTime default: return fmt.Errorf("unhandled stream %q", msg.StreamID) } wait, err := waitForActions(rt) if err != nil { return err } closeTime := hitTime + wait if msg.EventTimestamp >= closeTime || msg.EventTimestamp < rt.MinWatermark() { return nil } state, err := flow.OpenExternalState(rt, joinStateName, msg) if err != nil { return err } var window joinState if _, err := state.ConvertTo(&window); err != nil { return err } if hit != nil { window.HitPayload = &hit.HitPayload } else if action.IsClick { window.ClickTime = &action.ActionTime } else { window.ShowTime = &action.ActionTime } if err := state.ConvertFrom(&window); err != nil { return err } out.AddTimer(flow.TimerRequest{TriggerTimestamp: closeTime, EventTimestamp: hitTime}) return nil } ``` ### `OnTimer` {#on-timer} It closes the window: it publishes the join result if both sides arrived, and clears the state in any case: ```go func (*joinFunction) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { if timer.StreamID != timerStream { return fmt.Errorf("unhandled timer stream %q", timer.StreamID) } state, err := flow.OpenExternalState(rt, joinStateName, timer) if err != nil { return err } var window joinState if _, err := state.ConvertTo(&window); err != nil { return err } if window.ShowTime != nil && *window.ShowTime != 0 && window.HitPayload != nil { var key joinKey if err := timer.Key.ConvertTo(&key); err != nil { return err } joined, err := joinedAction(rt, key, window) if err != nil { return err } out.AddMessage(joined) } return state.Clear() } ``` ## Key patterns {#key-patterns} - External state through `flow.OpenExternalState(rt, joinStateName, msg)`: `ConvertTo` reads the window into `joinState`, the message changes the field it needs in the structure, and `ConvertFrom` saves the window. - A timer with two timestamps: `out.AddTimer(flow.TimerRequest{TriggerTimestamp: closeTime, EventTimestamp: hitTime})` — `TriggerTimestamp` sets when the window will close, `EventTimestamp` ties the timer to the event. Messages from both streams set the timer, not only `hit`: an action can arrive earlier than the impression it belongs to, and the window still has to be closed. A message that already falls outside its window or behind the watermark returns early and sets no timer. - Filtering out late data: `msg.EventTimestamp < rt.MinWatermark()` drops the messages of already closed windows — otherwise a second join result would be published for one impression. For details, see [watermarks](https://ytsaurus.tech/concepts/glossary.md#timestamps-and-watermarks). - Computation parameters: `rt.Parameters().Get(waitForActionsParameter, &spelled)` reads the window length from the spec instead of hardcoding it. - Handling several input streams: the branch on `msg.StreamID` splits the logic for `hit` and `action`, and an unknown stream returns an error instead of a silent skip. - The key instead of the state: `timer.Key` carries `hit_id` and `hit_time` from `group_by_schema`, so the window holds only the data being joined — there is no need to duplicate the key in it. ## See also - [Getting started (Go)](https://ytsaurus.tech/docs/getting-started.md) - [External State (Go)](https://ytsaurus.tech/docs/external-state.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Watermarks](https://ytsaurus.tech/concepts/watermarks.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/wait_click_join.md) # URL Downloader in YTsaurus Flow (Go) An example of a [pipeline](https://ytsaurus.tech/concepts/glossary.md#pipeline) that groups incoming URLs by host and processes them in batches by a [timer](https://ytsaurus.tech/concepts/glossary.md#timer). It shows the typical pattern "accumulate in the [state](https://ytsaurus.tech/concepts/glossary.md#state) → process by a timer → clear the state". [Source code](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/url_downloader) ## Structure {#structure} The pipeline consists of two [computations](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation): - `url_reader` — a native source (`TSwiftPassthroughOrderedSourceComputation`) declared directly in the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec): it reads the queue and publishes messages with the `host` and `url` fields to the `urls` [stream](https://ytsaurus.tech/concepts/glossary.md#stream-and-computation). It has no Go code. - `url_downloader` (`urlDownloadFunction`) — a transform computation served by the companion. The `url_downloader` computation works as follows: 1. It takes messages from the `urls` stream. 2. It appends the URL to the batch of its host in the internal YSON state `host-state`. 3. It sets a timer `flushDelay` (5 seconds) ahead on every new URL. 4. When the timer fires, it processes the whole accumulated batch, publishes the results to the `processed_urls` stream, and clears the state. The messages are grouped by host (`group_by_schema` with `farm_hash(host)` and `host`), so the state of the key being processed is the batch of exactly one host. ## `main.go` {#main-go} The entry point: creating the pipeline, registering the only computation, and starting it. [Source code: `main.go`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/examples/go/url_downloader/main.go) ```go type urlMessage struct { flow.YSONMessage Host string `yson:"host"` URL string `yson:"url"` } type processedURLMessage struct { flow.YSONMessage Host string `yson:"host"` URL string `yson:"url"` Data string `yson:"data"` } func main() { pipeline := flow.NewPipeline() pipeline.AddStreams( flow.NewYSONStream[urlMessage](https://ytsaurus.tech/docs/en/%22urls%22), flow.NewYSONStream[processedURLMessage](https://ytsaurus.tech/docs/en/outputStreamID), ) pipeline.Add(flow.NewRowComputation("url_downloader", &urlDownloadFunction{})) if err := pipeline.Run(); err != nil { fmt.Fprintf(os.Stderr, "url_downloader: %v\n", err) os.Exit(1) } } ``` ## `url_download_function.go` {#url-download-function-go} The state value is an ordinary Go structure with YSON tags: it holds the host name and the list of URLs that haven’t been processed yet. ```go type hostState struct { Host string `yson:"host"` PendingURLs []string `yson:"pending_urls"` } ``` `flow.RowFunction` implements both handlers: `OnMessage` accumulates the URLs in the state and sets the timer, `OnTimer` processes the batch as a whole. ```go type urlDownloadFunction struct{} var ( _ flow.RowFunction = (*urlDownloadFunction)(nil) _ flow.RowTimerFunction = (*urlDownloadFunction)(nil) ) func (*urlDownloadFunction) OnMessage( ctx context.Context, rt flow.Runtime, msg flow.ExtendedMessage, out flow.OutputCollector, ) error { var input urlMessage if err := msg.ConvertTo(&input); err != nil { return err } state, err := flow.OpenYSONState[hostState](https://ytsaurus.tech/docs/en/rt,%20hostStateName,%20msg) if err != nil { return err } fresh := state.Empty() batch := state.Value() if fresh { batch.Host = input.Host } batch.PendingURLs = append(batch.PendingURLs, input.URL) out.AddTimer(flow.TimerRequest{TriggerTimestamp: uint64(time.Now().Add(flushDelay).Unix())}) return nil } func (*urlDownloadFunction) OnTimer( ctx context.Context, rt flow.Runtime, timer flow.Timer, out flow.OutputCollector, ) error { state, err := flow.OpenYSONState[hostState](https://ytsaurus.tech/docs/en/rt,%20hostStateName,%20timer) if err != nil { return err } if state.Empty() { return nil } batch := state.Value() if len(batch.PendingURLs) == 0 { state.Clear() return nil } for _, url := range batch.PendingURLs { processed := flow.NewYSONMessage[processedURLMessage](https://ytsaurus.tech/docs/en/outputStreamID) processed.Host = batch.Host processed.URL = url processed.Data = processURL(url) encoded, err := flow.ConvertFrom(rt, processed) if err != nil { return err } out.AddMessage(encoded) } state.Clear() return nil } ``` ## Key patterns {#key-patterns} - Batch processing by a timer: `out.AddTimer(flow.TimerRequest{TriggerTimestamp: ...})` in `OnMessage` and the whole processing in `OnTimer` — the standard way to collect events into a time window. The worker keeps one timer per key, so a burst of messages collapses into a single firing rather than a firing per message. - Internal YSON state through `flow.OpenYSONState[hostState](https://ytsaurus.tech/docs/en/rt,%20hostStateName,%20msg)`: `Value()` returns the mutable batch, and `Clear()` empties it after processing. The state name (`host-state`) matches the name from `parameters.internal_states` of the computation in the spec. - The state key by host is set by `group_by_schema` from the [spec](https://ytsaurus.tech/concepts/glossary.md#spec-and-dynamic-spec), so every partition of the computation processes the URLs of one host in isolation. - Every result is created as a `processedURLMessage` and converted into an output message once, through `flow.ConvertFrom`. - Clearing the state on all paths: `OnTimer` calls `state.Clear()` both when the batch is empty and when it has been processed — a timer that fires always leaves the key clean, and the URLs that arrive after that form a new batch. ## See also - [Getting started (Go)](https://ytsaurus.tech/docs/getting-started.md) - [Computation (Go)](https://ytsaurus.tech/docs/computation.md) - [Internal State (Go)](https://ytsaurus.tech/docs/internal-state.md) - [Timers](https://ytsaurus.tech/concepts/timers.md) - [Similar example in C++](https://ytsaurus.tech/cpp/examples/url_downloader.md) # Quick start with YTsaurus Flow (YQL) Use YQL over Flow to describe a [pipeline](https://ytsaurus.tech/docs/concepts/glossary.md#pipeline) for streaming data processing as a declarative SQL query — without writing code in [C++](https://ytsaurus.tech/docs/cpp/getting-started.md), [Java](https://ytsaurus.tech/docs/java/getting-started.md), [Python](https://ytsaurus.tech/docs/python/getting-started.md), or [Go](https://ytsaurus.tech/docs/go/getting-started.md). The pipeline runs as a vanilla operation on the selected YTsaurus cluster. {% note warning %} This feature is under active development, and not all planned functionality is available yet. {% endnote %} ## Useful links - [YQL documentation](https://ytsaurus.tech/yql/index.md) — complete reference for YQL syntax - [YQL provider for YT Flow](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yql/providers/ytflow) — source code ## Pragmas {#pragmas} You control a YQL over Flow query with a set of pragmas. For the full list, see the [Pragma reference](https://ytsaurus.tech/docs/en/pragma-reference.md). ## First query {#first-query} Example: row-by-row transformation of a [stream](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) (map). ```yql -- select the Flow engine PRAGMA Engine = "ytflow"; -- cluster for the pipeline’s internal tables PRAGMA Ytflow.Cluster = ""; -- cluster for the vanilla operation PRAGMA Ytflow.RuntimeCluster = ""; -- directory with pipelines PRAGMA Ytflow.PipelineDirectory = "//home/my-project"; -- pipeline name PRAGMA Ytflow.PipelineName = "my-pipeline"; -- number of workers PRAGMA Ytflow.WorkerCount = "1"; -- read from the input queue, transform, write to the output queue INSERT INTO .`//home/my-project/sink_queue` SELECT string_field || "_processed" AS string_field, int64_field, EndsWith(string_field, "bar") AS predicate FROM .`//home/my-project/source_queue` WHERE int64_field > 1; ``` The query starts a pipeline that continuously processes messages from the input queue and writes the results to the output queue. The output table schemas are automatically inferred from the query. For a description of YQL features, see the [Features](https://ytsaurus.tech/docs/en/features.md) section. ## How to run {#how-to-run} {% note info "Prerequisites" %} You need read and write permissions for all directories mentioned in the query, as well as a compute quota on the YTsaurus cluster specified as `Ytflow.RuntimeCluster`. {% endnote %} You can run the query in two ways: **Via the YTsaurus UI**: open the **Queries** tab on the runtime cluster and run the query. **Via the Python client**: ```python from yt.wrapper import YtClient # any production cluster client = YtClient('') # run the query and wait for completion client.run_query( engine='yql', settings=dict( # pass the runtime cluster here cluster='', ), query='', sync=True, ) ``` After the query finishes, a pipeline starts on the cluster and runs continuously. If a pipeline with the same name already exists, it stops and finishes processing all internal streams, then the new version starts. ## Monitoring {#monitoring} To track the running pipeline, you can use: - **Dashboard** — **Flow → Monitoring** tab. - **Controller logs** (worker status, possible issues): ```bash yt --proxy flow show-logs //home/my-project/my-pipeline ``` - **Job logs** — via the vanilla operation, which is available through the link from the `flowPublish` cube in the pipeline graph. ## See also - [Features](https://ytsaurus.tech/docs/en/features.md) - [Basic concepts](https://ytsaurus.tech/docs/concepts/glossary.md) - [Connectors](https://ytsaurus.tech/docs/connectors/about.md) # YQL features in YTsaurus Flow {% note info %} This page describes YQL constructs specific to working with Flow. The general YQL syntax is described in the [YQL documentation](https://ytsaurus.tech/yql/index.md). {% endnote %} ## Row-wise stream transformation (map) {#map} The main construct is `INSERT INTO ... SELECT ... FROM`.
Use it to read a [stream](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) from an ordered dynamic table (a YTsaurus queue), transform it, and write the result to another queue. Example query: ```yql INSERT INTO .`//home/my-project/sink_queue` SELECT string_field || "_processed" AS processed_field, int64_field * 2 AS doubled, EndsWith(string_field, "bar") AS predicate FROM .`//home/my-project/source_queue` WHERE int64_field > 0; ``` You can work with files and UDFs (built-in and user-defined), lambda expressions, and code generation. You can combine multiple `INSERT INTO ... SELECT` operations in a single query. ## Splitting a stream {#multiple-outputs} You can reuse named streams in multiple `SELECT` queries to split a stream into parts that can be processed independently. In following example, input stream rows with a positive `value` go to `positive_queue` queue, and remaining rows go to `other_queue` queue: ```yql $events = SELECT key, value FROM .`//home/my-project/source_queue`; INSERT INTO .`//home/my-project/positive_queue` SELECT * FROM $events WHERE value > 0; INSERT INTO .`//home/my-project/other_queue` SELECT * FROM $events WHERE value <= 0; ``` ## Combining streams (UNION ALL) {#union-all} `UNION ALL` combines multiple streams for further processing as a single stream. In this example, output queue receives rows from both input queues: ```yql $events = SELECT key, value FROM .`//home/my-project/first_queue` UNION ALL SELECT key, value FROM .`//home/my-project/second_queue`; INSERT INTO .`//home/my-project/sink_queue` SELECT key, value + 1 AS value FROM $events; ``` ## Stream join with a dynamic table (lookup join) {#lookup-join} You can join a stream with a sorted dynamic table (a key-value table).
Supported join types for the “stream + KV table” pair are `LEFT`, `LEFT ONLY`, `LEFT SEMI`, and `INNER`.
For the “KV table + stream” pair, the types are symmetric. ```yql $input_stream = SELECT key, value, key || "_before" AS key_before FROM .`//home/my-project/source_queue` WHERE value > 2; $joined_stream = SELECT left_arg.key AS key, left_arg.value AS value, left_arg.key_before, right_arg.kv_value FROM $input_stream AS left_arg INNER JOIN .`//home/my-project/kv_table` AS right_arg USING (key); INSERT INTO .`//home/my-project/sink_queue` SELECT * from $joined_stream WHERE value * 2 <= kv_value; ``` You can chain joins with multiple key-value tables in one query, for example: ```yql INSERT INTO .`//home/my-project/enriched_queue` SELECT events.key AS key, events.value AS value, first_table.value AS first_value, second_table.value AS second_value FROM .`//home/my-project/source_queue` AS events LEFT JOIN .`//home/my-project/first_table` AS first_table ON events.key = first_table.key LEFT JOIN .`//home/my-project/second_table` AS second_table ON events.key = second_table.key; ``` ## Windowed aggregations (GROUP BY ... HOP) {#group-by-hop} Use `GROUP BY ... HOP` to calculate aggregates over fixed-length time intervals (windows). `HOP` arguments specify event time, grid of window start times, window duration, and window-closing delay for late events. Besides `HOP`, you must specify at least one additional grouping key. For general syntax, see [YQL reference](https://ytsaurus.tech/yql/syntax/group_by.md#group-by-hop). A window closes when event-time [watermark](https://ytsaurus.tech/docs/concepts/watermarks.md#event-watermark) passes window end plus window-closing delay.
Events whose timestamps fall within a window but arrive after it has closed (late events) are discarded for that window. Aggregate functions without the `DISTINCT` modifier are supported. Following example calculates the sum of `value` and the row count for each key and window.
Windows start every 15 seconds, last 30 seconds, and close one minute after window end. ```yql INSERT INTO .`//home/my-project/window_totals` SELECT HOP_START() AS window_start, key, COUNT(*) AS record_count, SUM(value) AS value_total FROM .`//home/my-project/source_queue` GROUP BY key, HOP(ts, "PT15S", "PT30S", "PT1M"); ``` ## Writing to a sorted dynamic table {#replace-into} Use `REPLACE INTO ... WITH PRIMARY_KEY = ...` to write to a sorted dynamic table. When writing, a new row fully replaces previous row with same key. `PRIMARY_KEY` hint must be a YSON list of one or more key columns.
If output table already exists, its non-computed key columns and their order must match key columns specified in `PRIMARY_KEY`.
If output table does not exist, it is created before query starts. Output tables with descending key sort order are not supported. Example: ```yql REPLACE INTO .`//home/my-project/window_totals_table` WITH PRIMARY_KEY = '[key;window_start]' SELECT key, window_start, record_count FROM .`//home/my-project/window_totals`; ``` ## Working with multiple clusters {#multiple-clusters} Input and output tables can reside on different YTsaurus clusters.
`Ytflow.Cluster` specifies cluster for the pipeline's internal tables, not a restriction on location of all sources and sinks. In this example, `source_cluster` and `destination_cluster` may be different from `Ytflow.Cluster` value: ```yql INSERT INTO destination_cluster.`//home/my-project/sink_queue` SELECT key, value FROM source_cluster.`//home/my-project/source_queue`; ``` ## See also - [Computation (concept)](https://ytsaurus.tech/docs/concepts/computation.md) - [Quick start (YQL)](https://ytsaurus.tech/docs/en/getting-started.md) # YQL over YTsaurus Flow pragma reference ## Syntax [YQL over Flow](https://ytsaurus.tech/docs/en/features.md) pragma names start with `Ytflow`.
An exception is `Engine`, common pragma for selecting query execution engine: ```yql PRAGMA Ytflow. = ''; PRAGMA Engine = 'ytflow'; ``` {% note warning %} Pass pragma values as quoted strings. Use single `'...'` or double `"..."` quotes. {% endnote %} ## Engine {#engine} | Possible values | Default value | Type | | --- | --- | --- | | `'ytflow'` | — | Static | Selects Flow for query execution.
You must specify this pragma to run query through Flow. ```yql PRAGMA Engine = 'ytflow'; ``` ## Ytflow.Cluster {#cluster} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | YTsaurus cluster name for internal pipeline tables. ```yql PRAGMA Ytflow.Cluster = ''; ``` ## Ytflow.RuntimeCluster {#runtime-cluster} | Possible values | Default value | Type | | --- | --- | --- | | String | Value of `Ytflow.Cluster` | Static | Cluster name for running pipeline's [vanilla operation](https://ytsaurus.tech/user-guide/data-processing/operations/vanilla.md). Controllers and workers run as two task types (`controller` and `worker`) within a single operation; job counts and resources are configured separately for each task type. ```yql PRAGMA Ytflow.RuntimeCluster = ''; ``` ## Ytflow.PipelineDirectory {#pipeline-directory} | Possible values | Default value | Type | | --- | --- | --- | | Path | — | Static | Directory containing pipelines in Cypress on `Ytflow.Cluster`, used together with `Ytflow.PipelineName`. Alternatively, set `Ytflow.PipelinePath` alone. Example Cypress paths start with `//` and have no trailing `/`. ```yql PRAGMA Ytflow.PipelineDirectory = '//home/my-project/pipelines'; ``` ## Ytflow.PipelineName {#pipeline-name} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | Pipeline name, used to create a subdirectory within `Ytflow.PipelineDirectory`, where internal pipeline tables are stored. Alternatively, explicitly set `Ytflow.PipelinePath`. ```yql PRAGMA Ytflow.PipelineName = 'my-pipeline'; ``` ## Ytflow.PipelinePath {#pipeline-path} | Possible values | Default value | Type | | --- | --- | --- | | Path | `{Ytflow.PipelineDirectory}/{Ytflow.PipelineName}` | Static | Full pipeline path in Cypress on `Ytflow.Cluster`; this directory stores internal pipeline tables. Alternatively, set `Ytflow.PipelineDirectory` and `Ytflow.PipelineName` separately. If `Ytflow.PipelinePath` is specified, it takes precedence over those two pragmas. ```yql PRAGMA Ytflow.PipelinePath = '//home/my-project/pipelines/my-pipeline'; ``` ## Ytflow.Auth {#auth} | Possible values | Default value | Type | | --- | --- | --- | | String | `default_ytflow` secret, if supplied by execution environment | Static | Name of a YQL secret in category `ytflow` containing a token for authentication in YTsaurus. This token must grant access to pipeline tables on `Ytflow.Cluster` and permission to start vanilla operation on `Ytflow.RuntimeCluster`. For more information about authentication, see [Authentication](https://ytsaurus.tech/docs/en/authentication.md). ```yql PRAGMA Ytflow.Auth = 'my_ytflow_credentials'; ``` ## Ytflow.WorkerCount {#worker-count} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 1 | Static | Number of worker jobs in vanilla operation on `Ytflow.RuntimeCluster`. ```yql PRAGMA Ytflow.WorkerCount = '5'; ``` ## Ytflow.WorkerCpuLimit {#worker-cpu-limit} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number | 1.0 | Static | Guaranteed CPU allocation per worker job in vanilla operation on `Ytflow.RuntimeCluster`. Actual CPU consumption may be higher if spare resources are available on a job's host. Unit: one CPU core. ```yql PRAGMA Ytflow.WorkerCpuLimit = '10.0'; ``` ## Ytflow.WorkerMemoryLimit {#worker-memory-limit} | Possible values | Default value | Type | | --- | --- | --- | | Size (for example, `20G`) | 1G | Static | Memory limit per worker job in vanilla operation on `Ytflow.RuntimeCluster`. ```yql PRAGMA Ytflow.WorkerMemoryLimit = '20G'; ``` ## Ytflow.EnableComputationPatternResources {#enable-computation-pattern-resources} | Possible values | Default value | Type | | --- | --- | --- | | `true` / `false` | `false` | Static | Enables reuse of computation patterns across graphs in one worker. ```yql PRAGMA Ytflow.EnableComputationPatternResources = 'true'; ``` ## Ytflow.ControllerCount {#controller-count} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 1 | Static | Number of controller jobs in vanilla operation on `Ytflow.RuntimeCluster`. One [controller](https://ytsaurus.tech/docs/concepts/glossary.md#controller) becomes leader; others remain on standby.
With a value of `1`, there is no standby controller, and pipeline management is temporarily unavailable while this sole controller restarts. ```yql PRAGMA Ytflow.ControllerCount = '3'; ``` ## Ytflow.ControllerCpuLimit {#controller-cpu-limit} | Possible values | Default value | Type | | --- | --- | --- | | Floating-point number | 1.0 | Static | Guaranteed CPU allocation per controller job in vanilla operation on `Ytflow.RuntimeCluster`. Actual CPU consumption may be higher if spare resources are available on job's host. Unit: one CPU core. ```yql PRAGMA Ytflow.ControllerCpuLimit = '3.0'; ``` ## Ytflow.ControllerMemoryLimit {#controller-memory-limit} | Possible values | Default value | Type | | --- | --- | --- | | Size (for example, `5G`) | 1G | Static | Memory limit per controller job in vanilla operation on `Ytflow.RuntimeCluster`. ```yql PRAGMA Ytflow.ControllerMemoryLimit = '5G'; ``` ## Ytflow.GracefulUpdate {#graceful-update} | Possible values | Default value | Type | | --- | --- | --- | | `true` / `false` | `true` | Static | Pipeline update mode: * `true` — gracefully stop and finish processing internal streams ([`stop`](https://ytsaurus.tech/docs/concepts/glossary.md#start-stop-pause-pipeline)) * `false` — pause immediately ([`pause`](https://ytsaurus.tech/docs/concepts/glossary.md#start-stop-pause-pipeline)). Once pipeline reaches its target state, its previous vanilla operation on `Ytflow.RuntimeCluster` is aborted and new version starts.
For restrictions on updating without draining, see [Hotfix](https://ytsaurus.tech/docs/devops/vanilla/releases.md#hotfix); for stop commands, see [CLI reference](https://ytsaurus.tech/docs/tools/cli.md). ```yql PRAGMA Ytflow.GracefulUpdate = 'true'; ``` ## Ytflow.UpdateTimeout {#update-timeout} | Possible values | Default value | Type | | --- | --- | --- | | Time interval (`1d`, `5s`, `2w`) | 600s | Static | Timeout for waiting for previous pipeline version to stop or pause during an update. If pipeline does not reach its target state within this time, query execution fails. This update does not stop its previous vanilla operation on `Ytflow.RuntimeCluster` or start new version.
Any stop or pause command already sent is not canceled. ```yql PRAGMA Ytflow.UpdateTimeout = '600s'; ``` ## Ytflow.TabletCellBundle {#tablet-cell-bundle} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | [Tablet cell bundle](https://ytsaurus.tech/user-guide/dynamic-tables/concepts.md#tablet_cell_bundles) (`tablet_cell_bundle`) for newly created tables. ```yql PRAGMA Ytflow.TabletCellBundle = 'my-bundle'; ``` ## Ytflow.Account {#account} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | [Account](https://ytsaurus.tech/user-guide/storage/accounts.md) for newly created tables. ```yql PRAGMA Ytflow.Account = 'my-account'; ``` ## Ytflow.PrimaryMedium {#primary-medium} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | [Primary medium](https://ytsaurus.tech/user-guide/storage/media.md) (`primary_medium`) for newly created tables. ```yql PRAGMA Ytflow.PrimaryMedium = 'default'; ``` ## Ytflow.Pool {#pool} | Possible values | Default value | Type | | --- | --- | --- | | String | — | Static | [Compute pool](https://ytsaurus.tech/user-guide/data-processing/scheduler/scheduler-and-pools.md) for pipeline's vanilla operation on `Ytflow.RuntimeCluster`. ```yql PRAGMA Ytflow.Pool = 'my-pool'; ``` ## Ytflow.YtPartitionCount {#yt-partition-count} | Possible values | Default value | Type | | --- | --- | --- | | Positive number | 1 | Static | Number of partitions in newly created output queues in YTsaurus. ```yql PRAGMA Ytflow.YtPartitionCount = '4'; ``` ## Ytflow.YtTtl {#yt-ttl} | Possible values | Default value | Type | | --- | --- | --- | | Time interval (`1d`, `5s`, `2w`) | — | Static | Retention period for rows in newly created output ordered dynamic tables (queues) in YTsaurus. Sets `max_data_ttl` and also sets: * `min_data_versions = 0` * `max_data_versions = 1` * `min_data_ttl = 0` * `auto_compaction_period` to half its TTL For more information, see [Automatic removal of old rows](https://ytsaurus.tech/user-guide/dynamic-tables/ordered-dynamic-tables.md#remove_old_data). ```yql PRAGMA Ytflow.YtTtl = '7d'; ``` ## Ytflow.YtConsumerVital {#yt-consumer-vital} | Possible values | Default value | Type | | --- | --- | --- | | `true` / `false` | `false` | Static | `vital` flag used when registering consumer with input queues in YTsaurus. When [automatic consumer-based queue trimming](https://ytsaurus.tech/user-guide/dynamic-tables/queues.md#automatic_trimming) is enabled, queues respect vital consumer offsets and do not delete rows these consumers have not read yet.
Non-vital consumer offsets do not prevent deletion of rows these consumers have not processed. ```yql PRAGMA Ytflow.YtConsumerVital = 'true'; ``` ## Ytflow.YtConsumerDirectory {#yt-consumer-directory} | Possible values | Default value | Type | | --- | --- | --- | | Path | `{Ytflow.PipelinePath}/yql_ytflow/consumers` | Static | YTsaurus consumer directory. ```yql PRAGMA Ytflow.YtConsumerDirectory = '//home/my-project/consumers'; ``` ## Ytflow.YtConsumerName {#yt-consumer-name} | Possible values | Default value | Type | | --- | --- | --- | | String | `default_consumer` | Static | YTsaurus consumer name. ```yql PRAGMA Ytflow.YtConsumerName = 'my-consumer'; ``` ## Ytflow.YtProducerDirectory {#yt-producer-directory} | Possible values | Default value | Type | | --- | --- | --- | | Path | `{Ytflow.PipelinePath}/yql_ytflow/producers` | Static | YTsaurus producer directory. ```yql PRAGMA Ytflow.YtProducerDirectory = '//home/my-project/producers'; ``` ## Ytflow.YtProducerName {#yt-producer-name} | Possible values | Default value | Type | | --- | --- | --- | | String | `default_producer` | Static | YTsaurus producer name. ```yql PRAGMA Ytflow.YtProducerName = 'my-producer'; ``` # Authentication in YQL over YTsaurus Flow To authenticate with YTsaurus as someone other than your query's submitting user, or with external providers, add token through YTsaurus UI. ## Uploading a token to Cypress {#upload-token} Before adding a token in UI settings, upload file containing it to Cypress. Example command: ```bash cat /path/to/local/secret | yt --proxy write-file //path/to/secret ``` {% note warning "Token file security" %} You are responsible for securing your token file using ACLs.
Only you should have access to this file — make sure no one else has permission to read this node. {% endnote %} ## Adding a token {#add-token} To add a token: 1. Open **Settings** using its gear icon at bottom left. 2. Select **Queries** in your settings sidebar. 3. Under **Request tokens**, click **Add Query Token** and specify Cypress path to your token file. 4. Attach tokens to your query: open **Tokens** using its key icon in your top toolbar and select tokens to attach. ## Token names {#token-names} To use a token by default without specifying additional pragmas, choose its name by provider: - `default_ytflow` — for access to internal YTsaurus Flow tables. - `default_yt` — for access to YTsaurus. To select a custom token for a query, use its provider's `{provider}.Auth` pragma. # YTsaurus Flow Connectors A [connector](https://ytsaurus.tech/docs/concepts/glossary.md#connector) is a component that links a pipeline to YTsaurus objects (a queue, a table, and so on). Each connector provides a [source](https://ytsaurus.tech/docs/concepts/glossary.md#source) for reading messages and/or a [sink](https://ytsaurus.tech/docs/concepts/glossary.md#sink) for writing. {% note info %} The connectors used by a pipeline directly affect the message processing guarantees it provides. Before you choose connectors, review the [Processing Guarantees](https://ytsaurus.tech/docs/concepts/guarantees.md) section. {% endnote %} Integrations with external (non-YTsaurus) systems are described in the [Extensions](https://ytsaurus.tech/docs/extensions/about.md) section. ## List of connectors {#list} #| || **Connector** | **Has source** | **Has sink** | **Description** || || [Queue](https://ytsaurus.tech/docs/en/queue.md) | ✓ | ✓ | Reading from and writing to an ordered dynamic table using the [Queue API](https://ytsaurus.tech/user-guide/dynamic-tables/queues.md) || || [Static Table](https://ytsaurus.tech/docs/en/static-table.md) | ✓ | ✓ | Reading from static tables: a fixed set or an unlimited sequence from a directory. Writing a continuous sequence of static tables in message arrival order || || Random | ✓ | 𐄂 | Reading random data generated on the fly. Used for testing || || [Service Log](https://ytsaurus.tech/docs/en/servicelog.md) | ✓ | 𐄂 | Generating a service log based on an external [state](https://ytsaurus.tech/docs/concepts/glossary.md#state) table. A service log is the generation of all keys from a dynamic table at a specified frequency; essentially, it’s a way to re-scan all states once per the configured time || || [Sorted Dynamic Table](https://ytsaurus.tech/docs/en/sorted-dynamic-table.md) | 𐄂 | ✓ | Writing to a sorted dynamic table || |# ## Changing the source {#source-change} The source identifies its [partitions](https://ytsaurus.tech/docs/concepts/glossary.md#partition) by parameters that describe the physical source; the specific set of these parameters is listed in the documentation for the corresponding connector. These parameters are part of the partition key, so when you change them, the source describes a different set of partitions. When you change the source, the partitions from the old source disappear from the set and are **completed** (`Completing` ⇒ `Completed`): the associated [state](https://ytsaurus.tech/docs/concepts/glossary.md#state), including stored offsets, is deleted and not saved with the expectation that the same partition will return (as during an interrupt during repartitioning). The partitions for the new source are created from scratch. {% note warning %} The parameters that identify the source are part of the static [`Spec`](https://ytsaurus.tech/docs/concepts/spec.md), so you can change them only when the pipeline is stopped. The new source is read independently of what the old source has already processed: if it contains the same data, the data will be written to the output streams again. {% endnote %} # QYT Connector in YTsaurus Flow This connector works with YTsaurus [queues](https://ytsaurus.tech/user-guide/dynamic-tables/queues.md) (QYT). You can find the connector code [here](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/connectors/queue). ## Reading from a Queue When you read from a queue using the simplest method, messages arrive in [`Computation`](https://ytsaurus.tech/docs/concepts/glossary.md#stream-and-computation) with the same schema as the queue table rows. By default, the message’s `event_time` and `system_time` are calculated based on the `$timestamp` column, which contains the YT timestamp of the row record (for more details, see the [documentation for ordered dynamic tables](https://ytsaurus.tech/user-guide/dynamic-tables/queues.md)). But if the queue has a column with metadata (see the static spec parameters for the source), the message’s `event_time` can be taken from the corresponding field. You can also get stream watermark information from the metadata. The [source](https://ytsaurus.tech/docs/concepts/glossary.md#source) class is `NYT::NFlow::TQueueSource`. Below are the source settings. {% note info %} The source is defined by the cluster and the queue path (`queue_path`). If you change them, the partitions are recreated and read again — see [Changing the Source](https://ytsaurus.tech/docs/en/about.md#source-change). {% endnote %} ### Managing Consumer Lag When you register a consumer for a queue, the consumer lag is set to the last stored data in the queue. So, after registration, if the policy for deleting old rows in the queue is 1 day, the lag will also be 1 day. This behavior can lead to the following scenario: when you roll out a release, the data lag will be 1 day, and message processing will pause until the lag is processed. You can avoid this in several ways: #### 1. Using the Standard YT CLI Use the standard YT CLI to set the consumer offsets to the desired values for each partition. This isn’t very convenient for the first read of a topic, because you need to know the value of the latest offset and set it for each [partition](https://ytsaurus.tech/docs/concepts/glossary.md#partition). Here’s an example command: ```bash yt advance-queue-consumer --proxy //home/service/stable/consumer ://home/source_service/Data/queue --partition-index 0 --new-offset 486482013 ``` For more advanced options, check the YT documentation or use the `-h` parameter. ### Static spec Source: [yt/yt/flow/library/cpp/common/registry-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/registry-inl.h) #| || **Parameter** | **Description** || || `finite` | **Type**: `bool` **Default value**: `false` Treat the source as finite: transition the stream to the completed state after reading all available messages. How the end of the data is determined depends on the connector. || || `queue_path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the queue including the cluster. || || `consumer_path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the queue consumer with the cluster specified. || || `try_parse_flow_queue_meta` | **Type**: `bool` **Default value**: `true` Whether to retrieve and parse meta from the queue (the meta contains watermarks provided by the writer to the queue). || || `flow_queue_meta_column` | **Type**: `std::string` **Default value**: `flow_queue_meta` Column from which to retrieve meta. || || `ignore_malformed_flow_queue_meta` | **Type**: `bool` **Default value**: `false` Whether to ignore invalid meta or fail. || || `partition_filter` | **Type**: `std::optional>>` || |# {% cut "**Additional parameters**" %} #| || `update_info_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `15s` Period for refreshing the source's auxiliary partition information. A connector may use this tick for status requests and read-session liveness checks. || || `byte_size_alpha` | **Type**: `double` **Default value**: `0.05` Exponential smoothing coefficient for the average byte total and message count per offset: larger values make the estimate react to new data faster. || || `update_partition_count_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` How often the controller should update the partition count in the queue (the controller changes the computation partition count according to the queue partition count). || || `update_partition_count_retry_min_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Initial delay before retrying a failed partition count update, subject to jitter. The value is capped by `update_partition_count_period`; subsequent delays increase exponentially. || |# {% endcut %} ### Dynamic spec Source: [yt/yt/flow/library/cpp/common/registry-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/registry-inl.h) #| || **Parameter** | **Description** || || `pull_queue_timeout` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` Timeout for a read request from the queue. || |# {% cut "**Additional parameters**" %} #| || `unavailable_threshold` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `5m` How long the source must be continuously unavailable for the partition to count as stably unavailable. Only time during which the job was running and seeing the failure counts: a gap between restarts is not charged, and any successful answer from the source resets the total. || |# {% endcut %} ## Writing to a Queue To write messages to a queue in a [sink](https://ytsaurus.tech/docs/concepts/glossary.md#sink), send messages with the same schema as the table where you plan to write. You can also configure writing metadata to a special column to pass information about the message’s `event_time` and the stream watermark to queue readers. There are two sink options: synchronous (`NYT::NFlow::TSyncQueueSink`) and asynchronous (`NYT::NFlow::TAsyncQueueSink`). Writing to a synchronous sink happens in the main transaction of the [epoch](https://ytsaurus.tech/docs/concepts/glossary.md#epoch). This is efficient, but you can only write to a queue on the main processing cluster. Writing to an asynchronous sink happens after the main epoch transaction, using messages stored in output messages. This is more resource-intensive, but you can write to a queue on any cluster. ### Parameters for synchronous sink specs #### Static spec Source: [yt/yt/flow/library/cpp/common/registry-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/registry-inl.h) #| || **Parameter** | **Description** || || `queue_path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the queue including the cluster. || || `write_flow_queue_meta` | **Type**: `bool` **Default value**: `false` Whether to write meta to the queue (watermarks are written in meta). || || `flow_queue_meta_column` | **Type**: `std::string` **Default value**: `flow_queue_meta` Column to which to write meta. || || `tablet_index_expression` | **Type**: `std::optional` Verbatim routing mode. A QL expression (builtins only, e.g. `farm_hash`) over the message columns whose value is written to the system column `$tablet_index` verbatim. Mutually exclusive with `tablet_index_routing_hash_expression`; when neither expression is set, routing is off (`$tablet_index` is not written and the tablet is chosen by the driver). Sharded queues are inconvenient to operate (object→tablet affinity is tightly coupled and a reshard remaps keys), so enable routing only when genuinely needed. Routing is supported only on sync queue sinks; async sinks reject the routing params at spec load (YTFLOW-766). || || `tablet_index_routing_hash_expression` | **Type**: `std::optional` Hash routing mode. A QL expression (builtins only, e.g. `farm_hash`) yielding a `uint64` hash that is reduced to a tablet index by `tablet_index_routing_hash_policy` over `tablet_count`. Mutually exclusive with `tablet_index_expression`. Supported only on sync queue sinks; async sinks reject the routing params (YTFLOW-766). || || `tablet_index_routing_hash_policy` | **Type**: `std::optional<`[NYT::NFlow::EQueueTabletIndexRoutingHashPolicy](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EQueueTabletIndexRoutingHashPolicy)`>` The policy for reducing the `tablet_index_routing_hash_expression` hash to a tablet index. Required together with it. `range` — contiguous equal-width hash ranges (rangeSize = 2^64 / tablet_count), recommended: a consumer range-partitioned by the same key reads only its own tablet. `modulo` — `hash % tablet_count`; discouraged: Flow computations are range-partitioned by key, so a modulo-partitioned queue forces every reader to read every tablet (a full mesh on read). || || `tablet_count` | **Type**: `std::optional` The number of tablets used to reduce `tablet_index_routing_hash_expression` to a tablet index. Optional. When unset, the sink periodically re-reads the target queue's `@tablet_count` (from the master cache) and follows a reshard without a restart. When set explicitly, the value is fixed (no queue read) and a reshard is reflected only by changing the config. || || `column_filter` | **Type**: `std::optional>` Which columns from the message to write to the queue (default — all). || |# {% cut "**Additional parameters**" %} #| || `update_partition_count_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` How often the controller should update the partition count in the queue (the controller changes the computation partition count according to the queue partition count). || || `update_partition_count_retry_min_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Initial delay before retrying a failed partition count update, subject to jitter. The value is capped by `update_partition_count_period`; subsequent delays increase exponentially. || |# {% endcut %} #### Dynamic spec Source: [yt/yt/flow/library/cpp/common/registry-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/registry-inl.h) **This structure has no main parameters.** {% cut "**Additional parameters**" %} #| || `flow_queue_meta_heartbeat_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` How often the controller will write heartbeats with meta containing watermarks to all queue partitions. || |# {% endcut %} ### Parameters for asynchronous sink specs #### Static spec Source: [yt/yt/flow/library/cpp/common/registry-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/registry-inl.h) #| || **Parameter** | **Description** || || `queue_path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the queue including the cluster. || || `write_flow_queue_meta` | **Type**: `bool` **Default value**: `false` Whether to write meta to the queue (watermarks are written in meta). || || `flow_queue_meta_column` | **Type**: `std::string` **Default value**: `flow_queue_meta` Column to which to write meta. || || `producer_path` | **Type**: `NYT::NYPath::TRichYPath` **Required parameter** Path to the queue producer with the cluster specified. || || `require_sync_replica` | **Type**: `bool` **Default value**: `true` The eponymous parameter when writing to the queue. Whether write to the queue without synchronous replicas is allowed. || || `tablet_index_expression` | **Type**: `std::optional` Verbatim routing mode. A QL expression (builtins only, e.g. `farm_hash`) over the message columns whose value is written to the system column `$tablet_index` verbatim. Mutually exclusive with `tablet_index_routing_hash_expression`; when neither expression is set, routing is off (`$tablet_index` is not written and the tablet is chosen by the driver). Sharded queues are inconvenient to operate (object→tablet affinity is tightly coupled and a reshard remaps keys), so enable routing only when genuinely needed. Routing is supported only on sync queue sinks; async sinks reject the routing params at spec load (YTFLOW-766). || || `tablet_index_routing_hash_expression` | **Type**: `std::optional` Hash routing mode. A QL expression (builtins only, e.g. `farm_hash`) yielding a `uint64` hash that is reduced to a tablet index by `tablet_index_routing_hash_policy` over `tablet_count`. Mutually exclusive with `tablet_index_expression`. Supported only on sync queue sinks; async sinks reject the routing params (YTFLOW-766). || || `tablet_index_routing_hash_policy` | **Type**: `std::optional<`[NYT::NFlow::EQueueTabletIndexRoutingHashPolicy](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_EQueueTabletIndexRoutingHashPolicy)`>` The policy for reducing the `tablet_index_routing_hash_expression` hash to a tablet index. Required together with it. `range` — contiguous equal-width hash ranges (rangeSize = 2^64 / tablet_count), recommended: a consumer range-partitioned by the same key reads only its own tablet. `modulo` — `hash % tablet_count`; discouraged: Flow computations are range-partitioned by key, so a modulo-partitioned queue forces every reader to read every tablet (a full mesh on read). || || `tablet_count` | **Type**: `std::optional` The number of tablets used to reduce `tablet_index_routing_hash_expression` to a tablet index. Optional. When unset, the sink periodically re-reads the target queue's `@tablet_count` (from the master cache) and follows a reshard without a restart. When set explicitly, the value is fixed (no queue read) and a reshard is reflected only by changing the config. || || `at_most_once_strategy` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TAtMostOnceStrategyParameters](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TAtMostOnceStrategyParameters)`>` Optional at-most-once delivery strategy. Connector support varies; check the connector documentation before enabling it. || || `column_filter` | **Type**: `std::optional>` Which columns from the message to write to the queue (default — all). || |# {% cut "**Additional parameters**" %} #| || `update_partition_count_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1m` How often the controller should update the partition count in the queue (the controller changes the computation partition count according to the queue partition count). || || `update_partition_count_retry_min_backoff` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` Initial delay before retrying a failed partition count update, subject to jitter. The value is capped by `update_partition_count_period`; subsequent delays increase exponentially. || |# {% endcut %} #### Dynamic spec Source: [yt/yt/flow/library/cpp/common/registry-inl.h](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/registry-inl.h) #| || **Parameter** | **Description** || || `at_most_once_strategy` | **Type**: `NYT::TIntrusivePtr<`[NYT::NFlow::TAtMostOnceStrategyDynamicParameters](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#NYT_NFlow_TAtMostOnceStrategyDynamicParameters)`>` **Default value**: `{}` Dynamic parameters for `at_most_once_strategy`. Connector support varies; check the connector documentation before configuring them. || |# {% cut "**Additional parameters**" %} #| || `flow_queue_meta_heartbeat_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `10s` How often the controller will write heartbeats with meta containing watermarks to all queue partitions. || || `write_period` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `100ms` || || `max_rows_per_write` | **Type**: `long` **Default value**: `1000` || || `max_bytes_per_write` | **Type**: `long` **Default value**: `1048576` || || `backoff_duration` | **Type**: [TDuration](https://ytsaurus.tech/docs/generated_docs/all_yson_structs#TDuration) **Default value**: `1s` || |# {% endcut %} ## See also - [List of Connectors](https://ytsaurus.tech/docs/en/about.md) - [Spec and DynamicSpec](https://ytsaurus.tech/docs/concepts/spec.md)