vrijdag 13 november 2026

Databricks series : Data contracts in Databricks (Part VIII)

Introduction

We are building upon what we already have:

Great. We are now going to fully integrate Data Contracts into our existing Databricks project, so that it’s no longer just a standalone example, but an integral part of our data ingestion pipeline. The new approach: every dataset has its own Data Contract. Before data is written to Delta Lake, we verify that it complies with the contract.





Moreover, we are making the solution somewhat more robust than our previous version: we will first read the data in as strings, validate the values, and then convert them into the agreed-upon data types.


Change the structure of the project

Open your project in Visual Studio Code. The new structure will be:

databricks-transfer/
│
├── databricks.yml
│
├── resources/
│   └── transfer_job.yml
│
├── src/
│   ├── __init__.py
│   ├── main.py
│   │
│   └── transfer/
│       ├── __init__.py
│       ├── config_loader.py
│       ├── contract_validator.py
│       └── processor.py
│
├── config/
│   └── entities.json
│
├── contracts/
│   ├── customers.json
│   └── transactions.json
│
├── data/
│   ├── customers.csv
│   ├── customers_update.csv
│   └── transactions.csv
│
├── requirements.txt
└── README.md


We are including two contract files and one Python module for validation.


Create a datacontract for customers

Open the 'contracts' directory and create 'customers.json'. Place the following inside it:

{
  "contract_name": "customers",
  "version": "1.0",
  "description": "Contract for customer master data",
  "owner": "Data Engineering",
  "allow_additional_columns": false,

  "columns": [
    {
      "name": "customer_id",
      "type": "string",
      "nullable": false
    },
    {
      "name": "first_name",
      "type": "string",
      "nullable": false
    },
    {
      "name": "last_name",
      "type": "string",
      "nullable": false
    },
    {
      "name": "country",
      "type": "string",
      "nullable": false
    }
  ],

  "primary_key": ["customer_id"],

  "rules": {
    "unique": ["customer_id"]
  }
}


This contract contains the agreements with the producer of the customer data. A few key properties:

  • version: allows us to manage changes to the contract.
  • owner: the party responsible for the contract.
  • allow_additional_columns: whether additional columns are permitted.
  • primary_key: the column(s) used to identify a record.
  • unique: which values ​​must be unique.

We intentionally use a strict contract: if someone unexpectedly adds an extra column, the dataset will be rejected.


Connect the contract with entities.json

Lets open config/entities.json and replace the content with:


{
  "customers": {
    "source": "/Volumes/main/demo/raw/customers.csv",
    "target": "main.demo.customers",
    "format": "csv",
    "mode": "overwrite",
    "contract": "contracts/customers.json"
  },

  "customers_update": {
    "source": "/Volumes/main/demo/raw/customers_update.csv",
    "target": "main.demo.customers",
    "format": "csv",
    "mode": "merge",
    "merge_key": "customer_id",
    "contract": "contracts/customers.json"
  }
}


For now, we are focusing on 'customers' and 'customers_update'. Both use the same contract, as they share the same data structure.  We will add the 'transactions' contract once we have verified the exact columns in 'transactions.csv'. This ensures that we don’t define a contract that doesn’t match your actual file.


Build the contract validator

Now comes the most important part. Open the file  src/transfer/contract_validator.py and replace the entire content with:


from pyspark.sql.functions import col, trim


TYPE_MAPPING = {
    "string": "string",
    "integer": "int",
    "long": "bigint",
    "double": "double",
    "boolean": "boolean",
    "date": "date",
    "timestamp": "timestamp"
}


