Skip to main content

Semantic Annotations

Preview feature

This is a preview feature. Please contact Bauplan to enable it.

In Bauplan, semantic annotations can be used to augment data pipelines with static type checking and semantic documentation. These annotations are used to build a rich representation of the data pipeline for validation of data constraints, type checking of table schemas, and propagation of metadata to and from the catalog.

Introduction

To highlight the building blocks for Bauplan's semantic annotations, we start with a simple data pipeline consisting of a single Python model, base_model whose output is specified to have the schema BaseResult. This model receives its input from the model src_table, whose schema is determined from the catalog directly.

This one-node DAG is defined in Python (with descriptive comments) as:

import pyarrow
from typing import Annotated

# Annotations are temporarily available in a `bpln_sdk` namespace
import bpln_sdk as bauplan
from bpln_sdk import TableField, TableSchema, Float64

# A `TableSchema` with one attribute
class BaseResult(TableSchema):
"""An output schema."""

# Annotated `TableField` for column metadata and constraints
tfield_double: Annotated[
Float64, # column data type
TableField(lineage="src_table['src_col_double']"), # column metadata
]

@bauplan.model()
@bauplan.python('3.11')
def base_model(
# a `pyarrow.Table` that is output by the `src_table` model
data: Annotated[pyarrow.Table, bauplan.Model('src_table')],

# returns a `pyarrow.Table` with the schema `BaseResult`
) -> Annotated[pyarrow.Table, BaseResult]:

# this model projects a single column from `data`
return data.select(['src_col_double']).rename_columns(['tfield_double'])

There are three building blocks for pipeline type annotations:

DataType

The type of data a table column will have. We support Apache Arrow types that are also supported by Apache Iceberg, but we use system-agnostic names that mostly follow Arrow's type names. Currently, we support the following data types: Bool, Int32, Int64, Float64, String, Binary, TimestampMicro, TimestampNano, TimestampMicroUTC, TimestampNanoUTC.

# Float64 without metadata
tfield_double: Float64

TableField

Metadata and constraints associated with a table column. Initial support includes: documentation fields (title and doc), nullable, and explicit column lineage.

# Float64 with metadata or constraints
tfield_double: Annotated[
Float64,
TableField(
title='TableField: tfield_double',
doc='An example TableField for column metadata',
nullable=True,
lineage="src_table['src_col_double']",
),
]

TableSchema

An ordered grouping of TableField attributes that can be associated with Bauplan models.

# A "schema contract" that is defined by subclassing `TableSchema`
class BaseResult(TableSchema):
"""An output schema."""

# Annotated `TableField` for column metadata and constraints
tfield_double: Annotated[
Float64, # column data type
TableField(lineage="src_table['src_col_double']"), # column metadata
]

Most semantic information is defined in TableField annotations that are grouped as a TableSchema; then, the TableSchema is associated with various input tables and the output table of a Bauplan model.

Working example

As a working example, here is a multi-language DAG with two nodes: (Node 1) a SQL model named sql_query and (Node 2) a Python model named py_model. All annotations are defined in Python, such as QuerySchema and ProjSchema, but they can be referenced from each model for cross-language guarantees.

The data flow is linear from the catalog to the SQL model to the Python model, but altogether you get a data pipeline that explicitly declares the output schema of each node and guarantees that there is a clear lineage for a column that flows from end to end.

class QuerySchema(TableSchema):
"""Schema of a result table from a SQL query."""

tfield_double: Float64
tfield_timestamp: Annotated[
TimestampMicroUTC,
TableField(
doc="timestamp for pyarrow.timestamp('us', tz='UTC') or timestamptz",
lineage="src_table['src_col_timestamp']",
),
]

class ProjSchema(TableSchema):
"""Schema for a simple projection."""

tfield_timestamp: Annotated[
TimestampMicroUTC,
TableField(lineage=QuerySchema['tfield_timestamp']),
]

@bauplan.model()
@bauplan.python('3.11')
def py_model(
data: Annotated[
pyarrow.Table,
bauplan.Model('sql_query', projection_schema=ProjSchema)
],

# returns a `pyarrow.Table` with the schema `ProjSchema`
) -> Annotated[pyarrow.Table, ProjSchema]:
"""Model that returns a simple projection of `sql_query` results."""
return data

Here is a SQL snippet showing a SQL model:

-- bauplan: name = sql_query
-- bauplan: output_schema = QuerySchema
SELECT src_col_double AS tfield_double
,src_col_timestamp AS tfield_timestamp
FROM src_table

Python models

A model must associate its output table with a defined schema in its return type annotation:

@bauplan.model()
@bauplan.python('3.11')
def py_model(...) -> Annotated[pyarrow.Table, ProjSchema]:
...

This associates the schema ProjSchema with the result table returned by py_model. This triggers validation that the result table schema matches the specified ProjSchema at execution time.

For input models, annotations include proper data types and an annotation to identify the source model:

@bauplan.model()
@bauplan.python('3.11')
def py_model(
data: Annotated[
pyarrow.Table,
bauplan.Model('sql_query', projection_schema=ProjSchema)
],
) -> Annotated[pyarrow.Table, ProjSchema]:
return data

For the parameter data, the use of pyarrow.Table as the first argument to Annotated tells a local type checker that data is a pyarrow.Table and the type checker will provide appropriate feedback even in the function body. Along with the return type annotation, the local type checker can help validate that data remains a pyarrow.Table throughout the function body.

The annotation bauplan.Model('sql_query', ...) specifies that data comes from the result of the sql_query model, which is specified to have the schema QuerySchema. In contrast to the base_model example, this annotation also specifies an argument for projection_schema so that py_model need not apply a projection in its function body. When applying the projection, Bauplan will validate that sql_query has the column tfield_timestamp available as specified by the lineage constraint for ProjSchema.tfield_timestamp.

SQL models

Semantic annotations are defined in Python and must be associated with SQL models with a directive:

-- bauplan: output_schema = QuerySchema

The definition of QuerySchema must exist in Python, but is referenced by a SQL query so that Bauplan can determine how to validate the result schema of sql_query. Attribute names in the TableSchema class are matched up against columns of the same name in the table results and validation will check both data types and specified constraints (nullable and lineage).