<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[From the Sidelines]]></title><description><![CDATA[Intersection of Tech, Data, Leadership & Sport.]]></description><link>https://immanueljoseph.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!BQkI!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17e6867e-3a25-4948-b2fd-7600984d8435_608x608.png</url><title>From the Sidelines</title><link>https://immanueljoseph.substack.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 08 May 2026 23:26:32 GMT</lastBuildDate><atom:link href="https://immanueljoseph.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Immanuel Joseph]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[immanueljoseph@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[immanueljoseph@substack.com]]></itunes:email><itunes:name><![CDATA[Immanuel Joseph]]></itunes:name></itunes:owner><itunes:author><![CDATA[Immanuel Joseph]]></itunes:author><googleplay:owner><![CDATA[immanueljoseph@substack.com]]></googleplay:owner><googleplay:email><![CDATA[immanueljoseph@substack.com]]></googleplay:email><googleplay:author><![CDATA[Immanuel Joseph]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Converting a large CSV to a Delta Table and comparing the size]]></title><description><![CDATA[A hands-on demo.]]></description><link>https://immanueljoseph.substack.com/p/converting-a-large-csv-to-a-delta</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/converting-a-large-csv-to-a-delta</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Sun, 19 Apr 2026 06:17:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!GM9i!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the second article in the series on Delta Lake storage.</p><ol><li><p><a href="https://immanueljoseph.substack.com/p/storing-cold-data-in-sql-server-theres">Part 1</a> talked about what Delta Tables are and why they&#8217;re ideal for long-term archiving</p></li><li><p>A hands-on demo: converting a large CSV to a Delta Table and comparing the size <em>&#8592; you&#8217;re here</em></p></li><li><p>Querying Delta Tables with DuckDB, no Spark required</p></li></ol><p>I wanted a dataset big enough to feel real. Something closer to what actually accumulates in a production database over a few years. We&#8217;ll be using the <a href="https://www.kaggle.com/datasets/new-york-city/nyc-parking-tickets">NYC Parking Violations dataset</a> from Kaggle. It contains four fiscal years of parking tickets issued across New York City.</p><ul><li><p><strong>42,339,438 rows</strong></p></li><li><p><strong>8.4 GB as a CSV</strong></p></li></ul><p>That felt like enough to work with.</p><div><hr></div><h3><strong>The Migration</strong></h3><p>I used <code>delta-rs</code>, the Python package, to write the data to a local Delta Table. No Spark, no cluster. Just Python reading the CSV in chunks and writing it out as Parquet files within the Delta Table format.</p><p>The full code is in the companion repo <a href="https://github.com/ImmanuelJoseph7/TableFormatComparison">here</a>. The <a href="https://github.com/ImmanuelJoseph7/TableFormatComparison/blob/main/README.md">README.md</a> file has instructions to follow along.</p><pre><code><code>import pandas as pd
import pyarrow as pa
from deltalake import write_deltalake

CHUNKSIZE = 1_000_000
first = True

for chunk in pd.read_csv("data/nyc_parking_violations.csv", chunksize=CHUNKSIZE, dtype=str):
    chunk.columns = [c.replace(" ", "_").replace("/", "_") for c in chunk.columns]
    table = pa.Table.from_pandas(chunk, preserve_index=False)
    write_deltalake("data/delta_table", table, mode="overwrite" if first else "append", schema_mode="merge")
    first = False
</code></code></pre><p>The script reads a million rows at a time, writes to Delta, and repeats until done.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GM9i!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GM9i!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!GM9i!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!GM9i!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!GM9i!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GM9i!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png" width="1408" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ecee0453-473c-4705-817b-40d37fa1220b_1408x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1408,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2499225,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/194667438?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GM9i!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 424w, https://substackcdn.com/image/fetch/$s_!GM9i!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 848w, https://substackcdn.com/image/fetch/$s_!GM9i!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 1272w, https://substackcdn.com/image/fetch/$s_!GM9i!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecee0453-473c-4705-817b-40d37fa1220b_1408x768.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/p/converting-a-large-csv-to-a-delta?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/p/converting-a-large-csv-to-a-delta?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h3><strong>What do the numbers say</strong></h3><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8tFF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8tFF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 424w, https://substackcdn.com/image/fetch/$s_!8tFF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 848w, https://substackcdn.com/image/fetch/$s_!8tFF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 1272w, https://substackcdn.com/image/fetch/$s_!8tFF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8tFF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png" width="532" height="154" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/65323e28-7334-4a18-9c68-4672441cf629_532x154.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:154,&quot;width&quot;:532,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:16674,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/194667438?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8tFF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 424w, https://substackcdn.com/image/fetch/$s_!8tFF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 848w, https://substackcdn.com/image/fetch/$s_!8tFF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 1272w, https://substackcdn.com/image/fetch/$s_!8tFF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65323e28-7334-4a18-9c68-4672441cf629_532x154.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p></p><p>The delta table was 4.9x smaller on the first write, and 6.5x after running a couple of extra steps.</p><p>The initial write produces one Parquet file per chunk. 43 Snappy-compressed files in this case. Running <code>OPTIMIZE</code> compacts those into fewer, larger files and rewrites them using ZSTD compression, which squeezes them down further. <code>VACUUM</code> then cleans up the old Snappy versions. Together they saved another 443 MB.</p><pre><code><code>from deltalake import DeltaTable
dt = DeltaTable("data/delta_table")
dt.optimize.compact()
dt.vacuum(retention_hours=0, enforce_retention_duration=False, dry_run=False)</code></code></pre><div><hr></div><h3><strong>Dictionary Encoding</strong></h3><p>Where Parquet shines is when you have columns with repeated values. It applies a concept called <a href="https://parquet.apache.org/docs/file-format/data-pages/encodings/#dictionary-encoding-plain_dictionary--2-and-rle_dictionary--8">Dictionary Encoding</a> automatically, when the ratio of unique values to total rows is low enough to make it worthwhile. A colour column with 5,745 unique values across 42 million rows is a perfect candidate. A column of unique transaction IDs is not.</p><p>In this case, we will take the column <code>Vehicle_Color</code> as an example.</p><p>Across 42 million rows, there are only <strong>5,745 unique colour values</strong> (5,745 unique colours? &#8212; clearly a free-text field?!). Things like <code>WHITE</code>, <code>BLACK</code>, <code>GREY</code>, <code>BLK</code>, <code>GY</code>. In a CSV, every row stores the full string. In Parquet, each unique value is stored once in a dictionary, and each row just stores a small integer pointing to it.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nj_X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nj_X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 424w, https://substackcdn.com/image/fetch/$s_!nj_X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 848w, https://substackcdn.com/image/fetch/$s_!nj_X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 1272w, https://substackcdn.com/image/fetch/$s_!nj_X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nj_X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png" width="409" height="118" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:118,&quot;width&quot;:409,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10581,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/194667438?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nj_X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 424w, https://substackcdn.com/image/fetch/$s_!nj_X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 848w, https://substackcdn.com/image/fetch/$s_!nj_X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 1272w, https://substackcdn.com/image/fetch/$s_!nj_X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe3afa71-1a01-4332-bc2b-572a07c13d34_409x118.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>5x smaller on a single column.</p><div><hr></div><h3><strong>A Note on the Delta Log</strong></h3><p>You may ask if Parquet does all the heavy lifting why store data in a delta table format. The Delta Table format is what maintains <a href="https://docs.delta.io/concurrency-control/">ACID</a> compliance. It keeps track of all the changes to the table from the time when it was created. When you look at the Delta Table folder after running everything, you&#8217;ll see this:</p><pre><code><code>data/delta_table/
&#9500;&#9472;&#9472; _delta_log/
&#9474;   &#9500;&#9472;&#9472; 00000000000000000000.json   &#8592; WRITE (overwrite, chunk 1, 1M rows)
&#9474;   &#9500;&#9472;&#9472; 00000000000000000001.json   &#8592; WRITE (append, chunk 2, 1M rows)
&#9474;   &#9500;&#9472;&#9472; ...                         &#8592; WRITE (append, chunks 3-42)
&#9474;   &#9500;&#9472;&#9472; 00000000000000000043.json   &#8592; OPTIMIZE (43 files to 20 files)
&#9474;   &#9500;&#9472;&#9472; 00000000000000000044.json   &#8592; VACUUM START (43 files to delete)
&#9474;   &#9492;&#9472;&#9472; 00000000000000000045.json   &#8592; VACUUM END (completed)
&#9500;&#9472;&#9472; part-00000-c6e7a34b-...zstd.parquet
&#9500;&#9472;&#9472; part-00000-bc09554a-...zstd.parquet
&#9492;&#9472;&#9472; ...
</code></code></pre><p>Opening the early log entries, you&#8217;ll find <code>snappy.parquet</code> in the <code>add</code> records. That&#8217;s the initial write using Snappy compression by default. In the OPTIMIZE entry, you&#8217;ll find those replaced with <code>.zstd.parquet</code> files. Snappy is the default in most tools because it&#8217;s fast to read and write. ZSTD trades a bit of compute for better compression ratios, making it a better fit for files you&#8217;re storing long term and reading less frequently. <a href="https://parquet.apache.org/docs/file-format/data-pages/compression/">The Parquet docs</a> cover the full list of supported codecs if you want to go deeper.</p><p>Each JSON entry records exactly what happened: which files were added, which were removed, how many rows, min/max values per column. Here&#8217;s what a single append entry looks like:</p><pre><code><code>{ "commitInfo": { "operation": "WRITE", "operationParameters": { "mode": "Append" },
    "operationMetrics": { "num_added_files": 1, "num_added_rows": 1000000 } } }
{ "add": { "path": "part-00000-27e284b6-...snappy.parquet", "size": 36893533,
    "stats": { "numRecords": 1000000,
      "minValues": { "Issue_Date": "01/01/2013", ... },
      "maxValues": { "Issue_Date": "12/25/2013", ... },
      "nullCount": { "Vehicle_Make": 5589, ... } } } }