def validate_dataframe(df, contract):

    errors = []

    contract_name = contract["contract_name"]
    version = contract["version"]

    print(f"Validating contract: {contract_name}")
    print(f"Contract version: {version}")

    expected = {
        item["name"]: item
        for item in contract["columns"]
    }

    actual_columns = set(df.columns)
    expected_columns = set(expected.keys())

    # 1. Check schema

    missing = expected_columns - actual_columns
    unexpected = actual_columns - expected_columns

    if missing:
        errors.append(
            f"Missing columns: {sorted(missing)}"
        )

    if unexpected and not contract.get(
        "allow_additional_columns", False
    ):
        errors.append(
            f"Unexpected columns: {sorted(unexpected)}"
        )

    if errors:
        raise ValueError(
            "Schema validation failed:\n"
            + "\n".join(errors)
        )

    # 2. Check required fields and datatypes

    validated_df = df

    for name, definition in expected.items():

        expected_type = definition["type"]
        nullable = definition["nullable"]

        if expected_type not in TYPE_MAPPING:
            raise ValueError(
                f"Unsupported datatype: {expected_type}"
            )

        source_col = col(name)

        # Required fields cannot be NULL or empty

        if not nullable:

            invalid = df.filter(
                source_col.isNull() |
                (trim(source_col) == "")
            ).limit(1).count()

            if invalid:
                errors.append(
                    f"Column {name} cannot be NULL or empty"
                )

        # Validate and convert datatype

        spark_type = TYPE_MAPPING[expected_type]

        if expected_type != "string":

            converted = source_col.cast(spark_type)

            invalid = df.filter(
                source_col.isNotNull() &
                (trim(source_col) != "") &
                converted.isNull()
            ).limit(1).count()

            if invalid:
                errors.append(
                    f"Invalid {expected_type} values "
                    f"in column {name}"
                )

            validated_df = validated_df.withColumn(
                name, converted
            )

    # 3. Check primary key

    for name in contract.get("primary_key", []):

        if name not in expected:
            errors.append(
                f"Primary key column {name} "
                "is not defined in contract"
            )

    # 4. Check unique constraints

    unique_columns = set(
        contract.get("rules", {}).get("unique", [])
    )

    unique_columns.update(
        contract.get("primary_key", [])
    )

    for name in unique_columns:

        if name not in expected:
            errors.append(
                f"Unique column {name} "
                "is not defined in contract"
            )
            continue

        duplicates = (
            validated_df
            .groupBy(name)
            .count()
            .filter(col("count") > 1)
            .limit(1)
            .count()
        )

        if duplicates:
            errors.append(
                f"Duplicate values found in {name}"
            )

    # 5. Final result

    if errors:
        raise ValueError(
            "Data contract validation failed:\n"
            + "\n".join(errors)
        )

    print(
        f"Contract {contract_name} "
        f"version {version} PASSED"
    )

    return validated_df


What do we have now?

Validation         Example

Schema                 Is `customer_id` missing?

Extra columns         Is `phone_number` being added unexpectedly?

Null check         Is `first_name` empty?

Data type                 Can an amount be converted to a `double`?

Primary key         Is `customer_id` unique?


The validator returns an error message if a constraint is violated.


Integrate the validator in processor.py

The next step is to open: src/transfer/processor.py. Here, we ensure that validation always takes place before the loading strategy.


from delta.tables import DeltaTable

from transfer.contract_validator import validate_dataframe


def transfer_data(spark, config, contract=None):

    source = config["source"]
    target = config["target"]
    file_format = config["format"]
    mode = config["mode"]

    print(f"Reading source: {source}")
    print(f"Target table: {target}")
    print(f"Loading mode: {mode}")

    # Read everything as strings first

    df = (
        spark.read
        .format(file_format)
        .option("header", "true")
        .option("inferSchema", "false")
        .load(source)
    )

    # Validate before writing

    if contract is None:
        raise ValueError(
            "No data contract configured for this entity"
        )

    df = validate_dataframe(df, contract)

    record_count = df.count()

    print(f"Validated records: {record_count}")

    # Apply loading strategy

    if mode == "overwrite":

        (
            df.write
            .format("delta")
            .mode("overwrite")
            .saveAsTable(target)
        )

    elif mode == "append":

        (
            df.write
            .format("delta")
            .mode("append")
            .saveAsTable(target)
        )

    elif mode == "merge":

        merge_key = config["merge_key"]

        if merge_key not in contract.get(
            "primary_key", []
        ):
            raise ValueError(
                f"Merge key {merge_key} is not "
                "defined as a contract primary key"
            )

        if spark.catalog.tableExists(target):

            delta_table = DeltaTable.forName(
                spark, target
            )

            (
                delta_table.alias("target")
                .merge(
                    df.alias("source"),
                    f"target.{merge_key} = source.{merge_key}"
                )
                .whenMatchedUpdateAll()
                .whenNotMatchedInsertAll()
                .execute()
            )

        else:

            (
                df.write
                .format("delta")
                .mode("overwrite")
                .saveAsTable(target)
            )

    else:
        raise ValueError(
            f"Unsupported loading mode: {mode}"
        )

    print(
        f"Successfully processed {record_count} records"
    )


