<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aniket Abhishek Soni</title>
    <description>The latest articles on DEV Community by Aniket Abhishek Soni (@aniketsoni).</description>
    <link>https://dev.to/aniketsoni</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3954381%2Fc17f147f-e19b-4160-be20-e2d4dd2af1dd.png</url>
      <title>DEV Community: Aniket Abhishek Soni</title>
      <link>https://dev.to/aniketsoni</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aniketsoni"/>
    <language>en</language>
    <item>
      <title>dbt snapshots are not an audit log</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Tue, 03 Nov 2026 16:26:58 +0000</pubDate>
      <link>https://dev.to/aniketsoni/dbt-snapshots-are-not-an-audit-log-1m4c</link>
      <guid>https://dev.to/aniketsoni/dbt-snapshots-are-not-an-audit-log-1m4c</guid>
      <description>&lt;p&gt;The persistent myth that dbt snapshots act as a foolproof audit log is a dangerous misunderstanding of how slowly changing dimensions (SCDs) function in modern data warehouses.&lt;/p&gt;

&lt;p&gt;Many engineers treat snapshots as a catch-all solution for "tracking changes." If a table changes, they point a dbt snapshot at it and assume they have a perfect history of every state transition. However, dbt snapshots are designed to track the current state of a dimension over time, not to capture the high-frequency event stream required for an audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;In 2022, the engineering team at Monzo shared a postmortem regarding a &lt;a href="https://monzo.com/blog/2022/07/27/how-we-fixed-a-bug-in-our-ledger-system/" rel="noopener noreferrer"&gt;data integrity incident&lt;/a&gt; where a miscalculation in their ledger system led to incorrect balances. While this specific incident involved application-level logic rather than a dbt snapshot, it serves as a critical reminder of the difference between "state" and "event."&lt;/p&gt;

&lt;p&gt;When systems rely on snapshots to reconstruct history, they often fail to capture the &lt;em&gt;sequence&lt;/em&gt; of events that occurred between snapshot runs. If your snapshot frequency is set to once per day, and a record is updated three times within those 24 hours, the snapshot will only record the state as it existed at the moment of the snapshot run. You have lost the intermediate transitions. If those transitions were critical for compliance or debugging, your "audit log" is fundamentally incomplete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1760307837453-ce60cb209e52%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxsZWRnZXIlMjBwYXBlcnxlbnwwfDB8fHwxNzkzNzE1NzcwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1760307837453-ce60cb209e52%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxsZWRnZXIlMjBwYXBlcnxlbnwwfDB8fHwxNzkzNzE1NzcwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="A close-up of an old, open ledger with handwritten notes" width="1080" height="811"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@cameraobscura2000?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;camera obscura&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The underlying failure pattern
&lt;/h2&gt;

