Turning a pipeline into Python
An exported pipeline file says exactly what every node does. This page tells a person, or an AI assistant, how to write it as PySpark or pandas code.
Using an AI assistant
Export the pipeline, then give the file to your AI assistant with this request. An assistant that can read web pages fetches this guide itself; otherwise paste the guide's text file, https://www.trinium.io/pipeline-to-python.txt, along with the file.
I am giving you a Trinium pipeline file (JSON). First read https://www.trinium.io/pipeline-to-python.txt, which explains the file and every node. Then write this pipeline as one Python script using PySpark (or: pandas). - One function per node, named after the node, called in the order the wires allow. - Use the file's connection and file names as placeholders I will fill in. - Follow the guide's rules on names, NULLs, integer division, row order and dates. - Where the code cannot do exactly what a node does, say so in a comment there. <paste the pipeline file here>
Treat the answer as a draft: run it on the same data as the pipeline and compare the results before you rely on it (see Checking the result).
The file
A pipeline file is one JSON object. Trinium writes it; import reads files Trinium wrote, so do not write one by hand.
| Key | What it holds |
|---|---|
trinium_pipeline | The file format, currently 1. |
about | One line saying what the file is and where this guide is. |
exported_from | The Trinium version, the time, and source: draft or a version such as v7. |
pipeline | The pipeline's name and description. |
manifest | The pipeline itself: nodes and edges. |
parameters | Values asked for when the pipeline runs: each has a name, a type and a default. |
connections | One entry per connection the nodes use: a ref (such as conn-1), its name and its type. |
files | Names of the uploaded files the nodes read. |
removed_values | Values left blank because they looked like passwords or keys, with the node and the setting. |
A node has an id, a node_type (the headings below), a label (its name on the canvas), its config (its settings) and a position. Nodes of type documentation and container are notes and boxes drawn on the canvas: they do nothing, so skip them.
An edge is a wire: from_node and from_port to to_node and to_port. A port is a named anchor, such as a Filter's true and false; each node below lists its ports. Several wires may feed one input port: their rows are combined. An edge from a port named files carries a list of files, not rows, to a File Action.
A connection is named by its ref in a node's connection_id. The file never holds a connection's address or credentials: put your own in their place.
The rules that change answers
The same code can give a different answer in Trinium and in Python. Keep these in your code:
- Order. Run each node after the nodes wired into it. Every node can start as soon as its inputs have rows; only the result matters.
- Column names are case-exact.
Amountandamountare two columns. Spark ignores case unless you setspark.sql.caseSensitiveto true; pandas is case-exact already. - Expressions are SQL. Filter, Formula, Route and the other nodes that take a rule hold a SQL expression.
[Order Date]in square brackets names a column, as does"Order Date"in double quotes; single quotes are text. In Spark write the column in backticks insideF.expr("...")orspark.sql; in pandas usedf["Order Date"]. - Integer division stays an integer.
7 / 2is3in Trinium when both sides are integers. Spark returns3.5(usedivor cast); in pandas use//. - NULL is not a value. A comparison with NULL is not true, so a Filter sends such a row to
false. A join never matches a NULL key. pandas treats missing keys as equal when merging, so drop or fill them first. - Dates are ISO. A date is
YYYY-MM-DDand a date-timeYYYY-MM-DD HH:MM:SS. A date in an expression is writtenDATE '2026-01-31'. - Row order matters for a few nodes. Unique keeps the first row it saw, Record ID numbers rows as they arrive, Sample takes the first rows, and Multi-Row Formula looks at the previous row. Spark has no row order across partitions: number the rows where you read them, and sort by that number where order matters.
- Text comparisons in Filter and Route ignore case in basic mode unless
case_sensitiveis true. - A failed type conversion is NULL, not an error, in Manage Columns. Spark 4 (Databricks Runtime 17 and later) raises an error on a bad cast unless you use
try_cast.
Parameters and date tokens
A setting can hold ${name}, replaced by the parameter's value when the pipeline runs (its default when none is given). These built-in tokens work in any setting:
| Token | Becomes |
|---|---|
${date} | The day the run was queued, YYYY-MM-DD, in the Trinium instance's time zone (not UTC). |
${date-1}, ${date+7} | That day minus or plus days. |
${date:%Y%m%d} | That day in a strftime format. |
${datetime} | The moment the run was queued, in UTC. |
${run_id} | The run's id. |
In a REST node's path, body and headers, ${column} is instead the current row's value of that column.
Expressions in Python
Most SQL in a pipeline file runs unchanged in Spark through F.expr or spark.sql. These functions differ:
| In the file | PySpark | pandas |
|---|---|---|
trunc(x) on a number | CAST(x AS BIGINT) (Spark's trunc is for dates) | np.trunc(x) |
strpos(s, 'x') | instr(s, 'x') | s.str.find("x") + 1 |
to_char(d, '%Y-%m-%d') | date_format(d, 'yyyy-MM-dd') | d.dt.strftime("%Y-%m-%d") |
to_timestamp(s, '%d/%m/%Y %H:%M') | to_timestamp(s, 'dd/MM/yyyy HH:mm') | pd.to_datetime(s, format="%d/%m/%Y %H:%M") |
regexp_match(s, 'p') | regexp_extract(s, 'p', 1) | s.str.extract("p") |
date_bin(INTERVAL '15 minutes', t) | window(t, '15 minutes').start | t.dt.floor("15min") |
starts_with(s, 'x') | startswith(s, 'x') | s.str.startswith("x") |
nvl(a, b), coalesce(a, b) | the same | a.fillna(b) |
Regular expressions in a pipeline have no look-around and no back-references, so they work unchanged in Python's re and in Spark; only a replacement's group reference changes (\1 in Python, $1 in Spark).
What has no Python equivalent
These belong to the platform that runs the code, not to the pipeline's logic:
- Bookmarks: reading only new files or new rows since the last successful run. Keep that list or high-water mark yourself.
- Schedules, file-arrival triggers, Flows and alerts: your scheduler's job.
- Staging and Production: run the code against each environment's connections.
- Test runs: a Trinium Test run reads 50 rows and writes nothing.
Every node
Each node lists its ports (the names its wires use), the settings in its config, and how to write it in PySpark and in pandas. In the code, df is the node's input, F is pyspark.sql.functions and W is pyspark.sql.Window.
Input
Apache Iceberg input: iceberg_input
Reads an Apache Iceberg table through its catalog (REST, AWS Glue or S3 Tables), optionally at a past snapshot.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the catalog and storage credentials |
namespace | the namespace (database) |
table | the table |
snapshot_id | read this snapshot |
as_of_timestamp | or the snapshot current at this time |
columns | only these columns |
PySpark
df = spark.read.format("iceberg").option("snapshot-id", sid).load(f"{catalog}.{namespace}.{table}")pandas
df = pyiceberg.catalog.load_catalog("c", **props).load_table(f"{namespace}.{table}").scan().to_pandas()Athena: athena_input
Reads a table, or a query's result, from Amazon Athena.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
query | or a SELECT in Athena's dialect |
batch_size | rows per batch |
PySpark
df = spark.table(f"{database}.{table}") # in Glue, the same table through the Glue catalogpandas
df = awswrangler.athena.read_sql_query(query, database=db)
Azure Blob Files: azure_blob_input
Reads one blob, or every matching blob under a folder, from Azure Blob Storage.
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the account, container and sign-in |
mode | key or prefix |
key | the blob, in key mode |
prefix | the folder, in prefix mode |
pattern | a file-name pattern |
recursive | include sub-folders |
format | csv, json, parquet, excel or xml |
options | the format's reader settings |
partition_columns | read key=value folders as text columns |
partition_filter | a SQL condition that prunes folders before download |
bookmark | read only files no successful run has read |
fan_out | one run per file |
PySpark
df = spark.read.parquet(f"abfss://{container}@{account}.dfs.core.windows.net/{prefix}")pandas
df = pd.read_parquet(f"abfs://{container}/{prefix}", storage_options={...}) # needs adlfsBigQuery input: bigquery_input
Reads a table, or a query's result, from Google BigQuery.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | dataset.table |
query | or a SELECT in BigQuery SQL |
batch_size | rows per batch |
PySpark
df = spark.read.format("bigquery").option("query", query).option("viewsEnabled", True).load()pandas
df = bigquery.Client(project=p).query(query).to_dataframe()
ClickHouse input: clickhouse_input
Reads a table, or a query's result, from ClickHouse.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
query | or a SELECT in ClickHouse SQL |
bookmark | incremental reads by a column |
batch_size | rows per batch |
PySpark
df = spark.read.format("jdbc").option("url", "jdbc:clickhouse://host:8123/db").option("query", query).load()pandas
df = clickhouse_connect.get_client(**creds).query_df(query)
CSV Files: csv_input
Reads CSV files uploaded to the pipeline's file library, as one table.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
files | file names in the pipeline's file library (not paths): point them at your own copies |
has_header | the first row holds column names (default true) |
delimiter | one character, default a comma |
quote | the quote character, default a double quote |
skip_rows | lines to drop above the header |
encoding | a text encoding label such as windows-1252; absent means UTF-8 |
infer_rows | how many rows decide each column's type; 0 scans the whole file |
columns | an explicit list of { name, data_type } that overrides inference |
source_file_column | if set, a column holding each row's file name |
on_schema_change | reconcile (default), skip or fail: what to do with a file whose columns differ from the first readable file's, or that cannot be read |
PySpark
df = spark.read.csv(paths, header=True, inferSchema=True, sep=",")
pandas
df = pd.concat([pd.read_csv(p) for p in paths], ignore_index=True)
Trinium matches the columns of later files to the first file that can be read, BY NAME: a missing column is NULL, an extra one is dropped, a file sharing no column, or one that cannot be read, is skipped. Spark's CSV reader matches by POSITION, so read each file and align by name (unionByName with allowMissingColumns=True) to keep that rule.
Databricks input: databricks_input
Reads a table, or a query's result, from a Databricks SQL warehouse.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | catalog.schema.table |
query | or a SELECT in Databricks SQL |
PySpark
df = spark.sql(query) # inside Databricks
pandas
with databricks.sql.connect(**creds) as c: df = pd.read_sql(query, c)
Excel Files: excel_input
Reads sheets from uploaded Excel workbooks.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
files | workbook names in the pipeline's file library |
sheet_mode | first, name (one named sheet) or all (every sheet, stacked) |
sheet_name | the sheet, in name mode |
sheet_name_column | in all mode, a column holding each row's sheet name |
has_header | the first row holds column names |
skip_rows | rows to drop above the header |
range | an optional cell range such as A1:F200 |
on_schema_change | reconcile (default), skip or fail |
PySpark
pdf = pd.read_excel(path, sheet_name=sheet, skiprows=skip_rows); df = spark.createDataFrame(pdf)
pandas
df = pd.read_excel(path, sheet_name=sheet_name or 0, skiprows=skip_rows) # sheet_name=None reads all
File from URL: url_input
Downloads one file from a web link and reads it.
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
url | the link; may hold date tokens |
connection_id | optional REST connection ref whose credentials the request carries |
format | csv, json, parquet, excel or xml |
options | the format's reader settings |
PySpark
pdf = pd.read_csv(url); df = spark.createDataFrame(pdf) # or download with requests first
pandas
df = pd.read_csv(io.BytesIO(requests.get(url, timeout=60).content))
GCS Files: gcs_input
Reads one object, or every matching object under a folder, from Google Cloud Storage.
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the bucket and service account |
mode | key or prefix |
key | the object, in key mode |
prefix | the folder, in prefix mode |
pattern | a file-name pattern |
recursive | include sub-folders |
format | csv, json, parquet, excel or xml |
options | the format's reader settings |
partition_columns | read key=value folders as text columns |
partition_filter | a SQL condition that prunes folders before download |
bookmark | read only files no successful run has read |
fan_out | one run per file |
PySpark
df = spark.read.parquet(f"gs://{bucket}/{prefix}")pandas
df = pd.read_parquet(f"gs://{bucket}/{prefix}") # needs gcsfsGoogle Sheets input: google_sheets_input
Reads a tab of a Google Sheets spreadsheet.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a Google Sheets connection ref |
spreadsheet | the spreadsheet id or link |
tab | the tab |
range | an optional A1 range |
has_header | the first row holds column names |
read_all_as_text | read every column as text |
PySpark
pdf = pd.DataFrame(gspread.service_account().open_by_key(sid).worksheet(tab).get_all_records()); df = spark.createDataFrame(pdf)
pandas
df = pd.DataFrame(gspread.service_account().open_by_key(sid).worksheet(tab).get_all_records())
A code with a leading zero, such as 007, stays text.
HubSpot: hubspot_input
Reads HubSpot CRM objects (contacts, companies, deals and so on).
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a HubSpot connection ref |
object | the CRM object |
properties | the properties read |
columns | declared columns |
max_records | stop after this many |
PySpark
# page through GET /crm/v3/objects/{object}?properties=...&after=... with requests, then spark.createDataFramepandas
df = pd.json_normalize(results) # results gathered from /crm/v3/objects/{object}Jira: jira_input
Reads Jira issues with a JQL query.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a Jira connection ref |
jql | the JQL query |
fields | the fields read |
columns | declared columns |
max_records | stop after this many |
PySpark
# page through /rest/api/3/search?jql=...&startAt=... with requests, then spark.createDataFrame
pandas
df = pd.json_normalize(issues) # issues gathered from /rest/api/3/search
JSON Files: json_input
Reads JSON files (an array of objects, or one object per line) uploaded to the pipeline's file library.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
files | file names in the pipeline's file library |
infer_rows | how many records decide each column's type; 0 scans everything |
source_file_column | if set, a column holding each row's file name |
on_schema_change | reconcile (default), skip or fail, as for CSV |
PySpark
df = spark.read.json(paths, multiLine=True) # multiLine=False for one object per line
pandas
df = pd.concat([pd.read_json(p) for p in paths], ignore_index=True) # lines=True for NDJSON
List Files: list_files
Lists the files in a folder, one row per file, without reading them: path, name, extension, parent, size_bytes, modified, change_token.
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
connection_id | a connection ref to S3, Azure Blob, Google Cloud Storage, SFTP or a network share |
prefix | the folder |
pattern | a file-name pattern |
recursive | include sub-folders |
max_files | refuse the run above this many files |
older_than_days | only files older than this many days |
PySpark
rows = [(o["Key"], o["Size"], o["LastModified"]) for o in s3.list_objects_v2(Bucket=b, Prefix=p)["Contents"]]; df = spark.createDataFrame(rows, ["path", "size_bytes", "modified"])
pandas
df = pd.DataFrame(s3.list_objects_v2(Bucket=b, Prefix=p)["Contents"]) # paginate for more than 1,000
MongoDB input: mongodb_input
Reads documents from a MongoDB collection as rows.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
collection | the collection |
filter | a MongoDB filter document, as JSON |
projection | a projection document, as JSON |
sample_size | documents read to decide the columns |
batch_size | rows per batch |
PySpark
df = spark.read.format("mongodb").option("connection.uri", uri).option("collection", coll).load()pandas
df = pd.DataFrame(list(pymongo.MongoClient(uri)[db][coll].find(json.loads(filter), json.loads(projection))))
MySQL input: mysql_input
Reads a table, or a query's result, from MySQL or MariaDB.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
query | or a SELECT in MySQL's dialect |
pre_sql | statements run first on the same session |
bookmark | incremental reads by a column |
batch_size | rows per batch |
PySpark
df = spark.read.format("jdbc").option("url", "jdbc:mysql://host/db").option("query", query).load()pandas
df = pd.read_sql(query, sqlalchemy.create_engine("mysql+pymysql://..."))Network Share Files: smb_input
Reads files from a Windows network share (SMB).
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the server, share and sign-in |
mode | key or prefix |
key | the file's path, in key mode |
prefix | the folder, in prefix mode |
pattern | a file-name pattern |
recursive | include sub-folders |
format | csv, json, parquet, excel or xml |
options | the format's reader settings |
wait_minutes | how long a file must stop changing before it is read |
bookmark | read only files no successful run has read |
fan_out | one run per file |
PySpark
# copy the files down with smbprotocol (smbclient), then spark.read.csv(local_paths)
pandas
with smbclient.open_file(r"\\server\share\path.csv") as f: df = pd.read_csv(f)
Parquet Files: parquet_input
Reads Parquet files uploaded to the pipeline's file library.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
files | file names in the pipeline's file library |
source_file_column | if set, a column holding each row's file name |
on_schema_change | reconcile (default), skip or fail |
PySpark
df = spark.read.parquet(*paths)
pandas
df = pd.concat([pd.read_parquet(p) for p in paths], ignore_index=True)
Postgres input: postgres_input
Reads a table, or a query's result, from PostgreSQL.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds host, database and credentials |
table | the table to read |
query | or a SELECT in PostgreSQL's own dialect |
pre_sql | statements run first on the same session (for example SET search_path) |
bookmark | read only rows past where the last successful run stopped: { column, start, lookback } |
batch_size | rows per batch |
PySpark
df = spark.read.format("jdbc").option("url", jdbc_url).option("query", query).option("user", u).option("password", p).load()pandas
df = pd.read_sql(query, sqlalchemy.create_engine(url))
NUMERIC columns arrive as exact text in Trinium; cast them to decimal where you need numbers.
Redshift input: redshift_input
Reads a table, or a query's result, from Amazon Redshift.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
query | or a SELECT in Redshift's dialect |
pre_sql | statements run first on the same session |
bookmark | incremental reads by a column |
batch_size | rows per batch |
PySpark
df = spark.read.format("jdbc").option("url", "jdbc:redshift://host:5439/db").option("query", query).load()pandas
df = pd.read_sql(query, redshift_connector.connect(**creds))
REST API (Read): rest_api_input
Reads records from a REST API, following its pages.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a REST connection ref; holds the base URL and sign-in |
path | the path after the base URL |
method | GET or POST |
query | list of { key, value } query parameters |
headers | list of { key, value } headers |
body | a request body, for POST |
records_path | where the records are in the response, such as data.items |
paging | how to follow pages: none, page number, offset, cursor, next link or Link header |
columns | declared columns { name, data_type } |
read_all_as_text | read every column as text |
PySpark
rows = []
while url:
r = requests.get(url, headers=h, timeout=60).json()
rows += r["data"]["items"]; url = r.get("next")
df = spark.createDataFrame(rows)pandas
df = pd.json_normalize(rows) # rows gathered page by page as above
S3 Files: s3_input
Reads one object, or every matching object under a folder, from Amazon S3 (or any S3-compatible store), in CSV, JSON, Parquet, Excel or XML.
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
connection_id | a connection ref (see connections in the file); holds the bucket and credentials |
mode | key (one object) or prefix (a folder) |
key | the object's key, in key mode |
prefix | the folder, in prefix mode; may hold date tokens such as ${date-1} |
pattern | a file-name pattern such as *.csv |
recursive | include sub-folders |
format | csv, json, parquet, excel or xml |
options | the format's reader settings (has_header, delimiter, on_schema_change and so on, as on the file inputs) |
partition_columns | read key=value folders as columns (always text) |
partition_filter | a SQL condition on those columns; folders it rules out are never downloaded |
bookmark | read only files no successful run has read |
fan_out | start one run per file |
PySpark
df = spark.read.option("header", True).csv(f"s3a://{bucket}/{prefix}") # .parquet(...), .json(...)pandas
df = pd.concat([pd.read_csv(f"s3://{bucket}/{k}") for k in keys]) # needs s3fsBookmark has no Spark equivalent here: keep a list of processed keys yourself, or use your platform's job bookmarks. Files ending .gz are read decompressed; a .zip is read when the pattern names it.
Salesforce input: salesforce_input
Reads Salesforce records with a SOQL query, following pages.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a Salesforce connection ref |
soql | the SOQL query |
object | or an object to read |
fields | the fields read |
columns | declared columns |
max_records | stop after this many |
PySpark
recs = Salesforce(**creds).query_all(soql)["records"]; df = spark.createDataFrame(pd.DataFrame(recs).drop(columns="attributes"))
pandas
df = pd.DataFrame(Salesforce(**creds).query_all(soql)["records"]).drop(columns="attributes") # simple-salesforce
SFTP Files: sftp_input
Reads files from an SFTP server folder.
Ports. In: none (a source). Out: output, files.
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the host and sign-in |
mode | key or prefix |
key | the file's path, in key mode |
prefix | the folder, in prefix mode |
pattern | a file-name pattern |
recursive | include sub-folders |
format | csv, json, parquet, excel or xml |
options | the format's reader settings |
wait_minutes | how long a file must stop changing before it is read (0 turns the check off) |
bookmark | read only files no successful run has read |
fan_out | one run per file |
PySpark
# Spark has no SFTP reader: download with paramiko, then spark.read.csv(local_paths)
pandas
with paramiko.SSHClient() ... sftp.get(remote, local); df = pd.read_csv(local)
Trinium skips a file that is still being written (its size or time keeps changing). Reproduce that check, or read only files your partner marks as complete.
Snowflake input: snowflake_input
Reads a table, or a query's result, from Snowflake.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
query | or a SELECT in Snowflake's dialect |
PySpark
df = spark.read.format("snowflake").options(**sf_options).option("query", query).load()pandas
df = snowflake.connector.connect(**creds).cursor().execute(query).fetch_pandas_all()
SQL Server input: sqlserver_input
Reads a table, or a query's result, from SQL Server or Azure SQL.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
query | or a SELECT in T-SQL |
pre_sql | statements run first on the same session |
bookmark | incremental reads by a column |
batch_size | rows per batch |
PySpark
df = spark.read.format("jdbc").option("url", "jdbc:sqlserver://host;databaseName=db").option("query", query).load()pandas
df = pd.read_sql(query, sqlalchemy.create_engine("mssql+pyodbc://..."))SQLite Files: sqlite_input
Reads a table, or the result of a query, from an uploaded SQLite database file.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
files | the database file's name in the pipeline's file library |
table | the table to read |
query | or a SELECT in SQLite's own dialect |
PySpark
pdf = pd.read_sql(query, sqlite3.connect(path)); df = spark.createDataFrame(pdf)
pandas
df = pd.read_sql(query or f"SELECT * FROM {table}", sqlite3.connect(path))Text Input: text_input
A small table typed into the pipeline itself. The columns are declared with a name and a type; the rows are the cells, by position.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
columns | list of { name, data_type } (data_type is one of the column types in the rules above) |
rows | list of rows; each row is a list of cell strings, in column order; a blank cell is NULL |
PySpark
df = spark.createDataFrame(rows, schema="region STRING, target DOUBLE")
pandas
df = pd.DataFrame(rows, columns=[c["name"] for c in columns]).astype({"target": "float64"})Cells are text in the file; cast each column to its declared type. A cell that does not read as its type is an error in Trinium, never a silent NULL.
Trino / Starburst input: trino_input
Reads a table, or a query's result, from Trino or Starburst.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | catalog.schema.table |
query | or a SELECT in Trino's dialect |
bookmark | incremental reads by a column |
batch_size | rows per batch |
PySpark
pdf = pd.read_sql(query, trino.dbapi.connect(host=h, port=443, user=u, http_scheme="https")); df = spark.createDataFrame(pdf)
pandas
df = pd.read_sql(query, trino.dbapi.connect(host=h, port=443, user=u, http_scheme="https"))
XML Files: xml_input
Reads XML files, turning each repeating element into a row.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
files | file names in the pipeline's file library |
record_path | the element that is one record, as a path such as Orders/Order |
mode | columns (each child element a column), name_value (one row per leaf) or extract (only the listed paths) |
fields | extract mode: list of { path, name, new_type }; a path like Amt/@Ccy reads an attribute, x[*] makes one row per repeat |
attributes | read attributes as columns (default true) |
attribute_prefix | prefix for attribute columns, default @ |
infer_rows | how many records decide each column's type |
on_schema_change | reconcile (default), skip or fail |
PySpark
df = spark.read.format("xml").option("rowTag", "Order").load(paths) # Spark 4 built in, spark-xml on 3.xpandas
df = pd.concat([pd.read_xml(p, xpath="//Order") for p in paths], ignore_index=True)
Element names are matched without namespaces.
Zendesk: zendesk_input
Reads Zendesk tickets, users or organizations.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
connection_id | a Zendesk connection ref |
resource | tickets, users or organizations |
query | an optional search query |
columns | declared columns |
max_records | stop after this many (Zendesk caps search at 10,000) |
PySpark
# page through /api/v2/{resource}.json with requests, then spark.createDataFramepandas
df = pd.json_normalize(records)
Preparation
Data Cleanse: data_cleanse
Tidies text columns and drops empty rows or columns.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
select_all | every column (default) |
columns | otherwise, the chosen columns |
trim_whitespace | trim leading and trailing spaces |
collapse_whitespace | runs of spaces become one |
remove_all_whitespace | remove every space |
remove_html | strip HTML tags |
remove_invisible | strip invisible characters |
remove_letters | remove letters (except letters_except) |
remove_numbers | remove digits (except numbers_except) |
remove_punct | remove punctuation (except punct_except) |
modify_case | upper, lower or title |
string_nulls | what a NULL text becomes, if anything |
numeric_nulls | what a NULL number becomes, if anything |
remove_null_rows | drop rows where every value is NULL |
remove_null_cols | drop columns where every value is NULL |
PySpark
for c in text_cols:
df = df.withColumn(c, F.trim(F.regexp_replace(c, r"\s+", " ")))pandas
df[text_cols] = df[text_cols].apply(lambda s: s.str.replace(r"\s+", " ", regex=True).str.strip())
Dropping all-NULL columns depends on the data, so the columns your code outputs can change from run to run.
Data Compare: data_compare
Compares two datasets on a key: one row per column on summary (types and how many values differ), and one row per differing cell, missing row or extra row on differences.
Ports. In: primary, target. Out: summary, differences.
| Setting | What it means |
|---|---|
key_columns | the columns that pair a primary row with a target row |
columns | the columns compared (blank = every shared column) |
PySpark
j = primary.alias("p").join(target.alias("t"), keys, "full_outer")
diff = j.filter(~F.col("p.amount").cast("string").eqNullSafe(F.col("t.amount").cast("string")))pandas
m = primary.merge(target, on=keys, how="outer", suffixes=("_p", "_t"), indicator=True)
diff = m[m["amount_p"].astype(str) != m["amount_t"].astype(str)]Values are compared as text, so a retyped column shows as one type difference rather than as every row differing. A key that is not unique is an error.
Data Profile: data_profile
Describes every column, one row per column: rows, NULLs, blanks, distinct count, min, max, mean, median, standard deviation, lengths and the top values.
Ports. In: input. Out: profile.
| Setting | What it means |
|---|---|
columns | the columns to profile (blank = all) |
top_values | how many most frequent values to list, default 5 |
PySpark
df.summary().show() # distinct: df.agg(*[F.countDistinct(c) for c in df.columns])
pandas
profile = df.describe(include="all").T
Trinium's distinct counts are exact over the whole dataset; median is approximate.
Data Validation: schema_validation
Checks rows against rules; rows that pass leave on valid, rows that fail on invalid. It can also fail the run or only warn.
Ports. In: input. Out: valid, invalid.
| Setting | What it means |
|---|---|
rules | list of checks: not_null, unique, type_is, range (min, max, inclusive), length (min, max), regex (pattern), allowed_values (values, case_sensitive) or expression; each with a column and an optional message |
on_violation | quarantine (default: send failing rows to invalid), fail or warn |
PySpark
ok = F.col("email").rlike(r"^[^@]+@[^@]+$") & F.col("amount").between(0, 10000)
valid_df, invalid_df = df.filter(ok), df.filter(~ok | ok.isNull())pandas
ok = df["email"].str.match(r"^[^@]+@[^@]+$", na=False) & df["amount"].between(0, 10000) valid_df, invalid_df = df[ok], df[~ok]
Dynamic Rename: dynamic_rename
Renames columns chosen by a rule: add or remove a prefix or suffix, replace text, change case, or compute the new name with an expression.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
scope | which columns, as a rule like Dynamic Select's |
mode | prefix, suffix, remove_prefix, remove_suffix, replace, case or expression |
text | the prefix or suffix |
replace_with | replace mode: the new text |
regex | replace mode: treat the text as a regular expression |
case_style | case mode: lower, upper, title, snake and similar |
expression | expression mode: a SQL expression over [name] giving the new name |
PySpark
df = df.toDF(*[re.sub(r"\W+", "_", c).lower() for c in df.columns])
pandas
df.columns = [re.sub(r"\W+", "_", c).lower() for c in df.columns]
snake case lowercases, splits camelCase and collapses punctuation to one underscore. Two columns renamed to the same name is an error.
Dynamic Select: dynamic_select
Keeps or drops columns chosen by a rule rather than by name, so new upstream columns are handled without editing.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
rule | basic: type families (text, number, boolean, datetime, other) and a name test; or an expression over each column's [name], [type], [index] and [nullable] |
action | keep (default) or drop the matching columns |
always_keep | column names kept whatever the rule says; a name not in the input is ignored |
PySpark
keep = [f.name for f in df.schema.fields if f.name.startswith("amt_")]
df = df.select(*keep)pandas
df = df[[c for c in df.columns if c.startswith("amt_")]]An empty rule matches every column. A rule that keeps nothing is an error.
Fill Missing: fill_missing
Fills NULL (and optionally blank) values per column with a fixed value or the column's mean, median or mode.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
rules | list of { column, strategy (value, mean, median or mode), value, blank_is_missing, missing_value } |
PySpark
mean = df.agg(F.avg("amount")).first()[0]
df = df.fillna({"amount": mean, "region": "Unknown"})pandas
df["amount"] = df["amount"].fillna(df["amount"].mean())
df["region"] = df["region"].fillna("Unknown")Filter: filter
Splits rows by a condition: rows where it is true leave on the true port; all others, including rows where it is NULL, leave on the false port.
Ports. In: input. Out: true, false.
| Setting | What it means |
|---|---|
mode | basic (column, operator, value) or custom (predicate) |
predicate | custom mode: a SQL boolean expression |
column | basic mode: the column tested |
operator | basic mode: eq, ne, gt, gte, lt, lte, contains, starts_with, ends_with, is_null, is_not_null and similar |
value | basic mode: the value compared with |
case_sensitive | basic mode: text comparisons ignore case unless this is true |
PySpark
cond = F.expr("amount > 100")
true_df = df.filter(cond)
false_df = df.filter(~cond | cond.isNull())pandas
m = df.eval("amount > 100").fillna(False).astype(bool)
true_df, false_df = df[m], df[~m]Formula: formula
Adds or replaces columns, each computed by a SQL expression, in order: a later formula can use an earlier one's result.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
formulas | ordered list of { output_field, expression } |
PySpark
df = df.withColumn("total", F.expr("qty * price")).withColumn("big", F.expr("total > 1000"))pandas
df["total"] = df["qty"] * df["price"] df["big"] = df["total"] > 1000
Formula Multi-Field: multi_field_formula
Applies one expression to many columns, where [_current] stands for each column in turn.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
select_all | every column (default true) |
columns | otherwise, the chosen columns |
expression | a SQL expression using [_current] |
create_new_fields | write new columns instead of replacing |
prefix | new columns' name prefix |
suffix | new columns' name suffix |
PySpark
for c in cols:
df = df.withColumn(c, F.expr(f"round(`{c}`, 2)"))pandas
df[cols] = df[cols].round(2)
Formula Multi-Row: multi_row_formula
Computes a column that looks at other rows: in window mode with SQL window functions (LAG, LEAD, running totals over [_row]); in running mode with [_prev], this expression's own result on the previous row of the same group.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
output_field | the column written |
expression | the SQL expression |
mode | absent for window mode, running for running mode |
partition_by | running mode: the group columns |
PySpark
# window mode
w = W.orderBy("_row")
df = df.withColumn("prev_amount", F.lag("amount").over(w))
# running mode needs a loop per group: use applyInPandaspandas
# running mode, e.g. GREATEST(0, COALESCE(_prev, 0) + in - out) per group
def run(g):
out, prev = [], None
for a, b in zip(g["in_qty"], g["out_qty"]):
prev = max(0, (prev or 0) + a - b); out.append(prev)
return g.assign(balance=out)
df = df.groupby(keys, group_keys=False).apply(run)Rows are taken in arrival order; put a Sort before it. [_row] is the row's position. [_prev] is NULL on a group's first row.
Generate Rows: generate_rows
Makes rows from nothing: a count, a number range, or a date or date-time range.
Ports. In: none (a source). Out: output.
| Setting | What it means |
|---|---|
column_name | the column created, default RowCount |
mode | count, number, date or datetime |
count | count mode: how many rows (numbered from 1) |
start | number mode: first value |
stop | number mode: last value |
step | number mode: step |
start_date | date modes: first date (YYYY-MM-DD) |
end_date | date modes: last date |
date_step | date modes: step size |
date_unit | date modes: day, week, month, hour and similar |
PySpark
df = spark.range(1, count + 1).withColumnRenamed("id", "RowCount")
# dates: spark.sql("SELECT explode(sequence(DATE'2026-01-01', DATE'2026-01-31')) AS d")pandas
df = pd.DataFrame({"RowCount": range(1, count + 1)})
# dates: pd.DataFrame({"d": pd.date_range(start_date, end_date, freq="D")})Manage Columns: manage_columns
Keeps, drops, renames, retypes, reorders and describes columns in one place.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
columns | ordered list of { name, include, rename, new_type, size, scale, description } |
keep_unlisted | keep columns not in the list (default true), after the listed ones |
PySpark
df = df.select(F.col("cust_id").cast("bigint").alias("customer_id"), "name")pandas
df = df.rename(columns={"cust_id": "customer_id"})[["customer_id", "name"]]
df["customer_id"] = pd.to_numeric(df["customer_id"], errors="coerce")A value that does not convert to its new type becomes NULL (use try_cast in Spark 4, or errors="coerce" in pandas). On a text column, size truncates to that many characters. A listed column that is not in the input fails the run.
Record ID: record_id
Adds a row number, or a unique id, to every row.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
field_name | the column, default RecordID |
mode | sequential (a counter) or uuid |
start_at | first number, default 1 |
position | first or last column |
PySpark
df = df.withColumn("RecordID", F.row_number().over(W.orderBy(F.monotonically_increasing_id())))pandas
df.insert(0, "RecordID", range(1, len(df) + 1))
Numbers follow arrival order.
Route: route
Sends each row to the first rule it matches, one port per rule, and rows matching none to unmatched. The ports partition the input.
Ports. In: input. Out: route_1, route_2, ... (one per rule, in order), then unmatched.
| Setting | What it means |
|---|---|
routes | ordered list of rules; each has a name and either mode basic (column, operator, value, case_sensitive) or mode custom (expression) |
PySpark
branch = F.when(F.expr(r1), 0).when(F.expr(r2), 1).otherwise(2)
df2 = df.withColumn("_route", branch)
r1_df = df2.filter("_route = 0").drop("_route") # and so onpandas
conds = [df.eval(r1), df.eval(r2)] route = np.select(conds, [0, 1], default=2) r1_df = df[route == 0]
First match wins, and a rule that is NULL for a row does not match it.
Sample: sample
Keeps part of the rows: the first N, the last N, all but the first N, every Nth, a chance per row, or N at random.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
mode | first, last, skip, every_nth, chance or random |
count | N, or the percentage for chance |
seed | a seed for repeatable random choices |
PySpark
df = df.limit(n) # random: df.sample(fraction=p, seed=seed)
pandas
df = df.head(n) # random: df.sample(n=n, random_state=seed)
Sort: sort
Sorts rows by one or more columns.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
keys | ordered list of { column, direction } with direction asc or desc |
PySpark
df = df.orderBy(F.col("region").asc_nulls_last(), F.col("amount").desc_nulls_last())pandas
df = df.sort_values(["region", "amount"], ascending=[True, False], na_position="last")
NULLs sort last in both directions.
Unique: unique
Splits rows into the first row of each key (unique) and the rest (duplicate).
Ports. In: input. Out: unique, duplicate.
| Setting | What it means |
|---|---|
select_all | the key is every column (default true) |
columns | otherwise, the key columns |
case_sensitive | text keys ignore case unless this is true |
PySpark
w = W.partitionBy(*keys).orderBy("_row")
df2 = df.withColumn("_row", F.monotonically_increasing_id()).withColumn("_n", F.row_number().over(w))
unique_df = df2.filter("_n = 1").drop("_row", "_n")
duplicate_df = df2.filter("_n > 1").drop("_row", "_n")pandas
dup = df.duplicated(subset=keys, keep="first") unique_df, duplicate_df = df[~dup], df[dup]
Trinium keeps the FIRST row it saw, in arrival order, and treats NULL keys as equal. Spark has no arrival order across partitions: number the rows where they are read if order matters.
Join
Append Fields: append_fields
Adds every column of the source input to every row of the target input (a cross join), typically a one-row table of constants.
Ports. In: target, source. Out: output.
| Setting | What it means |
|---|---|
collision_suffix | added to a source column whose name the target has, default _source |
renames | list of { source_column, output_name } |
record_limit | { action (off, warn or error), max_source_rows } guarding against a large source |
PySpark
df = target.crossJoin(F.broadcast(source))
pandas
df = target.merge(source, how="cross", suffixes=("", "_source"))Find & Replace: find_replace
Looks up each value of a column in a lookup table and replaces the matching text, or appends the lookup row's columns.
Ports. In: input, lookup. Out: output.
| Setting | What it means |
|---|---|
find_within_field | the input column searched |
find_value_field | the lookup column holding the text to find |
match_mode | beginning, any_part or entire |
case_insensitive | ignore case |
whole_word | match whole words only |
action | replace or append |
replace_with_field | replace: the lookup column holding the new text |
replace_multiple | any_part: replace every match, not only the first |
append_fields | append: the lookup columns added |
collision_suffix | append: added to clashing names |
PySpark
m = {r["find"]: r["repl"] for r in lookup.collect()}
expr = F.col("text")
for k, v in m.items():
expr = F.regexp_replace(expr, re.escape(k), v)
df = df.withColumn("text", expr)pandas
m = dict(zip(lookup["find"], lookup["repl"])) df["text"] = df["text"].replace(m, regex=False) # entire match; use str.replace for parts
Fuzzy Match: fuzzy_match
Finds rows that are probably the same thing although written differently. Dedupe groups similar rows of one input; Match pairs each input row with similar rows of a reference input.
Ports. In: input (and reference, in match mode). Out: output in dedupe mode (with group_id, is_primary, match_score); matched and unmatched in match mode.
| Setting | What it means |
|---|---|
mode | dedupe or match |
match_fields | list of { input_field, reference_field, method (exact, levenshtein, jaro_winkler, phonetic or token), phonetic, weight, threshold, case_insensitive, strip_punct } |
threshold | the overall score (0 to 100) a pair must reach, default 80 |
match_key | optional blocking { field, method (first_chars or phonetic), chars }: only rows sharing the key are compared |
guard | off, warn or error when too many pairs would be compared |
collision_suffix | match mode: added to clashing reference names |
PySpark
@F.pandas_udf("double")
def score(a: pd.Series, b: pd.Series) -> pd.Series:
return pd.Series([fuzz.ratio(x, y) for x, y in zip(a, b)])
# block first: join on the match key, then score pairs and keep score >= thresholdpandas
from rapidfuzz import fuzz, process pairs = [(i, j, fuzz.ratio(a, b)) for i, a in df["name"].items() for j, b in ref["name"].items()]
Trinium's scores are its own (normalized Levenshtein, Jaro-Winkler, Soundex and Metaphone, token sets, weighted and scaled to 100). A Python library gives close but not identical scores, so expect some borderline pairs to differ. Always block on a key before comparing.
Join: join
Joins two inputs on key columns. Matched rows leave on join; unmatched left rows on left; unmatched right rows on right. Wire join alone for an inner join, join and left for a left join.
Ports. In: left, right. Out: left, join, right.
| Setting | What it means |
|---|---|
mode | by_key (default) or by_position (row n with row n) |
keys | list of { left, right } column pairs |
case_insensitive | match text keys ignoring case |
output_fields | optional list of { side, name, include, rename, new_type } choosing and renaming the joined columns |
keep_unlisted | keep columns not in that list (default true) |
collision_suffix | added to a right column whose name the left already has, default _right |
PySpark
cond = [left["id"] == right["cust_id"]] join_df = left.join(right, cond, "inner") left_only = left.join(right, cond, "left_anti") right_only = right.join(left, [right["cust_id"] == left["id"]], "left_anti")
pandas
m = left.merge(right, left_on="id", right_on="cust_id", how="outer", indicator=True, suffixes=("", "_right"))
join_df = m[m["_merge"] == "both"]
left_only = m[m["_merge"] == "left_only"][left.columns]NULL keys never match. If the file wires join and left into a Union, that is a left join: write it as one LEFT JOIN. pandas matches NaN keys to each other, so drop or fill them first.
Join Multiple: join_multiple
Joins 2 to 16 inputs on their key columns at once: a full outer join by default, or inner only.
Ports. In: input_1, input_2, ... (one per input). Out: output.
| Setting | What it means |
|---|---|
input_count | how many inputs |
mode | by_key or by_position |
keys | list of { fields }: one key column per input, in input order |
inner_only | keep only rows present in every input |
case_insensitive | match text keys ignoring case |
output_fields | optional choosing and renaming of columns, per input |
keep_unlisted | keep columns not listed |
collision_suffix | added to clashing names |
PySpark
out = dfs[0]
for d in dfs[1:]:
out = out.join(d, on="id", how="full_outer")pandas
out = functools.reduce(lambda a, b: a.merge(b, on="id", how="outer"), dfs)
The key comes out as one coalesced column.
Union: union
Stacks 2 to 16 inputs into one table, matching columns by name (default) or by position.
Ports. In: input_1, input_2, ... (one per input). Out: output.
| Setting | What it means |
|---|---|
input_count | how many inputs |
mode | by_name or by_position |
field_policy | all (default: a column missing from an input is NULL), common (only shared columns) or error |
case_insensitive | match names ignoring case |
output_fields | optional choosing and renaming of columns |
keep_unlisted | keep columns not listed |
PySpark
out = functools.reduce(lambda a, b: a.unionByName(b, allowMissingColumns=True), dfs)
pandas
out = pd.concat(dfs, ignore_index=True) # join="inner" for common columns
Lookup
API Batch Lookup: rest_api_batch
Sends rows to a REST API many at a time and adds the answer for each row as new columns: one answer per row, in the order sent.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
connection_id | a REST connection ref |
path | the path after the base URL, the same for every request |
endpoint | calculates (a Test run calls it; a cut-off request is resent) or changes (neither) |
rows_per_request | rows per request (default 100) |
concurrency | requests in flight at once (default 4) |
rows_field | the request field the rows go in (default rows; blank sends the list itself) |
answers_path | where the answer list is in the response (default: the rows field's name) |
columns | declared answer columns { name, data_type } |
prefix | added to answer column names |
include_status | add an http_status column |
on_error | fail, or keep the request's rows with empty answers |
max_retries | retries for a rate limit, a server error or a dropped connection |
PySpark
def score(pdf: pd.DataFrame) -> pd.DataFrame:
out = []
for i in range(0, len(pdf), 100):
rows = pdf.iloc[i:i + 100].to_dict("records")
r = requests.post(url, json={"rows": rows}, headers=h, timeout=60)
r.raise_for_status()
out += r.json()["rows"]
return pdf.assign(score=[a["score"] for a in out])
df = df.groupBy(F.spark_partition_id()).applyInPandas(score, schema=df.schema.add("score", "double"))pandas
answers = []
for i in range(0, len(df), 100):
rows = df.iloc[i:i + 100].to_dict("records")
r = requests.post(url, json={"rows": rows}, headers=h, timeout=60)
r.raise_for_status()
answers += r.json()["rows"]
df["score"] = [a["score"] for a in answers]This is how a pipeline calls custom code or a hosted model in bulk: the code runs behind an HTTPS endpoint you host. Check that the answer holds exactly one entry per row.
API Lookup: rest_api_call
Calls a REST API once for every row and adds the response's fields to the row.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
connection_id | a REST connection ref |
path | the path; ${column} puts the row's value in it |
method | GET, POST, PUT, PATCH or DELETE |
query | list of { key, value }; values may hold ${column} |
headers | list of { key, value } |
body | a request body with ${column} holes |
send_row_as_body | send the whole row as the JSON body |
records_path | where the answer's records are |
columns | declared response columns { name, data_type } |
prefix | added to response column names |
include_status | add an http_status column |
include_body | add the raw response body as text |
response_format | json (default) or text |
concurrency | requests in flight at once |
on_error | fail, or keep the row with its status |
PySpark
@F.pandas_udf("string")
def call(ids: pd.Series) -> pd.Series:
return ids.map(lambda i: requests.get(f"{base}/customers/{i}", headers=h, timeout=30).text)
df = df.withColumn("resp", call("id"))pandas
df["resp"] = df["id"].map(lambda i: requests.get(f"{base}/customers/{i}", headers=h, timeout=30).json())This is also how a pipeline calls custom code: the code runs behind an HTTPS endpoint you host.
Database Lookup: db_lookup
Looks up each row's key in a database table or query: rows with matches leave on matched with the database's columns added; rows without, on not_found.
Ports. In: input. Out: matched, not_found.
| Setting | What it means |
|---|---|
connection_id | a PostgreSQL, SQL Server, MySQL or Trino connection ref |
table | the table looked up |
query | or a query |
keys | list of { input, column } pairs |
keys_per_query | keys sent per query, default 1,000 (Trino 256) |
collision_suffix | added to a looked-up column whose name the input has, default _lookup |
PySpark
lk = spark.read.format("jdbc").option("url", jdbc_url).option("dbtable", table).load()
matched = df.join(lk, df["id"] == lk["cust_id"], "inner")
not_found = df.join(lk, df["id"] == lk["cust_id"], "left_anti")pandas
keys = df["id"].dropna().unique().tolist()
lk = pd.read_sql("SELECT * FROM t WHERE cust_id = ANY(%(k)s)", engine, params={"k": keys})
matched = df.merge(lk, left_on="id", right_on="cust_id")The database decides whether two keys match, by its own collation: 'abc' may equal 'ABC'. A NULL key goes to not_found.
Parse
DateTime: datetime
Converts text to a date or date-time using a format, or formats a date as text.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
direction | to_date (text to date) or to_string (date to text) |
field | the input column |
output_field | the column written |
format | tokens: yyyy, yy, MM, Mon, Month, dd, day, dy, HH, hh, mm, ss, tt (AM/PM); default yyyy-MM-dd |
language | month and day names' language, default English |
on_error | null (default: an unreadable value becomes NULL with a warning) or fail |
PySpark
df = df.withColumn("d", F.to_date("txt", "dd/MM/yyyy")) # Spark patternspandas
df["d"] = pd.to_datetime(df["txt"], format="%d/%m/%Y", errors="coerce")
Translate the format: yyyy is %Y, MM %m, dd %d, HH %H, mm %M, ss %S, Mon %b, Month %B, tt %p. Spark's patterns happen to use the same letters for most tokens; Mon is MMM and Month MMMM there.
JSON: json_parse
Reads JSON held in a text column: extract chosen paths into columns, list every leaf as name and value rows, or flatten everything into columns.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
field | the column holding JSON text |
mode | extract, name_value or flatten |
fields | extract: list of { path, name, new_type }; paths look like a.b, items[0], items[*] (one row per element) |
arrays | flatten: explode (default), text or index |
separator | joins nested names, default a dot |
keep_original | keep the JSON column |
on_error | null (default: bad JSON gives NULLs for that row) or fail |
PySpark
schema = "struct<customer:struct<id:bigint>,items:array<struct<sku:string>>>"
df = df.withColumn("j", F.from_json("payload", schema)).select("*", "j.customer.id")pandas
parsed = df["payload"].map(json.loads) out = pd.json_normalize(parsed)
A row whose cell holds no records still comes out, with NULLs, so the row's other columns are not lost.
RegEx: regex
Uses a regular expression on a text column: test it (match), replace it, parse capture groups into columns, or split it into tokens.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
method | match, replace, parse or tokenize |
field | the input column |
pattern | the regular expression |
output_field | match: the boolean column; replace and tokenize: the column written |
replacement | replace: the new text, with $1 or ${name} for groups |
replace_all | replace every match (default true) |
columns | parse: list of { name, new_type } for the capture groups |
tokenize_layout | tokenize: columns or rows |
num_columns | tokenize into columns: how many |
token_group | tokenize: which capture group is the token (0 = the whole match) |
PySpark
df = df.withColumn("code", F.regexp_extract("text", r"([A-Z]{3})-(\d+)", 1))pandas
df[["code", "num"]] = df["text"].str.extract(r"([A-Z]{3})-(\d+)")Trinium's regular expressions have no look-around and no back-references. Python's re and Java's (Spark) accept more, so a pattern that works in Trinium works there; replacement groups are \1 in Python and $1 in Spark.
Text to Columns: text_to_columns
Splits a text column on delimiters into several columns, or into one row per piece.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
field | the input column |
delimiters | the delimiter characters (or string) |
delimiter_mode | any_char (each character is a delimiter) or whole_string |
layout | columns or rows |
num_columns | columns layout: how many columns |
columns | columns layout: optional { name, new_type } per column |
extra | leave_in_last (default), drop or error when there are more pieces than columns |
output_field | the column (rows layout) or name prefix (columns layout) |
trim | trim each piece |
collapse | treat runs of delimiters as one |
ignore_quotes | do not split inside quotes |
quote_char | the quote character |
emit_index | rows layout: add the piece's position |
index_field | rows layout: that column's name |
keep_original | keep the input column |
PySpark
parts = F.split("tags", ",")
df = df.select("*", *[parts[i].alias(f"tag_{i+1}") for i in range(3)]) # rows: F.explode(parts)pandas
df[["tag_1", "tag_2", "tag_3"]] = df["tags"].str.split(",", n=2, expand=True) # rows: df.explodeURL: url_parse
Splits a URL column into parts (scheme, host, domain, path, query and so on), or turns its query string into columns or rows.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
field | the URL column |
mode | components, query_columns or query_rows |
components | components: which parts (scheme, username, password, host, subdomain, domain, suffix, port, path, query, fragment, is_valid) |
params | query_columns: list of { key, name, new_type } |
prefix | components: prefix for the new columns |
assumed_scheme | used when a URL has none, default https |
keep_original | keep the URL column |
PySpark
df = df.withColumn("host", F.expr("parse_url(url, 'HOST')")).withColumn("q", F.expr("parse_url(url, 'QUERY', 'utm_source')"))pandas
p = df["url"].map(urllib.parse.urlsplit) df["host"] = p.map(lambda u: u.hostname)
Domain and suffix use the public suffix list (example.co.uk splits as example and co.uk). tldextract in Python does the same; Spark's parse_url does not.
XML: xml_parse
Reads XML held in a text column, with the same three modes as XML Files: columns, name_value or extract.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
field | the column holding XML text |
record_path | the element that is one record; blank means the cell's root element |
mode | columns, name_value or extract |
fields | extract: list of { path, name, new_type } |
attributes | read attributes as columns |
attribute_prefix | prefix for attribute columns |
keep_original | keep the XML column |
on_error | null (default) or fail |
PySpark
df = df.withColumn("amt", F.xpath_string("xml", F.lit("Doc/Amt"))) # or from_xml in Spark 4pandas
df["amt"] = df["xml"].map(lambda s: ET.fromstring(s).findtext("Amt"))Transform
Cross Tab: cross_tab
Pivots: one column per distinct value of a header column, filled with an aggregate of a value column.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
group_by | the row keys |
header_field | the column whose values become column names |
value_field | the column aggregated |
aggregation | sum, count, avg, min, max or concat |
header_order | asc or desc order of the new columns |
column_prefix | added to each new column's name |
concat_separator | concat: the separator |
fill_empty | what an empty cell holds |
max_columns | refuse the run above this many new columns |
PySpark
df = df.groupBy("region").pivot("month").agg(F.sum("amount"))pandas
df = df.pivot_table(index="region", columns="month", values="amount", aggfunc="sum").reset_index()
The new columns come from the data, so they change when the data does.
Summarize: summarize
Groups rows and aggregates them: sum, count, distinct count, min, max, average, median, standard deviation, variance, first, last, concatenate and percentile, each optionally only for rows matching a condition.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
group_by | the group columns (none = one row for the whole input) |
aggregations | list of { func, column, output_name, condition, separator, distinct, percentile }; func is sum, count, count_non_null, count_distinct, min, max, average, median, std_dev, variance, first, last, concatenate or percentile |
PySpark
df = df.groupBy("region").agg(F.sum("amount").alias("amount_sum"), F.count(F.when(F.expr("amount > 100"), 1)).alias("big"))pandas
df = df.groupby("region", dropna=False).agg(amount_sum=("amount", "sum")).reset_index()Output is ordered by the group columns. A NULL group key is its own group (pandas needs dropna=False). count counts rows; count_non_null counts values.
Transpose: transpose
Unpivots: turns chosen columns into rows of name and value, keeping the key columns.
Ports. In: input. Out: output.
| Setting | What it means |
|---|---|
key_fields | columns kept on every row |
data_all_others | unpivot every other column (default true) |
data_fields | otherwise, the columns unpivoted |
name_column | the name column, default Name |
value_column | the value column, default Value |
force_text_value | make the value column text |
drop_nulls | skip NULL values |
emit_index | add the column's position |
index_column | that column's name |
PySpark
df = df.unpivot(ids=["id"], values=["q1", "q2"], variableColumnName="Name", valueColumnName="Value") # Spark 3.4+
pandas
df = df.melt(id_vars=["id"], value_vars=["q1", "q2"], var_name="Name", value_name="Value")
Output
Apache Iceberg output: iceberg_output
Writes rows to an Apache Iceberg table through its catalog, as one snapshot per run.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the catalog and storage credentials |
namespace | the namespace (database) |
table | the table |
write_mode | append or overwrite |
create_if_not_exists | create the table if needed |
partition_by | a new table's partition columns |
PySpark
df.writeTo(f"{catalog}.{namespace}.{table}").append() # overwrite: .overwritePartitions() or .createOrReplace()pandas
load_catalog("c", **props).load_table(f"{namespace}.{table}").append(pa.Table.from_pandas(df)) # pyicebergAn append matches columns by name.
Azure Blob File: azure_blob_output
Writes a file to Azure Blob Storage, optionally split into key=value folders.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
key_template | the blob path, with the same tokens as S3 |
format | csv, json, parquet or excel |
write_mode | overwrite, error_if_exists, unique_suffix or replace_folder |
partition_by | columns that route rows into folders |
options | the format's writer settings |
PySpark
df.write.mode("overwrite").parquet(f"abfss://{container}@{account}.dfs.core.windows.net/{path}")pandas
df.to_parquet(f"abfs://{container}/{path}", storage_options={...})BigQuery output: bigquery_output
Writes rows to a BigQuery table with a load job.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | dataset.table |
write_mode | append or truncate |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
PySpark
df.write.format("bigquery").option("writeMethod", "direct").mode("append").save(table)pandas
bigquery.Client().load_table_from_dataframe(df, table).result()
ClickHouse output: clickhouse_output
Writes rows to a ClickHouse table, creating a MergeTree table if needed.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
write_mode | append or truncate |
sort_key | a new table's ORDER BY columns |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
batch_size | rows per insert |
PySpark
df.write.format("jdbc").option("url", "jdbc:clickhouse://host:8123/db").option("dbtable", table).mode("append").save()pandas
clickhouse_connect.get_client(**creds).insert_df(table, df)
CSV File: csv_output
Writes a CSV file the run keeps as a download.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
filename | the file's name |
has_header | write a header row |
delimiter | one character |
PySpark
df.coalesce(1).write.mode("overwrite").option("header", True).csv(out_dir)pandas
df.to_csv(filename, index=False)
Databricks output: databricks_output
Writes rows to a Databricks table through a Unity Catalog volume and COPY INTO.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | catalog.schema.table |
volume_path | the volume the file is staged in |
write_mode | append or truncate |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
PySpark
df.write.mode("append").saveAsTable(table) # inside Databrickspandas
# write Parquet to the volume with the Files API, then run COPY INTO through databricks-sql-connector
Excel Report: excel_output
Writes a styled Excel workbook the run keeps as a download: a sheet name, a table style, number formats, frozen header, optional chart.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
filename | the workbook's name; the other settings are presentation only |
PySpark
df.toPandas().to_excel(filename, sheet_name="Report", index=False)
pandas
with pd.ExcelWriter(filename, engine="xlsxwriter") as w:
df.to_excel(w, sheet_name="Report", index=False)File Action: file_action
Copies, moves or deletes the files a file input or List Files hands it on a files wire, before the run reads them or after it succeeds or fails. It reads no rows.
Ports. In: files (a wire from another node's files port). Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
action | copy, move or delete |
when | before, after_success or after_failure (delete is after_success only) |
which | after a run: all listed files, or only those the input read |
dest_connection_id | copy and move: a connection ref for the destination |
dest_folder | copy and move: the destination folder |
keep_structure | keep sub-folders under the destination |
if_exists | fail, overwrite or skip when the destination has the file |
max_files | delete: refuse above this many files |
PySpark
for key in processed_keys:
s3.copy_object(Bucket=b, Key=f"archive/{key}", CopySource={"Bucket": b, "Key": key})
s3.delete_object(Bucket=b, Key=key)pandas
for key in processed_keys:
shutil.move(src / key, archive / key)Do this after your job succeeds, never before it has finished reading.
GCS File: gcs_output
Writes a file to Google Cloud Storage, optionally split into key=value folders.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
key_template | the object path, with the same tokens as S3 |
format | csv, json, parquet or excel |
write_mode | overwrite, error_if_exists, unique_suffix or replace_folder |
partition_by | columns that route rows into folders |
options | the format's writer settings |
PySpark
df.write.mode("overwrite").parquet(f"gs://{bucket}/{path}")pandas
df.to_parquet(f"gs://{bucket}/{path}", index=False)Google Sheets output: google_sheets_output
Writes rows to a tab of a Google Sheets spreadsheet.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a Google Sheets connection ref |
spreadsheet | the spreadsheet id or link |
tab | the tab |
write_mode | overwrite or append |
include_header | overwrite: write a header row |
start_cell | overwrite: the top-left cell |
create_tab_if_missing | create the tab |
PySpark
ws.update([pdf.columns.tolist()] + pdf.astype(str).values.tolist()) # pdf = df.toPandas()
pandas
ws = gspread.service_account().open_by_key(sid).worksheet(tab) ws.update([df.columns.tolist()] + df.astype(str).values.tolist())
JSON File: json_output
Writes a JSON file the run keeps as a download.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
filename | the file's name |
format | array (one JSON array) or lines (one object per line) |
PySpark
df.coalesce(1).write.mode("overwrite").json(out_dir) # linespandas
df.to_json(filename, orient="records") # lines=True for lines
MongoDB output: mongodb_output
Writes rows as documents to a MongoDB collection.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
collection | the collection |
write_mode | append, replace or upsert |
key_column | upsert: the key |
all_or_nothing | replace: write a new collection and swap it in |
PySpark
df.write.format("mongodb").option("connection.uri", uri).option("collection", coll).mode("append").save()pandas
pymongo.MongoClient(uri)[db][coll].insert_many(df.to_dict("records"))MySQL output: mysql_output
Writes rows to a MySQL table.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
write_mode | append, truncate or upsert |
key_columns | upsert: the key columns (a unique index must cover them) |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
all_or_nothing | load in one transaction |
PySpark
df.write.format("jdbc").option("url", "jdbc:mysql://host/db").option("dbtable", table).mode("append").save()pandas
df.to_sql(table, engine, if_exists="append", index=False)
Network Share File: smb_output
Writes a file to a Windows network share.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
key_template | the file's path, with the same tokens as S3 |
format | csv, json, parquet or excel |
write_mode | overwrite, error_if_exists, unique_suffix or replace_folder |
partition_by | columns that route rows into folders |
options | the format's writer settings |
PySpark
with smbclient.open_file(remote, mode="w") as f: df.toPandas().to_csv(f, index=False)
pandas
with smbclient.open_file(remote, mode="w") as f: df.to_csv(f, index=False)
Parquet File: parquet_output
Writes a Parquet file the run keeps as a download.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
filename | the file's name |
compression | snappy, zstd, gzip or none |
PySpark
df.write.mode("overwrite").option("compression", "zstd").parquet(out_dir)pandas
df.to_parquet(filename, compression="zstd", index=False)
Postgres output: postgres_output
Writes rows to a PostgreSQL table, creating it if it does not exist.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table (schema.table) |
write_mode | append, truncate (empty it first) or upsert |
key_columns | upsert: the key columns |
pre_sql | statements run and committed before the load |
post_sql | statements run only after the load succeeded |
all_or_nothing | load in one transaction, so a failure leaves the table as it was (default on) |
PySpark
df.write.format("jdbc").option("url", jdbc_url).option("dbtable", table).mode("append").save()
# upsert: write to a staging table, then INSERT ... ON CONFLICT in post SQLpandas
df.to_sql(table, engine, if_exists="append", index=False, method="multi")
Run pre_sql before writing and post_sql after it succeeds, outside the load.
Redshift output: redshift_output
Writes rows to an Amazon Redshift table.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
write_mode | append, truncate or upsert (delete then insert) |
key_columns | upsert: the key columns |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
all_or_nothing | load in one transaction (default on) |
PySpark
df.write.format("jdbc").option("url", "jdbc:redshift://host:5439/db").option("dbtable", table).mode("append").save()pandas
awswrangler.redshift.to_sql(df, conn, table=table, schema=schema, mode="append")
REST API (Write): rest_api_output
Sends rows to a REST API, one request per row or many rows per request.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a REST connection ref |
path | the path after the base URL; ${column} puts a row's value in it |
method | POST, PUT or PATCH |
query | list of { key, value } query parameters |
headers | list of { key, value } headers |
body_shape | row (one request per row) or array (rows_per_request rows per request) |
rows_per_request | array shape: rows per request |
records_key | array shape: wrap the rows in an object under this key |
include_nulls | send NULL values |
on_error | fail or continue |
delay_ms | wait between requests |
PySpark
for batch in chunks(df.toPandas().to_dict("records"), 100):
requests.post(url, json={"records": batch}, headers=h, timeout=60).raise_for_status()pandas
for rec in df.to_dict("records"):
requests.post(url, json=rec, headers=h, timeout=60).raise_for_status()Never resend a write that may have reached the server: a retry can create the record twice.
S3 File: s3_output
Writes a file (CSV, JSON, Parquet or Excel) to Amazon S3, optionally split into key=value folders by column values.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref; holds the bucket and credentials |
key_template | the object key; may hold ${date}, ${date-1}, ${date:%Y%m%d}, ${datetime}, ${run_id}; ends in / when partitioned |
format | csv, json, parquet or excel |
write_mode | overwrite, error_if_exists, unique_suffix or replace_folder (empty exactly the folders this run writes) |
partition_by | columns that route rows into col=value/ folders |
options | the format's writer settings |
PySpark
df.write.mode("overwrite").partitionBy("region").parquet(f"s3a://{bucket}/{prefix}")
# replace_folder ~ spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")pandas
df.to_parquet(f"s3://{bucket}/{key}", index=False) # needs s3fs; partition_cols=[...] for folders${date} is the day the run was queued, in the instance's time zone, not UTC.
Salesforce output: salesforce_output
Inserts, updates, upserts or deletes Salesforce records, 200 a request, reading Salesforce's verdict for every record.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a Salesforce connection ref |
object | the object, such as Account |
operation | insert, update, upsert or delete |
external_id_field | upsert: the External ID field |
rows_per_request | at most 200 |
include_nulls | send NULLs (clearing fields) |
on_error | fail (allOrNone) or continue |
PySpark
sf = Salesforce(**creds)
res = sf.bulk.Account.upsert(df.toPandas().to_dict("records"), "Ext_Id__c", batch_size=200)pandas
res = Salesforce(**creds).bulk.Account.insert(df.to_dict("records"), batch_size=200)
failed = [r for r in res if not r["success"]]Salesforce answers HTTP 200 even when records failed: always read each record's success flag. Delete moves records to the Recycle Bin.
Send Email: email_output
Sends email: one message for the whole run, one per row, or one per group, with the group's rows attached as CSV or Excel. It outputs one row per message sent.
Ports. In: input. Out: delivery.
| Setting | What it means |
|---|---|
connection_id | an SMTP connection ref |
mode | single, per_row or per_group |
group_by | per_group: the group columns |
to | recipients; addresses separated by semicolons; {{ expression }} reads a column |
cc | copy recipients |
bcc | blind copy recipients |
reply_to | the reply-to address |
subject | the subject, with {{ }} holes |
body | the body, with {{ }} holes |
body_format | text or html |
attachment_name | the attachment's file name |
include_table_in_body | put the rows in the body as a table |
max_messages | refuse the run above this many messages |
messages_per_minute | send no faster than this |
on_error | fail or continue |
PySpark
for region, g in df.toPandas().groupby("region"):
msg = EmailMessage(); msg["To"] = g["manager_email"].iloc[0]
msg["Subject"] = f"Sales for {region}"; msg.add_attachment(g.to_csv(index=False), filename="sales.csv")
smtp.send_message(msg)pandas
with smtplib.SMTP(host, 587) as smtp:
smtp.starttls(); smtp.login(user, pw)
for region, g in df.groupby("region"): ...{{ }} holds a SQL expression evaluated on the group's rows. Trinium refuses a group whose recipient resolves to two different addresses; check that in your code too.
SFTP File: sftp_output
Writes a file to an SFTP server.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
key_template | the file's path, with the same tokens as S3 |
format | csv, json, parquet or excel |
write_mode | overwrite, error_if_exists, unique_suffix or replace_folder |
partition_by | columns that route rows into folders |
options | the format's writer settings |
PySpark
df.toPandas().to_csv(local, index=False); sftp.put(local, remote)
pandas
df.to_csv(local, index=False); sftp.put(local, remote) # paramiko
Trinium uploads to a temporary name and renames it, so a reader never sees a half-written file.
Snowflake output: snowflake_output
Writes rows to a Snowflake table.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
write_mode | append or truncate |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
PySpark
df.write.format("snowflake").options(**sf_options).option("dbtable", table).mode("append").save()pandas
snowflake.connector.pandas_tools.write_pandas(conn, df, table)
SQL Server output: sqlserver_output
Writes rows to a SQL Server table.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | the table |
write_mode | append, truncate or upsert (MERGE) |
key_columns | upsert: the key columns |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
all_or_nothing | load in one transaction |
PySpark
df.write.format("jdbc").option("url", "jdbc:sqlserver://host;databaseName=db").option("dbtable", table).mode("append").save()pandas
df.to_sql(table, engine, if_exists="append", index=False, fast_executemany=True)
SQLite File: sqlite_output
Writes a table into a SQLite database file the run keeps as a download.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
filename | the database file's name |
table | the table |
PySpark
df.toPandas().to_sql(table, sqlite3.connect(filename), if_exists="replace", index=False)
pandas
df.to_sql(table, sqlite3.connect(filename), if_exists="replace", index=False)
Trino / Starburst output: trino_output
Writes rows to a Trino table.
Ports. In: input. Out: none (it writes outside the pipeline).
| Setting | What it means |
|---|---|
connection_id | a connection ref |
table | catalog.schema.table |
write_mode | append or truncate |
pre_sql | statements before the load |
post_sql | statements after it succeeded |
all_or_nothing | append through a staging table and swap it in |
batch_size | rows per INSERT |
PySpark
# write the files to the table's location, or INSERT through the trino client in batches
pandas
df.to_sql(table, sqlalchemy.create_engine("trino://..."), if_exists="append", index=False, method="multi")Checking the result
Run the Python code and the pipeline on the same input and compare their outputs. In Trinium, a Data Compare node compares two datasets on a key and lists every value that differs, every missing row and every extra row: read the Python output back into a pipeline and compare it with the pipeline's own.