An important difference from our previous implementation: a MERGE can now only be performed if the merge key is also defined as the primary key in the Data Contract. Furthermore, duplicate keys in the source data are rejected before the MERGE is executed.


Modify main.py

Next step is to modify the main.py file.


import argparse
from pathlib import Path

from pyspark.sql import SparkSession

from transfer.config_loader import load_entity_config
from transfer.contract_validator import validate_dataframe
from transfer.processor import transfer_data


def main():

    parser = argparse.ArgumentParser()

    parser.add_argument(
        "--entity",
        required=True
    )

    args = parser.parse_args()

    spark = SparkSession.builder.getOrCreate()

    # Determine project root
    project_root = Path(__file__).resolve().parents[1]

    # Load entity configuration
    config_path = project_root / "config" / "entities.json"

    config = load_entity_config(
        config_path,
        args.entity
    )

    # Load associated data contract
    contract_path = (
        project_root / config["contract"]
    )

    import json

    with open(contract_path, "r") as file:
        contract = json.load(file)

    # Execute transfer with contract
    transfer_data(
        spark,
        config,
        contract
    )


if __name__ == "__main__":
    main()


This completes the chain. The Job only passes the entity; the rest is automatically retrieved from the configuration files.


Adjust the DAB configuration

We need to ensure that the contract files are included in the deployment. Open: databricks.yml and check whether your configuration looks like this:


bundle:
  name: databricks-transfer

