Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Important
Some or all of this functionality is available as part of a preview release. The content and the functionality are subject to change.
This example shows how to load a small Excel workbook into a dedicated bronze lakehouse and then merge the data into an existing wealth screening model of the Nonprofit data solutions.
This article includes the sample workbook data inline so you can recreate the workbook in Excel and use it as-is with the notebook examples.
This example assumes that the workbook resolves packaged sample Contact and Account source-system IDs to existing Constituent rows, and then updates the existing packaged sample WealthScreening rows for those same constituents.
The workbook is aligned to the packaged sample data in this repo. Its EntitySourceSystemId values are source-system IDs from packaged sample Contact and Account rows, not ConstituentId values.
The workbook updates existing WealthScreening rows and maps workbook codes onto these existing silver lookup tables:
WealthScreeningCapacityRangeWealthScreeningConstituentRating
What already exists in Nonprofit data solutions
This pattern assumes the base package already provides these assets:
- the
WealthScreening,WealthScreeningCapacityRange, andWealthScreeningConstituentRatingsilver tables - the downstream gold
FactWealthScreeningprocess - the packaged sample
Contact,Account,Constituent,WealthScreening, andSourceSystemIdMappingdata used by the workbook
Data flow
Excel workbook (Excel Bronze Files)
-> Excel Bronze staging tables
-> Silver Source registration (WealthScreeningExcel)
-> Existing Silver lookup tables:
WealthScreeningCapacityRange
WealthScreeningConstituentRating
-> Excel-source mapping rows for those lookups:
SourceSystemIdMapping
-> Existing Silver fact-like table updated in place:
WealthScreening
-> Existing Gold process:
FactWealthScreening
When to use this pattern
Use this pattern when:
- The workbook is a small external enrichment source.
- The workbook references constituents that already exist in silver.
- The workbook should behave as its own source system with its own
SourceId. - The workbook should update existing
WealthScreeningrows instead of creating parallel rows.
Don't use this pattern if the workbook is expected to create brand new constituents. That scenario is a broader source-onboarding scenario.
Prerequisites
- The packaged sample
Contact,Account,Constituent,WealthScreening, andSourceSystemIdMappingdata is already installed. - You can create a dedicated bronze lakehouse for the Excel source.
- You have access to create and run notebooks in the Fabric workspace.
If you're following this article against the packaged sample data, keep the example EntitySourceSystemId values as-is. They already point to sample Contact and Account source-system IDs that the solution installs.
Only replace the example EntitySourceSystemId values if you adapt this pattern to your own environment.
If you use your own workbook, also adjust the existing-row lookup logic in the prepare and sync sections so the notebook updates the correct existing WealthScreening row.
Create the workbook
Create a new workbook in Excel, keep the sheet names exactly as shown here, paste each block into cell A1 of the matching sheet, and save the file as WealthScreeningExcel-Example.xlsx.
Sheet: CapacityRange
CapacityRangeCode CapacityRangeName
LOW Under 10K
MEDIUM 10K-100K
HIGH 100K+
MAJOR 500K+
Sheet: ConstituentRating
ConstituentRatingCode ConstituentRatingName
LOW Low
MEDIUM Medium
HIGH High
EXCEPTIONAL Exceptional
Sheet: WealthScreening
WealthScreeningExcelId EntityType EntitySourceSystemId CapacityRangeCode ConstituentRatingCode LastScreeningDate
WS-0001 contact 7d0db8e9-a964-4f25-9390-3095e5beaaf9 HIGH HIGH 2026-03-01
WS-0002 contact 6dd2ca43-dc5f-44f0-b675-30729ce6e996 MEDIUM LOW 2026-03-02
WS-0003 account c0c27239-c493-4246-8884-9cafa35f2df8 MAJOR EXCEPTIONAL 2026-03-03
WS-0004 contact 3f41080b-6333-437f-8ff7-20c01871cee4 LOW MEDIUM 2026-03-04
WS-0005 account 1fbea1f2-d901-4505-b2d1-c05fa95ee7eb HIGH HIGH 2026-03-05
WS-0006 contact 313bda3f-959d-4e76-bd5f-d893ecb156c9 MEDIUM MEDIUM 2026-03-06
Upload the workbook
Upload the workbook you created to a dedicated bronze lakehouse for the Excel source, such as Fundraising_Excel_BR.
The package doesn't include a generic shared bronze lakehouse artifact. The packaged lakehouses are source-specific, such as Fundraising_SalesforceNPSP_BR, plus the shared Fundraising_SL and Fundraising_GD lakehouses.
Recommended upload path:
Files/WealthScreeningExcel-Example.xlsx
In the Fabric UI:
- Open the Excel bronze lakehouse.
- Open the Files pane.
- Upload
WealthScreeningExcel-Example.xlsx.
If you change the file name, update the path in the notebook examples.
The examples in this article use /lakehouse/default/Files/WealthScreeningExcel-Example.xlsx, which resolves against the lakehouse attached to the notebook as the default lakehouse.
Create a dedicated notebook
Create a notebook such as Fundraising_Excel_Bronze_Silver_Transform and attach the Excel bronze lakehouse as the notebook's default lakehouse.
Packaged notebooks get their lakehouse metadata during installation, but a manually created notebook doesn't. That difference is why you need to attach the default lakehouse for the custom Excel notebook.
Important
You must attach a default lakehouse. Without a default lakehouse, Spark SQL statements that use partial namespaces fail with UnsupportedOperationException: No default context found.
In this step, create the notebook, attach its default lakehouse, and define the cell layout for the remaining implementation steps.
Use the notebook cells in this order:
| Cell | Purpose |
|---|---|
| Cell 1 | Run Fundraising_Config |
| Cell 2 | Add imports and constants |
| Cell 3 | Register the Excel source |
| Cell 4 | Load workbook sheets into bronze staging tables |
| Cell 5 | Prepare existing WealthScreening rows for update |
| Cell 6 | Map CapacityRangeCode |
| Cell 7 | Map ConstituentRatingCode |
| Cell 8 | Resolve workbook rows to existing constituents |
| Cell 9 | Sync WealthScreening |
If a step contains multiple code blocks separated by explanation, keep them in the same target cell unless the article explicitly says otherwise.
Add the notebook setup cells
Add this code to Cell 1:
%run {PREFIX}_Fundraising_Config
Add this code to Cell 2:
import pandas as pd
from pyspark.sql import functions as F
WEALTH_SCREENING_EXCEL_SOURCE_ID = '5d4dd0d4-1df9-4f69-a0ee-4f0c0b5f6d2d'
WEALTH_SCREENING_EXCEL_SOURCE_NAME = 'WealthScreeningExcel'
PACKAGED_SAMPLE_SOURCE_ID = 'bb05204b-9818-4e53-9036-6b29b936b0fb'
excel_path = '/lakehouse/default/Files/WealthScreeningExcel-Example.xlsx'
Fundraising_Config provides reusable helpers for the silver lakehouse and source mapping logic.
PACKAGED_SAMPLE_SOURCE_ID is the source identifier for the packaged sample data used by the existing Contact, Account, Constituent, WealthScreening, and lookup rows.
Ensure that the notebook is attached to a default lakehouse in the Fabric notebook UI. Any attached lakehouse is enough to establish the default Spark context; the examples in this article assume the Excel bronze lakehouse is that default.
excel_path = '/lakehouse/default/Files/WealthScreeningExcel-Example.xlsx' only works when the workbook you created exists in the attached default lakehouse under Files.
Keep the path as /lakehouse/default/... when you use pd.read_excel(). The packaged sample-data notebook uses file:/lakehouse/default/... for Spark and mssparkutils, but the pandas example in this article shouldn't use the file: prefix.
Use unqualified table names for bronze staging tables in this notebook because the notebook is already attached to the bronze lakehouse.
This notebook is intentionally separate from the packaged source transforms because Excel processing in this example isn't source-system watermark sync.
Register the new source
Add this code to Cell 3 to register the workbook as its own source in silver.
Use the WEALTH_SCREENING_EXCEL_SOURCE_ID value from the setup cell as the stable source GUID for this Excel integration across environments.
spark.sql(f"""
MERGE INTO {silver_lakehouse_name}.Source AS target
USING (
SELECT
'{WEALTH_SCREENING_EXCEL_SOURCE_ID}' AS SourceId,
current_timestamp() AS CreatedDate,
current_timestamp() AS ModifiedDate,
'{WEALTH_SCREENING_EXCEL_SOURCE_NAME}' AS Name
) AS source
ON target.SourceId = source.SourceId
WHEN MATCHED THEN UPDATE SET
target.ModifiedDate = source.ModifiedDate,
target.Name = source.Name
WHEN NOT MATCHED THEN INSERT (
SourceId, CreatedDate, ModifiedDate, Name
) VALUES (
source.SourceId, source.CreatedDate, source.ModifiedDate, source.Name
)
""")
You need this source registration so the workbook mappings stay separate from the packaged sample source mappings.
This separation matters because:
- workbook-driven lookup mappings must carry the Excel
SourceId SourceSystemIdMappingmust distinguish workbook keys from packaged sample keys- cross-source joins should always resolve through
SourceSystemIdMapping
Load the workbook into bronze staging tables
Add this code to Cell 4.
capacity_range_pd = pd.read_excel(excel_path, sheet_name='CapacityRange')
constituent_rating_pd = pd.read_excel(excel_path, sheet_name='ConstituentRating')
wealth_screening_pd = pd.read_excel(excel_path, sheet_name='WealthScreening')
capacity_range_df = (
spark.createDataFrame(capacity_range_pd)
.select(
F.col('CapacityRangeCode').cast('string').alias('CapacityRangeCode'),
F.col('CapacityRangeName').cast('string').alias('CapacityRangeName')
)
)
constituent_rating_df = (
spark.createDataFrame(constituent_rating_pd)
.select(
F.col('ConstituentRatingCode').cast('string').alias('ConstituentRatingCode'),
F.col('ConstituentRatingName').cast('string').alias('ConstituentRatingName')
)
)
wealth_screening_df = (
spark.createDataFrame(wealth_screening_pd)
.select(
F.col('WealthScreeningExcelId').cast('string').alias('WealthScreeningExcelId'),
F.lower(F.col('EntityType').cast('string')).alias('EntityType'),
F.col('EntitySourceSystemId').cast('string').alias('EntitySourceSystemId'),
F.col('CapacityRangeCode').cast('string').alias('CapacityRangeCode'),
F.col('ConstituentRatingCode').cast('string').alias('ConstituentRatingCode'),
F.to_date('LastScreeningDate').alias('LastScreeningDate')
)
)
capacity_range_df.write.mode('overwrite').format('delta').saveAsTable(
'WealthScreeningExcel_CapacityRange'
)
constituent_rating_df.write.mode('overwrite').format('delta').saveAsTable(
'WealthScreeningExcel_ConstituentRating'
)
wealth_screening_df.write.mode('overwrite').format('delta').saveAsTable(
'WealthScreeningExcel_WealthScreening'
)
Use overwrite semantics in this example because the workbook is treated as a small batch source.
Recommended bronze staging table shapes:
WealthScreeningExcel_CapacityRange:CapacityRangeCode,CapacityRangeNameWealthScreeningExcel_ConstituentRating:ConstituentRatingCode,ConstituentRatingNameWealthScreeningExcel_WealthScreening:WealthScreeningExcelId,EntityType,EntitySourceSystemId,CapacityRangeCode,ConstituentRatingCode,LastScreeningDate
If you need true incremental processing, add file versioning or change-tracking logic before you adapt this notebook.
Prepare existing WealthScreening rows for update
Add this helper to Cell 5.
def get_existing_wealth_screening():
return (
get_silver_table('WealthScreening')
.select(
F.col('WealthScreeningId'),
F.col('ConstituentId'),
F.col('CreatedDate').alias('ExistingCreatedDate')
)
)
This helper lets the notebook reuse the existing WealthScreening rows instead of creating new rows for the same constituent.
This separation matters because:
- The sample data already contains one
WealthScreeningrow per constituent. - Matching by
ConstituentIdkeeps the total row count stable across reruns.
Map CapacityRangeCode to the existing lookup table
Add this code to Cell 6.
capacity_range_bridge = spark.createDataFrame(
[
('LOW', 'Under $10,000'),
('MEDIUM', '$10,000 - $49,999'),
('HIGH', '$100,000 - $499,999'),
('MAJOR', '$500,000 - $999,999')
],
['SourceSystemId', 'TargetName']
)
existing_capacity_ranges = (
get_silver_table('WealthScreeningCapacityRange')
.filter(F.col('SourceId') == F.lit(PACKAGED_SAMPLE_SOURCE_ID))
.select(
F.col('Name').alias('TargetName'),
F.col('WealthScreeningCapacityRangeId')
)
)
capacity_range_stage = (
spark.table('WealthScreeningExcel_CapacityRange')
.select(F.col('CapacityRangeCode').cast('string').alias('SourceSystemId'))
.dropna(subset=['SourceSystemId'])
.dropDuplicates(['SourceSystemId'])
.join(capacity_range_bridge, 'SourceSystemId', 'left')
.join(existing_capacity_ranges, 'TargetName', 'left')
.select(
F.col('WealthScreeningCapacityRangeId'),
F.current_timestamp().alias('CreatedDate'),
F.current_timestamp().alias('ModifiedDate'),
F.lit(WEALTH_SCREENING_EXCEL_SOURCE_ID).alias('SourceId'),
F.col('SourceSystemId')
)
)
capacity_range_mapping_stage = (
capacity_range_stage
.select(
F.col('WealthScreeningCapacityRangeId').alias('SilverRecordId'),
F.col('SourceSystemId'),
F.col('SourceId'),
F.lit('WealthScreeningCapacityRange').alias('SourceTable')
)
)
invalid_capacity_ranges = capacity_range_stage.filter(
F.col('WealthScreeningCapacityRangeId').isNull()
)
if not invalid_capacity_ranges.rdd.isEmpty():
display(invalid_capacity_ranges)
raise ValueError('CapacityRange sheet contains codes that are not mapped to an existing WealthScreeningCapacityRange row.')
capacity_range_mapping_stage.createOrReplaceTempView('capacity_range_mapping_stage')
spark.sql(f"""
MERGE INTO {silver_lakehouse_name}.SourceSystemIdMapping AS target
USING capacity_range_mapping_stage AS source
ON target.SourceId = source.SourceId
AND target.SourceSystemId = source.SourceSystemId
AND target.SourceTable = source.SourceTable
WHEN MATCHED THEN UPDATE SET
target.SilverRecordId = source.SilverRecordId
WHEN NOT MATCHED THEN INSERT (SilverRecordId, SourceSystemId, SourceId, SourceTable)
VALUES (source.SilverRecordId, source.SourceSystemId, source.SourceId, source.SourceTable)
""")
This step preserves the packaged sample mappings and adds a second set of Excel-owned mappings for the same lookup rows.
Map ConstituentRatingCode to the existing lookup table
Add this code to Cell 7.
constituent_rating_bridge = spark.createDataFrame(
[
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('EXCEPTIONAL', 'Exceptional')
],
['SourceSystemId', 'TargetName']
)
existing_constituent_ratings = (
get_silver_table('WealthScreeningConstituentRating')
.filter(F.col('SourceId') == F.lit(PACKAGED_SAMPLE_SOURCE_ID))
.select(
F.col('Name').alias('TargetName'),
F.col('WealthScreeningConstituentRatingId')
)
)
constituent_rating_stage = (
spark.table('WealthScreeningExcel_ConstituentRating')
.select(F.col('ConstituentRatingCode').cast('string').alias('SourceSystemId'))
.dropna(subset=['SourceSystemId'])
.dropDuplicates(['SourceSystemId'])
.join(constituent_rating_bridge, 'SourceSystemId', 'left')
.join(existing_constituent_ratings, 'TargetName', 'left')
.select(
F.col('WealthScreeningConstituentRatingId'),
F.current_timestamp().alias('CreatedDate'),
F.current_timestamp().alias('ModifiedDate'),
F.lit(WEALTH_SCREENING_EXCEL_SOURCE_ID).alias('SourceId'),
F.col('SourceSystemId')
)
)
constituent_rating_mapping_stage = (
constituent_rating_stage
.select(
F.col('WealthScreeningConstituentRatingId').alias('SilverRecordId'),
F.col('SourceSystemId'),
F.col('SourceId'),
F.lit('WealthScreeningConstituentRating').alias('SourceTable')
)
)
invalid_constituent_ratings = constituent_rating_stage.filter(
F.col('WealthScreeningConstituentRatingId').isNull()
)
if not invalid_constituent_ratings.rdd.isEmpty():
display(invalid_constituent_ratings)
raise ValueError('ConstituentRating sheet contains codes that are not mapped to an existing WealthScreeningConstituentRating row.')
constituent_rating_mapping_stage.createOrReplaceTempView('constituent_rating_mapping_stage')
spark.sql(f"""
MERGE INTO {silver_lakehouse_name}.SourceSystemIdMapping AS target
USING constituent_rating_mapping_stage AS source
ON target.SourceId = source.SourceId
AND target.SourceSystemId = source.SourceSystemId
AND target.SourceTable = source.SourceTable
WHEN MATCHED THEN UPDATE SET
target.SilverRecordId = source.SilverRecordId
WHEN NOT MATCHED THEN INSERT (SilverRecordId, SourceSystemId, SourceId, SourceTable)
VALUES (source.SilverRecordId, source.SourceSystemId, source.SourceId, source.SourceTable)
""")
This step also preserves the packaged sample mappings and adds Excel-owned mappings for the same silver lookup rows.
Resolve workbook rows to existing constituents
Add this code to Cell 8.
constituent_mapping = (
get_silver_table('SourceSystemIdMapping')
.filter(F.col('SourceId') == F.lit(PACKAGED_SAMPLE_SOURCE_ID))
.filter(F.col('SourceTable').isin('Contact', 'Account'))
.select(
F.col('SourceSystemId').alias('MappedEntitySourceSystemId'),
F.col('SourceTable').alias('MappedSourceTable'),
F.col('SilverRecordId').alias('MappedPartySilverId')
)
)
constituents = (
get_silver_table('Constituent')
.select('ConstituentId', 'ContactId', 'AccountId')
)
excel_ws = spark.table('WealthScreeningExcel_WealthScreening')
resolved_parties = (
excel_ws.join(
constituent_mapping,
(F.col('EntitySourceSystemId') == F.col('MappedEntitySourceSystemId')) &
(
((F.col('EntityType') == F.lit('contact')) & (F.col('MappedSourceTable') == F.lit('Contact'))) |
((F.col('EntityType') == F.lit('account')) & (F.col('MappedSourceTable') == F.lit('Account')))
),
'left'
)
)
resolved_constituents = (
resolved_parties.join(
constituents,
(
((F.col('EntityType') == F.lit('contact')) & (F.col('MappedPartySilverId') == F.col('ContactId'))) |
((F.col('EntityType') == F.lit('account')) & (F.col('MappedPartySilverId') == F.col('AccountId')))
),
'left'
)
)
Why not join directly on silver GUIDs:
- The workbook knows source-system IDs, not silver IDs.
SourceSystemIdMappingis the canonical bridge across sources.
Sync WealthScreening
Add this code to Cell 9.
capacity_mapping = (
get_silver_table('SourceSystemIdMapping')
.filter(F.col('SourceId') == F.lit(WEALTH_SCREENING_EXCEL_SOURCE_ID))
.filter(F.col('SourceTable') == F.lit('WealthScreeningCapacityRange'))
.select(
F.col('SourceSystemId').alias('MappedCapacityRangeCode'),
F.col('SilverRecordId').alias('CapacityRangeId')
)
)
rating_mapping = (
get_silver_table('SourceSystemIdMapping')
.filter(F.col('SourceId') == F.lit(WEALTH_SCREENING_EXCEL_SOURCE_ID))
.filter(F.col('SourceTable') == F.lit('WealthScreeningConstituentRating'))
.select(
F.col('SourceSystemId').alias('MappedConstituentRatingCode'),
F.col('SilverRecordId').alias('ConstituentRatingId')
)
)
resolved_wealth_screening = (
resolved_constituents
.join(capacity_mapping, F.col('CapacityRangeCode') == F.col('MappedCapacityRangeCode'), 'left')
.join(rating_mapping, F.col('ConstituentRatingCode') == F.col('MappedConstituentRatingCode'), 'left')
.join(get_existing_wealth_screening(), 'ConstituentId', 'left')
)
invalid_rows = resolved_wealth_screening.filter(
F.col('ConstituentId').isNull() |
F.col('CapacityRangeId').isNull() |
F.col('ConstituentRatingId').isNull() |
F.col('WealthScreeningId').isNull()
)
if not invalid_rows.rdd.isEmpty():
display(invalid_rows)
raise ValueError('Workbook contains rows that cannot be resolved to existing Constituent, lookup, or WealthScreening IDs.')
wealth_screening_stage = (
resolved_wealth_screening
.select(
F.col('WealthScreeningId'),
F.col('ConstituentId'),
F.col('CapacityRangeId'),
F.col('ConstituentRatingId'),
F.col('ExistingCreatedDate').alias('CreatedDate'),
F.current_timestamp().alias('ModifiedDate'),
F.col('LastScreeningDate'),
F.lit(WEALTH_SCREENING_EXCEL_SOURCE_ID).alias('SourceId'),
F.col('WealthScreeningExcelId').cast('string').alias('SourceSystemId')
)
)
wealth_screening_stage.createOrReplaceTempView('wealth_screening_stage')
spark.sql(f"""
MERGE INTO {silver_lakehouse_name}.WealthScreening AS target
USING (
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY WealthScreeningId ORDER BY ModifiedDate DESC) AS rn
FROM wealth_screening_stage
) WHERE rn = 1
) AS source
ON target.WealthScreeningId = source.WealthScreeningId
WHEN MATCHED THEN UPDATE SET
target.ConstituentId = source.ConstituentId,
target.CapacityRangeId = source.CapacityRangeId,
target.ConstituentRatingId = source.ConstituentRatingId,
target.SourceId = source.SourceId,
target.SourceSystemId = source.SourceSystemId,
target.ModifiedDate = source.ModifiedDate,
target.LastScreeningDate = source.LastScreeningDate
""")
This approach keeps the existing WealthScreeningId stable while assigning ownership of the row to the Excel source system.
Update orchestration
For recurring execution, use pipeline orchestration rather than manual notebook runs.
Add the Excel notebook to Fundraising_BR_Ingestion.DataPipeline, not to the top-level Fundraising_Orchestration pipeline.
Recommended activity order:
Import sample dataTransform Excel wealth screening to silver- The remaining source activities in their existing order
This order ensures that the packaged sample mappings already exist before the Excel notebook runs.
Verify the results
Verify the source and lookup mappings
display(spark.sql(f"""
SELECT 'Source' AS CheckType, Name AS CheckValue, COUNT(*) AS RowCount
FROM {silver_lakehouse_name}.Source
WHERE Name = '{WEALTH_SCREENING_EXCEL_SOURCE_NAME}'
GROUP BY Name
UNION ALL
SELECT 'Mapping' AS CheckType, SourceTable AS CheckValue, COUNT(*) AS RowCount
FROM {silver_lakehouse_name}.SourceSystemIdMapping
WHERE SourceId = '{WEALTH_SCREENING_EXCEL_SOURCE_ID}'
AND SourceTable IN ('WealthScreeningCapacityRange', 'WealthScreeningConstituentRating')
GROUP BY SourceTable
"""))
Expected results:
- one
Sourcerow withName = 'WealthScreeningExcel' - mapping rows for
WealthScreeningCapacityRange - mapping rows for
WealthScreeningConstituentRating
WealthScreening mapping rows aren't expected in this update-in-place pattern because the article reuses the existing WealthScreeningId values instead of creating Excel-owned IDs for that table.
Verify that WealthScreening now uses the Excel source
display(spark.sql(f"""
SELECT SourceId, COUNT(*) AS RowCount
FROM {silver_lakehouse_name}.WealthScreening
GROUP BY SourceId
ORDER BY RowCount DESC
"""))
Expected result: The rows updated by this workbook now use {WEALTH_SCREENING_EXCEL_SOURCE_ID} as their SourceId, because the workbook acts as the owning source system for WealthScreening.
Verify that the row count stays stable
display(spark.sql(f"""
SELECT COUNT(*) AS RowCount
FROM {silver_lakehouse_name}.WealthScreening
"""))
Expected result: if you start from the packaged sample data used in this example, the row count remains stable at 1200.
Verify that gold enrichment picks up the data
display(spark.sql(f"""
SELECT COUNT(*)
FROM {gold_lakehouse_name}.FactWealthScreening
"""))
Expected result: rows exist after Fundraising_SL_GD_Enrichment runs.
Troubleshoot common issues
Workbook reruns create duplicate silver rows
In this update-in-place pattern, reruns shouldn't create new WealthScreening rows at all.
Verify that the WealthScreening sync logic still:
- joins
get_existing_wealth_screening()byConstituentId - validates that
WealthScreeningIdisn't null before the merge - uses only
WHEN MATCHED THEN UPDATEand doesn't includeWHEN NOT MATCHED THEN INSERT
The workbook contains unresolved rows
If the notebook raises ValueError: Workbook contains rows that cannot be resolved to existing Constituent, lookup, or WealthScreening IDs., check these items:
- The
ConstituentRatingCodemapping step is completed fully. The notebook must not stop at buildingconstituent_rating_stage; it must also mergeconstituent_rating_mapping_stageintoSourceSystemIdMapping. Otherwise,ConstituentRatingIdremains null in the final validation. - The unresolved rows are displayed before the exception.
- Every
EntitySourceSystemIdin the workbook exists in the packaged sampleSourceSystemIdMappingrows. - Every workbook lookup code is present in the bridge tables used in the capacity range and constituent rating mapping sections.
- Every resolved constituent already has one
WealthScreeningrow to update.
The workbook file isn't found
If the notebook raises FileNotFoundError: /lakehouse/default/Files/WealthScreeningExcel-Example.xlsx, verify that:
In this article, /lakehouse/default/... refers to the notebook's attached default lakehouse, not some other lakehouse in the workspace.
Don't change this path to file:/lakehouse/default/... when you use pd.read_excel(). That file: prefix is used in the packaged sample-data notebook for Spark and mssparkutils, not for the pandas Excel read shown here.
Fix it as follows:
- Check which lakehouse is attached to the notebook as default.
- Verify that the workbook you created exists in that lakehouse under
Files. - If you saved the workbook with a different file name or uploaded it to a different lakehouse, change
excel_pathto match the actual location you want to read. - If you used the temporary shortcut and uploaded the workbook to
Fundraising_SL/Files, keepFundraising_SLattached while you run the workbook load step.
After you fix the file location or lakehouse attachment, rerun the setup cells and then rerun the workbook load cell.
The notebook has no default context
If the notebook raises UnsupportedOperationException: No default context found, attach a default lakehouse to the notebook and rerun the setup cells in order.
The SQL examples in this article use partial namespaces such as Fundraising_SL.Source, which Fabric resolves only when a default lakehouse context is attached.
After you attach the lakehouse:
- Confirm that
excel_pathstill points to the attached lakehouse. - Rerun Cell 1, Cell 2, and Cell 3 in order.
- If your deployed environment prefixes item names, attach the prefixed Excel bronze lakehouse rather than the unprefixed example name.
Summary
This pattern lets you integrate a small external workbook without changing the packaged source connectors:
- load the workbook into bronze staging tables
- register the workbook as its own source
- keep lookup-code mapping stable through
SourceSystemIdMapping - resolve cross-source references through existing source mappings
- map workbook lookup codes onto the existing wealth screening lookup tables
- update the existing
WealthScreeningrows in place - reuse the existing gold
FactWealthScreeningprocess
The same structure works well for other small external sources such as CSV exports, vendor files, or manually maintained enrichment workbooks.