<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title><![CDATA[Jerrylog Blog]]></title>
    <link>https://jerrylog.dev/en</link>
    <description><![CDATA[The latest posts from Jerrylog.]]></description>
    <language>en</language>
    <lastBuildDate>Thu, 22 Oct 2026 15:50:01 GMT</lastBuildDate>
    <atom:link href="https://jerrylog.dev/en/blog/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title><![CDATA[Serving One Delta Lake Copy to Four Data Platforms]]></title>
      <link>https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving</link>
      <guid isPermaLink="true">https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving</guid>
      <description><![CDATA[How I merged data kept twice in S3 Parquet and BigQuery into one Delta Lake copy, served through Delta Sharing to Databricks and as Iceberg via UniForm.]]></description>
      <content:encoded><![CDATA[<p>At a previous job, I worked on delivering on-chain and market data to US institutional clients. Real-time metrics went out through an API, but some clients also wanted large volumes of historical data, for backtesting and similar work. We kept that history as Parquet files under Hive-partitioned S3 paths, with no table format, and gave clients read access to those paths. For clients who wanted to query it with SQL right away, we also copied the same data into BigQuery tables as a Query-as-a-Service offering and granted them read access there.</p>
<p><img src="https://jerrylog.dev/assets/images/delta-uniform/query-as-a-service.png" alt="A SQL editor querying a BigQuery table in the Query-as-a-Service offering"/></p>
<p>This setup had a problem from day one. Because it was nothing more than a pile of files, a client reading while we were writing, or while the schema was changing, could end up with inconsistent data. That is exactly the problem open table formats such as Delta Lake and Iceberg exist to solve.</p>
<p>As the client base grew, a different problem took over. Some clients worked on AWS directly, but quite a few were on BigQuery, Databricks, or Snowflake, and with a data estate of this size we could not afford one more copy for every platform we added. We needed to move to an open table format in a way that let several platforms read the same copy. And since the consumers were external clients rather than teams inside the company, picking a table format also meant picking which client platforms we would be compatible with.</p>
<p>This post walks through how we merged the S3 Parquet plus BigQuery setup into a single Delta Lake copy and served it to clients on four platforms through Delta Sharing and UniForm. For how each platform feature behaves, I checked the official documentation as of October 2026, so details may differ from what we ran at the time.</p>
<h2 id="the-old-setup-and-its-problems"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#the-old-setup-and-its-problems">The old setup and its problems</a></h2>
<h3 id="the-limits-of-handing-out-files"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#the-limits-of-handing-out-files">The limits of handing out files</a></h3>
<p>Handing out raw files had three problems.</p>
<figure class="dg-figure"><div class="zoomable zoomable--clickable"><div class="dg-viewport"><svg class="dg" viewBox="0 0 1000 540" role="img" aria-labelledby="dba-title dba-desc" xmlns="http://www.w3.org/2000/svg">
  <title id="dba-title">From duplicate storage to a single copy</title>
  <desc id="dba-desc">Before, the same data was stored twice, as S3 Parquet and as a BigQuery replica, and served to customers from both. After, a single Delta Lake table is served through two paths: Delta Sharing and UniForm (Iceberg).</desc>
  <defs>
    <marker id="dba-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="dg-arrow" d="M0 0L10 5L0 10z"/></marker>
    <marker id="dba-a-delta" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="dg-arrow dg-arrow--delta" d="M0 0L10 5L0 10z"/></marker>
    <marker id="dba-a-ice" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="dg-arrow dg-arrow--iceberg" d="M0 0L10 5L0 10z"/></marker>
  </defs>

  <!-- before -->
  <rect class="dg-group" x="20" y="22" width="460" height="508" rx="16"/>
  <rect class="dg-group-tab" x="40" y="11" width="232" height="22" rx="11"/>
  <text class="dg-group-label" x="156" y="26.5" text-anchor="middle">Before: a copy per platform</text>

  <rect class="dg-node dg-node--internal" x="170" y="72" width="160" height="80" rx="10"/>
  <g class="dg-icon dg-icon--internal" transform="translate(240 82) scale(0.8333)"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="8" stroke-width="3.2" stroke-dasharray="3 3.3"/></g>
  <text class="dg-label" x="250" y="130" text-anchor="middle">Internal pipeline</text>

  <path class="dg-edge" d="M250 152V184H135V206" marker-end="url(#dba-a)"/>
  <path class="dg-edge" d="M250 152V184H365V206" marker-end="url(#dba-a)"/>
  <text class="dg-edge-label" x="192" y="174" text-anchor="middle">load</text>
  <text class="dg-edge-label" x="308" y="174" text-anchor="middle">replicate</text>

  <path class="dg-node dg-node--store" d="M40 220L54 294A81 10 0 0 0 216 294L230 220"/>
  <ellipse class="dg-node dg-node--store" cx="135" cy="220" rx="95" ry="12"/>
  <text class="dg-label" x="135" y="260" text-anchor="middle">S3 Parquet</text>
  <text class="dg-sub" x="135" y="282" text-anchor="middle">Shared via read access</text>

  <path class="dg-node dg-node--bad" d="M270 218V294A95 10 0 0 0 460 294V218"/>
  <ellipse class="dg-node dg-node--bad" cx="365" cy="218" rx="95" ry="10"/>
  <text class="dg-label" x="365" y="258" text-anchor="middle">BigQuery replica</text>
  <text class="dg-sub" x="365" y="280" text-anchor="middle">Same data, stored again</text>

  <path class="dg-edge" d="M135 304V360" marker-end="url(#dba-a)"/>
  <path class="dg-edge" d="M365 304V360" marker-end="url(#dba-a)"/>

  <rect class="dg-node dg-node--consumer" x="40" y="362" width="190" height="60" rx="10"/>
  <text class="dg-label" x="135" y="398" text-anchor="middle">S3 customers</text>
  <rect class="dg-node dg-node--consumer" x="270" y="362" width="190" height="60" rx="10"/>
  <text class="dg-label" x="365" y="398" text-anchor="middle">BigQuery customers</text>

  <text class="dg-note" x="40" y="468">· Schema changes are hard to manage per table.</text>
  <text class="dg-note" x="40" y="490">· No transactional guarantee when a write lands mid-read.</text>
  <text class="dg-note" x="40" y="512">· The same data is stored in two places.</text>

  <!-- transition -->
  <path class="dg-edge" d="M486 288H514" marker-end="url(#dba-a)"/>

  <!-- after -->
  <rect class="dg-group" x="520" y="22" width="460" height="508" rx="16"/>
  <rect class="dg-group-tab" x="540" y="11" width="168" height="22" rx="11"/>
  <text class="dg-group-label" x="624" y="26.5" text-anchor="middle">After: a single copy</text>

  <rect class="dg-node dg-node--internal" x="670" y="72" width="160" height="80" rx="10"/>
  <g class="dg-icon dg-icon--internal" transform="translate(740 82) scale(0.8333)"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="8" stroke-width="3.2" stroke-dasharray="3 3.3"/></g>
  <text class="dg-label" x="750" y="130" text-anchor="middle">Internal pipeline</text>

  <path class="dg-edge" d="M750 152V206" marker-end="url(#dba-a)"/>
  <text class="dg-edge-label" x="762" y="192">write</text>

  <path class="dg-node dg-node--store" d="M605 220L619 294A131 10 0 0 0 881 294L895 220"/>
  <ellipse class="dg-node dg-node--store" cx="750" cy="220" rx="145" ry="12"/>
  <text class="dg-label" x="750" y="260" text-anchor="middle">Delta Lake table</text>
  <text class="dg-sub" x="750" y="282" text-anchor="middle">Single copy on S3 (UniForm on)</text>

  <path class="dg-edge dg-edge--delta" d="M635 302V360" marker-end="url(#dba-a-delta)"/>
  <text class="dg-edge-label" x="645" y="336">Delta Sharing</text>
  <path class="dg-edge dg-edge--iceberg" d="M865 302V360" marker-end="url(#dba-a-ice)"/>
  <text class="dg-edge-label" x="875" y="336">Iceberg</text>

  <rect class="dg-node dg-node--consumer" x="540" y="362" width="190" height="60" rx="10"/>
  <text class="dg-label" x="635" y="398" text-anchor="middle">Databricks customers</text>
  <rect class="dg-node dg-node--consumer" x="770" y="362" width="190" height="60" rx="10"/>
  <text class="dg-label" x="865" y="392" text-anchor="middle">Iceberg platforms</text>
  <text class="dg-sub" x="865" y="411" text-anchor="middle">AWS · Snowflake · BigQuery</text>

  <text class="dg-note" x="540" y="468">· Writes go to Delta only.</text>
  <text class="dg-note" x="540" y="490">· Schema changes and transactions are managed per table.</text>
  <text class="dg-note" x="540" y="512">· No more duplicate storage.</text>
</svg>
</div></div><figcaption>Before: the same data stored twice, in S3 Parquet and BigQuery. After: a single Delta Lake copy.</figcaption></figure>
<ul>
<li><strong>Schema evolution</strong>: Nothing recorded the schema, so readers had to infer it file by file. Adding a column meant old and new files had different schemas, and since every engine merges them differently, the new column could simply be missing on the client side.</li>
<li><strong>No transactions</strong>: If a write landed on a path while a client was reading it, the client could read a half-updated state.</li>
<li><strong>Duplicate storage</strong>: The same data lived twice, once in S3 and once in BigQuery.</li>
</ul>
<p>A table format fixes the first two. Iceberg and Delta Lake track the schema and the list of files as metadata and make each commit an atomic write. Choosing a table format or a processing engine, however, raised a different question.</p>
<h3 id="institutional-clients-use-many-platforms"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#institutional-clients-use-many-platforms">Institutional clients use many platforms</a></h3>
<p>Institutional clients, financial institutions in particular, often run on SaaS cloud data platforms such as Databricks, BigQuery, and Snowflake. So choosing a table format was not purely a technical call; we also had to think about how easy it would be for clients to read.</p>
<table><thead><tr><th>Client platform</th><th>How they want to read it</th></tr></thead><tbody><tr><td><strong>AWS direct</strong></td><td>Read tables on S3 with Spark, Athena, and the like</td></tr><tr><td><strong>Databricks</strong></td><td>Read it as a table in their own workspace</td></tr><tr><td><strong>Snowflake</strong></td><td>Attach it as an external table and query it with SQL</td></tr><tr><td><strong>BigQuery</strong></td><td>Attach it as an external table and query it with SQL</td></tr></tbody></table>
<p>We needed every client to pull the data comfortably from their own platform while storing as few duplicates as possible.</p>
<h2 id="alternatives-we-considered"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#alternatives-we-considered">Alternatives we considered</a></h2>
<h3 id="iceberg"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#iceberg">Iceberg</a></h3>
<p>In the industry, the mainstream open-source, engine-neutral table format was Iceberg. Fortunately, the SaaS platforms above were moving beyond their own optimized managed storage (native tables) toward supporting open table formats. Snowflake, BigQuery, and Databricks&#x27; Spark engine can all read external Iceberg tables, so at first the front-runner was Iceberg tables in the Glue catalog, written by EMR Spark.</p>
<p>The closer we looked, though, the more two things bothered us.
First, with Iceberg as the source of truth, we could not give Databricks clients a Delta table. Spark can read and write Iceberg tables, but letting Databricks clients pick the data up as a Delta table in their own workspace only works when the source is Delta. In the other direction, a table written in Delta Lake can carry Iceberg metadata alongside it through UniForm, so Iceberg clients can read it too. At the time there was no way to do the reverse and make an Iceberg table readable as Delta.<sup><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#user-content-fn-iceberg" id="user-content-fnref-iceberg" data-footnote-ref="true" aria-describedby="footnote-label">1</a></sup>
Second, for a small data engineering team, running and learning EMR Spark cost far more than Databricks. The company had many analysts, so the sheer usability of Databricks notebooks was attractive, and with job compute and warm-up used well, Databricks was not more expensive than EMR.</p>
<h3 id="delta-lake-and-uniform"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#delta-lake-and-uniform">Delta Lake and UniForm</a></h3>
<p>So in the end we decided to go with the Databricks platform. Databricks&#x27; Delta UniForm generates Iceberg metadata whenever it writes a Delta table. The data files (Parquet), which make up nearly all of the storage, exist only once, yet they can be read both as Delta and as Iceberg.</p>
<p>Clients only read, and all writes happen in our internal pipeline, so giving up Iceberg writes cost us little. On top of that, Delta Sharing made it easy to deliver data to Databricks clients inside the platform itself.</p>
<table><thead><tr><th>Option</th><th>Pros</th><th>Cons</th><th>Verdict</th></tr></thead><tbody><tr><td><strong>Keep the status quo</strong></td><td>No extra development cost</td><td>Duplicate data, no schema or transaction story</td><td>Rejected</td></tr><tr><td><strong>Native Iceberg</strong> (EMR Spark)</td><td>Engine-neutral</td><td>Heavy EMR operations load for a small team</td><td>Rejected</td></tr><tr><td><strong>Delta Lake + Delta Sharing + UniForm</strong></td><td>One copy to write, readable on all four platforms, light Databricks operations</td><td>No Iceberg writes, Databricks lock-in</td><td>Adopted</td></tr></tbody></table>
<h2 id="architecture"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#architecture">Architecture</a></h2>
<figure class="dg-figure"><div class="zoomable zoomable--clickable"><div class="dg-viewport"><svg class="dg" viewBox="0 0 1000 430" role="img" aria-labelledby="dus-title dus-desc" xmlns="http://www.w3.org/2000/svg">
  <title id="dus-title">Delta UniForm serving architecture</title>
  <desc id="dus-desc">The internal pipeline writes a single copy of the data files in Delta format, and that copy is read through two kinds of metadata: the Delta log and Iceberg metadata. Through Unity Catalog, Databricks customers receive it via Delta Sharing and customers on Iceberg-compatible platforms via the Iceberg REST Catalog.</desc>
  <defs>
    <marker id="dus-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="dg-arrow" d="M0 0L10 5L0 10z"/></marker>
    <marker id="dus-a-delta" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="dg-arrow dg-arrow--delta" d="M0 0L10 5L0 10z"/></marker>
    <marker id="dus-a-ice" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path class="dg-arrow dg-arrow--iceberg" d="M0 0L10 5L0 10z"/></marker>
  </defs>

  <!-- single copy -->
  <rect class="dg-group" x="260" y="60" width="416" height="320" rx="16"/>
  <rect class="dg-group-tab" x="272" y="49" width="380" height="22" rx="11"/>
  <text class="dg-group-label" x="462" y="64.5" text-anchor="middle">Delta table in Unity Catalog · single copy on S3</text>

  <!-- internal pipeline -->
  <rect class="dg-node dg-node--internal" x="20" y="164" width="180" height="120" rx="10"/>
  <g class="dg-icon dg-icon--internal" transform="translate(100 176) scale(0.8333)"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="8" stroke-width="3.2" stroke-dasharray="3 3.3"/></g>
  <text class="dg-label" x="110" y="224" text-anchor="middle">Internal pipeline</text>
  <text class="dg-sub" x="110" y="246" text-anchor="middle">Ingest and transform (writer)</text>

  <!-- data files -->
  <path class="dg-node dg-node--store" d="M284 108L298 342A56 10 0 0 0 410 342L424 108"/>
  <ellipse class="dg-node dg-node--store" cx="354" cy="108" rx="70" ry="12"/>
  <text class="dg-label" x="354" y="214" text-anchor="middle">Parquet</text>
  <text class="dg-label" x="354" y="236" text-anchor="middle">data files</text>
  <text class="dg-sub" x="354" y="262" text-anchor="middle">Stored once</text>

  <!-- metadata -->
  <rect class="dg-node dg-node--delta" x="500" y="96" width="156" height="80" rx="10"/>
  <text class="dg-label" x="578" y="132" text-anchor="middle">Delta log</text>
  <text class="dg-sub" x="578" y="154" text-anchor="middle">_delta_log</text>

  <rect class="dg-node dg-node--iceberg" x="500" y="272" width="156" height="80" rx="10"/>
  <text class="dg-label" x="578" y="308" text-anchor="middle">Iceberg metadata</text>
  <text class="dg-sub" x="578" y="330" text-anchor="middle">Generated by UniForm</text>

  <!-- write -->
  <path class="dg-edge" d="M200 224H289" marker-end="url(#dus-a)"/>
  <text class="dg-edge-label" x="228" y="212" text-anchor="middle">write</text>

  <!-- reference -->
  <path class="dg-edge dg-edge--delta" d="M500 136H422" marker-end="url(#dus-a-delta)"/>
  <text class="dg-edge-label" x="461" y="126" text-anchor="middle">refers</text>
  <path class="dg-edge dg-edge--iceberg" d="M500 312H411" marker-end="url(#dus-a-ice)"/>
  <text class="dg-edge-label" x="456" y="302" text-anchor="middle">refers</text>

  <!-- UniForm conversion -->
  <path class="dg-edge dg-edge--dashed" d="M578 176V270" marker-end="url(#dus-a)"/>
  <text class="dg-edge-label" x="592" y="218">UniForm</text>
  <text class="dg-edge-label" x="592" y="236">conversion</text>

  <!-- consumers -->
  <path class="dg-node dg-node--consumer" d="M32 96H116A28 28 0 0 0 124 41A38 38 0 0 0 54 30A30 30 0 0 0 28 96Z" transform="translate(772 58) scale(1.44 1.18)" vector-effect="non-scaling-stroke"/>
  <text class="dg-label" x="880" y="127" text-anchor="middle">Databricks customers</text>
  <text class="dg-sub" x="880" y="149" text-anchor="middle">Read the Delta table as is</text>

  <path class="dg-node dg-node--consumer" d="M32 96H116A28 28 0 0 0 124 41A38 38 0 0 0 54 30A30 30 0 0 0 28 96Z" transform="translate(772 234) scale(1.44 1.18)" vector-effect="non-scaling-stroke"/>
  <text class="dg-label" x="880" y="303" text-anchor="middle">Iceberg platforms</text>
  <text class="dg-sub" x="880" y="325" text-anchor="middle">AWS · Snowflake · BigQuery</text>

  <path class="dg-edge dg-edge--delta" d="M656 136H777" marker-end="url(#dus-a-delta)"/>
  <text class="dg-edge-label" x="724" y="126" text-anchor="middle">Delta Sharing</text>
  <path class="dg-edge dg-edge--iceberg" d="M656 312H777" marker-end="url(#dus-a-ice)"/>
  <text class="dg-edge-label" x="724" y="285" text-anchor="middle">Iceberg</text>
  <text class="dg-edge-label" x="724" y="301" text-anchor="middle">REST Catalog</text>

  <text class="dg-note" x="500" y="410" text-anchor="middle">One copy of the data files, two kinds of metadata, served to different platforms through Unity Catalog.</text>
</svg>
</div></div><figcaption>One Delta table served through two paths: Delta Sharing and the Iceberg REST Catalog</figcaption></figure>
<p>Real-time data lands in an operational database as it arrives, and we replicate it with CDC to use it for analytics. Spark jobs, each defined for its own domain, then load the data into Delta tables registered in Unity Catalog (INSERT plus UPSERT). Given the nature of blockchain data, the UPSERTs only rewrote the last n intervals, like a sliding window, and because the tables were clustered, we never rewrote a whole table.</p>
<ul>
<li><strong>Delta Sharing</strong>: Clients on Databricks get the Delta table shared as is. We managed access with Databricks&#x27; governance features.</li>
<li><strong>UniForm + Iceberg REST Catalog</strong>: Clients on AWS directly or on Snowflake read the tables as Iceberg on their own platform through the Iceberg REST Catalog that Unity Catalog exposes. Clients on BigQuery read the Iceberg tables through BigQuery Omni.</li>
</ul>
<h3 id="enabling-uniform"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#enabling-uniform">Enabling UniForm</a></h3>
<p>UniForm is turned on through a Delta table&#x27;s <code>TBLPROPERTIES</code>. The table must be registered in Unity Catalog, and you need Databricks Runtime 14.3 LTS or later.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="sql" data-theme="vitesse-dark-jerrylog vitesse-light-jerrylog"><code data-language="sql" data-theme="vitesse-dark-jerrylog vitesse-light-jerrylog" style="display:grid"><span data-line=""><span style="--shiki-dark:#87A0F0;--shiki-light:#2E5BDA">CREATE</span><span style="--shiki-dark:#87A0F0;--shiki-light:#2E5BDA"> TABLE</span><span style="--shiki-dark:#80A665;--shiki-light:#59873A"> market</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">.</span><span style="--shiki-dark:#C99076;--shiki-light:#A65E2B">history</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">.</span><span style="--shiki-dark:#C99076;--shiki-light:#A65E2B">exchange_flows</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34"> (</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">  asset STRING,</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">  exchange STRING,</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">  ts </span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959">TIMESTAMP</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">,</span></span>
<span data-line=""><span style="--shiki-dark:#87A0F0;--shiki-light:#2E5BDA">  value</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34"> DOUBLE</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">)</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">TBLPROPERTIES (</span></span>
<span data-line=""><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">  &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">delta.columnMapping.mode</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959"> =</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977"> &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">id</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">,</span></span>
<span data-line=""><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">  &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">delta.enableIcebergCompatV2</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959"> =</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977"> &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">true</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">,</span></span>
<span data-line=""><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">  &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">delta.universalFormat.enabledFormats</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959"> =</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977"> &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">iceberg</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">);</span></span></code></pre></figure>
<p>UniForm only works on tables with column mapping enabled. Turning UniForm on when you create a new table enables column mapping along with it, and Databricks recommends <code>id</code> mode, as above, for compatibility. To enable it on an existing table, you set column mapping to <code>name</code> mode at the same time, on Databricks Runtime 15.4 LTS or later <span class="citation" id="citation--databricks2026uniform--1">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-databricks2026uniform">Databricks, 2026</a>)</span>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="sql" data-theme="vitesse-dark-jerrylog vitesse-light-jerrylog"><code data-language="sql" data-theme="vitesse-dark-jerrylog vitesse-light-jerrylog" style="display:grid"><span data-line=""><span style="--shiki-dark:#87A0F0;--shiki-light:#2E5BDA">ALTER</span><span style="--shiki-dark:#87A0F0;--shiki-light:#2E5BDA"> TABLE</span><span style="--shiki-dark:#C99076;--shiki-light:#A65E2B"> market</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">.</span><span style="--shiki-dark:#C99076;--shiki-light:#A65E2B">history</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">.exchange_flows </span><span style="--shiki-dark:#87A0F0;--shiki-light:#2E5BDA">SET</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34"> TBLPROPERTIES (</span></span>
<span data-line=""><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">  &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">delta.columnMapping.mode</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959"> =</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977"> &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">name</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">,</span></span>
<span data-line=""><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">  &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">delta.enableIcebergCompatV2</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959"> =</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977"> &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">true</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">,</span></span>
<span data-line=""><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">  &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">delta.universalFormat.enabledFormats</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span><span style="--shiki-dark:#CB7676;--shiki-light:#AB5959"> =</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977"> &#x27;</span><span style="--shiki-dark:#C98A7D;--shiki-light:#B56959">iceberg</span><span style="--shiki-dark:#C98A7D77;--shiki-light:#B5695977">&#x27;</span></span>
<span data-line=""><span style="--shiki-dark:#DBD7CAEE;--shiki-light:#393A34">);</span></span></code></pre></figure>
<h2 id="constraints-we-accepted"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#constraints-we-accepted">Constraints we accepted</a></h2>
<h3 id="read-only-iceberg"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#read-only-iceberg">Read-only Iceberg</a></h3>
<p>A Delta table with UniForm enabled is read-only to Iceberg clients. Our clients never needed to write, so this was not an issue.</p>
<h3 id="freshness-lag"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#freshness-lag">Freshness lag</a></h3>
<p>Delta Sharing clients can see new data as soon as a commit finishes. Clients reading through Iceberg, however, hit two separate delays before they see it.</p>
<p>The first delay is the UniForm conversion. The compute that finished the Delta commit generates the Iceberg metadata asynchronously after the commit. Only one conversion runs at a time on a given compute, so commits that arrive while a conversion is in progress are picked up together by the next one. As a result, there is no guarantee that Delta versions and Iceberg snapshots map one to one <span class="citation" id="citation--databricks2026uniform--2">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-databricks2026uniform">Databricks, 2026</a>)</span>.</p>
<p>The second delay comes from how each consumer refreshes its catalog view. Clients that talk to the Iceberg REST Catalog directly, such as Spark, fetch the latest metadata location from the catalog when they open a table. Iceberg&#x27;s Spark catalog does cache loaded tables for 30 seconds by default, though, so within a single session a new snapshot can show up that much later <span class="citation" id="citation--apache2026spark--3">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-apache2026spark">Apache Iceberg, 2026</a>)</span>. Snowflake and BigQuery, on the other hand, register the table definition on their side, so a new snapshot only becomes visible after they re-read the metadata. In Snowflake, if you enable <code>AUTO_REFRESH</code> when creating the table, it polls the catalog every 30 seconds by default. The option is off by default, and tables without it have to be refreshed by hand with <code>ALTER ICEBERG TABLE ... REFRESH</code> <span class="citation" id="citation--snowflake2026create--snowflake2026autorefresh--snowflake2026refresh--4">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-snowflake2026refresh">Snowflake, 2026</a>c, <a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-snowflake2026autorefresh">2026</a>b, <a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-snowflake2026create">2026</a>a)</span>. On the BigQuery side, we used BigQuery Omni to register the Iceberg tables on S3 as BigLake tables <span class="citation" id="citation--google2026omni--5">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-google2026omni">Google Cloud, 2026</a>)</span>. A table registered this way points at one specific <code>metadata.json</code> file, so it keeps reading the old snapshot until that path is updated, even after new snapshots appear <span class="citation" id="citation--google2026iceberg--6">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-google2026iceberg">Google Cloud, 2026b</a>)</span>. We therefore ran a separate job that periodically found the latest <code>metadata.json</code> path and wrote it into the BigQuery table definition, which means data queried through BigQuery lags by up to that interval.<sup><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#user-content-fn-bq" id="user-content-fnref-bq" data-footnote-ref="true" aria-describedby="footnote-label">2</a></sup></p>
<figure class="dg-figure"><div class="zoomable zoomable--clickable"><div class="dg-viewport"><svg class="dg" viewBox="0 0 1000 384" role="img" aria-labelledby="uwv-title uwv-desc" xmlns="http://www.w3.org/2000/svg">
  <title id="uwv-title">From one write to customer visibility</title>
  <desc id="uwv-desc">Once the internal pipeline commits to the Delta table, Delta Sharing customers can read the new data right away. Customers reading through Iceberg see it only after UniForm generates the Iceberg metadata, and Snowflake and BigQuery customers only after their engine re-reads that metadata. The freshness lag splits into two stretches: UniForm conversion lag and consumer engine refresh lag.</desc>
  <defs>
    <marker id="uwv-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path class="dg-arrow" d="M0 0L10 5L0 10z"/></marker>
  </defs>

  <text class="dg-group-label" x="16" y="30">Time</text>
  <path class="dg-edge" d="M16 44H984" marker-end="url(#uwv-a)"/>

  <!-- 1 write -->
  <rect class="dg-node dg-node--internal" x="16" y="72" width="156" height="100" rx="10"/>
  <text class="dg-label" x="94" y="108" text-anchor="middle">Write</text>
  <text class="dg-sub" x="94" y="130" text-anchor="middle">Internal pipeline</text>
  <text class="dg-sub" x="94" y="148" text-anchor="middle">commits to Delta</text>
  <circle class="dg-badge" cx="16" cy="72" r="12"/>
  <text class="dg-badge-text" x="16" y="76.5" text-anchor="middle">1</text>

  <!-- 2 Delta visible -->
  <rect class="dg-node dg-node--delta" x="220" y="72" width="156" height="100" rx="10"/>
  <text class="dg-label" x="298" y="108" text-anchor="middle">Delta visible</text>
  <text class="dg-sub" x="298" y="130" text-anchor="middle">Parquet files and</text>
  <text class="dg-sub" x="298" y="148" text-anchor="middle">Delta log updated</text>
  <circle class="dg-badge" cx="220" cy="72" r="12"/>
  <text class="dg-badge-text" x="220" y="76.5" text-anchor="middle">2</text>

  <!-- 3 UniForm conversion -->
  <rect class="dg-node dg-node--iceberg" x="424" y="72" width="156" height="100" rx="10"/>
  <text class="dg-label" x="502" y="108" text-anchor="middle">UniForm</text>
  <text class="dg-sub" x="502" y="130" text-anchor="middle">Async after commit,</text>
  <text class="dg-sub" x="502" y="148" text-anchor="middle">writes Iceberg metadata</text>
  <circle class="dg-badge" cx="424" cy="72" r="12"/>
  <text class="dg-badge-text" x="424" y="76.5" text-anchor="middle">3</text>

  <!-- 4 Iceberg visible -->
  <rect class="dg-node dg-node--iceberg" x="628" y="72" width="156" height="100" rx="10"/>
  <text class="dg-label" x="706" y="108" text-anchor="middle">Iceberg visible</text>
  <text class="dg-sub" x="706" y="130" text-anchor="middle">Catalog points to</text>
  <text class="dg-sub" x="706" y="148" text-anchor="middle">the new snapshot</text>
  <circle class="dg-badge" cx="628" cy="72" r="12"/>
  <text class="dg-badge-text" x="628" y="76.5" text-anchor="middle">4</text>

  <!-- 5 engine refresh -->
  <rect class="dg-node dg-node--consumer" x="832" y="72" width="156" height="100" rx="10"/>
  <text class="dg-label" x="910" y="108" text-anchor="middle">Engine refresh</text>
  <text class="dg-sub" x="910" y="130" text-anchor="middle">Snowflake, BigQuery</text>
  <text class="dg-sub" x="910" y="148" text-anchor="middle">read the new snapshot</text>
  <circle class="dg-badge" cx="832" cy="72" r="12"/>
  <text class="dg-badge-text" x="832" y="76.5" text-anchor="middle">5</text>

  <path class="dg-edge" d="M172 122H218" marker-end="url(#uwv-a)"/>
  <path class="dg-edge" d="M376 122H422" marker-end="url(#uwv-a)"/>
  <path class="dg-edge" d="M580 122H626" marker-end="url(#uwv-a)"/>
  <path class="dg-edge" d="M784 122H830" marker-end="url(#uwv-a)"/>

  <!-- readable from -->
  <text class="dg-sub" x="220" y="208">Databricks customers (Delta Sharing): from step 2</text>
  <rect class="dg-node dg-node--delta" x="220" y="216" width="768" height="8" rx="4"/>

  <text class="dg-sub" x="616" y="260" text-anchor="end">Clients on the Iceberg REST Catalog (Spark etc.): from step 4</text>
  <rect class="dg-node dg-node--iceberg" x="628" y="252" width="360" height="8" rx="4"/>

  <text class="dg-sub" x="820" y="296" text-anchor="end">Snowflake, BigQuery customers: from step 5</text>
  <rect class="dg-node dg-node--consumer" x="832" y="288" width="156" height="8" rx="4"/>

  <!-- freshness lag -->
  <path class="dg-edge dg-edge--dashed" d="M222 336H626" marker-start="url(#uwv-a)" marker-end="url(#uwv-a)"/>
  <text class="dg-note" x="424" y="362" text-anchor="middle">UniForm conversion lag</text>
  <path class="dg-edge dg-edge--dashed" d="M630 336H830" marker-start="url(#uwv-a)" marker-end="url(#uwv-a)"/>
  <text class="dg-note" x="730" y="362" text-anchor="middle">Engine refresh lag</text>