&lt;p&gt;The failure occurs because dbt snapshots (as documented in &lt;a href="https://docs.getdbt.com/docs/build/snapshots" rel="noopener noreferrer"&gt;dbt Labs' official docs&lt;/a&gt;) are built to implement SCD Type 2. They compare the current state of a source record against the last recorded state.&lt;/p&gt;

&lt;p&gt;Consider this hypothetical configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="n"&gt;user_status_snapshot&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{{&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;target_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'snapshots'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;unique_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'check'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;check_cols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'subscription_level'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'raw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endsnapshot&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;dbt snapshot&lt;/code&gt; runs, it performs a comparison. If the &lt;code&gt;status&lt;/code&gt; column changed from 'active' to 'pending' and back to 'active' between two runs, the snapshot engine sees no difference. The historical record of that 'pending' state is lost.&lt;/p&gt;

&lt;p&gt;Furthermore, snapshots are designed to be idempotent and batch-oriented. They are not designed to handle high-velocity write-ahead logs. Relying on them for an audit trail assumes that the source system provides a clean, consistent view of state at the time of execution. If the source system itself is subject to race conditions or eventual consistency—common in distributed architectures—the snapshot simply inherits that ambiguity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1643000867361-cd545336249b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxN3x8ZGF0YWJhc2UlMjBzY2hlbWF8ZW58MHwwfHx8MTc5MzcxNTc3MXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1643000867361-cd545336249b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxN3x8ZGF0YWJhc2UlMjBzY2hlbWF8ZW58MHwwfHx8MTc5MzcxNTc3MXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="a purple background with a black and blue circle surrounded by blue and green cubes" width="1080" height="711"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@dengxiangs?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Deng Xiang&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to defend against it
&lt;/h2&gt;

&lt;p&gt;To build a reliable audit trail, you must move away from state-based snapshots and toward event-based logging.&lt;/p&gt;

&lt;p&gt;First, treat your data sources as event streams. Instead of relying on a &lt;code&gt;users&lt;/code&gt; table that represents the current state of a user, ingest the underlying transaction or change logs. If you are using a database like PostgreSQL, tools like Debezium capture these row-level changes (CDC) and stream them into your warehouse. This ensures that every state transition is recorded as an immutable event, regardless of how many times a record changes in a single hour.&lt;/p&gt;

&lt;p&gt;Second, separate your analytical snapshots from your audit requirements. It is perfectly acceptable to use dbt snapshots for building SCD Type 2 dimensions for your BI tools. These are designed for "as-of" reporting, where you want to know what a user's subscription level was on a specific date. They are not, however, an acceptable replacement for a granular audit log.&lt;/p&gt;

&lt;p&gt;Third, enforce immutability in your storage layer. If you are using technologies like Apache Iceberg, leverage table properties to maintain history via snapshots or versioning. When you need to audit a change, query the table history directly rather than relying on a dbt-managed table that has discarded intermediate states. &lt;/p&gt;

&lt;p&gt;If you must use dbt for auditing, you are likely using the wrong tool. An audit log requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Completeness:&lt;/strong&gt; Every state change must be captured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering:&lt;/strong&gt; The sequence of changes must be preserved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance:&lt;/strong&gt; The origin and timestamp of the event must be immutable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;dbt snapshots provide none of these guarantees by default. They are a convenience for modeling, not a mechanism for compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The distinction between capturing state and capturing events is the difference between an analytical convenience and a system of record. When you configure a dbt snapshot, you are choosing to manage the complexity of SCD Type 2 dimensions. You are not choosing to archive your system's history.&lt;/p&gt;

&lt;p&gt;Use dbt snapshots to simplify your JOINs and power your dashboards. Use event-driven architectures and immutable logs when you need to prove exactly what happened, and when. Never confuse the two, or you will eventually find yourself trying to debug a "missing" state transition that your snapshot infrastructure was never designed to preserve in the first place.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@pray4bokeh?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Bruno Guerrero&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>analytics</category>
      <category>dataengineering</category>
      <category>dataquality</category>
    </item>
    <item>
      <title>Trimming Container Bloat with Docker History and Dive</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Mon, 02 Nov 2026 18:09:50 +0000</pubDate>
      <link>https://dev.to/aniketsoni/trimming-container-bloat-with-docker-history-and-dive-1k6j</link>
      <guid>https://dev.to/aniketsoni/trimming-container-bloat-with-docker-history-and-dive-1k6j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Large container images increase pull times during CI/CD, consume unnecessary storage in container registries, and expand the attack surface by including extraneous binary dependencies. Managing image size is a fundamental practice for maintaining lean, reproducible data pipelines that scale predictably across environments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Docker image is not a single file; it is a stack of filesystem changes. A common misconception is that deleting a file in a later &lt;code&gt;RUN&lt;/code&gt; command removes it from the final image. It does not. The file persists in the layer where it was created, and subsequent layers simply mark it as "hidden" or "deleted," meaning your image size remains bloated by the original data.&lt;/p&gt;

&lt;p&gt;If you are shipping data processing agents or heavy analytics environments, you are likely carrying "ghost" data—temporary caches, source code, or build artifacts—that provide no runtime value. Finding these hidden bytes requires more than just &lt;code&gt;docker images&lt;/code&gt;. You need to peel back the layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this tutorial, you need the following installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Engine&lt;/strong&gt; (version 20.10 or higher).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dive&lt;/strong&gt; (a tool for exploring a docker image, layer by layer). See the &lt;a href="https://github.com/wagoodman/dive" rel="noopener noreferrer"&gt;official repository&lt;/a&gt; for installation instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A terminal environment&lt;/strong&gt; with permissions to build images.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1768839724256-28cd4a373209%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8bWFnbmlmeWluZyUyMGdsYXNzfGVufDB8MHx8fDE3OTM1NjgxMDV8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1768839724256-28cd4a373209%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8bWFnbmlmeWluZyUyMGdsYXNzfGVufDB8MHx8fDE3OTM1NjgxMDV8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Calculator, magnifying glass, and chart with gears on paper" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@sasun1990?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Sasun Bughdaryan&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Inspecting layers with docker history
&lt;/h2&gt;

&lt;p&gt;Before using specialized tools, understand what Docker reports natively. The &lt;code&gt;docker history&lt;/code&gt; command displays the commands used to create each layer and the resulting size of that layer.&lt;/p&gt;

&lt;p&gt;Create a dummy &lt;code&gt;Dockerfile&lt;/code&gt; to visualize the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gcc g++
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./data_processing_script.py /app/script.py&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.2.0
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /root/.cache/pip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the image:&lt;br&gt;
&lt;code&gt;docker build -t bloat-test:latest .&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, run the history command:&lt;br&gt;
&lt;code&gt;docker history --no-trunc bloat-test:latest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The output shows every instruction. Look for layers with large sizes. You will notice that the &lt;code&gt;apt-get install&lt;/code&gt; layer is significant. Even though you might think you are cleaning up in a later layer, the initial download and extraction remain permanently in the image history.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Using Dive to visualize the bloat
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker history&lt;/code&gt; is text-based and difficult to parse for large images. &lt;code&gt;Dive&lt;/code&gt; provides an interactive TUI (Text User Interface) that allows you to navigate each layer and see exactly which files were added, modified, or removed.&lt;/p&gt;

&lt;p&gt;Run Dive on your image:&lt;br&gt;
&lt;code&gt;dive bloat-test:latest&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Navigate the interface using the arrow keys:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Left Panel:&lt;/strong&gt; Lists the Docker layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right Panel:&lt;/strong&gt; Shows the contents of the filesystem for the selected layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Watch for the "Image Efficiency Score" in the top right. If you see a low score, it usually indicates that you are creating files in one layer and deleting them in another. Dive highlights these changes in red or green. If a file appears in a layer and is later deleted, it is still consuming space in your final build.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: Implementing multi-stage builds
&lt;/h2&gt;

&lt;p&gt;The most effective way to eliminate bloat is by using multi-stage builds. This approach ensures that build-time dependencies (like compilers or header files) never make it into the final runtime image.&lt;/p&gt;

&lt;p&gt;Modify your &lt;code&gt;Dockerfile&lt;/code&gt; to use a build stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build dependencies&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gcc g++
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Stage 2: Runtime image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="c"&gt;# Copy only the installed packages from the builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /root/.local /root/.local&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH=/root/.local/bin:$PATH&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "script.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the new image:&lt;br&gt;
&lt;code&gt;docker build -t lean-image:latest .&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;dive&lt;/code&gt; again on &lt;code&gt;lean-image:latest&lt;/code&gt;. You will observe a significant reduction in the "Efficiency Score" and total size, as the &lt;code&gt;gcc&lt;/code&gt; and &lt;code&gt;g++&lt;/code&gt; compilers were discarded at the end of the &lt;code&gt;builder&lt;/code&gt; stage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Minimizing layers with build techniques
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;RUN&lt;/code&gt; command in a Dockerfile creates a new layer. You can reduce the number of layers by chaining commands. This is particularly useful for package management.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining these into a single &lt;code&gt;RUN&lt;/code&gt; instruction ensures that the temporary files created by &lt;code&gt;apt-get&lt;/code&gt; are removed within the same layer, preventing them from being committed to the final image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1651340550839-3b295d930048%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMHx8Y2lyY3VpdCUyMGJvYXJkfGVufDB8MHx8fDE3OTM1NjgxMDZ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1651340550839-3b295d930048%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMHx8Y2lyY3VpdCUyMGJvYXJkfGVufDB8MHx8fDE3OTM1NjgxMDZ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="a close-up of a circuit board" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@polarmermaid?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Anne Nygård&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;.dockerignore&lt;/code&gt; oversight:&lt;/strong&gt; If you don't use a &lt;code&gt;.dockerignore&lt;/code&gt; file, you are likely copying local &lt;code&gt;.git&lt;/code&gt; directories, virtual environments, or &lt;code&gt;__pycache__&lt;/code&gt; folders into your image. Always add these to a &lt;code&gt;.dockerignore&lt;/code&gt; file in your root directory to keep the build context clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-reliance on &lt;code&gt;apt-get clean&lt;/code&gt;:&lt;/strong&gt; While cleaning package lists is good practice, it is secondary to avoiding the installation of unnecessary tools entirely. Always prefer &lt;code&gt;distroless&lt;/code&gt; or &lt;code&gt;alpine&lt;/code&gt; base images if your application dependencies allow it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring cache mounts:&lt;/strong&gt; If you are using BuildKit (enabled by default in modern Docker), use &lt;code&gt;--mount=type=cache&lt;/code&gt; for your package manager (e.g., &lt;code&gt;pip&lt;/code&gt; or &lt;code&gt;apt&lt;/code&gt;). This caches files &lt;em&gt;during&lt;/em&gt; the build but does not include them in the resulting image layer, significantly speeding up subsequent builds without bloating the output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Bloated containers are a silent tax on your data engineering velocity. By inspecting layers with &lt;code&gt;dive&lt;/code&gt; and enforcing multi-stage builds, you ensure that your production artifacts contain only what is strictly necessary to execute your code. This not only speeds up deployment cycles but also improves the security posture of your infrastructure by reducing the number of binaries and libraries present in your environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Take a current production Dockerfile that you suspect is oversized, run &lt;code&gt;dive&lt;/code&gt; on it today, and identify the single largest layer. Then, apply a multi-stage build pattern to isolate that layer's dependencies and compare the final image size.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@nessa_rin?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Rinson Chory&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>containers</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Photon is not a magic speed button for every Spark query</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Mon, 02 Nov 2026 03:09:41 +0000</pubDate>
      <link>https://dev.to/aniketsoni/photon-is-not-a-magic-speed-button-for-every-spark-query-1g9k</link>
      <guid>https://dev.to/aniketsoni/photon-is-not-a-magic-speed-button-for-every-spark-query-1g9k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Photon is a vectorized query engine that replaces portions of the standard Spark JVM-based execution with C++. Understanding when it triggers and where it fails prevents wasted compute spend on workloads that gain zero performance benefit from the engine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The most persistent myth in the Databricks ecosystem is that switching to a Photon-enabled runtime automatically makes every Spark job run faster. It is easy to see why: "Photon" sounds like a performance upgrade, and the marketing literature promises significant speedups for ETL and SQL. However, Photon is not a universal accelerator. It is a specialized engine designed for specific types of data processing. When your workload falls outside its supported surface area, your cluster spends time overhead-mapping Spark operations to Photon kernels for no gain, or worse, falling back to the standard JVM execution path mid-query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this tutorial, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A Databricks workspace with a cluster running Databricks Runtime (DBR) 11.3 LTS or higher.&lt;/li&gt;
&lt;li&gt;  The &lt;code&gt;databricks-runtime&lt;/code&gt; provides Photon by default on supported instance types.&lt;/li&gt;
&lt;li&gt;  Access to the Spark UI to inspect query plans.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;pyspark&lt;/code&gt; installed locally if you wish to mock the data, though the query profile analysis must be performed on a cluster with Photon enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1682559736721-c2e77ff4c650%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw0fHxzZXJ2ZXIlMjByYWNrJTIwaGFyZHdhcmV8ZW58MHwwfHx8MTc5MzQ3OTE0MHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1682559736721-c2e77ff4c650%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw0fHxzZXJ2ZXIlMjByYWNrJTIwaGFyZHdhcmV8ZW58MHwwfHx8MTc5MzQ3OTE0MHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="a bunch of wires that are connected to a server" width="1080" height="564"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@lightsabercollection?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Lightsaber Collection&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Generating a workload that Photon loves
&lt;/h2&gt;

&lt;p&gt;Photon shines brightest when processing large volumes of data using SQL-like operations: joins, aggregations, and scanning columnar formats like Parquet or Delta. It is designed to minimize the overhead of the Java object model by processing data in a vectorized, columnar format in native memory.&lt;/p&gt;

&lt;p&gt;Consider this illustrative scenario where we join two large tables. Photon will attempt to vectorize the join and the subsequent aggregation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;

&lt;span class="c1"&gt;# Create a local SparkSession for testing
&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PhotonTest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Create hypothetical data
&lt;/span&gt;&lt;span class="n"&gt;df_sales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;selectExpr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id as sales_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cast(rand()*100 as int) as region_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cast(rand()*1000 as double) as amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df_regions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;selectExpr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id as region_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concat(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Region_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, cast(id as string)) as region_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# A classic join and aggregate query
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_sales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_regions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Inspecting the Query Profile
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;explain()&lt;/code&gt; output in your notebook provides a glimpse, but the Spark UI is where you confirm Photon’s activity. When you run the above code on a Photon-enabled cluster, open the &lt;strong&gt;Spark UI&lt;/strong&gt; and navigate to the &lt;strong&gt;SQL / DataFrame&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;Click on the query description. You are looking for the "Photon" indicators:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Operator Names:&lt;/strong&gt; Look for operators prefixed with "Photon" (e.g., &lt;code&gt;PhotonScan&lt;/code&gt;, &lt;code&gt;PhotonHashAggregate&lt;/code&gt;, &lt;code&gt;PhotonShuffleExchange&lt;/code&gt;). If you see these, the engine is actively executing that node.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Physical Plan:&lt;/strong&gt; If you see standard Spark operators (like &lt;code&gt;HashAggregate&lt;/code&gt; instead of &lt;code&gt;PhotonHashAggregate&lt;/code&gt;), Photon is not handling that specific operation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your query is entirely standard Spark, the "Photon" column in the SQL tab will be blank or indicate 0% usage. This is your primary diagnostic tool: if your expensive jobs show 0% Photon usage, you are paying for the Photon-enabled instance type without getting the benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Identifying the fallback mechanism
&lt;/h2&gt;

&lt;p&gt;Photon does not support every expression or data type in the Spark ecosystem. When it encounters an unsupported operation (like a custom UDF that requires the JVM), it performs a "fallback."&lt;/p&gt;

&lt;p&gt;In the Spark UI, look for the transition points. If a large portion of your DAG is "Photon" but a specific stage reverts to standard JVM operators, you have found a bottleneck. Often, this is caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Python UDFs:&lt;/strong&gt; Photon cannot run Python code directly.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Complex Types:&lt;/strong&gt; Certain nested types or specific custom serialization formats.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Unsupported SQL functions:&lt;/strong&gt; Rare, but specific legacy Spark functions may not have a native C++ implementation yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Testing for "Photon-hostile" code
&lt;/h2&gt;

&lt;p&gt;To see Photon get bypassed, run a query that relies heavily on a Python UDF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;udf&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StringType&lt;/span&gt;

&lt;span class="c1"&gt;# A Python UDF forces a transition to the JVM/Python process
&lt;/span&gt;&lt;span class="nd"&gt;@udf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;returnType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expensive_py_udf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;expensive_py_udf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transformed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you view the query profile for this, you will notice the plan lacks the &lt;code&gt;Photon&lt;/code&gt; prefix on the projection operator. By introducing the UDF, you effectively "de-vectorized" that part of the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1594729095022-e2f6d2eece9c%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxN3x8Y29kZSUyMGdyYXBoJTIwdmlzdWFsaXphdGlvbnxlbnwwfDB8fHwxNzkzNDc5MTQwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1594729095022-e2f6d2eece9c%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxN3x8Y29kZSUyMGdyYXBoJTIwdmlzdWFsaXphdGlvbnxlbnwwfDB8fHwxNzkzNDc5MTQwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="black red and white textile" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@markusspiske?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Markus Spiske&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Instance Type Misalignment:&lt;/strong&gt; Photon is not available on all instance types. If you select an instance type that is not optimized for it, the cluster will start, but Photon will be disabled. Always check the Databricks documentation for your specific cloud provider's Photon-supported instance types.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Over-reliance on UDFs:&lt;/strong&gt; If your entire ETL pipeline is wrapped in Python UDFs to perform standard transformations, you have effectively neutered Photon. Where possible, rewrite UDFs using native Spark SQL functions (&lt;code&gt;pyspark.sql.functions&lt;/code&gt;). Native functions are heavily optimized for Photon.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Small Data overhead:&lt;/strong&gt; For very small datasets, the overhead of moving data into the Photon native memory space can sometimes make the query slightly slower than the standard JVM engine. Do not optimize for Photon if your data fits in a single partition and runs in milliseconds.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Assuming 100% coverage:&lt;/strong&gt; You will rarely see 100% Photon usage in complex pipelines. Aim for "Photon-heavy" segments in your largest data-shuffling stages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Photon is a powerful tool for large-scale data processing, but it is not a "set it and forget it" feature. It requires you to write "Photon-friendly" code—avoiding custom UDFs in favor of native SQL expressions and ensuring your data joins and aggregations are well-structured.&lt;/p&gt;

&lt;p&gt;Use the Spark UI as your source of truth. If you see the "Photon" prefix in your query plan, you are getting the speedup. If you see the standard JVM operators, investigate the node and determine if a UDF or an unsupported function is forcing the engine to fall back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Take one of your existing, high-latency SQL pipelines, run it on a Photon-enabled cluster, and inspect the Spark UI. Identify the stages that are &lt;em&gt;not&lt;/em&gt; running as Photon, and attempt to replace any UDFs in those stages with native Spark SQL functions. Observe the change in the Spark UI execution plan.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@jedgpz?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Trevor Kay&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>pyspark</category>
      <category>spark</category>
      <category>performance</category>
    </item>
    <item>
      <title>Don't treat Fabric data ingestion as a one-size-fits-all problem</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:07:16 +0000</pubDate>
      <link>https://dev.to/aniketsoni/dont-treat-fabric-data-ingestion-as-a-one-size-fits-all-problem-1dge</link>
      <guid>https://dev.to/aniketsoni/dont-treat-fabric-data-ingestion-as-a-one-size-fits-all-problem-1dge</guid>
      <description>&lt;p&gt;The persistent myth in modern data architecture is that there is a "gold standard" for ingesting data into a lakehouse. You will often hear that one method—usually the one with the lowest latency—is inherently superior to the others. In reality, choosing between OneLake shortcuts, mirroring, and copy jobs is not a quest for the "best" tool, but an exercise in managing technical trade-offs between governance, compute overhead, and data freshness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;p&gt;When building a data platform in Microsoft Fabric, you have three primary ways to land data in your Lakehouse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;OneLake shortcuts:&lt;/strong&gt; These are symbolic links that point to data stored in ADLS Gen2, Amazon S3, or other Fabric workspaces. You aren't moving the data; you are creating a transparent reference.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Mirroring:&lt;/strong&gt; A high-frequency, automated replication process that keeps your operational data (like Azure SQL Database or Cosmos DB) in sync with OneLake in near real-time, specifically optimized for analytical workloads.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Copy jobs (Data Pipelines/Dataflows):&lt;/strong&gt; The classic ETL/ELT approach. You define an activity to read from a source and write to a destination. The data is physically moved, transformed, and saved as Delta tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1732979887771-f80bb448b18b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMXx8ZGF0YSUyMGZsb3clMjBhYnN0cmFjdHxlbnwwfDB8fHwxNzkzMzk2NDM2fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1732979887771-f80bb448b18b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMXx8ZGF0YSUyMGZsb3clMjBhYnN0cmFjdHxlbnwwfDB8fHwxNzkzMzk2NDM2fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="A blurry photo of a mountain range" width="1080" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@steve_j?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Steve A Johnson&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance and security scope
&lt;/h2&gt;

&lt;p&gt;The most significant differentiator is the "blast radius" of your security model.&lt;/p&gt;

&lt;p&gt;Shortcuts are identity-agnostic at the point of ingestion. Because the data stays in its source location, you must manage permissions in two places: the source system and the Fabric workspace. If you use a shortcut, you are essentially extending the governance perimeter of your Fabric workspace to include the source cloud storage.&lt;/p&gt;

&lt;p&gt;Mirroring, however, is a first-class citizen within the Fabric governance model. Once mirrored, the data resides within OneLake as Delta tables. You can apply Fabric-native security, such as sensitivity labels and column-level security, directly on the mirrored items.&lt;/p&gt;

&lt;p&gt;Copy jobs offer the most granular control. Because you are explicitly moving the data, you can apply data quality checks, PII masking, or schema enforcement during the copy process. If your regulatory environment requires an immutable audit trail of exactly what was moved and when, the explicit nature of a copy job is easier to document for compliance than a background sync process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and operational overhead
&lt;/h2&gt;

&lt;p&gt;Performance is often conflated with "how fast the data arrives." In reality, we must look at how the data performs under query load.&lt;/p&gt;

&lt;p&gt;Shortcuts are essentially free in terms of ingest compute. You incur no data transfer costs within the same region, and there is zero latency. However, if the underlying storage format is not optimized for analytical queries (e.g., a massive collection of small JSON files), your queries will suffer when hitting the shortcut directly via Direct Lake or Spark.&lt;/p&gt;

&lt;p&gt;Mirroring is purpose-built for performance. It converts operational data into a Delta format optimized for the engine. The trade-off is the overhead on the source system. While Microsoft designs mirroring to minimize impact, you are still running a replication engine that must track changes in the source.&lt;/p&gt;

&lt;p&gt;Copy jobs are the most resource-intensive. They require compute (Spark or Data Integration pipelines) to perform the move. If you have a massive dataset, you are paying for the compute to read, serialize, and write that data. The benefit is that you can optimize the schema (e.g., partitioning, Z-ordering) during the copy, which can significantly improve performance for downstream Power BI reports using Direct Lake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and observability
&lt;/h2&gt;

&lt;p&gt;Engineering resilience requires us to assume that the pipeline will fail. &lt;/p&gt;

&lt;p&gt;Copy jobs are the most observable. They provide standard logging in the Fabric pipeline monitoring tab. You can retry individual activities, set up email alerts on failure, and build sophisticated dependency chains. If a copy job fails, you know exactly which batch of data failed to load.&lt;/p&gt;

&lt;p&gt;Mirroring is a "black box" by design. It is meant to be set-and-forget. While you can monitor the status of the mirror, you have less control over the granular execution. If the replication encounters a schema conflict or a source-side permission issue, your ability to "patch" the data in transit is limited.&lt;/p&gt;

&lt;p&gt;Shortcuts have the fewest failure modes because they aren't "running" anything. If the source data is deleted or the credentials for the shortcut expire, the shortcut breaks. This is a binary failure: the data is either there or it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary of trade-offs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Shortcuts&lt;/th&gt;
&lt;th&gt;Mirroring&lt;/th&gt;
&lt;th&gt;Copy Jobs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Movement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (Reference)&lt;/td&gt;
&lt;td&gt;Replication&lt;/td&gt;
&lt;td&gt;Physical Move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Near-zero&lt;/td&gt;
&lt;td&gt;Near-real-time&lt;/td&gt;
&lt;td&gt;Batch/Triggered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transformability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low (Config)&lt;/td&gt;
&lt;td&gt;High (Pipeline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dual-system&lt;/td&gt;
&lt;td&gt;Fabric-native&lt;/td&gt;
&lt;td&gt;Fabric-native&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1790746717945-425f6e0c03bd%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw3fHxzZXJ2ZXIlMjByb29tJTIwbmV0d29ya3xlbnwwfDB8fHwxNzkzMzk2NDM2fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1790746717945-425f6e0c03bd%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw3fHxzZXJ2ZXIlMjByb29tJTIwbmV0d29ya3xlbnwwfDB8fHwxNzkzMzk2NDM2fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="White rectangular towers with copper wiring connected to a curved teal arch" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@brechtcorbeel?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Brecht Corbeel&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to measure it yourself
&lt;/h2&gt;

&lt;p&gt;Before committing to one approach, test the end-to-end query performance of your target dataset. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a sample dataset in your source system.&lt;/li&gt;
&lt;li&gt;Ingest it via all three methods into three separate Lakehouses.&lt;/li&gt;
&lt;li&gt;Use the following Spark snippet in a Fabric notebook to measure the time it takes to perform a simple aggregation:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Illustrative benchmark: Measure read time for a table
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lakehouse_name.table_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read and count took: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat this for your most common analytical queries (e.g., joins, filters on high-cardinality columns). If your query performance is identical across all three, prioritize the method that provides the lowest operational burden (usually shortcuts). If your reports are sluggish, you have the evidence needed to justify the overhead of a copy job or mirroring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd pick, and why
&lt;/h2&gt;

&lt;p&gt;I default to &lt;strong&gt;OneLake shortcuts&lt;/strong&gt; whenever possible. If the data is already in Parquet or Delta format in ADLS Gen2, there is rarely a technical justification for moving it. Why pay for compute to move bytes that are already accessible?&lt;/p&gt;

&lt;p&gt;I opt for &lt;strong&gt;Mirroring&lt;/strong&gt; when the source is a transactional database (like Azure SQL) and the business requirement is "near-real-time" analytics. Mirroring removes the complexity of managing custom Change Data Capture (CDC) pipelines.&lt;/p&gt;

&lt;p&gt;I reserve &lt;strong&gt;Copy jobs&lt;/strong&gt; for when the data requires "cleaning." If your source system has messy, denormalized, or non-compliant data, use a copy job to transform it into a high-performance, governed structure in your Lakehouse. Never use a copy job simply to "move" data that is already in a compatible format; you are only adding points of failure and unnecessary compute costs to your architecture.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@fakurian?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Milad Fakurian&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microsoftfabric</category>
      <category>dataengineering</category>
      <category>analytics</category>
      <category>azure</category>
    </item>
    <item>
      <title>How Window Functions Actually Scale (or Don't)</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Fri, 30 Oct 2026 03:34:29 +0000</pubDate>
      <link>https://dev.to/aniketsoni/how-window-functions-actually-scale-or-dont-25i</link>
      <guid>https://dev.to/aniketsoni/how-window-functions-actually-scale-or-dont-25i</guid>
      <description>&lt;p&gt;Most data engineers treat window functions as "black box" syntax that magically computes aggregates without collapsing rows, but they are actually the primary cause of unoptimized shuffles in production pipelines.&lt;/p&gt;

&lt;p&gt;The common assumption is that &lt;code&gt;OVER(PARTITION BY ... ORDER BY ...)&lt;/code&gt; is just a way to write cleaner SQL. In reality, you are describing a specific physical execution plan that forces your query engine to reorganize data across a cluster. If you don't understand the physical shuffle cost, you are essentially gambling with your compute budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;When you execute a window function, the engine performs a "window sort" or a "window shuffle." Consider a hypothetical dataset of user transactions distributed across a cluster. &lt;/p&gt;

&lt;p&gt;In Apache Spark, for example, the &lt;code&gt;WindowSpec&lt;/code&gt; defines how data is grouped and ordered before the function (like &lt;code&gt;SUM&lt;/code&gt; or &lt;code&gt;ROW_NUMBER&lt;/code&gt;) is applied. If you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;transaction_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;transaction_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;running_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine must ensure that all records for a specific &lt;code&gt;user_id&lt;/code&gt; land on the same executor node. This is a wide transformation. The data is partitioned by &lt;code&gt;user_id&lt;/code&gt;, and then sorted locally within each partition by &lt;code&gt;transaction_date&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-window.html" rel="noopener noreferrer"&gt;Apache Spark SQL documentation&lt;/a&gt;, if your partition key has high cardinality (e.g., billions of unique user IDs), the shuffle process will involve moving nearly the entire dataset over the network. If your data is already clustered (e.g., using &lt;code&gt;CLUSTER BY&lt;/code&gt; in Snowflake or &lt;code&gt;Z-ORDER&lt;/code&gt; in Delta Lake), the engine might skip the shuffle, but the documentation warns that the optimizer's ability to leverage existing distribution depends heavily on the specific join or window conditions matching your physical data layout.&lt;/p&gt;

&lt;p&gt;If you are using PostgreSQL, the mechanics differ. PostgreSQL uses a "window aggregate" node in its execution plan. It performs a sort operation if the data isn't already ordered by the partitioning and ordering keys. If the dataset exceeds &lt;code&gt;work_mem&lt;/code&gt;, PostgreSQL will spill the sort to disk (the &lt;code&gt;temp_files&lt;/code&gt; statistic in &lt;code&gt;pg_stat_database&lt;/code&gt; will reveal this).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1509834828289-fd7dd7732f0a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMHx8Z2xhc3MlMjB3aW5kb3clMjByZWZsZWN0aW9ufGVufDB8MHx8fDE3OTMzMTEwNzd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1509834828289-fd7dd7732f0a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMHx8Z2xhc3MlMjB3aW5kb3clMjByZWZsZWN0aW9ufGVufDB8MHx8fDE3OTMzMTEwNzd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="assorted trees collage" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@pizlfilip?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Filip Pižl&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Design choices and tradeoffs
&lt;/h2&gt;

&lt;p&gt;The most significant tradeoff in window functions is the "All-or-Nothing" memory pressure. &lt;/p&gt;

&lt;p&gt;When you use &lt;code&gt;RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt;, the engine must maintain a buffer of all rows in the partition that satisfy the condition. If your window partition is massive—for instance, an entire year of data for a single high-activity account—the executor or the database worker must hold that state in memory.&lt;/p&gt;

&lt;p&gt;In distributed systems, this leads to the "Skewed Partition" problem. If 99% of your data belongs to one partition key, one executor handles 99% of the work while the others sit idle. The query finishes as fast as the slowest, most overloaded node. &lt;/p&gt;

&lt;p&gt;Another subtle trap is the interaction between window functions and predicate pushdown. Many users assume that putting a &lt;code&gt;WHERE&lt;/code&gt; clause after a window function is efficient. However, because window functions operate on the result set of the preceding operations, the engine often cannot push filters through the window node. You are essentially asking the database to perform a window calculation on the entire set before discarding the rows you didn't actually need.&lt;/p&gt;

&lt;p&gt;Always check your execution plan (using &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; in PostgreSQL or the Spark UI's SQL tab) to see if a &lt;code&gt;Filter&lt;/code&gt; node appears &lt;em&gt;after&lt;/em&gt; the &lt;code&gt;Window&lt;/code&gt; node. If it does, you are paying the full price for the calculation of rows that will never reach the final output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1644325349124-d1756b79dd42%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwzfHxhYnN0cmFjdCUyMGRhdGElMjBmbG93fGVufDB8MHx8fDE3OTMzMTEwNzd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1644325349124-d1756b79dd42%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwzfHxhYnN0cmFjdCUyMGRhdGElMjBmbG93fGVufDB8MHx8fDE3OTMzMTEwNzd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="a blue abstract background with lines and dots" width="1080" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@choys_?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Conny Schneider&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  When to reach for it (and when not to)
&lt;/h2&gt;

&lt;p&gt;Use window functions when the logic is inherently relative. Deduplication (using &lt;code&gt;ROW_NUMBER()&lt;/code&gt;), identifying gaps in sequences, and calculating running totals are the canonical use cases where the code readability is worth the performance cost.&lt;/p&gt;

&lt;p&gt;However, do not use window functions as a replacement for joins when you need to calculate a global constant. A common anti-pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Don't do this&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forces the engine to calculate a global sum using a window shuffle. It is significantly more performant to calculate the sum in a separate CTE or subquery and join it, or use a window function only if you specifically need the per-row context.&lt;/p&gt;

&lt;p&gt;Avoid window functions entirely in high-frequency streaming applications if the state must be maintained over long durations. If your window requires a long "lookback" (like a 30-day running average), the state management cost in systems like Apache Flink or Spark Structured Streaming can lead to checkpointing bottlenecks. In those cases, it is often better to pre-aggregate data in smaller time buckets and join those buckets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Window functions are not free primitives; they are explicit instructions to re-partition and sort your data. By understanding that &lt;code&gt;PARTITION BY&lt;/code&gt; is a physical data movement command, you can spot potential bottlenecks before they manifest as failed jobs or runaway warehouse costs. Always verify your execution plan: if you see a shuffle or a disk-spill warning, your window function is likely doing more work than your business logic requires.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@jandira_sonnendeck?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Jandira Sonnendeck&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>dataengineering</category>
      <category>spark</category>
      <category>performance</category>
    </item>
    <item>
      <title>Signing Container Images With Cosign for Secure Data Pipelines</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Thu, 29 Oct 2026 03:21:02 +0000</pubDate>
      <link>https://dev.to/aniketsoni/signing-container-images-with-cosign-for-secure-data-pipelines-ehj</link>
      <guid>https://dev.to/aniketsoni/signing-container-images-with-cosign-for-secure-data-pipelines-ehj</guid>
      <description>&lt;p&gt;Most engineers treat container registries as glorified file servers, assuming that if a tag exists, it is the one they built. This is a dangerous oversight in any environment handling sensitive data. Relying on image tags alone provides no guarantee that the image running in your orchestration layer is the same bits-and-bytes package that passed your CI suite.&lt;/p&gt;

&lt;p&gt;If you aren't cryptographically signing your container images, your data pipelines are vulnerable to man-in-the-middle attacks, registry poisoning, and unauthorized image swapping. Signing images with &lt;a href="https://docs.sigstore.dev/cosign/" rel="noopener noreferrer"&gt;Sigstore Cosign&lt;/a&gt; allows you to attach a verifiable digital signature to your image in the container registry, ensuring that only trusted artifacts reach your production environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this tutorial, you need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker:&lt;/strong&gt; Version 20.10 or later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cosign:&lt;/strong&gt; Installed via &lt;code&gt;brew install cosign&lt;/code&gt; or the official binary from the &lt;a href="https://docs.sigstore.dev/system_config/installation/" rel="noopener noreferrer"&gt;Sigstore documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Container Registry:&lt;/strong&gt; A registry that supports OCI artifacts (e.g., Docker Hub, GitHub Container Registry, or AWS ECR).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment:&lt;/strong&gt; A local machine with Docker daemon access and an authenticated session to your registry (&lt;code&gt;docker login&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We assume you are using standard Linux-based images. This guide focuses on signing with local key pairs, though Cosign supports Keyless signing using OIDC identities, which is recommended for mature CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1580271758056-bd4f1d69f5b9%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNHx8ZGlnaXRhbCUyMHNpZ25hdHVyZSUyMGFyY2hpdGVjdHVyZXxlbnwwfDB8fHwxNzkzMjI0MzgxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1580271758056-bd4f1d69f5b9%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNHx8ZGlnaXRhbCUyMHNpZ25hdHVyZSUyMGFyY2hpdGVjdHVyZXxlbnwwfDB8fHwxNzkzMjI0MzgxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="white and black metal frame" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@catcherxuefeng?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;catcher xuefeng&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Generate a signing key pair
&lt;/h2&gt;

&lt;p&gt;Before you can sign an image, you need a private key to perform the signing and a public key to verify it. Cosign generates these as a pair.&lt;/p&gt;

&lt;p&gt;Run the following command to generate a key pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cosign generate-key-pair
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create two files: &lt;code&gt;cosign.key&lt;/code&gt; and &lt;code&gt;cosign.pub&lt;/code&gt;. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cosign.key&lt;/code&gt;: The private key used for signing. Keep this secret. In a real CI environment, you would store this in a secret manager like AWS Secrets Manager, GitHub Secrets, or HashiCorp Vault.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cosign.pub&lt;/code&gt;: The public key used for verification. This is what your deployment infrastructure (like a Kubernetes admission controller or a CI gate) will use to check if an image is legitimate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Build and push a hardened image
&lt;/h2&gt;

&lt;p&gt;For this example, imagine a standard Python-based data processing image. Use a multi-stage Dockerfile to keep the footprint minimal and the security posture strong.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use a pinned version for reproducibility&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="c"&gt;# Prevent python from writing pyc files and enable unbuffered logging&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONDONTWRITEBYTECODE=1&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Final stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim-bookworm&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;-r&lt;/span&gt; appuser &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; appuser appuser
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /root/.local /home/appuser/.local&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser . .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s \&lt;/span&gt;
  CMD curl -f http://localhost:8080/health || exit 1

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["python", "main.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build and push the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;your-registry&amp;gt;/&amp;lt;your-repo&amp;gt;:&amp;lt;tag&amp;gt; &lt;span class="nb"&gt;.&lt;/span&gt;
docker push &amp;lt;your-registry&amp;gt;/&amp;lt;your-repo&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Sign the image
&lt;/h2&gt;

&lt;p&gt;Now that the image is in the registry, you can sign it. Cosign stores the signature as a separate OCI artifact in the same registry, linked to the image digest.&lt;/p&gt;

&lt;p&gt;Execute the following command to sign the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cosign sign &lt;span class="nt"&gt;--key&lt;/span&gt; cosign.key &amp;lt;your-registry&amp;gt;/&amp;lt;your-repo&amp;gt;@&amp;lt;digest&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we are using the &lt;strong&gt;digest&lt;/strong&gt; (&lt;code&gt;@sha256:...&lt;/code&gt;) rather than the tag. Tags are mutable and can be pointed to different images; digests are immutable content hashes. Always sign the digest to ensure you are signing the exact bytes you tested.&lt;/p&gt;

&lt;p&gt;You can verify that the signature was pushed by checking your registry's UI or by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cosign triangulate &amp;lt;your-registry&amp;gt;/&amp;lt;your-repo&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Verify the signature before deployment
&lt;/h2&gt;

&lt;p&gt;This is the most critical step for your CI/CD pipeline. Whether you are running a standalone script or a Kubernetes cluster, you must block any execution if the image signature doesn't match your &lt;code&gt;cosign.pub&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;To verify manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cosign verify &lt;span class="nt"&gt;--key&lt;/span&gt; cosign.pub &amp;lt;your-registry&amp;gt;/&amp;lt;your-repo&amp;gt;:&amp;lt;tag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the signature is valid, Cosign returns the signature metadata and exits with code &lt;code&gt;0&lt;/code&gt;. If someone has replaced the image, or if the image was not signed with the corresponding private key, the command will fail and exit with a non-zero code.&lt;/p&gt;

&lt;p&gt;In your CI pipeline (e.g., GitHub Actions), you should include this as a mandatory step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify Image Signature&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cosign verify --key cosign.pub &amp;lt;your-registry&amp;gt;/&amp;lt;your-repo&amp;gt;:&amp;lt;tag&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this step fails, the pipeline stops, and no vulnerable or unauthorized code is deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1548063286-eb9bfe8440e6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyM3x8c2hpcHBpbmclMjBjb250YWluZXIlMjBsb2NrfGVufDB8MHx8fDE3OTMyMjQzODJ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1548063286-eb9bfe8440e6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyM3x8c2hpcHBpbmclMjBjb250YWluZXIlMjBsb2NrfGVufDB8MHx8fDE3OTMyMjQzODJ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="grayscale photo of corrugated sheet" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@ankit_manoharan?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Ankit Manoharan&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Signing tags instead of digests
&lt;/h3&gt;

&lt;p&gt;As mentioned, tags are mutable. If you sign &lt;code&gt;my-image:latest&lt;/code&gt;, an attacker can push a new image to the &lt;code&gt;latest&lt;/code&gt; tag, and the registry will point to it. While the signature on the &lt;em&gt;previous&lt;/em&gt; image remains, the &lt;code&gt;latest&lt;/code&gt; tag becomes untrusted. Always use the &lt;code&gt;@sha256:&lt;/code&gt; digest in your signatures and your deployment manifests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key management
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;cosign.key&lt;/code&gt; file is your root of trust. If this is leaked, an attacker can sign malicious images that your pipeline will blindly accept. Never commit &lt;code&gt;cosign.key&lt;/code&gt; to source control. Use your CI provider’s secret storage. For more advanced setups, look into &lt;a href="https://docs.sigstore.dev/cosign/kms/" rel="noopener noreferrer"&gt;Cosign’s support for KMS&lt;/a&gt; to keep your keys in hardware security modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring verification failures
&lt;/h3&gt;

&lt;p&gt;A signature check is only effective if your deployment process strictly enforces it. If your deployment scripts continue execution even after a verification failure, you have provided a false sense of security. Ensure your automation scripts use &lt;code&gt;set -e&lt;/code&gt; or equivalent error handling to halt immediately upon a non-zero exit code from &lt;code&gt;cosign verify&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cryptographic signing is not an optional feature for regulated environments; it is the baseline for supply chain integrity. By moving from "trusting the registry" to "verifying the signature," you eliminate the risk of image tampering and ensure that your data pipelines run exactly what you intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Generate a local key pair, sign a "Hello World" image, and intentionally change the Dockerfile to break the hash. Run the verification command and observe how the tool correctly identifies the mismatch between the signed digest and the modified image.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@danielmiksha?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Daniel Miksha&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Is Your Databricks Job Still Using Your Personal Access Token?</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Wed, 28 Oct 2026 11:58:05 +0000</pubDate>
      <link>https://dev.to/aniketsoni/is-your-databricks-job-still-using-your-personal-access-token-3274</link>
      <guid>https://dev.to/aniketsoni/is-your-databricks-job-still-using-your-personal-access-token-3274</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Hardcoded personal access tokens are a primary vector for privilege escalation and data exfiltration in managed data environments. Moving to Service Principals decouples authentication from individual identities, ensuring that pipeline access persists beyond the lifecycle of any single user account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consider a hypothetical pipeline that syncs data from a third-party API into a Delta Lake table. The job is configured to run on a cluster using a Python script that requires an API key. For months, the job runs perfectly. Then, the engineer who originally set up the job leaves the organization. Three days later, the job fails with a generic &lt;code&gt;401 Unauthorized&lt;/code&gt; or &lt;code&gt;403 Forbidden&lt;/code&gt; error, despite the code remaining unchanged. &lt;/p&gt;

&lt;p&gt;This is the "Orphaned Token" anti-pattern. When a user account is deactivated, any Personal Access Token (PAT) associated with that account is immediately revoked, silently breaking every downstream automation tied to that identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms
&lt;/h2&gt;

&lt;p&gt;An engineer investigating this failure often sees the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The "It Works on My Machine" trap:&lt;/strong&gt; The pipeline runs successfully when executed manually by a senior engineer with broad permissions, leading to the false conclusion that the issue is transient or related to "network blips."&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Confusing Logs:&lt;/strong&gt; Spark logs might show &lt;code&gt;java.io.IOException&lt;/code&gt; or &lt;code&gt;com.databricks.backend.common.rpc.DatabricksSqlException&lt;/code&gt; with cryptic authentication messages.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Silent Failures:&lt;/strong&gt; If the code doesn't explicitly log the full stack trace of the authentication error, the job might just hang while waiting for an API response that will never arrive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1777652062282-9b2985cd28d9%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw0fHxzZWN1cmUlMjB2YXVsdCUyMGRvb3J8ZW58MHwwfHx8MTc5MzEzNjUwMXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1777652062282-9b2985cd28d9%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw0fHxzZWN1cmUlMjB2YXVsdCUyMGRvb3J8ZW58MHwwfHx8MTc5MzEzNjUwMXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="A gleaming golden vault door stands open" width="1080" height="719"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@lucaakalu?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Lu&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Finding the Hardcoded Secret
&lt;/h2&gt;

&lt;p&gt;The most common cause is a string literal hidden in a configuration file or a &lt;code&gt;dbutils&lt;/code&gt; call. To confirm, check the job configuration in the Databricks workspace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt;&lt;br&gt;
Search the codebase for &lt;code&gt;dbutils.secrets.get&lt;/code&gt; calls or, worse, raw strings like &lt;code&gt;api_key = "secret123"&lt;/code&gt;. If you are using Databricks Asset Bundles (DABs), check your &lt;code&gt;databricks.yml&lt;/code&gt; for environment variables that might be pulling from local files rather than the Databricks Secret Manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt;&lt;br&gt;
Stop referencing secrets directly in your code. Instead, use a Secret Scope. If you are on Databricks, use the &lt;a href="https://docs.databricks.com/en/security/secrets/secrets.html" rel="noopener noreferrer"&gt;Databricks Secret CLI&lt;/a&gt; to create a scope and add the secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a scope backed by Databricks&lt;/span&gt;
databricks secrets create-scope my-app-scope

&lt;span class="c"&gt;# Add the secret (do this interactively to avoid shell history logging)&lt;/span&gt;
databricks secrets put-secret my-app-scope my-api-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The Identity Mismatch (Personal vs. Service Principal)
&lt;/h2&gt;

&lt;p&gt;Even if you use secret scopes, the job might still be running under the execution context of a user. If the cluster is configured to use "Single User" mode, it inherits the identity of the user who started the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt;&lt;br&gt;
Check the Job Run details. Does it list an individual user as the "Run As" identity? In a secure architecture, this should always be a Service Principal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt;&lt;br&gt;
Create a Service Principal in your cloud provider (e.g., Entra ID, AWS IAM) and grant it the necessary permissions in your Unity Catalog or Hive Metastore. Then, update your Databricks Job definition to use this Service Principal for the run.&lt;/p&gt;

&lt;p&gt;If using Databricks Asset Bundles, configure the &lt;code&gt;run_as&lt;/code&gt; block in your &lt;code&gt;bundle.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;my_sync_job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync-api-to-delta&lt;/span&gt;
      &lt;span class="na"&gt;run_as&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;service_principal_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;application-id-of-service-principal&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
          &lt;span class="na"&gt;spark_python_task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;python_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/sync.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Scope ACLs and Permissions
&lt;/h2&gt;

&lt;p&gt;Sometimes the job fails because the Service Principal lacks the "READ" permission on the secret scope itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt;&lt;br&gt;
Run a test script using the Service Principal’s token (or via a job run as the SP) to access the secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Test script to verify secret access
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;secret_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dbutils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-app-scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access successful&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access denied: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt;&lt;br&gt;
Grant the Service Principal access to the scope using the Databricks CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;databricks secrets put-acl my-app-scope &amp;lt;service-principal-application-id&amp;gt; READ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Environment Variable Injection
&lt;/h2&gt;

&lt;p&gt;If you are using Docker containers or custom Spark configurations, you might be injecting secrets as environment variables in the cluster config. This is dangerous because these values are often visible in the Spark UI "Environment" tab to anyone with "Can View" access to the cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt;&lt;br&gt;
Instead of setting &lt;code&gt;MY_API_KEY&lt;/code&gt; in the Spark Config, keep it in the Secret Scope. Update your Python code to fetch it at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use the Databricks utility to fetch the secret at runtime
# This keeps the secret out of the Spark UI environment variables
&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dbutils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-app-scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preventing it
&lt;/h2&gt;

&lt;p&gt;To prevent these issues from recurring, implement these systemic safeguards:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Enforce "Run As" Policies:&lt;/strong&gt; Configure your CI/CD pipelines to reject any Databricks Job definition that does not specify a &lt;code&gt;run_as&lt;/code&gt; block pointing to a Service Principal.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Linting:&lt;/strong&gt; Use custom scripts in your CI pipeline to scan your Python code for patterns like &lt;code&gt;os.environ.get("SECRET_KEY")&lt;/code&gt; or hardcoded strings that look like API keys.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Unity Catalog:&lt;/strong&gt; Move away from legacy secret scopes where possible and leverage Unity Catalog’s external locations and credentials. By using Unity Catalog, you can grant the Service Principal direct access to cloud storage (S3/ADLS/GCS) without ever needing to manage a secret key for the storage layer itself.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Audit Logs:&lt;/strong&gt; Regularly review Databricks Audit Logs for jobs running as individual users, and use that list to target your migration efforts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By removing the reliance on personal tokens, you move your platform from a state of "fragile convenience" to "hardened automation." The goal is for your pipelines to be identity-agnostic, running with the same permission set regardless of who triggered the deployment.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #databricks #security #dataengineering #cicd&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@stevo6960?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Stephen Hickman&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>security</category>
      <category>dataengineering</category>
      <category>cicd</category>
    </item>
    <item>
      <title>How to Integrate SQL Databases with Fabric Lakehouses</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Tue, 27 Oct 2026 11:51:01 +0000</pubDate>
      <link>https://dev.to/aniketsoni/how-to-integrate-sql-databases-with-fabric-lakehouses-596</link>
      <guid>https://dev.to/aniketsoni/how-to-integrate-sql-databases-with-fabric-lakehouses-596</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Data platforms often suffer from the "latency-vs-governance" trade-off, where transactional data sits in a silo away from analytical engines. By orchestrating SQL databases alongside Fabric Lakehouses, you can maintain transactional integrity while enabling high-performance analytics without complex ETL re-engineering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many engineers treat the Lakehouse as a monolithic destination for all data, leading to complex ingestion pipelines just to view simple operational state. Consider a hypothetical scenario where you have a primary application database (e.g., Azure SQL Database) and an analytical workspace in Fabric.&lt;/p&gt;

&lt;p&gt;The "Before" approach involves heavy ELT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The "Before" trap: Staging everything as raw Parquet&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="s1"&gt;'abfss://&amp;lt;container&amp;gt;@&amp;lt;storage&amp;gt;.dfs.core.windows.net/raw/'&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transactions&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FILE_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PARQUET'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "After" approach, leveraging Fabric’s ability to interact with external SQL sources via shortcuts or Mirrored Databases, allows for a more streamlined architecture. You keep your operational schema where it lives, exposing it to the Lakehouse for unified semantic modeling without constant full-load migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  A Microsoft Fabric workspace with a capacity (Fabric trial or SKU) assigned.&lt;/li&gt;
&lt;li&gt;  An existing Azure SQL Database (or SQL Server 2022 instance) with public or private endpoint connectivity.&lt;/li&gt;
&lt;li&gt;  Appropriate permissions: "Contributor" or "Administrator" on the Fabric workspace and "db_datareader" on the source SQL database.&lt;/li&gt;
&lt;li&gt;  Knowledge of the &lt;a href="https://learn.microsoft.com/en-us/fabric/database/mirroring-get-started" rel="noopener noreferrer"&gt;Microsoft Fabric Mirroring&lt;/a&gt; feature (currently in public preview for Azure SQL Database).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1664526937033-fe2c11f1be25%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8ZGF0YSUyMGZsb3clMjBkaWFncmFtfGVufDB8MHx8fDE3OTMwNTE0MTR8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1664526937033-fe2c11f1be25%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8ZGF0YSUyMGZsb3clMjBkaWFncmFtfGVufDB8MHx8fDE3OTMwNTE0MTR8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="A network of interconnected cubes and thin blue lines against a light blue background" width="1080" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@guerrillabuzz?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;GuerrillaBuzz&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Establishing a Mirrored Database in Fabric
&lt;/h2&gt;

&lt;p&gt;Instead of writing scripts to move data, you can use the Mirroring feature to create a replica of your SQL database directly within your Fabric workspace. This process creates a specialized item that synchronizes data near real-time from your source to the OneLake.&lt;/p&gt;

&lt;p&gt;Navigate to your workspace, select &lt;strong&gt;New&lt;/strong&gt;, and choose &lt;strong&gt;Mirrored Azure SQL Database&lt;/strong&gt;. You will need the server name and database name. Once configured, Fabric manages the synchronization process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This is handled by the Fabric Mirroring engine&lt;/span&gt;
&lt;span class="c1"&gt;-- Once linked, you can query via T-SQL in the Fabric SQL Analytics endpoint&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Transactions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TransactionDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATEADD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GETDATE&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a mirrored representation of your data in Delta format inside OneLake, which is automatically cataloged in the Fabric metastore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Referencing Operational Data in Lakehouse Notebooks
&lt;/h2&gt;

&lt;p&gt;Once the mirrored data is available in OneLake, you can join it with other tables in your Lakehouse using PySpark. This is particularly useful for data quality checks where you need to compare transactional totals against landing zone aggregates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Accessing the mirrored table using the SQL Analytics endpoint path
&lt;/span&gt;&lt;span class="n"&gt;df_mirrored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dbo.Transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Perform a quick validation check
&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_mirrored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total records in transactional mirror: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Hypothetical quality check: ensure no null IDs
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df_mirrored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TransactionID IS NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data quality check failed: Null IDs detected.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Enabling Direct Lake for Operational Reporting
&lt;/h2&gt;

&lt;p&gt;The power of having this operational data in Fabric is the ability to use &lt;strong&gt;Direct Lake&lt;/strong&gt; mode for your semantic models. Direct Lake allows Power BI to read the Delta files directly from OneLake without importing them into the model or requiring a DirectQuery gateway, providing performance comparable to Import mode with the freshness of DirectQuery.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; In your workspace, create a &lt;strong&gt;Semantic model&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Select the mirrored table as a source.&lt;/li&gt;
&lt;li&gt; Ensure the storage mode is set to &lt;strong&gt;Direct Lake&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Define your DAX measures:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- DAX measure for operational monitoring
Total Sales = SUM('Transactions'[Amount])

-- Daily average with time-intelligence
Daily Avg Sales = 
CALCULATE(
    AVERAGE('Transactions'[Amount]),
    DATESYTD('Date'[Date])
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1782330300479-02550a1a3c7a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxhYnN0cmFjdCUyMGNsb3VkJTIwbmV0d29ya3xlbnwwfDB8fHwxNzkzMDUxNDE1fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1782330300479-02550a1a3c7a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxhYnN0cmFjdCUyMGNsb3VkJTIwbmV0d29ya3xlbnwwfDB8fHwxNzkzMDUxNDE1fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="A glowing circuit board with a central processing unit" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@brechtcorbeel?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Brecht Corbeel&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Schema Evolution:&lt;/strong&gt; When using Mirroring, be aware that changes to the source schema might take a moment to propagate. Always validate your downstream Power BI reports after significant DDL changes on the source SQL database.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Capacity Throttling:&lt;/strong&gt; Mirroring consumes compute resources within your Fabric capacity. If you have high-frequency transactional data, monitor your "Fabric Capacity Metrics app" to ensure you aren't hitting utilization limits during peak sync periods.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Permission Mismatches:&lt;/strong&gt; Ensure that the service principal or user identity used to create the connection has consistent access. A common failure pattern is having "Read" access on the SQL Database but failing to configure the Fabric workspace identity for cross-region data access.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Preview Limitations:&lt;/strong&gt; Features like "Mirroring" are subject to change. Always check the &lt;a href="https://learn.microsoft.com/en-us/fabric/database/mirroring-get-started" rel="noopener noreferrer"&gt;official Microsoft Learn documentation&lt;/a&gt; for the most current limitations, such as supported data types or specific SQL features that might not be replicated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By treating the SQL database as a first-class citizen of the Lakehouse rather than a source to be "dumped," you reduce pipeline fragility and latency. You gain the ability to perform complex analytical validation on transactional data without duplicating the storage footprint unnecessarily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Identify one operational table that is currently being extracted via a daily batch job. Attempt to enable the Mirrored Azure SQL Database feature in a sandbox workspace and see if you can replace that batch job with a Direct Lake semantic model.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@fennings?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Marc PEZIN&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>microsoftfabric</category>
      <category>dataengineering</category>
      <category>sql</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Package Shared Python Libraries with pyproject.toml</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:16:11 +0000</pubDate>
      <link>https://dev.to/aniketsoni/how-to-package-shared-python-libraries-with-pyprojecttoml-15el</link>
      <guid>https://dev.to/aniketsoni/how-to-package-shared-python-libraries-with-pyprojecttoml-15el</guid>
      <description>&lt;p&gt;You ship the job. It passes CI. Then, a colleague runs it in a notebook environment, and suddenly your shared utility library throws an &lt;code&gt;ImportError&lt;/code&gt; because it pulled in a different version of &lt;code&gt;pandas&lt;/code&gt; than the one used in your orchestration layer.&lt;/p&gt;

&lt;p&gt;Many data teams still manage shared code by appending directory paths to &lt;code&gt;sys.path&lt;/code&gt; or bundling messy &lt;code&gt;setup.py&lt;/code&gt; scripts that haven't been updated since the mid-2010s. This is a fragile architecture that invites inconsistency. If you want a robust data platform, you should stop treating your internal tooling like an afterthought and start packaging it as a proper Python distribution using &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the common approach falls short
&lt;/h2&gt;

&lt;p&gt;The "copy-paste-folder" or "sys.path.append" strategy is an engineering debt bomb. When you rely on environment-level path hacks, you lose the ability to manage versioning, track dependencies, and enforce consistent execution environments across local development, CI/CD runners, and production compute clusters.&lt;/p&gt;

&lt;p&gt;Legacy &lt;code&gt;setup.py&lt;/code&gt; files are equally problematic. They rely on arbitrary code execution during the installation process, which is opaque, slow, and increasingly discouraged by the Python Packaging Authority (PyPA). If your build process requires a Python script to resolve its own environment, you’ve already lost control of your supply chain. &lt;/p&gt;

&lt;p&gt;The transition to PEP 517 and PEP 621 standards, centered on the &lt;code&gt;pyproject.toml&lt;/code&gt; file, moves configuration away from executable code into static metadata. This allows build tools to understand your project structure without running arbitrary code, leading to safer, more predictable builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1590496793907-4d66e2994b4d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxOXx8c2hpcHBpbmclMjBjb250YWluZXJ8ZW58MHwwfHx8MTc5Mjk1ODIxM3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1590496793907-4d66e2994b4d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxOXx8c2hpcHBpbmclMjBjb250YWluZXJ8ZW58MHwwfHx8MTc5Mjk1ODIxM3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="A container ship being loaded by cranes at a busy industrial port at dusk" width="1080" height="574"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@timelabpro?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Timelab&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Standardizing with pyproject.toml
&lt;/h2&gt;

&lt;p&gt;To build a modern shared library, you need to define your metadata and build backend in &lt;code&gt;pyproject.toml&lt;/code&gt;. I recommend using &lt;code&gt;hatchling&lt;/code&gt; or &lt;code&gt;setuptools&lt;/code&gt; (&amp;gt;= 61.0.0). Here is a standard configuration that ensures your library is installable and its dependencies are tracked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;build-system&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"setuptools&amp;gt;=61.0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;build-backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"setuptools.build_meta"&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"data-utility-lib"&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Data Platform Team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"platform@example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Shared utilities for data pipelines"&lt;/span&gt;
&lt;span class="n"&gt;requires-python&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&amp;gt;=3.9"&lt;/span&gt;
&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"pandas&amp;gt;=2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"pydantic&amp;gt;=2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"pyarrow&amp;gt;=12.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optional-dependencies&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"pytest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"ruff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By defining these requirements in a static file, you enable tools like &lt;code&gt;pip&lt;/code&gt; or &lt;code&gt;uv&lt;/code&gt; to resolve the entire dependency graph before installation begins. This prevents the "dependency drift" that occurs when an environment installs one version of a library at the start of a pipeline and a conflicting version halfway through a dependency resolution chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing consistent environments
&lt;/h2&gt;

&lt;p&gt;Once your library is packaged, you should treat it as a primary dependency in your project-specific environments. In your downstream data pipelines, you no longer rely on shared folders; you reference the library in your &lt;code&gt;pyproject.toml&lt;/code&gt; or &lt;code&gt;requirements.txt&lt;/code&gt; via a private package registry or a direct git URL.&lt;/p&gt;

&lt;p&gt;When you install your library, you are pulling a defined artifact. If you need to upgrade a core dependency like &lt;code&gt;pyarrow&lt;/code&gt;, you update the &lt;code&gt;pyproject.toml&lt;/code&gt; in your library, bump the version, and propagate that change to your pipelines. This creates an auditable trail of who changed which dependency and when—a critical requirement for any regulated environment where data lineage and software provenance are non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1606206886378-e49a19ad0933%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxnZWFyJTIwbWFjaGluZXJ5fGVufDB8MHx8fDE3OTI5NTgyMTN8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1606206886378-e49a19ad0933%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxnZWFyJTIwbWFjaGluZXJ5fGVufDB8MHx8fDE3OTI5NTgyMTN8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="green and black power tool" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@testalizeme?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Testalize.me&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The objections (and my answers)
&lt;/h2&gt;

&lt;p&gt;"But we have a small team, and packaging feels like overkill."&lt;/p&gt;

&lt;p&gt;It feels like overkill until you spend four hours debugging why a UDF in a Spark job works locally but crashes on the cluster. Packaging your code forces you to define exactly what your code needs to run. If you can't state your dependencies explicitly in a &lt;code&gt;pyproject.toml&lt;/code&gt;, you don't actually know what your code is doing.&lt;/p&gt;

&lt;p&gt;"Won't this slow down our development velocity?"&lt;/p&gt;

&lt;p&gt;Initially, yes. You have to learn how to version your internal library. However, the alternative is "dependency hell," where every engineer has a slightly different version of &lt;code&gt;numpy&lt;/code&gt; or &lt;code&gt;pandas&lt;/code&gt; in their local environment, leading to "works on my machine" syndromes that cost far more developer time than a formal packaging process ever will.&lt;/p&gt;

&lt;p&gt;"What about internal package management?"&lt;/p&gt;

&lt;p&gt;You don't need a complex Artifactory setup to start. You can install directly from a private Git repository using &lt;code&gt;pip install git+https://github.com/org/repo@v0.1.0&lt;/code&gt;. Once your team matures, you can move to a simple private PyPI mirror, but the &lt;code&gt;pyproject.toml&lt;/code&gt; format remains the constant that ensures your setup is compatible with modern ecosystem tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Packaging your shared code is not just about "cleaner project structure"; it is about platform stability. By moving away from imperative &lt;code&gt;setup.py&lt;/code&gt; scripts and loose file structures toward declarative &lt;code&gt;pyproject.toml&lt;/code&gt; configurations, you transform your shared library from a liability into a reliable piece of infrastructure. &lt;/p&gt;

&lt;p&gt;Stop appending to &lt;code&gt;sys.path&lt;/code&gt;. Start building artifacts. Your pipelines—and your future self—will thank you when you can finally deploy with confidence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@juanjodev02?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Juanjo Jaramillo&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>cicd</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Stop mocking your database for integration tests</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Sun, 25 Oct 2026 15:18:36 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-mocking-your-database-for-integration-tests-27g8</link>
      <guid>https://dev.to/aniketsoni/stop-mocking-your-database-for-integration-tests-27g8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Mocking database drivers in application tests creates a false sense of security that ignores SQL syntax errors, constraint violations, and transaction behavior. Using transient, containerized Postgres instances ensures your integration tests run against the same engine as production, catching schema-level failures before they reach your CI/CD pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most data engineers start by mocking &lt;code&gt;psycopg2&lt;/code&gt; or &lt;code&gt;SQLAlchemy&lt;/code&gt; calls. This is a trap. You end up testing your ability to mock a library rather than the integrity of your queries. The alternative—a dedicated "test" database—is often a fragile, manually managed instance that drifts from the production schema, leading to "works on my machine" failures.&lt;/p&gt;

&lt;p&gt;The robust approach is to treat your database as a disposable, versioned component of your testing lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;The goal is to spin up a clean, ephemeral Postgres instance for every test run, apply migrations, execute your test suite, and destroy the container immediately after. &lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;docker-compose&lt;/code&gt; to handle the lifecycle. This ensures that the network stack, environment variables, and volumes are consistent across local development and CI environments like GitHub Actions.&lt;/p&gt;

&lt;p&gt;Here is a minimal &lt;code&gt;docker-compose.yml&lt;/code&gt; configured for test automation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16.3-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_user&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_db&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test_db"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Understanding the configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;tmpfs&lt;/code&gt;&lt;/strong&gt;: This is critical. By mounting the data directory to &lt;code&gt;tmpfs&lt;/code&gt; (RAM), we bypass disk I/O latency. Since the database is disposable, we don't care about persistence after the container exits.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;healthcheck&lt;/code&gt;&lt;/strong&gt;: Never assume a container is "running" just because the process started. &lt;code&gt;pg_isready&lt;/code&gt; ensures the application code only attempts to connect once the Postgres socket is actually accepting signals.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;image&lt;/code&gt;&lt;/strong&gt;: Pinned to a specific digest or tag. Never use &lt;code&gt;latest&lt;/code&gt;, as a minor update to the Postgres image could change default collation settings or extensions, breaking your tests unpredictably.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Automating schema and seeding
&lt;/h3&gt;

&lt;p&gt;Your test runner should handle the migration logic. Using a tool like &lt;code&gt;alembic&lt;/code&gt; (for Python) or &lt;code&gt;flyway&lt;/code&gt; (for JVM), your test initialization script should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Start the container&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; postgres-test

&lt;span class="c"&gt;# 2. Wait for the healthcheck to pass&lt;/span&gt;
&lt;span class="k"&gt;until &lt;/span&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.State.Health.Status}}'&lt;/span&gt; postgres-test | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# 3. Run migrations&lt;/span&gt;
alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;span class="c"&gt;# 4. Run your test suite&lt;/span&gt;
pytest tests/integration/

&lt;span class="c"&gt;# 5. Teardown&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; flag in the teardown command is vital; it removes the anonymous volumes created by the container, ensuring the next test run starts from a truly blank slate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1763568258533-d0597f86ce62%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNXx8dGVzdCUyMGF1dG9tYXRpb24lMjBjb2RlfGVufDB8MHx8fDE3OTI4NzA5OTd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1763568258533-d0597f86ce62%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNXx8dGVzdCUyMGF1dG9tYXRpb24lMjBjb2RlfGVufDB8MHx8fDE3OTI4NzA5OTd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Computer screen displaying code with a context menu" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@dkomow?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Daniil Komov&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Design choices and tradeoffs
&lt;/h2&gt;

&lt;p&gt;The biggest downside of this pattern is &lt;strong&gt;CI latency&lt;/strong&gt;. Spinning up a container, waiting for the OS to boot, and running migrations takes time. If you have hundreds of small integration tests, running this lifecycle for every single test is inefficient.&lt;/p&gt;

&lt;p&gt;I recommend a &lt;strong&gt;test-suite-level teardown&lt;/strong&gt; rather than a test-level one. Group your integration tests together so the container boots once, processes all tests, and shuts down at the end. &lt;/p&gt;

&lt;p&gt;Another consideration is &lt;strong&gt;security&lt;/strong&gt;. Running containers in CI requires careful management of the Docker daemon. If your CI runner is shared, ensure you are not running the container as &lt;code&gt;root&lt;/code&gt;. While the Postgres image defaults to &lt;code&gt;postgres&lt;/code&gt; user, if you extend the image, ensure your &lt;code&gt;Dockerfile&lt;/code&gt; includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; postgres:16.3-alpine&lt;/span&gt;
&lt;span class="c"&gt;# Create a dedicated user for the app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid mounting local directories into the container during testing. It introduces dependency on the host's file system structure. Keep your migrations inside the container image or copy them in during the build phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it (and when not to)
&lt;/h2&gt;

&lt;p&gt;Use ephemeral containers when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You are performing complex joins or window functions where the SQL dialect nuances matter.&lt;/li&gt;
&lt;li&gt;  You need to verify that your migrations actually execute without conflict.&lt;/li&gt;
&lt;li&gt;  Your application uses specific Postgres extensions (e.g., &lt;code&gt;pg_trgm&lt;/code&gt; or &lt;code&gt;postgis&lt;/code&gt;) that are difficult to mock.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use ephemeral containers when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You are testing pure business logic that happens in memory. Those should be fast, unit-level tests.&lt;/li&gt;
&lt;li&gt;  The database is massive. If your "seed" data is gigabytes in size, the time taken to restore that state into a new container will turn your CI pipeline into a bottleneck. In such cases, consider using a permanent, shared test database and use transaction-level rollbacks to clean up individual test states.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Mocking databases is a legacy habit from an era when spinning up infrastructure was slow and painful. Today, Docker handles the isolation and orchestration for us. By treating your database container as a disposable, health-checked component, you bridge the gap between development and production, effectively turning your test suite into a real-world validation of your schema and query logic. &lt;/p&gt;

&lt;p&gt;Start by isolating your database interactions into a controlled container lifecycle. Your future self, debugging a production migration failure that you already caught in CI, will thank you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@kierinsightarchives?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Kier in Sight Archives&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dockercompose</category>
      <category>postgres</category>
      <category>testing</category>
    </item>
    <item>
      <title>Stop Writing Custom File Listeners: Use Auto Loader Instead</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Sat, 24 Oct 2026 16:56:52 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-writing-custom-file-listeners-use-auto-loader-instead-2b78</link>
      <guid>https://dev.to/aniketsoni/stop-writing-custom-file-listeners-use-auto-loader-instead-2b78</guid>
      <description>&lt;p&gt;You ship the job. It passes CI. Then, a source system upstream changes its naming convention, or a vendor pushes a malformed JSON file at 3 AM. Your custom file listener, built on top of &lt;code&gt;listStatus&lt;/code&gt; or a polling mechanism, silently skips the file or crashes the entire streaming micro-batch. You are now spending your morning hunting down missing records in S3 or ADLS.&lt;/p&gt;

&lt;p&gt;The industry has spent years reinventing the wheel with custom &lt;code&gt;foreachBatch&lt;/code&gt; logic and manual checkpointing to track which files have been processed. Databricks Auto Loader (&lt;code&gt;cloudFiles&lt;/code&gt;) exists to solve exactly this by offloading the file discovery and state management to the cloud provider's native notification services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this tutorial, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Databricks workspace (AWS, Azure, or GCP).&lt;/li&gt;
&lt;li&gt;Access to a UC-enabled catalog to store the target Delta table.&lt;/li&gt;
&lt;li&gt;A local environment or Databricks notebook with a Spark runtime (Databricks Runtime 10.4 LTS or higher is recommended for full feature support).&lt;/li&gt;
&lt;li&gt;Basic permissions to read from a cloud storage bucket (e.g., S3, ADLS Gen2, GCS).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1762278804996-eab062703db3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwzMHx8ZGlnaXRhbCUyMGRhdGElMjBzdHJlYW18ZW58MHwwfHx8MTc5Mjc4NzkxNXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1762278804996-eab062703db3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwzMHx8ZGlnaXRhbCUyMGRhdGElMjBzdHJlYW18ZW58MHwwfHx8MTc5Mjc4NzkxNXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Abstract blue and white wavy lines on black background" width="1080" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@loganvoss?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Logan Voss&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Configuring the Stream with CloudFiles
&lt;/h2&gt;

&lt;p&gt;Instead of using the standard &lt;code&gt;spark.readStream.format("json")&lt;/code&gt;, which requires you to provide a schema upfront and assumes a stable file list, we use &lt;code&gt;cloudFiles&lt;/code&gt;. This instructs Databricks to use native file notification services (like SQS/SNS for AWS or Event Grid for Azure) to detect new files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Hypothetical setup: reading JSON files from a landing zone
&lt;/span&gt;&lt;span class="n"&gt;cloud_source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://your-landing-bucket/raw-data/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;checkpoint_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://your-checkpoint-bucket/checkpoints/raw-data/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.schemaLocation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpoint_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Essential for schema inference
&lt;/span&gt;      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud_source&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# This DataFrame is now lazily evaluated and ready to write
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By setting &lt;code&gt;cloudFiles.schemaLocation&lt;/code&gt;, you trigger Auto Loader’s schema evolution engine. It will infer the schema from the files and store it in the checkpoint location. When a new file arrives with a different structure, Auto Loader tracks those changes automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Rescuing Malformed Data
&lt;/h2&gt;

&lt;p&gt;In regulated environments, you cannot simply drop data that doesn't match your schema. If a field changes type—for example, a &lt;code&gt;transaction_amount&lt;/code&gt; shifts from an integer to a string—you need a way to capture the "bad" data without stopping the pipeline. &lt;/p&gt;

&lt;p&gt;Enable the "rescued data" column to capture any data that cannot be parsed into your defined schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df_rescued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.inferColumnTypes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.schemaEvolutionMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;addNewColumns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.rescuedDataColumn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_rescued_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# The magic column
&lt;/span&gt;              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud_source&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Write to a Delta table
&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_rescued&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeStream&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpointLocation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpoint_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.raw.transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a record fails to map to your inferred schema, it isn't discarded. Instead, it is packaged into the &lt;code&gt;_rescued_data&lt;/code&gt; column as a JSON string. Your data analysts can then query &lt;code&gt;SELECT * FROM main.raw.transactions WHERE _rescued_data IS NOT NULL&lt;/code&gt; to identify and fix upstream issues without the pipeline ever failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Optimizing for Performance
&lt;/h2&gt;

&lt;p&gt;Auto Loader defaults to using file notification mode (&lt;code&gt;cloudFiles.useNotifications&lt;/code&gt;). This is the most efficient method because it avoids expensive directory listings. In large buckets with millions of files, a &lt;code&gt;listStatus&lt;/code&gt; call (the standard Spark approach) can time out or incur significant API costs.&lt;/p&gt;

&lt;p&gt;However, if you are testing locally or in a sandbox without sufficient cloud permissions to create notification resources, you can fall back to &lt;code&gt;fileDiscoveryMode&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use this only if you cannot configure cloud notifications
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloudFiles.useNotifications&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Fallback mode
&lt;/span&gt;      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud_source&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mechanism Check:&lt;/strong&gt; When &lt;code&gt;useNotifications&lt;/code&gt; is true, Databricks creates an SQS queue (on AWS) or Event Grid (on Azure) to listen for storage events. This means the ingestion latency is near-instant, regardless of how many files exist in the bucket. When &lt;code&gt;false&lt;/code&gt;, Spark performs a recursive list of the directory, which scales linearly with the number of files, leading to performance degradation as your volume grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Testing Schema Evolution
&lt;/h2&gt;

&lt;p&gt;To verify that your pipeline handles structural changes, simulate a schema drift. Create a JSON file with an unexpected column and drop it into your landing directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Illustrative: Writing a file to trigger schema evolution
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;new_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_feature_flag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;dbutils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cloud_source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/drift_test.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because you set &lt;code&gt;cloudFiles.schemaEvolutionMode&lt;/code&gt; to &lt;code&gt;addNewColumns&lt;/code&gt;, the Delta table will automatically alter its schema to include &lt;code&gt;new_feature_flag&lt;/code&gt;. If you prefer a stricter approach, use &lt;code&gt;failOnLoad&lt;/code&gt; to force the job to stop when the schema changes, preventing "dirty" data from entering your production tables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1561766109-2e6daa602aa6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMnx8Y29tcGxleCUyMG1hY2hpbmVyeSUyMGJsdWVwcmludHxlbnwwfDB8fHwxNzkyNzg3OTE1fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1561766109-2e6daa602aa6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMnx8Y29tcGxleCUyMG1hY2hpbmVyeSUyMGJsdWVwcmludHxlbnwwfDB8fHwxNzkyNzg3OTE1fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="grayscale photograhy of machine" width="1080" height="525"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@alphabetania?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tania Melnyczuk&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoint location is not optional:&lt;/strong&gt; Auto Loader stores the state of "seen" files in the checkpoint directory. If you delete this directory, the ingestion will restart from the beginning, leading to duplicate records in your Delta table. Ensure your checkpoint paths are managed with the same lifecycle policies as your production code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission Bloat:&lt;/strong&gt; In production, ensure the IAM role or Service Principal running the Databricks cluster has &lt;code&gt;PutObject&lt;/code&gt; and &lt;code&gt;ListBucket&lt;/code&gt; permissions for the storage, but also permissions to create/delete queues and subscriptions if using &lt;code&gt;useNotifications&lt;/code&gt;. If these are restricted, the pipeline will fail at startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-inferring Types:&lt;/strong&gt; Schema inference is convenient but not free. It samples the files to determine data types. On very large, messy datasets, this can add latency to the stream startup. If you know your schema, define it explicitly using &lt;code&gt;cloudFiles.schema&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case Sensitivity:&lt;/strong&gt; Auto Loader is case-insensitive by default regarding file names, but schema inference can be sensitive to JSON key casing. Ensure your upstream source is consistent if you want to avoid fragmented column names (e.g., &lt;code&gt;user_id&lt;/code&gt; vs &lt;code&gt;User_ID&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building custom file listeners is a liability. By leveraging Auto Loader, you shift the burden of file discovery and schema management to a managed service designed for scale. You get the benefits of native cloud notifications, automatic schema evolution, and a safety net for malformed data—all while keeping your Spark code readable and maintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Configure a small test stream using &lt;code&gt;cloudFiles&lt;/code&gt; against a dummy S3/ADLS bucket. Drop a malformed JSON file into the bucket and observe how the &lt;code&gt;_rescued_data&lt;/code&gt; column captures the error without interrupting the stream. Then, change the schema and watch the Delta table evolve automatically.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@rstar50?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Roger Starnes Sr&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>deltalake</category>
      <category>dataengineering</category>
      <category>spark</category>
    </item>
    <item>
      <title>Why are your field parameters breaking report performance?</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:03:34 +0000</pubDate>
      <link>https://dev.to/aniketsoni/why-are-your-field-parameters-breaking-report-performance-2g0l</link>
      <guid>https://dev.to/aniketsoni/why-are-your-field-parameters-breaking-report-performance-2g0l</guid>
      <description>&lt;p&gt;Consider a hypothetical pipeline that delivers high-fidelity financial metrics into a Microsoft Fabric Lakehouse. To make the data accessible to diverse end-users, an engineer implements Field Parameters in a Power BI semantic model, allowing users to toggle between "Gross Revenue," "Net Revenue," and "Tax Adjustment" measures, as well as swapping dimensions like "Region," "Legal Entity," and "Business Unit" on the fly. &lt;/p&gt;

&lt;p&gt;The implementation works in Power BI Desktop. Once published to a Fabric workspace, however, users report that the report hangs indefinitely or returns "Resource Governing" errors, even though the underlying Direct Lake model should be lightning-fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Symptoms
&lt;/h2&gt;

&lt;p&gt;The symptoms usually manifest as inconsistent query performance. Some users experience a complete timeout when selecting certain combinations of parameters, while others see a "Visual has exceeded the available resources" error message. &lt;/p&gt;

&lt;p&gt;A common false lead is to assume the issue lies with the underlying storage mode. Because the semantic model uses Direct Lake, the knee-jerk reaction is to check for table fragmentation or poorly performing DAX measures. While those are valid concerns, the issue often stems from how the storage engine generates expanded queries when field parameters are used in complex visual filters or multi-level hierarchies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1746470427556-57aca51f73ec%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw4fHxkYXRhJTIwZ3JpZHxlbnwwfDB8fHwxNzkyNzAxOTE0fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1746470427556-57aca51f73ec%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw4fHxkYXRhJTIwZ3JpZHxlbnwwfDB8fHwxNzkyNzAxOTE0fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Green, patterned light against a dark background" width="1080" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@loganvoss?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Logan Voss&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  1. The "Column Cardinality Explosion" Trap
&lt;/h2&gt;

&lt;p&gt;When you use a field parameter for a dimension, you are essentially telling the engine to dynamically switch the grouping context. If a user selects "Legal Entity" and then chooses a high-cardinality "Transaction ID" via another parameter, the query plan changes significantly. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; Open Performance Analyzer in Power BI Desktop and run the problematic visual. Copy the DAX query generated and paste it into DAX Studio. Check the "Query Plan" and "Server Timings." If you see a massive number of storage engine (SE) requests or high "Total" time compared to "Formula Engine" (FE) time, you are likely hitting an aggregation wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt; Limit the options in your field parameter to columns that share a similar cardinality. If you must include high-cardinality columns, ensure the field parameter is not used in a way that forces a Cartesian product in the underlying query. Use the &lt;code&gt;SELECTEDVALUE&lt;/code&gt; function in your DAX measures to define explicit behavior when a user has not yet made a selection, reducing the engine's need to evaluate all possible combinations.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Inconsistent Data Types in Parameters
&lt;/h2&gt;

&lt;p&gt;Field parameters act as a wrapper. If you mix columns with different data types—for example, a &lt;code&gt;String&lt;/code&gt; (Region) and a &lt;code&gt;Decimal&lt;/code&gt; (Revenue Amount)—the parameter table's &lt;code&gt;Value&lt;/code&gt; column will default to a type that can accommodate both, often leading to implicit type conversion during query evaluation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; Inspect the calculated table created by the field parameter in the Model view. Check the data type of the &lt;code&gt;Value&lt;/code&gt; column. If it is &lt;code&gt;Variant&lt;/code&gt; or &lt;code&gt;String&lt;/code&gt; when it should be numeric, your DAX measures will perform implicit conversions on every row, slowing down the engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt; Ensure that all columns included in a single field parameter are of the same data type. If you need to mix measures and dimensions, create separate field parameters for each category. Keep your measure parameters and dimension parameters distinct to maintain clean DAX type casting.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Excessive Filter Context on Parameters
&lt;/h2&gt;

&lt;p&gt;Users often attempt to use field parameters inside slicers that also have complex row-level security (RLS) or object-level security (OLS) applied. When a field parameter is used as a filter in a visual, the engine must evaluate the visibility of every item in the parameter list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; Check the "Relationship View" to see if the field parameter table is incorrectly linked to your fact tables in a way that creates circular dependencies or ambiguous paths. If your model has multiple active relationships between the parameter source and the fact table, the engine may default to a cross-filter that is far more expensive than necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt; Explicitly set the &lt;code&gt;Cross-filter direction&lt;/code&gt; to &lt;code&gt;Single&lt;/code&gt; for the relationship between your field parameter table and your model's dimension/fact tables. Avoid using field parameters in complex &lt;code&gt;CALCULATE&lt;/code&gt; filters; instead, use them as the primary axes in your visuals, allowing the engine's native slicing mechanism to handle the filtering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1667372525822-d226d23018dc%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMXx8c2VydmVyJTIwY29vbGluZyUyMGZhbnN8ZW58MHwwfHx8MTc5MjcwMTkxNHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1667372525822-d226d23018dc%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMXx8c2VydmVyJTIwY29vbGluZyUyMGZhbnN8ZW58MHwwfHx8MTc5MjcwMTkxNHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="a close-up of a game" width="1080" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@growtika?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Growtika&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. DAX Measure "Bloat"
&lt;/h2&gt;

&lt;p&gt;When you create a field parameter for measures, you are essentially creating a proxy for a set of existing DAX measures. If those original measures are not optimized, the field parameter acts as a force multiplier for inefficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm:&lt;/strong&gt; If the report is slow, check the &lt;code&gt;DAX&lt;/code&gt; code of the measures being swapped. If they contain &lt;code&gt;FILTER(ALL(Table), ...)&lt;/code&gt; patterns, they are likely performing full table scans every time the parameter changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to fix:&lt;/strong&gt; Refactor your measures to use &lt;code&gt;KEEPFILTERS&lt;/code&gt; or &lt;code&gt;SUMMARIZE&lt;/code&gt; patterns that leverage the model's existing relationships. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Instead of:
Revenue_Measure = CALCULATE(SUM(Sales[Amount]), ALL(Sales))

-- Use the relationship-aware version:
Revenue_Measure = SUM(Sales[Amount])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By ensuring the underlying measures are efficient, you remove the burden from the field parameter's evaluation context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing it
&lt;/h2&gt;

&lt;p&gt;Systemic safeguards are vital when scaling self-service reporting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Semantic Model Documentation:&lt;/strong&gt; Use the "Description" field in the Model view to explain the intent of each field parameter. If an engineer sees "Do not use in high-cardinality visuals," they are less likely to break the report.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Performance Budgeting:&lt;/strong&gt; Treat every field parameter as an additional level of complexity in your query plan. If a report requires more than five field parameters, consider splitting the report into multiple pages, each with a focused purpose.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Validation with Semantic Link:&lt;/strong&gt; If you are working in Fabric, use the &lt;code&gt;sempy&lt;/code&gt; (Semantic Link) library in a notebook to validate your semantic model. You can programmatically check for redundant relationships or data type mismatches that often plague field parameter tables. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Field parameters are a powerful tool for user empowerment, but they require a rigorous approach to data modeling. By treating them as dynamic query generators rather than simple UI elements, you can prevent the performance degradation that typically follows their introduction.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #powerbi #microsoftfabric #analytics #dataengineering&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@kevinmsegal?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Kevin Segal&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powerbi</category>
      <category>microsoftfabric</category>
      <category>analytics</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