</code></code></pre><p>And here&#8217;s what the OPTIMIZE entry looks like:</p><pre><code><code>{ "commitInfo": { "operation": "OPTIMIZE",
    "operationMetrics": {
      "numFilesAdded": 20,
      "numFilesRemoved": 43,
      "filesAdded":   { "totalFiles": 20, "totalSize": 1370418125 },
      "filesRemoved": { "totalFiles": 43, "totalSize": 1834829450 } } } }
{ "remove": { "path": "part-00000-357b4b21-...snappy.parquet", "size": 51763870 } }
{ "add":    { "path": "part-00000-c6e7a34b-...zstd.parquet",   "size": 77495774,
    "stats": { "numRecords": 2000000, ... } } }
</code></code></pre><p>43 small Snappy files compacted into 20 larger ZSTD files. Total size dropped from 1.83 GB to 1.37 GB.</p><p>And once VACUUM runs:</p><pre><code><code>{ "commitInfo": { "operation": "VACUUM START",
    "operationMetrics": { "numFilesToDelete": 43, "sizeOfDataToDelete": 1834829450 } } }
{ "commitInfo": { "operation": "VACUUM END",
    "operationParameters": { "status": "COMPLETED" },
    "operationMetrics": { "numDeletedFiles": 43 } } }
</code></code></pre><p>43 files, 1.75 GB reclaimed.</p><p>It&#8217;s a complete audit trail. Every operation, in order, with the numbers to back it up. In a future post I will write in detail about how you can infer more information from these json files using a function built within delta_rs.</p><div><hr></div><h3><strong>Bringing It Back to the Cost</strong></h3><p>The numbers here are for a CSV, not a SQL Server table. I don&#8217;t have a SQL Server instance that can hold 43 million rows for free, but I&#8217;m planning to do that comparison in a future post. But the point from Part 1 still stands. SQL Server stores data row by row, adds index overhead, and keeps an instance running whether you&#8217;re querying or not. </p><div class="pullquote"><p>The same 42 million rows that sit in 1.3 GB as a Delta Table would take up considerably more in a relational database and cost you more to keep around. For data that gets queried occasionally, that overhead is hard to justify.</p></div><div><hr></div><h3><strong>What&#8217;s Next</strong></h3><p>Part 3: querying this Delta Table with <strong>DuckDB</strong>. No Spark, no cluster. Just SQL on your laptop against 42 million rows. I hope this series has been helpful. Please subscribe if you want to be notified for the future posts. I will add an additional post to this series to show how the above example works the same way in a cost effective object storage like ADLS. See you soon.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading From the Sidelines! Subscribe for free to receive new posts.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><div><hr></div>]]></content:encoded></item><item><title><![CDATA[Storing Cold Data in SQL Server? There's a Better Way.]]></title><description><![CDATA[Why Delta Tables on blob storage are the right home for data you don't query every day.]]></description><link>https://immanueljoseph.substack.com/p/storing-cold-data-in-sql-server-theres</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/storing-cold-data-in-sql-server-theres</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Mon, 06 Apr 2026 06:30:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!unMx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you&#8217;ve been in data long enough, you&#8217;ve probably inherited a database that&#8217;s grown a little too comfortable. Tables that haven&#8217;t been queried in months. Historical records from three years ago sitting right next to your live reporting data. Nobody wants to touch it, but nobody wants to delete it either.</p><p>This is the first article in a series on Delta Lake storage. Over the next few posts, we&#8217;ll cover:</p><ol><li><p><strong>What Delta Tables are and why they&#8217;re ideal for long-term archiving</strong> <em>&#8592; you&#8217;re here</em></p></li><li><p>A hands-on demo: converting a large CSV to a Delta Table in ADLS (object storage) and comparing the size</p></li><li><p>Querying Delta Tables with DuckDB, no Spark required</p></li></ol><p>Let&#8217;s start with the foundation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!unMx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!unMx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!unMx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!unMx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!unMx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!unMx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:807087,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/193320499?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!unMx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 424w, https://substackcdn.com/image/fetch/$s_!unMx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 848w, https://substackcdn.com/image/fetch/$s_!unMx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 1272w, https://substackcdn.com/image/fetch/$s_!unMx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9f5a81b-959f-4b53-85f3-a3dc84b53960_1024x559.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h3><strong>Not All Data Needs the Same Home</strong></h3><p>In most data teams, there&#8217;s a natural split in the data you manage. There&#8217;s the stuff that gets hit constantly: current records, recent transactions, the tables your dashboards and reports depend on. And then there&#8217;s the historical data: audit logs, aged-out transactions, archived records that might get queried once a quarter, during a compliance review, or when someone needs to investigate something from two years ago.</p><p>SQL Server and Postgres are great at what they do. Fast lookups, structured storage, supporting the day-to-day reporting and transformation workloads your team runs. But when you&#8217;re also storing hundreds of millions of rows that nobody touches regularly, it&#8217;s worth asking: does this data need to live here?</p><p>For cold data, there&#8217;s often a better fit. Blob storage, structured as a <strong>Delta Table</strong>, gives you the same query ability at a fraction of the cost, without the overhead of a database instance sitting idle most of the time.</p><div><hr></div><h3><strong>What Is a Parquet File?</strong></h3><p>Before we get to Delta Tables, we need to talk about Parquet, because Delta Tables are built on top of it.</p><p>Parquet is a <strong>columnar storage format</strong>. That distinction matters more than it sounds.</p><p>In a row-based format like a CSV, data is stored row by row. If an analyst wants to calculate the average of a single column across 100 million rows, the system has to scan every row to get there.</p><p>Parquet flips this. Data is stored column by column. If you only need three columns out of fifty, Parquet reads only those three. For the kinds of workloads data teams run (aggregations, filters, summaries) this is a meaningful difference.</p><p>On top of that, Parquet applies <strong>compression per column</strong>. Because each column tends to hold similar values (think of a status column that only ever contains &#8220;active&#8221;, &#8220;inactive&#8221;, or &#8220;pending&#8221;), <a href="https://parquet.apache.org/docs/file-format/data-pages/compression/">compression algorithms</a> like Snappy or ZSTD can squeeze that data down significantly. A CSV that&#8217;s 2GB on disk might land at 200-300MB as a Parquet file. We&#8217;ll show this with a real demo in the next article.</p><h3><strong>What Is a Delta Table?</strong></h3><p><a href="https://delta.io/">Delta Lake</a> is an open source storage framework that brings reliability to data lakes. A Delta Table is a layer built on top of Parquet files that adds reliability and manageability.</p><p>Here&#8217;s how it looks on disk:</p><pre><code><code>/my-delta-table/
&#9500;&#9472;&#9472; _delta_log/
&#9474;   &#9500;&#9472;&#9472; 00000000000000000000.json
&#9474;   &#9500;&#9472;&#9472; 00000000000000000001.json
&#9474;   &#9492;&#9472;&#9472; ...
&#9500;&#9472;&#9472; part-00000-abc123.snappy.parquet
&#9500;&#9472;&#9472; part-00001-def456.snappy.parquet
&#9492;&#9472;&#9472; ...</code></code></pre><p>The Parquet files are the actual data. The _delta_log/ folder is what makes it a Delta Table. It&#8217;s a transaction log that records every operation ever performed on the table: inserts, updates, deletes, schema changes. But it also stores statistics about the data itself &#8212; things like the minimum and maximum values in each column, null counts, and row counts per file. This metadata is what allows Delta to skip over entire files when you run a query with a filter, without having to open and scan them first.</p><p>This gives you things that raw Parquet files don&#8217;t. Writes are atomic, so a failed pipeline run doesn&#8217;t leave your table in a broken state. Schema enforcement means Delta rejects data that doesn&#8217;t match the table&#8217;s schema, so you don&#8217;t silently corrupt your archive. Time travel lets you query the table as it existed at any point in the past. <code>VERSION AS OF 5 </code>or <code>TIMESTAMP AS OF &#8216;2024-01-01&#8217;</code> are real queries you can run. And partition pruning means that if you partition your table by date or region, queries that filter on that column skip entire partitions, making reads much faster even on large datasets.</p><h3><strong>Putting It Together</strong></h3><p>Let&#8217;s say your team has five years of order records. They&#8217;re not going anywhere, but they&#8217;re also not being queried every day. Here&#8217;s what the two approaches look like side by side:</p><p><strong>Keeping it in SQL Server / Postgres:</strong></p><p>You&#8217;re paying for a managed database instance even when nobody&#8217;s querying that data. Storage costs are higher than blob storage. Backups and maintenance windows cover this data too. Over time, the database grows heavier, which can affect the performance of your active workloads.</p><p><strong>Moving it to a Delta Table on Object Storage:</strong></p><p>Data sits in blob storage, one of the cheapest storage tiers available. You pay for storage only and compute comes in only when you query. No instance to manage, no maintenance windows. It scales to petabytes without many infrastructure changes, and tools like Spark, Databricks, Azure Synapse, Microsoft Fabric and (as we&#8217;ll cover later) DuckDB can query it directly.</p><div class="pullquote"><p>You&#8217;re not giving up query-ability or reliability. You&#8217;re just not paying database-tier prices for data that doesn&#8217;t need it.</p></div><h3><strong>A Pattern That Works in Practice</strong></h3><p>A common approach: your database handles current and recent data, and a scheduled pipeline (Airflow, Azure Data Factory, or even GitHub Actions, we&#8217;ve covered those in <a href="https://immanueljoseph.substack.com/p/automating-data-workflows-with-github">earlier posts</a>) moves aged-out records to a Delta Table in object storage on a regular cadence. Your database stays lean, your archive stays query-able, and your storage costs stay predictable.</p><p>Different data, different home. It&#8217;s a simple idea, but it makes a real difference at scale.</p><div><hr></div><h3><strong>What&#8217;s Next</strong></h3><p>In the next article, we&#8217;ll make this concrete. I&#8217;ll take a large CSV file, write it to ADLS as a Delta Table using Python and the <code>delta-rs</code> library, and show you the size difference side by side. No Spark cluster required.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Subscribe so you don&#8217;t miss it.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/p/storing-cold-data-in-sql-server-theres?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/p/storing-cold-data-in-sql-server-theres?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p>]]></content:encoded></item><item><title><![CDATA[Monitoring dbt Runs: Logging Execution Metadata to Your Database]]></title><description><![CDATA[Automatic tracking of every model run, test result, and execution time.]]></description><link>https://immanueljoseph.substack.com/p/monitoring-dbt-runs-logging-execution</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/monitoring-dbt-runs-logging-execution</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Fri, 27 Feb 2026 11:39:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CSt3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a href="https://immanueljoseph.substack.com/p/cicd-for-dbt-running-transformations">previous post</a>, I walked through setting up a CI/CD pipeline for dbt using GitHub Actions. Today, I&#8217;m extending that setup to capture and store execution metadata directly in the database.</p><p>The goal: <strong>every time dbt runs, log the results including model names, execution times, test outcomes, and timestamps into a dedicated table for monitoring and analysis.</strong></p><p>You can find the updated repository here: <strong><a href="https://github.com/ImmanuelJoseph7/DBTActionRunner">DBTActionRunner</a></strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>Why Log Run Results?</h2><p>When dbt runs in production (or CI/CD), you lose visibility into what happened unless you dig through logs. By persisting run metadata to a database table, you can:</p><ul><li><p><strong>Track performance trends</strong> over time</p></li><li><p><strong>Monitor test failures</strong> and model execution status</p></li><li><p><strong>Audit pipeline runs</strong> with unique invocation IDs</p></li><li><p><strong>Query execution history</strong> using SQL instead of parsing log files</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CSt3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CSt3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 424w, https://substackcdn.com/image/fetch/$s_!CSt3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 848w, https://substackcdn.com/image/fetch/$s_!CSt3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!CSt3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CSt3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg" width="1024" height="541" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:541,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85323,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/189349132?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CSt3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 424w, https://substackcdn.com/image/fetch/$s_!CSt3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 848w, https://substackcdn.com/image/fetch/$s_!CSt3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!CSt3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F159a2266-415a-45c8-83f7-5e8277e63054_1024x541.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>The Implementation</h2><p>dbt-core provides <strong>on-run-end hooks</strong> which are SQL or macros that execute after a dbt run completes. These hooks have access to the <code>results</code> context variable, which contains metadata about every model and test that ran.</p><h3>Step 1: Add the Hook to <code>dbt_project.yml</code></h3><p>First, configure dbt to call a custom macro after each run:</p><pre><code><code>on-run-end:
  - "{{ log_run_results() }}"</code></code></pre><p>This tells dbt to execute the <code>log_run_results()</code> macro when the run finishes.</p><h3>Step 2: Create the Logging Macro</h3><p>Create a new file at <code>macros/log_run_results.sql</code>:</p><pre><code><code>{% macro log_run_results() %}
    {% if execute %}

        -- Create the logging table if it doesn't exist
        {% set create_table_sql %}
            IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'dbt_run_results' AND schema_id = SCHEMA_ID('{{ target.schema }}'))
            BEGIN
                CREATE TABLE {{ target.schema }}.dbt_run_results (
                    run_id UNIQUEIDENTIFIER DEFAULT NEWID(),
                    invocation_id NVARCHAR(255),
                    run_started_at DATETIME2,
                    model_name NVARCHAR(255),
                    status NVARCHAR(50),
                    execution_time FLOAT,
                    rows_affected INT,
                    message NVARCHAR(MAX),
                    created_at DATETIME2 DEFAULT GETDATE()
                )
            END
        {% endset %}

        {% do run_query(create_table_sql) %}

        -- Insert run results
        {% for result in results %}
            {% set insert_sql %}
                INSERT INTO {{ target.schema }}.dbt_run_results 
                (invocation_id, run_started_at, model_name, status, execution_time, rows_affected, message)
                VALUES (
                    '{{ invocation_id }}',
                    '{{ run_started_at }}',
                    '{{ result.node.unique_id }}',
                    '{{ result.status }}',
                    {{ result.execution_time }},
                    {% if result.adapter_response.get('rows_affected') %}{{ result.adapter_response.rows_affected }}{% else %}NULL{% endif %},
                    '{{ result.message | replace("'", "''") }}'
                )
            {% endset %}

            {% do run_query(insert_sql) %}
        {% endfor %}

        {{ log("Run results logged to " ~ target.schema ~ ".dbt_run_results", info=True) }}

    {% endif %}
{% endmacro %}
</code></code></pre><p><strong>What this does:</strong></p><ol><li><p><strong>Creates the table</strong> (<code>dbt_run_results</code>) if it doesn&#8217;t exist</p></li><li><p><strong>Loops through each result</strong> from the dbt run</p></li><li><p><strong>Inserts metadata</strong> for every model and test executed</p></li><li><p><strong>Captures key metrics</strong>: execution time, status, invocation ID, timestamps</p></li></ol><div><hr></div><h2>What Gets Logged</h2><p>After running <code>dbt build</code> or <code>dbt run</code>, the table contains entries like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YiQL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YiQL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 424w, https://substackcdn.com/image/fetch/$s_!YiQL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 848w, https://substackcdn.com/image/fetch/$s_!YiQL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 1272w, https://substackcdn.com/image/fetch/$s_!YiQL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YiQL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png" width="1456" height="193" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:193,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85566,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/189349132?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YiQL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 424w, https://substackcdn.com/image/fetch/$s_!YiQL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 848w, https://substackcdn.com/image/fetch/$s_!YiQL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 1272w, https://substackcdn.com/image/fetch/$s_!YiQL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F054c9573-2e80-4d02-89f6-03079c79bad9_2044x271.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p></p><p><strong>Key fields:</strong></p><ul><li><p><code>invocation_id</code>: Groups all models/tests from a single dbt run</p></li><li><p><code>model_name</code>: Full identifier (e.g., <code>model.Football_Analytics.stg_goals_per_season</code>)</p></li><li><p><code>status</code>: <code>success</code>, <code>error</code>, <code>pass</code>, <code>fail</code>, <code>skipped</code></p></li><li><p><code>execution_time</code>: Seconds to execute</p></li><li><p><code>rows_affected</code>: Actual row count for models (NULL for tests)</p></li><li><p><code>created_at</code>: When the log entry was written</p></li></ul><div><hr></div><h2>Querying the Results</h2><p>Now you can analyze your dbt runs using SQL:</p><h3>Find the slowest models</h3><pre><code><code>SELECT 
    model_name,
    AVG(execution_time) as avg_execution_time,
    MAX(execution_time) as max_execution_time,
    COUNT(*) as run_count