include:
  - resources/*.yml

sync:
  include:
    - src/**
    - config/**
    - contracts/**
    - requirements.txt

targets:
  dev:
    default: true
    mode: development


This synchronizes the JSON contracts with the rest of your project. The existing resources/transfer_job.yml file does not need to be modified. The job will continue to receive only the 'entity' parameter.


Testing: first, a successful validation

Now, we will check whether everything is working. Open the terminal in Visual Studio Code.


Validate the Bundle

Now we can deploy the project. Open the terminal in Visual Studio Code and navigate to the root of your project.  Execute:


databricks bundle validate -t dev


Resulting in : 




And if everything is right : 


databricks bundle deploy -t dev


Next, you can start the job:


databricks bundle run transfer_job -t dev --params entity=customers


You can expect logging roughly like this:

Working directory: /Workspace/Users/xxxx@xxxx.com/.bundle/databricks-transfer/dev/files/src
Project root: /Workspace/Users/xxxx@xxxx.com/.bundle/databricks-transfer/dev/files
Reading source: /Volumes/main/demo/raw/customers.csv Target table: main.demo.customers Loading mode: overwrite Validating contract: customers Contract version: 1.0 Contract customers version 1.0 PASSED Validated records: 4 Successfully processed 4 records


The exact logging may vary slightly, but contract validation must succeed before the table is updated.


databricks bundle run transfer_job -t dev --params entity=customers_update


This dataset must also comply with the same contract. Next, check:


SELECT *
FROM main.demo.customers
ORDER BY customer_id;


You are expecting five customers.





Testing an incorrect Data Contract

Now, we will intentionally introduce an error. This is important to demonstrate that our Data Contract integration is actually working. In Visual Studio Code, create: data/customers_invalid.csv


With the following content:


customer_id,first_name,last_name,country
1001,Jan,Jansen,Netherlands
1002,,Peeters,Belgium
1003,Anna,Smith,United Kingdom


Upload het bestand naar:/Volumes/main/demo/raw/customers_invalid.csv


Next, temporarily add this entity to config/entities.json:

"customers_invalid": {
  "source": "/Volumes/main/demo/raw/customers_invalid.csv",
  "target": "main.demo.customers",
  "format": "csv",
  "mode": "overwrite",
  "contract": "contracts/customers.json"
}


Execute the folowing again :

databricks bundle deploy -t dev


And test the invalid schema :

databricks bundle run transfer_job -t dev --params entity=customers_invalid

Now the expected result is that the validation is failed


Task transfer_data FAILED:

ValueError: Data contract validation failed:

Column first_name cannot be NULL or empt


The job stops before the overwrite takes place. Consequently, your existing 'customers' table remains intact. This is a key advantage of this architecture: we validate before writing any data.


Now, this contract implementation is a custom solution. Next step is have a look at the Open Data Contract Standard (ODCS).


Hennie

dinsdag 10 november 2026

Databricks series : Loading strategies in Databricks (Part VII)

Introduction

This blogpost is the seventh in a series of blogposts about Databricks. In these series I showed and will show several features of Databricks with easy to understand examples. In this blogpost we'll describe a more configurable ingestion framework.


Currently, we have a Python script that reads a CSV file and writes it to a Delta table. Now we will take the next step in building a data engine.

We support three loading strategies:
  • Overwrite. The existing table is replaced by the new dataset. Suitable for full snapshots.
  • Append. New records are added to the existing table. Suitable for transactional data.
  • Merge. Existing records are updated, and new records are added. Suitable for changes to customer data.
For this, we will modify only one file: src/transfer/processor.py.


Extend the loading strategies

from delta.tables import DeltaTable

def transfer_data(spark, config):

    source = config["source"]
    target = config["target"]
    file_format = config["format"]
    mode = config["mode"]

    df = (
        spark.read
        .format(file_format)
        .option("header", "true")
        .option("inferSchema", "true")
        .load(source)
    )

    if mode == "overwrite":

        df.write.format("delta") \
            .mode("overwrite") \
            .saveAsTable(target)

    elif mode == "append":

        df.write.format("delta") \
            .mode("append") \
            .saveAsTable(target)

    elif mode == "merge":

        merge_key = config["merge_key"]

        if spark.catalog.tableExists(target):

            delta_table = DeltaTable.forName(spark, target)

            (
                delta_table.alias("target")
                .merge(
                    df.alias("source"),
                    f"target.{merge_key} = source.{merge_key}"
                )
                .whenMatchedUpdateAll()
                .whenNotMatchedInsertAll()
                .execute()
            )

        else:

            df.write.format("delta") \
                .mode("overwrite") \
                .saveAsTable(target)

    else:
        raise ValueError(f"Unsupported mode: {mode}")

    print(f"Successfully processed {df.count()} records")


Note: For a Merge, we add a 'merge_key' to the configuration. We must also ensure that the source data does not contain duplicate keys; otherwise, the Merge may fail.


Open `config/entities.json` and modify it as follows:


{
  "customers": {
    "source": "/Volumes/main/demo/raw/customers.csv",
    "target": "main.demo.customers",
    "format": "csv",
    "mode": "overwrite"
  },
  "customers_update": {
    "source": "/Volumes/main/demo/raw/customers_update.csv",
    "target": "main.demo.customers",
    "format": "csv",
    "mode": "merge",
    "merge_key": "customer_id"
  },
  "transactions": {
    "source": "/Volumes/main/demo/raw/transactions.csv",
    "target": "main.demo.transactions",
    "format": "csv",
    "mode": "append"
  }
}


The configuration now determines how the data is processed. You no longer need to modify the Python code when you want to use a different loading strategy for an entity.


Testing with modified customer data

To properly demonstrate the Merge functionality, we will create a second customers file containing modified data. We name the file : customers_update.csv


customer_id,first_name,last_name,country
1001,Jan,Jansen,Netherlands
1002,Pieter,Peeters,Belgium
1005,Sarah,Johnson,USA


We use two files: customers.csv


customer_id First name Last name    Country

1001         Jan                Jansen             Netherlands

1002         Piet                  Peeters            Belgium

1003         Anna          Smith              United Kingdom

1004         Marie          Dubois             France



And we have customers_update.csv :


customer_id First name Last name        Country

1001         Jan                 Jansen              Netherlands

1002         Pieter         Peeters               Belgium

1005         Sarah         Johnson            USA


After a MERGE, we expect the following:

  • 1001 remains unchanged.
  • 1002 is updated from Piet to Pieter.
  • 1003 remains.
  • 1004 remains.
  • 1005 is added.


Upload the file to Databricks





Deploying to Databricks

Now we can deploy the project. Open the terminal in Visual Studio Code and navigate to the root of your project.  Execute:


databricks bundle validate -t dev


Resulting in : 




And if everything is right : 


databricks bundle deploy -t dev


Next, you can start the job:


databricks bundle run transfer_job -t dev --params entity=customers


Check in Databricks SQL 


SELECT *
FROM main.demo.customers
ORDER BY customer_id


This will give the following results :


Second run: customers_update

Now for the interesting part.  We run the same Job again, but with a different parameter:


databricks bundle run transfer_job -t dev --params entity=customers_update


Check in Databricks SQL :

SELECT *
FROM main.demo.customers
ORDER BY customer_id


Resulting in :



And so it succeeds quite well. Piet has been changed to Pieter and we have a new record. 


Now to be complete, lets test the append.



databricks bundle run transfer_job -t dev --params entity=transactions 




and again : 


databricks bundle run transfer_job -t dev --params entity=transactions 

And we test the table we have duplicate records in the table





Thats it!


Hennie

zondag 8 november 2026

Databricks series : A configrable framework in Databricks (Part VI)

Introduction

This blogspost is the sixth in a series of blogposts about databricks. In these series I showed and will show several features of Databricks with easy to understand examples. In this blogpost we'll describe a more configurable ingestion framework. we'll replace the parameters in the job with a configrable file.

Currently, we have a Python script that reads a CSV file and writes it to a Delta table.

We want to extend this so that we no longer need to hardcode the source, target table, and loading strategy for each dataset within the Python code.

Instead, we will create a single configuration file that defines what needs to be done. The major advantage is that if you add a new entity tomorrow, you essentially only need to expand the configuration.

The new project structure

First, lets open the existing project, 'databricks-transfer', in Visual Studio Code. We are going to add a new directory called 'config'. We will add a file entities.json in there.

The project should now look like this:

databricks-transfer/
│
├── databricks.yml
│
├── resources/
│   └── transfer_job.yml
│
├── src/
│   ├── __init__.py
│   └── main.py
│
├── config/
│   └── entities.json
│
├── data/
│   ├── customers.csv
│   └── transactions.csv
│
├── requirements.txt
└── README.md


This file contains the configuration for all the entities we want to process.


{
  "customers": {
    "source": "/Volumes/main/demo/raw/customers.csv",
    "target": "main.demo.customers",
    "format": "csv",
    "mode": "overwrite"
  },
  "transactions": {
    "source": "/Volumes/main/demo/raw/transactions.csv",
    "target": "main.demo.transactions",
    "format": "csv",
    "mode": "append"
  },
  "products": {
    "source": "/Volumes/main/demo/raw/products.json",
    "target": "main.demo.products",
    "format": "json",
    "mode": "overwrite"
  }
}


Let’s take a look at what we are doing here.

  • customers. This is the name of the entity
  • source. The location of the source file
  • target. The target table in Unity Catalog
  • format. The file format
  • mode. The loading strategy

We have now defined three datasets. Note: 'products.json' is included as an example for a future expansion. You do not need to create that file yet. Why are we doing this? Suppose you want to add a fourth entity later, for example, 'suppliers'. In that case, you simply add an extra configuration block. You don’t need to write a new Python function.


Create a separate Python module for the configuration

Now, we will actually read the configuration. Open the 'src' directory. Create a new directory inside it:


src/
├── __init__.py
├── main.py
└── transfer/
    ├── __init__.py
    └── config_loader.py


Create the file src/transfer/config_loader.py. Add the following to it:


import json


def load_entity_config(config_path, entity):

    with open(config_path, "r") as file:
        config = json.load(file)

    if entity not in config:
        raise ValueError(
            f"Unknown entity: {entity}"
        )

    return config[entity]


What does this function do?

1. Opens the JSON configuration file.

2. Reads the content into a Python dictionary.

3. Looks up the requested entity.

4. Returns the corresponding configuration.

For example, when the 'customers' job is passed, we get:


{
    "source": "/Volumes/main/demo/raw/customers.csv",
    "target": "main.demo.customers",
    "format": "csv",
    "mode": "overwrite"
}


Create a separate Python module for data transfer

We will move the actual data transfer logic out of main.py. Create a new file: 'src/transfer/processor.py'. Add the following to it:


from pyspark.sql import SparkSession


def transfer_data(spark, config):

    source = config["source"]
    target = config["target"]
    file_format = config["format"]
    mode = config["mode"]

    print(f"Reading source: {source}")
    print(f"Target table: {target}")
    print(f"Format: {file_format}")
    print(f"Mode: {mode}")

    # Read source file
    df = (
        spark.read
        .format(file_format)
        .option("header", "true")
        .load(source)
    )

    # Write to Delta table
    (
        df.write
        .format("delta")
        .mode(mode)
        .saveAsTable(target)
    )

    print(f"Processed {df.count()} records")


We have now divided the responsibilities:

  • config_loader.py: determines what needs to be done.
  • processor.py: carries out the data transfer.

This is an important step in developing maintainable Python applications.


Modify main.py

Now, let’s bring everything together. Open src/main.py. Replace the existing content with:


import argparse
from pathlib import Path

from pyspark.sql import SparkSession

from transfer.config_loader import load_entity_config
from transfer.processor import transfer_data


def main():

    parser = argparse.ArgumentParser()

    parser.add_argument(
        "--entity",
        required=True
    )

    args = parser.parse_args()

    spark = SparkSession.builder.getOrCreate()

    # Configuration file location
    config_path = (
        Path(__file__).resolve().parents[1]
        / "config"
        / "entities.json"
    )

    # Load entity configuration
    config = load_entity_config(
        config_path,
        args.entity
    )

    # Execute data transfer
    transfer_data(spark, config)


if __name__ == "__main__":
    main()


What has changed? Previously, we provided three parameters:

entity
source_path
target_table


But now, we only provide:

entity


The rest is retrieved from entities.json. That is exactly what we want to achieve!


Ensure that DAB also deploys the configuration file

This is an important step that you must not overlook. Your Python code is located in `src`, while our JSON file is in `config`. We need to ensure that both directories are included in the deployment. Open `databricks.yml`. Modify its content:


bundle:
  name: databricks-transfer

include:
  - resources/*.yml

sync:
  include:
    - src/**
    - config/**
    - requirements.txt

targets:
  dev:
    default: true
    mode: development


Now, both the Python files and the JSON configuration file are synchronized.


Adjust the Databricks Job

Since we are now passing only the entity, we also need to update our Job configuration. Open: resources/transfer_job.yml and replace the contents with:


resources:
  jobs:
    transfer_job:

      name: python-data-transfer

      parameters:
        - name: entity
          default: customers

      tasks:
        - task_key: transfer_data

          spark_python_task:
            python_file: ../src/main.py

            parameters:
              - --entity
              - "{{job.parameters.entity}}"

          environment_key: default

      environments:
        - environment_key: default
          spec:
            environment_version: "2"


Our Job now has only one parameter: `entity`. When running it, you can choose from the following options: 'customers', 'transactions,  'products'. The Python code and the Job itself do not need to be modified for this.


Step 8 – Deploying to Databricks

Now we can deploy the project. Open the terminal in Visual Studio Code and navigate to the root of your project.  Execute:


databricks bundle validate -t dev


Resulting in : 




And if everything is right : 


databricks bundle deploy -t dev


Next, you can start the job:


> databricks bundle run transfer_job -t dev

Run URL: https://dbc-xxxxxx-d21b.cloud.databricks.com/jobs/846415909479637/runs/750647492568451?o=1134000616576115 2026-11-08 13:31:06 "[dev xxxxx] python-data-transfer" RUNNING 2026-11-08 13:31:47 "[dev xxxxx] python-data-transfer" TERMINATED SUCCESS Output: ======= Task transfer_data: Processing entity: customers ======= Task transfer_data: Processing entity: customers Successfully processed 4 records


The default value is customers.




Do you want to perform transactions? You can override the parameter when starting:

databricks bundle run transfer_job -t dev \
  --params entity=transactions


The parameter is then passed to main.py.




Important: ensure that the configuration files are actually available at the deployed path. In the example above, we use a path relative to main.py; therefore, the directory structure must be preserved during a DAB deployment.

The tables are now created : 






That's it!


Hennie

donderdag 5 november 2026

Databricks series : Execute the job in Databricks (Part V)

Introduction

This blogspost is the fifth in a series of blogposts. This blogpost describes the execution of the job in Databricks. In order to do that we need to do one thing and that is we need to place the files at the raw folder. Next is to execute job and as the final part we examine the results.

Create a volume in databricks

First we need to create a folder structure at the location :  /Volumes/main/demo/raw/


Open a notebook in Databricks and execute the following code :

%sql
CREATE CATALOG IF NOT EXISTS main;

CREATE SCHEMA IF NOT EXISTS main.demo;

CREATE VOLUME IF NOT EXISTS main.demo.raw;


This means : 

main                       ← Catalog

 └── demo            ← Schema

      └── raw          ← Volume


Upload the files to the location :






And here are the files stored at the locations. Everything is ready to execute the job.



Uitvoeren van de job

You can upload the files from our local data folder for this purpose.

Subsequently, we can run the same job twice.


entity = customers

source_path = /Volumes/main/demo/raw/customers.csv

target_table = main.demo.customers


entity = transactions

source_path  = /Volumes/main/demo/raw/transactions.csv

target_table = main.demo.transactions


The result should be: 

Job         Delta-table                 Records

Run 1 main.demo.customers 4

Run 2 main.demo.transactions 4


So, we have used one Python script and one Job for two different data streams.


Open de job and press on Run Now




The job is running now



Here you find information which code is executed in the job.



More information is available in the Job execution window




And in the unity catalog we see the following information.



Now execute the job with other parameters!



Enter the parameters and execute again.



And here information about the second run of the job



And this results in two tables with customer and transaction information.




Hennie

woensdag 4 november 2026

Databricks series : Deployment from Visual Studio Code (Part IV)

Introduction 

I've written a couple of blogposts about a small project to create a Databricks project. This blogpost is the fourth in the series. Now the fun part: we don’t have to manually create the Job in the Databricks interface. We’ll use the Databricks CLI for this!

Previous blogposts : 


Deploy the code

Open a terminal window in Visual Studio Code and authenticate with databricks auth login


 > databricks auth login --host https://dbc-xxxxxx-d21b.cloud.databricks.com/
--profile DBprofile


Validate the bundle

Next step is validate the code with databricks bundle validate


> databricks bundle validate

results in :

Name: DatabricksProject Target: dev Workspace: Host: https://dbc-xxxxxx-d21b.cloud.databricks.com User: xxxxxx@gmail.com Path: /Workspace/Users/xxxxx@gmail.com/.bundle/DatabricksProject/dev


Deploy the code

And deploy the code with databricks bundle deploy

> databricks bundle deploy

results in :

Uploading bundle files to /Workspace/Users/xxxxr@gmail.com/.bundle/DatabricksProject/dev/files... Created jobs.transfer_job Files: 1 uploaded, 0 deleted Resources: 1 created, 0 changed, 0 deleted, 0 unchanged


Databricks now deploys:

  • The Python code.
  • The job configuration.
  • The required project files.


Where is everything in Databricks?

In your Databricks Workspace, go to: Workspace → Users → your user → .bundle

There, you will typically find a structure such as:



Job-configuration

The Job itself is not necessarily found as a YAML file within the Workspace.

Go to: Workflows → Jobs & Pipelines

Search for: python-data-transfer

There, you can view:

  • The Job configuration.
  • The configured parameters.
  • The Python Script Task.
  • The execution history.
  • The logs and results.


The YAML configuration remains part of your local DAB project in Visual Studio Code. Databricks uses that configuration to create or update the Job.



So that's it. The code is deployed to Databricks with DAB


Hennie