Databricks Notebook → Lakehouse Migration Guide: Retail Data Medallion Pipeline
If your data engineering pipeline runs on Databricks Notebooks, the migration effort to Singdata Lakehouse Studio is lower than you might expect. Databricks' PySpark DataFrame API — select, filter, join, withColumn, when, Window functions — has identical syntax in ZettaPark. Changes are limited to just 5 mechanical substitutions: import paths, session acquisition method, table path prefix, one API casing difference, and replacing dbutils.notebook.run() with Studio task dependencies.
This article validates this with a real project: a Databricks-based retail data Medallion pipeline (Bronze → Silver → Gold three-layer architecture, 14 Notebooks, 81 code cells) fully migrated to Singdata Lakehouse, offering three migration options, all 20 automated validations passing.
Full code on GitHub: databricks2lakehouse-bootcamp
Source Project
databricks2lakehouse-bootcamp is forked from DataWithBaraa/databricks_bootcamp_2026 (⭐335). The original tech stack is Databricks + PySpark + Delta Lake + Unity Catalog. The project implements a complete retail data warehouse from dual-source CRM/ERP ingestion to a star schema, covering 18,484 customers, 397 products, and 60,398 sales records, with complete data cleansing, type conversion, code mapping, and dimensional modeling.
Migrated code is in the 03_lakehouse/ directory, comparable file-by-file with 01_source/.
Conclusion First
You don't need to rewrite any business logic, or retrain your team. All 5 changes are mechanical substitutions.
| Change | Effort | Notes |
|---|---|---|
| Import path replacement | Very low | pyspark.sql → clickzetta.zettapark, global search-replace |
| Session acquisition method | Very low | spark (global injection) → Studio tasks use clickzetta_dbutils, local use Session.builder.configs({}) |
| Table path prefix | Very low | workspace.bronze.X → bronze.X, remove catalog prefix |
| StructField casing | Very low | field.dataType → field.datatype (ZettaPark API difference) |
| Orchestration method | Low | dbutils.notebook.run(nb) → Studio task dependencies (DAG) |
select, filter, join, withColumn, when, coalesce, trim, regexp_replace, to_date, cast, isNotNull, Window functions, ROW_NUMBER() — these core data engineering operations have identical syntax and require no changes.
Tech Stack Comparison
| Databricks Notebook | Lakehouse Studio Task | |
|---|---|---|
| Compute engine | Apache Spark (Databricks) | Singdata Lakehouse |
| DataFrame API | PySpark (pyspark.sql) | ZettaPark (clickzetta.zettapark) |
| Session acquisition | spark (Databricks global injection) | clickzetta_dbutils.get_active_lakehouse_engine() |
| Table naming | workspace.bronze.crm_cust_info (3-level) | bronze.crm_cust_info (2-level) |
| File path | /Volumes/workspace/bronze/raw_sources/... | vol://bronze.raw_sources/... |
| StructField | field.dataType | field.datatype |
| SQL execution | spark.sql(q) executes immediately | session.sql(q).collect() triggers execution |
| Notebook chaining | dbutils.notebook.run(nb, timeout_seconds=0) | Studio task dependencies (--deps parameter) |
| Scheduling orchestration | Databricks Jobs | Studio task DAG |
Project Background
Data comes from a bicycle retailer's dual-source CRM + ERP system, including 6 CSV files:
| Data Source | Table Name | Row Count | Notes |
|---|---|---|---|
| CRM | crm_cust_info | 18,494 | Customer info (includes dirty data and inconsistent casing) |
| CRM | crm_prd_info | 397 | Product info (includes category ID encoded in product key) |
| CRM | crm_sales_details | 60,398 | Sales transactions (includes yyyyMMdd format dates) |
| ERP | erp_cust_az12 | 18,484 | Customer master data (includes NAS prefix, future dates) |
| ERP | erp_loc_a101 | 18,484 | Customer addresses (includes hyphens and country code abbreviations) |
| ERP | erp_px_cat_g1v2 | 37 | Product categories (includes YES/NO maintenance flags) |
Medallion architecture in three layers:
- Bronze: Raw CSV → 6 Delta tables (no transformation)
- Silver: Cleansing + normalization → 6 wide tables (trim, type conversion, enum mapping, column renaming)
- Gold: Star schema → dim_customers + dim_products + fact_sales
The original project has 14 Databricks Notebooks (81 code cells), chained via dbutils.notebook.run().
Migration Steps
Step 1: Replace Import Paths
Mechanical global replacement, no logic changes:
Step 2: Replace Session Acquisition Method
Databricks injects spark into every Notebook without requiring explicit creation. Studio tasks obtain it via clickzetta_dbutils, also without needing to manage passwords or connection parameters:
Step 3: Remove Catalog Prefix from Table Paths
Unity Catalog uses three-level naming (catalog.schema.table). Lakehouse only requires two levels within a single workspace:
Step 4: Fix StructField Casing
This is an API difference discovered in testing, affecting all code that iterates field types with df.schema.fields:
Step 5: Replace dbutils.notebook.run() with Studio Task Dependencies
The original project chains 14 Notebooks via dbutils.notebook.run(), which maps directly to Studio task DAG:
Execute DAG (equivalent to the original orchestration notebook's chained calls):
Studio Task DAG After Migration
The original Databricks project used 2 orchestration notebooks chained in series. After migration, the Studio platform automatically manages dependencies and parallelism:
The original Databricks ran 6 silver notebooks sequentially; Studio runs 6 silver tasks in parallel — same logic, shorter overall runtime.
Fully Compatible Parts
The following code has identical syntax on both sides, validated by testing — no changes needed:
E2E Validation Results
Tested on AWS Singapore instance (aws_singapore_prod), 20/20 all passed:
| Check | Expected | Result |
|---|---|---|
| bronze.crm_cust_info | 18,494 | ✅ |
| bronze.crm_prd_info | 397 | ✅ |
| bronze.crm_sales_details | 60,398 | ✅ |
| bronze.erp_cust_az12 | 18,484 | ✅ |
| bronze.erp_loc_a101 | 18,484 | ✅ |
| bronze.erp_px_cat_g1v2 | 37 | ✅ |
| silver.crm_customers | 18,490 | ✅ |
| silver.crm_products | 397 | ✅ |
| silver.crm_sales | 60,398 | ✅ |
| silver.erp_customers | 18,484 | ✅ |
| silver.erp_customer_location | 18,484 | ✅ |
| silver.erp_product_category | 37 | ✅ |
| gold.dim_customers | 18,490 | ✅ |
| gold.dim_products | 397 | ✅ |
| gold.fact_sales | 89,833 | ✅ |
| Total sales amount | 43,538,800 | ✅ |
| Rows with negative sales | 5 (raw data) | ✅ |
| Distinct customer count | 18,484 | ✅ |
| Distinct product SKUs | 295 | ✅ |
| Rows with null order date | 24 (raw data format issue) | ✅ |
Three Migration Options Compared
This project provides three options suited to different team backgrounds and deployment needs:
| Option | File Location | Session Method | Change Volume | Use Case |
|---|---|---|---|---|
| A. ZettaPark Local | 03_lakehouse/{bronze,silver,gold}/ | Session.builder.configs({}).create() | ~5% | Local development, CI/CD debugging |
| B. Pure SQL | 03_lakehouse/sql/ | Not needed | Full rewrite (logic unchanged) | SQL-first teams, Studio SQL tasks |
| C. Studio Task | 03_lakehouse/tasks/ | clickzetta_dbutils | ~5% | Production deployment (recommended) |
All three options produce identical results — row counts, metrics, and data are consistent. Option C is the recommended path for production, directly mapping to the Databricks Notebook + Jobs architecture.
Notes
field.datatypecasing: ZettaPark'sStructFieldproperty isdatatype(all lowercase); PySpark usesdataType(camelCase). This is the only non-mechanical API difference. Search for all uses ofschema.fieldsand update individually.clickzetta_dbutilstemplate for Studio tasks: Theget_active_lakehouse_engine()call returns the current task's connection info; parse the URL to build the Session. This template code is fixed and can be extracted to a shared module for reuse.- Volume path format: Databricks
/Volumes/catalog/schema/volume/path→ ZettaParkvol://schema.volume/path, note the removal of the catalog level. .format("delta")not needed: ZettaPark'ssaveAsTable()writes in Lakehouse native format by default; no explicit format specification required.- Silver row count difference (18,494 → 18,490): The CRM source data contains 4 records with NULL customer_id, filtered during Silver cleansing. This is expected behavior.
Related Documentation
ZettaPark DataFrame API
- ZettaPark Quick Start: Session creation, DataFrame basics
- ZettaPark DataFrame API Guide: Full reference for select, filter, join, withColumn, etc.
- ZettaPark Common Functions Reference: Usage of trim, when, regexp_replace, Window, etc.
- ZettaPark Data Engineering in Practice: Complete ETL pipeline examples
Studio Tasks and Orchestration
- Lakehouse Studio Introduction: Studio platform concepts and architecture
- Task Development and Scheduling: Creating, developing, and scheduling Studio tasks
- Task Scheduling Dependencies:
--depsparameter for configuring task DAG - Python Task: Creating and running Python tasks in Studio
- Studio Task Development and Operations (cz-cli): Managing Studio tasks via cz-cli command line
Other Migration Guides
- PySpark → ZettaPark Migration Guide (Formula 1): PySpark DataFrame API query-by-query migration
- Snowpark → ZettaPark Migration Guide: Snowflake Python DataFrame migration
- Spark Migration Guide: Spark ecosystem migration overview and common issues
- SQL Compatibility Reference: Cross-platform SQL syntax differences