FROM dbt_run_results
WHERE model_name LIKE 'model.%'
GROUP BY model_name
ORDER BY avg_execution_time DESC
</code></code></pre><h3>Check recent test failures</h3><pre><code><code>SELECT 
    run_started_at,
    model_name,
    status,
    message
FROM dbt_run_results
WHERE model_name LIKE 'test.%'
  AND status = 'fail'
ORDER BY run_started_at DESC
</code></code></pre><h3>Track runs over time</h3><pre><code><code>SELECT 
    CAST(run_started_at AS DATE) as run_date,
    COUNT(DISTINCT invocation_id) as total_runs,
    SUM(CASE WHEN status IN ('success', 'pass') THEN 1 ELSE 0 END) as successful_items,
    SUM(CASE WHEN status IN ('error', 'fail') THEN 1 ELSE 0 END) as failed_items
FROM dbt_run_results
GROUP BY CAST(run_started_at AS DATE)
ORDER BY run_date DESC
</code></code></pre><div><hr></div><h2>Integration with CI/CD</h2><p>Since this runs as an <code>on-run-end</code> hook, it works automatically in your GitHub Actions pipeline. Every time the workflow executes <code>dbt build</code>, the results get logged and no additional configurations are needed.</p><p>From the <a href="https://github.com/ImmanuelJoseph7/DBTActionRunner/blob/main/.github/workflows/main.yml">workflow file</a>:</p><pre><code><code>- name: Run dbt Build and Test
  run: |
    echo "Running dbt debug to confirm connection..."
    dbt debug --target ci

    echo "Running dbt build (runs, tests, seeds) on CI target..."
    dbt build --target ci