</svg>
</div></div><figcaption>When new data becomes visible to each kind of client after a Delta commit</figcaption></figure>
<p>What clients consuming the data through the data lake wanted was to backfill historical data, not real-time freshness, so this lag was acceptable. When conversion falls behind, you can trigger it manually with <code>MSCK REPAIR TABLE &lt;table&gt; SYNC METADATA</code>, which runs synchronously.</p>
<h3 id="dependence-on-databricks"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#dependence-on-databricks">Dependence on Databricks</a></h3>
<p>Both writes and data governance are tied to Databricks (Unity Catalog). We judged the platform&#x27;s usability to be excellent, and we accepted this dependence as the price of not running EMR ourselves.</p>
<h2 id="results"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#results">Results</a></h2>
<ul>
<li>We consolidated hundreds of tables into Delta Lake and could cut hundreds of terabytes of data that had been stored twice, in S3 and in BigQuery.</li>
<li>Write transactions, schema changes, and the like became simple Spark SQL statements against an abstract table-format interface.</li>
<li>Clients on Snowflake and BigQuery could connect to the data directly from their own platform, which gave us a sales channel for data beyond the API.</li>
</ul>
<h2 id="looking-back"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#looking-back">Looking back</a></h2>
<p>There was a lot to like, from governance on Unity Catalog to getting rid of duplicate data. What I regret is not putting SLAs (service level agreements) in place at the same time, since they are the data quality measure institutional clients care about, along with a data quality platform to manage them. We were planning to build out DataHub back then, but Unity Catalog keeps adding quality and lineage features, so I suspect it alone may be enough going forward.</p>
<section data-footnotes="true" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-iceberg">
<p>As of October 2026, Databricks offers managed Iceberg tables, which you create directly in Unity Catalog and can both read and write (Databricks Runtime 16.4 LTS or later) <span class="citation" id="citation--databricks2026iceberg--7">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-databricks2026iceberg">Databricks, n.d.</a>)</span>. <a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#user-content-fnref-iceberg" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-bq">
<p>As of October 2026, Google recommends against Iceberg external tables that point at a <code>metadata.json</code> file in most cases. For tables managed by a remote catalog such as Unity Catalog, it suggests connecting through cross-cloud data access in the Lakehouse runtime catalog. Lakehouse is the new name for BigLake, and this feature attaches to the remote catalog&#x27;s Iceberg REST endpoint and syncs the metadata periodically. It is still in Preview <span class="citation" id="citation--google2026iceberg--google2026crosscloud--8">(<a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-google2026crosscloud">Google Cloud, 2026</a>c, <a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#bib-google2026iceberg">2026</a>a)</span>. <a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#user-content-fnref-bq" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section><h2 id="references"><a href="https://jerrylog.dev/en/blog/delta-uniform-multi-platform-serving#references">References</a></h2><ul id="refs" class="references"><li class="csl-entry" id="bib-apache2026spark">Apache Iceberg. (2026). <i>Spark configuration</i>. <a href="https://iceberg.apache.org/docs/latest/spark-configuration/">https://iceberg.apache.org/docs/latest/spark-configuration/</a></li><li class="csl-entry" id="bib-databricks2026iceberg">Databricks. (n.d.). <i>What is Apache Iceberg in Databricks?</i> Retrieved October 22, 2026, from <a href="https://docs.databricks.com/aws/iceberg">https://docs.databricks.com/aws/iceberg</a></li><li class="csl-entry" id="bib-databricks2026external">Databricks. (2026a). <i>Access Databricks data using external systems</i>. <a href="https://docs.databricks.com/aws/en/external-access/">https://docs.databricks.com/aws/en/external-access/</a></li><li class="csl-entry" id="bib-databricks2026uniform">Databricks. (2026b). <i>Read Delta Lake tables with Iceberg clients</i>. <a href="https://docs.databricks.com/aws/en/delta/uniform">https://docs.databricks.com/aws/en/delta/uniform</a></li><li class="csl-entry" id="bib-databricks2026credential">Databricks. (2026c). <i>Unity Catalog credential vending for external system access</i>. <a href="https://docs.databricks.com/aws/en/external-access/credential-vending">https://docs.databricks.com/aws/en/external-access/credential-vending</a></li><li class="csl-entry" id="bib-google2026crosscloud">Google Cloud. (2026a). <i>About cross-cloud data access</i>. <a href="https://docs.cloud.google.com/lakehouse/docs/about-cross-cloud-data-access">https://docs.cloud.google.com/lakehouse/docs/about-cross-cloud-data-access</a></li><li class="csl-entry" id="bib-google2026omni">Google Cloud. (2026b). <i>Create Amazon S3 BigLake tables</i>. <a href="https://docs.cloud.google.com/bigquery/docs/omni-aws-create-external-table">https://docs.cloud.google.com/bigquery/docs/omni-aws-create-external-table</a></li><li class="csl-entry" id="bib-google2026iceberg">Google Cloud. (2026c). <i>Create Apache Iceberg external tables</i>. <a href="https://docs.cloud.google.com/bigquery/docs/iceberg-external-tables">https://docs.cloud.google.com/bigquery/docs/iceberg-external-tables</a></li><li class="csl-entry" id="bib-microsoft2026iceberg">Microsoft. (2026). <i>Access Azure Databricks tables from Apache Iceberg clients</i>. <a href="https://learn.microsoft.com/en-us/azure/databricks/external-access/iceberg">https://learn.microsoft.com/en-us/azure/databricks/external-access/iceberg</a></li><li class="csl-entry" id="bib-snowflake2026refresh">Snowflake. (2026a). <i>ALTER ICEBERG TABLE … REFRESH</i>. <a href="https://docs.snowflake.com/en/sql-reference/sql/alter-iceberg-table-refresh">https://docs.snowflake.com/en/sql-reference/sql/alter-iceberg-table-refresh</a></li><li class="csl-entry" id="bib-snowflake2026autorefresh">Snowflake. (2026b). <i>Automatically refresh Apache Iceberg tables</i>. <a href="https://docs.snowflake.com/en/user-guide/tables-iceberg-auto-refresh">https://docs.snowflake.com/en/user-guide/tables-iceberg-auto-refresh</a></li><li class="csl-entry" id="bib-snowflake2026create">Snowflake. (2026c). <i>CREATE ICEBERG TABLE (Iceberg REST catalog)</i>. <a href="https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table-rest">https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table-rest</a></li></ul>]]></content:encoded>
      <pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate>
      <category><![CDATA[delta-lake]]></category><category><![CDATA[iceberg]]></category><category><![CDATA[uniform]]></category><category><![CDATA[delta-sharing]]></category><category><![CDATA[databricks]]></category><category><![CDATA[data-platform]]></category>
    </item>
  </channel>
</rss>