</code></code></pre><p>After this step completes, the <code>dbt_run_results</code> table is automatically populated.</p><div><hr></div><h2>How Row Counts Are Captured</h2><p>The macro queries each model after it&#8217;s built to get the actual row count:</p><pre><code><code>{% set count_query %}
    SELECT COUNT(*) as row_count FROM {{ relation }}
{% endset %}
</code></code></pre><p>This adds minimal overhead (one COUNT query per model) but provides valuable insight into data volume trends over time. Tests show NULL for <code>rows_affected</code> since they&#8217;re validation queries, not data transformations.</p><div><hr></div><h2>Summary</h2><p>By leveraging dbt&#8217;s <code>on-run-end</code> hooks, you can build a lightweight monitoring system that:</p><ul><li><p>Requires no external tools</p></li><li><p>Stores data in your existing database</p></li><li><p>Integrates seamlessly with CI/CD</p></li><li><p>Provides queryable execution history</p></li></ul><p>This approach gives you full visibility into your dbt pipeline without needing dbt Cloud or third-party observability platforms.</p><p>Check out the full implementation in the <a href="https://github.com/ImmanuelJoseph7/DBTActionRunner">DBTActionRunner repo</a>, and feel free to adapt it for your own setup.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading From the Sidelines! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[CI/CD for dbt: Running Transformations via GitHub Action Runners]]></title><description><![CDATA[A technical walkthrough of automating dbt builds without local intervention.]]></description><link>https://immanueljoseph.substack.com/p/cicd-for-dbt-running-transformations</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/cicd-for-dbt-running-transformations</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Wed, 04 Feb 2026 06:34:01 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/829ec6c7-9e13-4d8a-93ef-ead7cbbd3462_1424x752.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my last post, I touched on the basics of GitHub Actions. Today, I&#8217;m moving the needle forward by integrating <strong>dbt (data build tool)</strong> into that workflow. The goal is simple: automate the <code>dbt run</code>  cycle using a GitHub-hosted runner.</p><p>You can find the full repository here: <strong><a href="https://github.com/ImmanuelJoseph7/DBTActionRunner">DBTActionRunner</a></strong></p><h3>The Architecture</h3><p>The setup involves three main components:</p><ol><li><p><strong>dbt Project:</strong> The standard models, seeds, and tests.</p></li><li><p><strong>Profiles.yml:</strong> Configured to use environment variables for database connectivity.</p></li><li><p><strong>GitHub Workflow:</strong> The <code>.yml</code> file that orchestrates the environment setup and execution.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!awpI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!awpI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 424w, https://substackcdn.com/image/fetch/$s_!awpI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 848w, https://substackcdn.com/image/fetch/$s_!awpI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 1272w, https://substackcdn.com/image/fetch/$s_!awpI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!awpI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png" width="1424" height="752" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:752,&quot;width&quot;:1424,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1547234,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/186822376?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!awpI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 424w, https://substackcdn.com/image/fetch/$s_!awpI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 848w, https://substackcdn.com/image/fetch/$s_!awpI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 1272w, https://substackcdn.com/image/fetch/$s_!awpI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3b015ad-849a-41b6-8a7b-a76aa12c8465_1424x752.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h3>1. Handling the Profile</h3><p>The biggest hurdle in running dbt in a CI/CD environment is the <code>profiles.yml</code>. You don&#8217;t want to check credentials into version control. Instead, I&#8217;ve configured the profile to pull from environment variables:</p><pre><code>football_analytics:
  target: ci

  outputs:
    ci:
      type: sqlserver
      driver: 'ODBC Driver 18 for SQL Server' # Adjust the version if necessary
      
      # Use environment variables (which will be populated by GitHub Secrets)
      server: "{{ env_var('DBT_SERVER') }}" 
      port: 1433
      database: "{{ env_var('DBT_DATABASE') }}" 
      schema: "{{ env_var('DBT_SCHEMA') }}" 
      user: "{{ env_var('DBT_USER') }}" 
      password: "{{ env_var('DBT_PASSWORD') }}" 
      </code></pre><h3>2. Managing Secrets</h3><p>To make the above work, I&#8217;ve stored the actual database credentials in <strong>GitHub Secrets</strong> (<code>Settings &gt; Secrets and variables &gt; Actions</code>). This ensures that the runner has access to the database during execution without exposing sensitive data in the logs.</p><h3>3. The Workflow Implementation</h3><p>The workflow file (<code>.github/workflows/main.yml</code>) defines the lifecycle of the runner. Here&#8217;s the breakdown of the steps I implemented:</p><ul><li><p><strong>Trigger:</strong> Set to <code>on: push</code> to the main branch.</p></li><li><p><strong>Environment:</strong> Uses <code>ubuntu-latest</code>.</p></li><li><p><strong>Python Setup:</strong> Installs Python 3.12 to handle the dbt core and adapter installation.</p></li></ul><p><strong>The core steps in the YAML:</strong></p><pre><code>name: dbt CI/CD Pipeline - Football Analytics
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
  workflow_dispatch: null
jobs:
  dbt_ci_job:
    runs-on: ubuntu-latest
    env:
      DBT_SERVER: ${{ secrets.DBT_SERVER }}
      DBT_DATABASE: ${{ secrets.DBT_DATABASE }}
      DBT_SCHEMA: ${{ secrets.DBT_SCHEMA }}
      DBT_USER: ${{ secrets.DBT_USER }}
      DBT_PASSWORD: ${{ secrets.DBT_PASSWORD }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: 3.12
      - name: Install dbt and Dependencies
        run: |
          # 1. Install prerequisites for the MS SQL Driver (unixODBC)
          sudo apt-get update
          sudo apt-get install -y unixodbc unixodbc-dev
          
          # 2. Add Microsoft repositories to fetch the official driver
          curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
          # Assuming Ubuntu 22.04 runner, adjust if using a different image
          curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list

          # 3. Update repos and install the ODBC Driver 18 (msodbcsql18)
          sudo apt-get update
          sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18

          # 4. Install dbt adapter and packages
          pip install dbt-sqlserver
          dbt deps
      - name: Run dbt Build and Test
        run: &gt;
          echo "Running dbt debug to confirm connection..."
          dbt debug --target ci
          echo "Running dbt build (runs, tests, seeds) on CI target..."
          dbt build --target ci
      - name: Clean up
        run: &gt;
          echo "Dbt run complete."</code></pre><div><hr></div><h3>Summary</h3><p>By offloading the execution to a GitHub Action Runner, the project is no longer dependent on a local machine&#8217;s uptime or configuration. It provides a clean, reproducible environment for every change pushed to the repo.</p><p>If you&#8217;re looking to implement this, feel free to fork the <a href="https://github.com/ImmanuelJoseph7/DBTActionRunner">DBTActionRunner</a> repo and swap out the adapter and credentials for your own.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/p/cicd-for-dbt-running-transformations?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/p/cicd-for-dbt-running-transformations?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Automating Data Workflows with GitHub Actions: A Data Engineer’s Secret Weapon]]></title><description><![CDATA[Why GitHub Actions Matter for Data Engineers]]></description><link>https://immanueljoseph.substack.com/p/automating-data-workflows-with-github</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/automating-data-workflows-with-github</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Thu, 21 Aug 2025 09:02:10 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5a1665d8-755c-453c-8569-54fe70df111a_512x358.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the most valuable skills for a modern data engineer is the ability to move data seamlessly between platforms, whether that means databases, cloud services, APIs, or warehouses. Every organization uses a mix of tools, and your value as a data engineer grows when you can connect these tools and make them work together.</p><p>A powerful but often overlooked way to do this is with GitHub Actions runners. These allow you to execute Python (or any language) scripts inside a CI/CD pipeline, triggered by events such as code pushes, schedules, or manual dispatches. In other words, you can automate data tasks without standing up servers or relying on heavier ETL platforms.</p><h3>Why GitHub Actions Matter for Data Engineers</h3><p>Traditionally, data engineers rely on managed ETL tools like Airflow, Azure Data Factory, or AWS Glue. These are excellent in many cases, but they do not always fit every scenario. Sometimes you need to integrate with platforms that don&#8217;t have a ready-made connector. Sometimes you want lightweight automation without setting up infrastructure. Other times, you need fine-grained control over how data is pulled, transformed, and pushed.</p><p>GitHub Actions runners fill that gap. If you can write it in Python, you can run it in a runner. You can set up jobs to run on a schedule, after a code push, or whenever you manually trigger them. Secrets and credentials can be stored securely in GitHub Secrets, and the entire process lives alongside your code under version control. This combination of automation, security, and flexibility makes runners especially powerful for data engineers.</p><h3>An Example Using My Repository</h3><p>To make this more concrete, let&#8217;s look at my <a href="https://github.com/ImmanuelJoseph7/RunPythonWorkflow/tree/main">GitHub repository</a> . It contains an example integration script that uses pandas to capture data from the GitHub API, writes the results to a CSV file, and saves the file as a build artifact. The GitHub Actions workflow takes care of setting up Python, installing dependencies, running the script, and saving the output.</p><p>Here is the workflow file:</p><pre><code>name: Run Python Script

on: 
  workflow_dispatch:   # run manually from GitHub
  push:                # run when code is pushed to main
    branches: [ main ]

jobs:
  run-script:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.10"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run my script
        run: python test.py

      - name: Upload CSV artifact
        uses: actions/upload-artifact@v4
        with:
          name: github-events
          path: github_events_*.csv
          retention-days: 1</code></pre><p>This workflow ensures that whenever it runs, the script executes in a fresh environment and the resulting CSV is stored for one day as an artifact that can be downloaded from the Actions page. It demonstrates the core idea: runners can automate the boring parts of data movement while letting you focus on the logic inside your script.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/subscribe?"><span>Subscribe now</span></a></p><h3>Why This Matters for Your Career</h3><p>Being able to set up lightweight integrations with GitHub Actions shows that you understand more than just databases and SQL. You are applying software engineering practices like version control and CI/CD to data engineering work. You can handle cross-platform data flows without being tied to a single vendor. And you can deliver secure, flexible automation at a moment&#8217;s notice.</p><p>For hiring managers, this signals that you are adaptable and resourceful. For your team, it means faster prototyping and fewer infrastructure headaches. For you, it is an extra tool in your toolkit that will make you stand out as a data engineer who is not only comfortable with data, but also with the software practices that surround it.</p><div class="pullquote"><p>GitHub Actions runners are a way to grow into the kind of data engineer who bridges the gap between data and software, and who thrives in any environment where systems need to be connected.</p></div><p>Managed ETL tools are not going away, and they will always play a big role. But data engineers who know how to use runners and workflows have a secret weapon. You can integrate anything with an API. You can spin up quick jobs without waiting for infrastructure. You can deliver value faster and more flexibly.</p>]]></content:encoded></item><item><title><![CDATA[What Being Agile Really Means in Tech]]></title><description><![CDATA[Beyond the buzzwords, it&#8217;s about how we think, work, and adapt.]]></description><link>https://immanueljoseph.substack.com/p/what-being-agile-really-means-in</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/what-being-agile-really-means-in</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Sat, 28 Jun 2025 04:26:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7512cf95-f39a-4150-a028-bfd8633b722c_420x300.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When someone asks if we&#8217;re &#8220;agile&#8221;, I usually pause. Not because we aren&#8217;t, but because the question rarely means what it should. Sometimes it&#8217;s, &#8220;Do you follow Scrum?&#8221; Other times it&#8217;s, &#8220;Do you have daily standups?&#8221; More often, it&#8217;s just shorthand for, &#8220;Do you move fast and have a Jira board?&#8221;</p><p>The reality is, agility in a tech environment isn&#8217;t about rituals or tools. It&#8217;s about how we think, and how willing we are to change when it matters.</p><p>For me, being agile means staying open to feedback, making room for course correction, and moving forward without getting stuck in our own plans. It&#8217;s not about skipping structure. It&#8217;s about not being owned by it. You still plan, just in smaller chunks. You still commit, but with eyes open to what might shift. Agile teams listen more. They adapt quickly. They know that learning is part of the process, not something that happens after a launch.</p><p>And honestly, none of it works without the right team culture. You can follow the ceremonies perfectly and still be rigid. Or you can have no formal framework and still respond well to change. It comes down to whether people feel trusted to take ownership and safe to challenge ideas. The tools don&#8217;t create that. The people do.</p><div class="pullquote"><p>The mistake I&#8217;ve seen is focusing too much on <strong>doing agile</strong> and not enough on <strong>being agile</strong>. The standups, the boards and sprints are just scaffolding. If they help you work better, great. But if they become the goal, you&#8217;ve missed the point.</p></div><p>Agility is less about speed and more about staying aligned. With your users, your purpose, and your team. It&#8217;s about working in a way that lets you learn something, and then actually do something with it.</p><p>So if you're in tech, ask yourself. Are you using agile tools, or are you actually working in an agile way?</p><p><em>If this resonated, I write about tech, teams, and working with intent. Subscribe to get future posts straight to your inbox.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Being Intentional!]]></title><description><![CDATA[Is there room for intention in the everyday?]]></description><link>https://immanueljoseph.substack.com/p/being-intentional</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/being-intentional</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Fri, 23 May 2025 11:23:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f125c3bd-9b7e-4161-9084-d08d8d1f9830_420x300.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lately I&#8217;ve been thinking about how monotonous work can feel. With so many of us working from home, the lines between days blur. Conversations become more transactional. It&#8217;s easy to forget the human element behind the work. And when that happens, something important slips away.</p><p>I&#8217;ve found that work goes better when I&#8217;m intentional. It&#8217;s not always easy, and I don&#8217;t always get it right, but I try to be mindful of how I show up. How I speak, how I listen, how I respond. But every time I put in the effort to be intentional, it has turned out positive. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SIbk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SIbk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 424w, https://substackcdn.com/image/fetch/$s_!SIbk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 848w, https://substackcdn.com/image/fetch/$s_!SIbk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 1272w, https://substackcdn.com/image/fetch/$s_!SIbk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SIbk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png" width="552" height="469.3333333333333" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:704,&quot;width&quot;:828,&quot;resizeWidth&quot;:552,&quot;bytes&quot;:39680,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/164219225?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SIbk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 424w, https://substackcdn.com/image/fetch/$s_!SIbk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 848w, https://substackcdn.com/image/fetch/$s_!SIbk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 1272w, https://substackcdn.com/image/fetch/$s_!SIbk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd32b2b20-2063-45e8-9200-a289c8812ed6_828x704.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://www.oxfordlearnersdictionaries.com/definition/english/intend?q=intend">This is what the oxford dictionary says about the word Intend </a></figcaption></figure></div><p>It can be as simple as taking a moment before sending a message, or making sure I really hear what someone&#8217;s saying in a meeting instead of just planning what I&#8217;ll say next. Sometimes it&#8217;s checking my tone. Other times, it&#8217;s holding back a reaction I&#8217;d rather not lead with.</p><p>I try to give feedback with some care. To ask questions instead of assuming. To notice when something feels off and not ignore it.</p><p>It&#8217;s not about being perfect or polished. It&#8217;s just about trying to be present. To match what I do with what I mean, and to do my part in making things a bit easier for the people I work with.</p><p>This isn&#8217;t something I&#8217;m writing to boast. I&#8217;m just trying to figure out how to keep relationships at work feeling human in a world that can feel increasingly flat and repetitive. If you&#8217;ve been feeling the same way, I hope this helps you feel a little less alone.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[How I Set Up Airflow Locally (and Dodged Cloud Costs)]]></title><description><![CDATA[Running Apache Airflow Locally for Hands-On Learning and Fun]]></description><link>https://immanueljoseph.substack.com/p/how-i-set-up-airflow-locally-and</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/how-i-set-up-airflow-locally-and</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Fri, 16 May 2025 12:07:09 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/99143280-e910-42a2-8598-5e73b1e48acb_420x300.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My day job, and let&#8217;s be honest, my happy place, has always been in data engineering. There&#8217;s something oddly satisfying about watching a data pipeline run perfectly and getting that little green checkmark. It&#8217;s like a gold star for grownups.</p><p>I started my career wrangling data with SSIS (SQL Server Integration Services). Later, I shifted to Python as the industry evolved. But wow, has the landscape changed. These days, being a data engineer doesn&#8217;t just mean moving data around. You&#8217;re expected to be part DevOps, part software engineer, part cloud whisperer. And sometimes, all of that.</p><p>For a while, I was drowning in the learning curve. Every new tool came with a learning path longer than my weekend to-do list. I began to think, maybe I&#8217;d get more value from diving deep into one tool or concept at a time, and learning it by doing. In my experience, the real &#8220;aha!&#8221; moments come when you&#8217;re knee-deep in terminal errors and finally get something working.</p><p>That moment hit when I got curious about <a href="https://airflow.apache.org/">Apache Airflow</a>. I&#8217;d heard all the buzz: DAGs this, orchestration that. I wanted to get hands-on, but running it in the cloud? Not so fast. Most cloud platforms don&#8217;t hand out free credits like they used to, and I wasn&#8217;t exactly keen to hand over my credit card just to spin up a test environment.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sRB3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sRB3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 424w, https://substackcdn.com/image/fetch/$s_!sRB3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 848w, https://substackcdn.com/image/fetch/$s_!sRB3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 1272w, https://substackcdn.com/image/fetch/$s_!sRB3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sRB3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png" width="361" height="139" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:139,&quot;width&quot;:361,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5364,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/163698293?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!sRB3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 424w, https://substackcdn.com/image/fetch/$s_!sRB3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 848w, https://substackcdn.com/image/fetch/$s_!sRB3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 1272w, https://substackcdn.com/image/fetch/$s_!sRB3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F531788ab-fbf1-4be6-8168-7e0755458bbf_361x139.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>But then it hit me. Airflow is open source. Linux is open source. And I&#8217;m on Windows, with WSL (Windows Subsystem for Linux). Lightbulb moment. Why not just run Airflow locally for free, using Linux on Windows? No billing alerts, no surprise charges. Just me, my laptop, and a fresh Ubuntu instance.</p><p>So that&#8217;s what I did. And it worked.</p><p>I&#8217;ve included a step-by-step guide for setting up Airflow on WSL, for anyone else who wants to skip the cloud bills and get straight to learning. It&#8217;s not perfect, but it&#8217;s real, it&#8217;s practical, and most importantly, it&#8217;s yours.</p><div><hr></div><p><strong>Instructions to Install Apache Airflow Using WSL with Ubuntu</strong></p><p><em><strong>1. Enable WSL and Install Ubuntu</strong></em></p><p>Open <strong>PowerShell as Administrator</strong> and run:</p><pre><code><code>wsl --install</code></code></pre><p>Restart your computer if prompted.</p><p><em><strong>2. Install Ubuntu from Microsoft Store</strong></em></p><p>Open the <strong>Microsoft Store</strong>, search for <strong>Ubuntu 22.04 LTS</strong>, and install it.</p><p>Or you can download it from <a href="https://releases.ubuntu.com/jammy/">https://releases.ubuntu.com/jammy/</a></p><p><em><strong>3. Set Up Ubuntu</strong></em></p><p>Launch Ubuntu from the Start menu and complete the initial setup by creating a username and password.</p><p><em><strong>4. Update Package Lists</strong></em></p><p>In the Ubuntu terminal, run:</p><blockquote><p><em>Bash is used in cmd (Command Prompt) in Windows to provide a Linux-like environment for running Linux-based commands and tools, allowing users to work with Linux-compatible software on Windows</em></p></blockquote><pre><code><code>bash
sudo apt update</code></code></pre><p><em><strong>5. Install Required Dependencies</strong></em></p><pre><code><code>Install Python, pip, and venv:
sudo apt install python3 python3-pip python3-venv</code></code></pre><p><em><strong>6. Create and Activate a Virtual Environment</strong></em></p><p>Create a virtual environment:</p><pre><code><code>bash
python3 -m venv airflow_env</code></code></pre><p>Activate the virtual environment:</p><pre><code><code>bash
source airflow_env/bin/activate</code></code></pre><p><em><strong>7. Install Apache Airflow</strong></em></p><p>Install Airflow:</p><pre><code><code>bash
pip install apache-airflow</code></code></pre><p><em><strong>8. Initialize the Airflow Database</strong></em></p><p>Initialize the database:</p><pre><code><code>bash
airflow db init</code></code></pre><p><em><strong>9. Start Airflow Components</strong></em></p><p><strong>Open Two Terminal Windows</strong>:</p><p>In the <strong>first terminal</strong>, activate the virtual environment and start the scheduler:</p><pre><code><code>bash
source airflow_env/bin/activate
airflow scheduler</code></code></pre><p>In the <strong>second terminal</strong>, activate the virtual environment again and start the web server:</p><pre><code><code>bash
source airflow_env/bin/activate
airflow webserver --port 8080</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Srhl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Srhl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 424w, https://substackcdn.com/image/fetch/$s_!Srhl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 848w, https://substackcdn.com/image/fetch/$s_!Srhl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 1272w, https://substackcdn.com/image/fetch/$s_!Srhl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Srhl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png" width="1456" height="423" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a658308d-5d73-491f-9140-3cc6e0471b70_2212x643.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:423,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130079,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/163698293?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa658308d-5d73-491f-9140-3cc6e0471b70_2212x643.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Srhl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 424w, https://substackcdn.com/image/fetch/$s_!Srhl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 848w, https://substackcdn.com/image/fetch/$s_!Srhl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 1272w, https://substackcdn.com/image/fetch/$s_!Srhl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb36e171-4061-46e5-9c3a-1744fe77c205_2212x643.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">If the steps were successful the two terminal windows will look something like this side by side.</figcaption></figure></div><p><em><strong>10. Access Airflow UI</strong></em></p><p>Open a web browser and navigate to <em>http://localhost:8080</em> to access the Airflow UI.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!G2ZZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 424w, https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 848w, https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 1272w, https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png" width="1456" height="418" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png&quot;,&quot;srcNoWatermark&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/990eb5f0-2ac4-45d4-892f-f2bb6898e137_2559x734.png&quot;,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:418,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:48524,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://immanueljoseph.substack.com/i/163698293?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F990eb5f0-2ac4-45d4-892f-f2bb6898e137_2559x734.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 424w, https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 848w, https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 1272w, https://substackcdn.com/image/fetch/$s_!G2ZZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67512f29-c1b8-44c6-830c-a2acdfd58f99_2559x734.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The UI to get into Airflow will be something like this.</figcaption></figure></div><p>If you have been in tech for a while the username and password should be obvious to you ;). If it isn&#8217;t please reach out! </p><p>Start Airflow-ing, the fun (and free) way!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading From the Sidelines! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Hello there!]]></title><description><![CDATA[Welcome to the Intersection of Tech, Data, Leadership & Sport.]]></description><link>https://immanueljoseph.substack.com/p/hello-there</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/hello-there</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Fri, 09 May 2025 11:27:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d08d063a-5a5a-47f7-aaba-42eb35bdcad3_3999x2666.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m thrilled to welcome you to this space, my corner of the internet where I&#8217;ll be writing about the things that bring me joy: technology, data, coaching, leadership and sport.</p><p>You might be wondering why these topics? Its mainly because I can&#8217;t pick one&#8230;.. So I thought why not write about the the things I love and have some level of experience in. On their own, they&#8217;re powerful. Lets see where this goes together. </p><h3>What can you expect?</h3><p>Every week (or two or three &#128523;), I&#8217;ll be sharing:</p><ul><li><p>Reflections from the tech world that I can keep on top of</p></li><li><p>Insights on using data to make smarter, braver decisions</p></li><li><p>Graphs I wish existed in sports</p></li><li><p>Stories and lessons from coaching</p></li><li><p>Frameworks, questions, and mental models for thoughtful leadership</p></li></ul><p>This newsletter is for the curious, the analytical, and the quietly ambitious. For people who build, lead, coach, or just want to think more deeply about how we work and what drives us. </p><h3>Why am I doing this? </h3><p>I often find myself wondering why certain tools succeed, why some sports teams consistently win, and why some IT teams outperform others. I usually have opinions on these things&#8212;but I&#8217;ve rarely shared them. Part of that&#8217;s fear, part of it&#8217;s the habit of keeping ideas to myself. Let&#8217;s see if I can overcome this fear and be willing to share my ideas.</p><p>I&#8217;ve also spent countless late nights buried in Stack Overflow threads (yeah, before ChatGPT was around), searching for answers to tricky technical problems. Over time, I&#8217;ve built up a quiet library of knowledge&#8212;tucked away in notes and saved files&#8212;that might actually help someone else. So this newsletter is my way of starting to share what I&#8217;ve learned, what I think (not that everything is useful &#128513;) , and what I&#8217;m still figuring out.</p><p>So&#8230; if any of this sounds interesting, feel free to hit subscribe, follow along, or click whatever button feels right. I&#8217;d love to have you on the journey.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Coming soon]]></title><description><![CDATA[This is From the Sidelines.]]></description><link>https://immanueljoseph.substack.com/p/coming-soon</link><guid isPermaLink="false">https://immanueljoseph.substack.com/p/coming-soon</guid><dc:creator><![CDATA[Immanuel Joseph]]></dc:creator><pubDate>Fri, 09 May 2025 04:21:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!BQkI!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17e6867e-3a25-4948-b2fd-7600984d8435_608x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is From the Sidelines.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://immanueljoseph.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://immanueljoseph.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item></channel></rss>