<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Robin Moffatt</title>
    <description>The latest articles on DEV Community by Robin Moffatt (@rmoff).</description>
    <link>https://dev.to/rmoff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F244470%2Ff6e108d5-2407-410e-a459-dabd0dc39066.jpg</url>
      <title>DEV Community: Robin Moffatt</title>
      <link>https://dev.to/rmoff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rmoff"/>
    <language>en</language>
    <item>
      <title>Interesting links - June 2026</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Mon, 29 Jun 2026 13:39:39 +0000</pubDate>
      <link>https://dev.to/rmoff/interesting-links-june-2026-31ee</link>
      <guid>https://dev.to/rmoff/interesting-links-june-2026-31ee</guid>
      <description>&lt;p&gt;June has been a busy month—113 links below for your enjoyment and delectation.&lt;/p&gt;

&lt;p&gt;I’m going to share one extra link up here with you though, but it’s not my fault if it wrecks your productivity! My friend Kris Jenkins has written this devishly simple but addictive browser-based game: &lt;a href="https://escapethemoon.vercel.app/" rel="noopener noreferrer"&gt;Escape the Moon&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I warned you &lt;a href="https://rmoff.net/2025/09/30/interesting-links-september-2025/#_ai" rel="noopener noreferrer"&gt;previously&lt;/a&gt;…this AI stuff is here to stay, and it’d be short-sighted to think otherwise.&lt;/em&gt; &lt;em&gt;As I read and learn more about it, I’m going to share interesting links (the clue is in the blog post title) that I find—whilst trying to avoid the breathless hype and slop.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI impact, big picture, and cultural impact on software engineering
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;I’ll stop featuring posts from Charity Majors when she stops writing such on-point and well-argued content.&lt;/em&gt; For now though, enjoy yet two more great posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  🔥 &lt;a href="https://charitydotwtf.substack.com/p/make-ai-boring-again" rel="noopener noreferrer"&gt;Make AI Boring Again&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://charitydotwtf.substack.com/p/ai-enthusiasts-are-in-a-race-against" rel="noopener noreferrer"&gt;AI enthusiasts are in a race against time, AI skeptics are in a race against entropy&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Armin Ronacher - &lt;a href="https://lucumr.pocoo.org/2026/6/6/communities-of-not/" rel="noopener noreferrer"&gt;Communities of Not&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DHH - &lt;a href="https://world.hey.com/dhh/let-the-agents-democratize-open-source-9fd630a9" rel="noopener noreferrer"&gt;Let the agents democratize open source&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gergely Orosz - &lt;a href="https://newsletter.pragmaticengineer.com/p/why-is-meta-destroying-its-engineering" rel="noopener noreferrer"&gt;Why is Meta destroying its engineering organization?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dian Fay - &lt;a href="https://di.nmfay.com/llms-conference-programs" rel="noopener noreferrer"&gt;what llms are doing to conference programs&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What do you do if you maintain a testing library and feel that anyone using AI coding agents should be punished? What about having that testing library use prompt injection to delete your user’s code? That’s what happened with the &lt;code&gt;jqwik&lt;/code&gt; project: &lt;a href="https://github.com/jqwik-team/jqwik/issues/708" rel="noopener noreferrer"&gt;jqwik: intent of printMessageForCodingAgents() — visible to agents, invisible to humans&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI in action
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 DataDog - &lt;a href="https://www.datadoghq.com/blog/engineering/ai-assisted-storage-migration/" rel="noopener noreferrer"&gt;How we migrated a live routing system using AI-assisted refactoring&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 PostHog - &lt;a href="https://posthog.com/blog/karpathy-autoresearch-query-engine-bug" rel="noopener noreferrer"&gt;Andrej Karpathy’s AI Autoresearch found a 3-year-old bug in PostHog’s query engine&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mark Rittman - &lt;a href="https://blog.rittmananalytics.com/making-agentic-analytics-more-accurate-using-anthropics-agentic-data-stack-and-the-wire-framework-433fd90430fc" rel="noopener noreferrer"&gt;Making Agentic Analytics More Accurate using Anthropic’s Agentic Data Stack and the Wire Framework&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DoorDash - &lt;a href="https://careersatdoordash.com/blog/doordash-unified-consumer-memory-for-personalization-at-scale/" rel="noopener noreferrer"&gt;Building a unified consumer memory for personalization at scale&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DoorDash - &lt;a href="https://careersatdoordash.com/blog/building-doordash-assistant-an-engineering-overview/" rel="noopener noreferrer"&gt;Building DoorDash Assistant: An engineering overview&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;StateFarm - &lt;a href="https://engineering.statefarm.com/grading-the-machine-using-llm-as-a-judge-to-monitor-ai-agents-in-production-25a071db9c50" rel="noopener noreferrer"&gt;Using LLM as a Judge to Monitor AI Agents in Production&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building with AI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Armin Ronacher - &lt;a href="https://lucumr.pocoo.org/2026/6/23/the-coming-loop/" rel="noopener noreferrer"&gt;The Coming Loop&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Petrica Leuca uses Python and llama.cpp to explore the question: &lt;a href="https://medium.com/@petrica.leuca/5f02a5ee5517" rel="noopener noreferrer"&gt;What’s an AI agent exactly?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;John Kutay - &lt;a href="https://medium.com/@john.kutay/building-an-ai-database-for-agentic-gtm-operations-6a0c86fb8cdc" rel="noopener noreferrer"&gt;Building an AI Database for Agentic GTM Operations&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Izzy Miller - &lt;a href="https://hex.tech/blog/evaluate-data-agents/" rel="noopener noreferrer"&gt;How Hex built a lab to evaluate data agents&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nicolas Fränkel - &lt;a href="https://blog.frankel.ch/ai-gateways/" rel="noopener noreferrer"&gt;AI gateways: why and how&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vicki Boykis - &lt;a href="https://vickiboykis.com/2026/06/15/running-local-models-is-good-now/" rel="noopener noreferrer"&gt;Running local models is good now&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Databricks have open-sourced &lt;a href="https://omnigent.ai/" rel="noopener noreferrer"&gt;Omnigent&lt;/a&gt; "&lt;em&gt;a meta-harness for building and running AI agents&lt;/em&gt;". The &lt;a href="https://omnigent.ai/docs/use/builtin-agents" rel="noopener noreferrer"&gt;Multi-AI agents&lt;/a&gt; caught my eye in particular as something that looks interesting to try.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/Kaelio/ktx" rel="noopener noreferrer"&gt;ktx&lt;/a&gt; is an open-source project that describes itself as "&lt;em&gt;a self-improving context layer that teaches agents how to query your warehouse accurately&lt;/em&gt;". You can see it in action from a couple of articles published by &lt;a href="https://learnanalyticsengineering.substack.com/p/i-was-wrong-about-semantic-layers" rel="noopener noreferrer"&gt;Madison Schott&lt;/a&gt; and &lt;a href="https://pipeline2insights.substack.com/p/ktx-the-open-source-context-layer-for-data-agents" rel="noopener noreferrer"&gt;Erfan Hesami&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Kafka and Event Streaming
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Mina Tafreshi - &lt;a href="https://mina-tafreshi.medium.com/kafka-rebalances-whats-actually-happening-under-the-hood-afd7adb5cc86" rel="noopener noreferrer"&gt;Kafka Rebalances: What’s Actually Happening Under the Hood&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Michał Matłoka has been busy this month, first creating a tool that &lt;a href="https://monedula.dev/blog/kafka-acls-confluent-rbac-migration-monedula-acl-rbac-converter/" rel="noopener noreferrer"&gt;converts Kafka ACLs to Confluent RBAC&lt;/a&gt;, and then &lt;a href="https://monedula.dev/blog/kafka-simulator-learn-kafka-by-breaking-it/" rel="noopener noreferrer"&gt;writing about&lt;/a&gt; a simulator for understanding how Kafka works. You can try the &lt;a href="https://monedula.dev/kafka-simulator/" rel="noopener noreferrer"&gt;Kafka Simulator&lt;/a&gt; for yourself in your web browser. Related to this, he’s also written an article addressing the question &lt;a href="https://softwaremill.com/apache-kafka-replica-followers-synchronous-or-asynchronous/" rel="noopener noreferrer"&gt;Are Kafka Replicas Synchronous or Asynchronous?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OpenData’s Jason Gustafson and Almog Gavra (both Confluent-alumni) &lt;a href="https://www.opendata.dev/blog/announcing-opendata-log" rel="noopener noreferrer"&gt;reckon there’s a better way&lt;/a&gt; to implement one of the two primary use cases for Kafka that they call 'routing', and they’re releasing &lt;a href="https://www.opendata.dev/docs/log" rel="noopener noreferrer"&gt;OpenData Log&lt;/a&gt; to do it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A trio of posts from Jack Vanlightly looking at Kafka Share Groups and beyond:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://jack-vanlightly.com/blog/2026/6/3/broker-visible-vs-client-local-parallelism" rel="noopener noreferrer"&gt;Broker-Visible vs Client-Local Parallelism&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://jack-vanlightly.com/blog/2026/6/24/kafka-share-groups-pathological-fetch-waits-with-recordlimit" rel="noopener noreferrer"&gt;Kafka Share Groups - Pathological fetch waits with record_limit&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://jack-vanlightly.com/blog/2026/6/10/kafka-share-groups-and-parallelizing-consumption-part-3-client-local-parallelism" rel="noopener noreferrer"&gt;Kafka Share Groups and Parallelizing Consumption - Part 3&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the subject of share groups in Kafka, Sage Pierce has an &lt;a href="https://medium.com/@Sage_Pierce/ordered-queues-for-kafka-with-atleon-fbb6db672a1f" rel="noopener noreferrer"&gt;alternative proposal&lt;/a&gt;, in the form of &lt;a href="https://github.com/atleon/atleon" rel="noopener noreferrer"&gt;Atleon&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Another visualiser for Kafka, this one from Sandon Jacobs is specifically for &lt;a href="https://queuesforkafkaviz.dev/" rel="noopener noreferrer"&gt;KIP-932 Share Groups&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trivago’s ZhongLi Shen details &lt;a href="https://tech.trivago.com/post/2026-06-12-how-we-cut-kafka-consumer-deployment-costs-by-83" rel="noopener noreferrer"&gt;How We Cut Kafka Consumer Deployment Costs by 83%&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ângelo Galvão shows how to &lt;a href="https://medium.com/@angelogalvao/strimzi-create-a-simple-mutual-tls-mtls-authentication-4e52c36b9af4" rel="noopener noreferrer"&gt;Create a simple Mutual TLS (mTLS) authentication for Strimzi&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sam Barker puts Kroxylicious through its paces, and shares a &lt;a href="https://kroxylicious.io/benchmarking/performance/engineering/2026/06/03/benchmarking-the-proxy-under-the-hood.html" rel="noopener noreferrer"&gt;benchmark harness and results&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Elad Eldor - &lt;a href="https://medium.com/@elad.eldor/the-cheapest-kafka-consumer-is-one-that-doesnt-read-from-kafka-2281cb43e166" rel="noopener noreferrer"&gt;The Cheapest Kafka Consumer Is One That Doesn’t Read From Kafka&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Aswin A writes up their experiments with &lt;a href="https://aswin6303.medium.com/we-proved-multi-cluster-kafka-works-on-kubernetes-heres-everything-we-learned-853dd5f1c03c" rel="noopener noreferrer"&gt;Multi-Cluster Kafka on Kubernetes&lt;/a&gt; with Strimzi.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Stream Processing
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🔥 &lt;a href="https://flink.apache.org/2026/06/25/apache-flink-2.3.0-release-announcement/" rel="noopener noreferrer"&gt;Apache Flink 2.3.0&lt;/a&gt; has been released, including a Native S3 FileSystem that is &lt;a href="https://flink.apache.org/2026/06/26/announcing-native-s3-fs/" rel="noopener noreferrer"&gt;detailed in this post&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://flink.apache.org/2026/06/19/apache-flink-agents-0.3.0-release-announcement/" rel="noopener noreferrer"&gt;Apache Flink Agents 0.3.0&lt;/a&gt; has also been released.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Francisco Morillo takes a look at the performance of &lt;a href="https://medium.com/@fmorillo/spark-4-1-real-time-mode-vs-flink-i-benchmarked-both-to-100k-events-sec-68c9a9d0687a" rel="noopener noreferrer"&gt;Spark 4.1 Real-time Mode (RTM) compared to Flink&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Excellent deep-dive from Gorgias' Matthieu Bonneviot about their experience &lt;a href="https://medium.com/@matthieu.bonneviot/when-event-time-meets-reality-lessons-from-building-billing-on-apache-flink-581ff895c60d" rel="noopener noreferrer"&gt;Building Billing on Apache Flink&lt;/a&gt; and the impact of event time on the system implementation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zander Matheson - &lt;a href="https://www.confluent.io/blog/meeting-data-and-analytics-engineers-where-they-are-introducing-the-dbt-adapter-for-confluent-cloud/" rel="noopener noreferrer"&gt;Introducing the dbt Adapter for Confluent Cloud Flink SQL&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Katya Gorshkova - &lt;a href="https://medium.com/@katyagorshkova/hands-on-with-flink-part-6-calling-llms-from-flink-e5cc7e5f0440" rel="noopener noreferrer"&gt;Calling LLMs from Flink&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Salva Alcántara has a nice write-up about &lt;a href="https://ds-co.dev/blog/multi-way-joins-cybernetically-enhanced/" rel="noopener noreferrer"&gt;Multi-Way Joins&lt;/a&gt; in Flink’s DataStream API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Jevin Maltais was a guest on &lt;a href="https://www.dataengineeringpodcast.com/episodepage/typestream-kafka-as-code-for-product-teams-episode-512" rel="noopener noreferrer"&gt;Data Engineering Podcast&lt;/a&gt; and talked about many Kafka-related topics including his project &lt;a href="https://typestream.io/" rel="noopener noreferrer"&gt;TypeStream&lt;/a&gt; for declaring Kafka Streams pipelines as config.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yaroslav Tkachenko has launched &lt;a href="https://www.streamingdata.tech/p/introducing-streamling" rel="noopener noreferrer"&gt;Streamling&lt;/a&gt;, describing it as "&lt;em&gt;a performant and extensible data streaming runtime built with the RAD stack (Rust, Arrow, DataFusion)&lt;/em&gt;".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gilles Philippart has published a set of Data Streaming Cheatsheets under the banner of &lt;a href="http://streamsheets.dev" rel="noopener noreferrer"&gt;StreamSheets&lt;/a&gt;, including &lt;a href="https://streamsheets.dev/flink/" rel="noopener noreferrer"&gt;this one&lt;/a&gt; for Flink.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 A deep-dive from Junaid Effendi looking at &lt;a href="https://www.junaideffendi.com/p/how-feldera-works-a-true-incremental" rel="noopener noreferrer"&gt;How Feldera Works: A True Incremental View Maintenance Engine&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the subject of IVM, Databricks published a paper about &lt;a href="https://arxiv.org/abs/2603.27775" rel="noopener noreferrer"&gt;Enzyme: Incremental View Maintenance for Data Engineering&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Analytics
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Kyle Cheung - &lt;a href="https://www.greybeam.ai/blog/duckdb-internals-part-1" rel="noopener noreferrer"&gt;DuckDB Internals: Why is DuckDB Fast? (Part 1)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 &lt;a href="https://clickhouse.com/blog/building-clickcannon-a-tool-for-benchmark-clickhouse" rel="noopener noreferrer"&gt;Building ClickCannon - a tool for benchmarking ClickHouse&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It’s ten years since ClickHouse was created, and their blog has a series of interesting posts reflecting on both the tool and the community around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Alexey Milovidov - &lt;a href="https://clickhouse.com/blog/open-source-10" rel="noopener noreferrer"&gt;Ten years of ClickHouse in open source&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Tyler Hannan - &lt;a href="https://clickhouse.com/blog/ten-years-of-open-source-a-stained-glass-view" rel="noopener noreferrer"&gt;Ten years of open source: a stained-glass view&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Al Brown - &lt;a href="https://clickhouse.com/blog/what-a-difference-10-years-of-open-source-makes" rel="noopener noreferrer"&gt;What a difference 10 years of open source makes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Al Brown - &lt;a href="https://clickhouse.com/blog/the-open-ecosystem-around-clickhouse" rel="noopener noreferrer"&gt;The open ecosystem around ClickHouse&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Platforms, Architectures, and Modelling
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Jack Vanlightly asks: &lt;a href="https://jack-vanlightly.com/blog/2026/6/21/can-we-agree-on-a-storage/workload-architecture-taxonomy" rel="noopener noreferrer"&gt;Can We Agree on a Storage/Workload Architecture Taxonomy?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Animesh Kumar et al assert that &lt;a href="https://moderndata101.substack.com/p/data-products-the-essential-context" rel="noopener noreferrer"&gt;&lt;em&gt;Most enterprise AI-for-data agents failed in 2025 because they lacked context, and Data Products, built above the engine, are the fix&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A recording and transcript of a talk that OpenAI’s Bonnie Xu did at InfoQ discussing their use of &lt;a href="https://www.infoq.com/presentations/data-aware-ai-agents/" rel="noopener noreferrer"&gt;AI Agents to Make Sense of Data at OpenAI&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pavlina Mitsou and Jonathan Warburton write about &lt;a href="https://engineering.atspotify.com/2026/6/encoding-your-domain-expert-the-context-layer-behind-spotifys-data-assistant" rel="noopener noreferrer"&gt;The Context Layer Behind Spotify’s Data Assistant&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chen Chang and colleagues at Anthropic discuss &lt;a href="https://claude.com/blog/how-anthropic-enables-self-service-data-analytics-with-claude" rel="noopener noreferrer"&gt;How Anthropic enables self-service data analytics with Claude&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Fresha continue their run of excellent blog posts, with this one from Daniel Wiszowaty: &lt;a href="https://medium.com/fresha-data-engineering/a-cdc-stream-is-not-a-table-rebuilding-postgres-inside-snowflake-one-thousand-times-003767d48a17" rel="noopener noreferrer"&gt;Everything Everywhere As Of Once: Rebuilding Postgres Inside Snowflake&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yordan Ivanov - &lt;a href="https://www.datagibberish.com/p/how-to-build-a-data-status-page-with-ai" rel="noopener noreferrer"&gt;How I Made My Data Platform’s Failures Public and Earned My Stakeholders' Trust&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Goutham Budati - &lt;a href="https://practicaldatacommunity.substack.com/p/why-technically-excellent-data-teams" rel="noopener noreferrer"&gt;Why Technically Excellent Data Teams Still Fail&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Booking.com’s Tiago Ferreira writes about &lt;a href="https://medium.com/booking-com-development/broken-windows-of-data-7f7d12ce2cd0" rel="noopener noreferrer"&gt;scaling a shared DWH across multiple teams without turning governance into a delivery bottleneck&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amer Hesson and colleagues at Netflix discuss &lt;a href="https://netflixtechblog.com/data-projects-managing-data-assets-at-netflix-scale-7ca25888591e" rel="noopener noreferrer"&gt;Managing Data Assets at Netflix Scale&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Uber’s Daniel Musgrave and colleagues write about &lt;a href="https://www.uber.com/us/en/blog/data-abstraction-layer/" rel="noopener noreferrer"&gt;Simplifying Data and Product Integrations with a Data Abstraction Layer&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Details of GuideWire’s &lt;a href="https://medium.com/guidewire-engineering-blog/federated-query-platform-for-ml-at-scale-architecture-and-multi-tenancy-3a2c060ff3e5" rel="noopener noreferrer"&gt;Federated Query Platform for ML at Scale&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Patrick Lam - &lt;a href="https://medium.com/airbnb-engineering/scaling-beyond-one-how-airbnb-evolved-its-data-architecture-for-a-multi-product-world-6125645d470c" rel="noopener noreferrer"&gt;Airbnb evolved its data architecture&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rohit Channe and Simran Mirchandani - &lt;a href="https://eng.lyft.com/metric-semantic-layer-how-lyft-governs-and-scales-key-data-definitions-56bee3643c29" rel="noopener noreferrer"&gt;How Lyft Governs and Scales Key Data Definitions&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;René Luijk discusses the &lt;a href="https://blog.dataengineerthings.org/why-no-one-understands-your-data-platform-but-you-2e67d5571423" rel="noopener noreferrer"&gt;use of the C4 model in the context of data platforms&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Engineering and Pipelines
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Joe Reis - &lt;a href="https://practicaldatamodeling.substack.com/p/the-turf-wars-are-over-time-to-cross" rel="noopener noreferrer"&gt;The Turf Wars Are Over. Time to Cross-Train&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Ben Rogojan - &lt;a href="https://seattledataguy.substack.com/p/in-2026-the-data-fundamentals-matter" rel="noopener noreferrer"&gt;In 2026 The Data Fundamentals Matter More Than Ever&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Madison Mae - &lt;a href="https://learnanalyticsengineering.substack.com/p/4-analytic-engineering-fundamentals" rel="noopener noreferrer"&gt;4 Analytic Engineering Fundamentals That Haven’t Changed&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A reminder of &lt;a href="https://ssbipolar.com/2021/05/31/roches-maxim/" rel="noopener noreferrer"&gt;Roche’s Maxim of Data Transformation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data should be transformed as far upstream as possible, and as far downstream as necessary.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bruno Masciarelli - &lt;a href="https://practicaldatacommunity.substack.com/p/how-to-build-a-simple-bulletproof" rel="noopener noreferrer"&gt;How to Build a Simple, Bulletproof Data Pipeline&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Couple of good posts from Joshua Kim about building a pipeline from scratch with dbt &lt;a href="https://joshua-data.medium.com/building-an-end-to-end-data-platform-1-the-lakehouse-before-the-first-dbt-model-5034b1446246" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; / &lt;a href="https://joshua-data.medium.com/building-an-end-to-end-data-platform-2-the-first-dbt-models-on-a-polymorphic-event-stream-f736d387a2e2" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Charly Clairmont - &lt;a href="https://medium.com/@egwada/why-dbt-state-matters-more-than-you-think-and-why-it-should-become-the-foundation-of-the-3c819178407f" rel="noopener noreferrer"&gt;Why dbt-state Matters More Than You Think&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Joachim Hodana - &lt;a href="https://blog.dataengineerthings.org/5-dbt-mistakes-i-see-in-every-startup-c638ed24b95d" rel="noopener noreferrer"&gt;5 dbt mistakes I see in every startup&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;dbtLabs published their &lt;a href="https://www.getdbt.com/resources/state-of-analytics-engineering-2026" rel="noopener noreferrer"&gt;2026 State of Analytics Engineering Report&lt;/a&gt;, with some interesting data around use of AI (ofc).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Details of &lt;a href="https://docs.getdbt.com/blog/dbt-core-v2-is-here?version=2.0&amp;amp;name=Fusion" rel="noopener noreferrer"&gt;dbt Core v2&lt;/a&gt; from Joel Labes and Grace Goheen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vikas Rai describes the &lt;a href="https://blogs.halodoc.io/enabling-data-intelligence-data-profiling-framework-at-halodoc/" rel="noopener noreferrer"&gt;Data Profiling Framework at Halodoc&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Artem Golubin - &lt;a href="https://rushter.com/blog/clickhouse-data-processing/" rel="noopener noreferrer"&gt;Using local ClickHouse for data processing&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CDC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 George Zefko - &lt;a href="https://georgioszefkilis.substack.com/p/building-a-cdc-pipeline-part-3-from" rel="noopener noreferrer"&gt;Building a CDC pipeline, part 3: From Kafka events to an analytical event log&lt;/a&gt; (previously: &lt;a href="https://substack.com/home/post/p-195536093" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;, &lt;a href="https://substack.com/home/post/p-196797293" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Andreas Andreakis - &lt;a href="https://aandreakis.com/posts/why-dblog-is-snapshot-equivalent" rel="noopener noreferrer"&gt;Why DBLog Is Snapshot-Equivalent&lt;/a&gt; (see also the &lt;a href="https://arxiv.org/abs/2010.12597" rel="noopener noreferrer"&gt;original DBLog paper&lt;/a&gt; and the more recent &lt;a href="https://arxiv.org/abs/2605.31475" rel="noopener noreferrer"&gt;A Theoretical Study of DBLog: Certified Virtual Cuts&lt;/a&gt; - both also by Andreas).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DoorDash’s Vinay Chella and Akshat Goel spoke at InfoQ about &lt;a href="https://www.infoq.com/presentations/write-ahead-intent-log/" rel="noopener noreferrer"&gt;Write-Ahead Intent Log: A Foundation for Efficient CDC at Scale&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Open Table Formats (OTF), Catalogs, Lakehouses etc.
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Gunnar Morling has announced the 1.0 release of &lt;a href="https://www.morling.dev/blog/hardwood-1-0-fast-lightweight-apache-parquet-reader-for-the-jvm/" rel="noopener noreferrer"&gt;Hardwood&lt;/a&gt;, which is, in his words, a fast, lightweight Apache Parquet reader for the JVM. He’s published some impressive benchmark figures too, showing just how much of a performance improvement can be had when reading Parquet files by using multi-threading.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/dfa1/vortex-java" rel="noopener noreferrer"&gt;vortex-java&lt;/a&gt; is a project that builds on Hardwood, implementing the Vortex columnar format in pure Java.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you want to see first-hand how open-source projects navigate issues that are not straightforward and have strong arguments on either side, this issue on the Iceberg project is fascinating: &lt;a href="https://github.com/apache/iceberg/pull/14797#issuecomment-4796713932" rel="noopener noreferrer"&gt;#14797 Implement Iceberg Kafka Connect with Delta Writer Support in DV Mode&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Apache Hudi project has been busy, releasing &lt;a href="https://hudi.apache.org/blog/2026/06/07/apache-hudi-release-1-2-announcement/" rel="noopener noreferrer"&gt;1.2.0&lt;/a&gt; with support for &lt;a href="https://hudi.apache.org/blog/2026/06/10/stateless-global-upserts-for-flink-streaming-in-apache-hudi-1-2-0/" rel="noopener noreferrer"&gt;Stateless Global Upserts for Flink&lt;/a&gt;, along with publishing details of how Hudi is used at &lt;a href="https://hudi.apache.org/blog/2026/06/15/apache-hudi-at-southwest-airlines/" rel="noopener noreferrer"&gt;Southwest Airlines&lt;/a&gt; and &lt;a href="https://hudi.apache.org/blog/2026/06/15/apache-hudi-at-penn-interactive/" rel="noopener noreferrer"&gt;Penn Entertainment&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sivabalan Narayanan discusses on the Hudi blog &lt;a href="https://hudi.apache.org/blog/2026/06/05/why-metadata-has-to-be-mutation-friendly/" rel="noopener noreferrer"&gt;Why Metadata Has to Be Mutation-Friendly&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/sarthaksin1857/IcebergDoctor" rel="noopener noreferrer"&gt;Iceberg Doctor&lt;/a&gt; is a tool from Sarthak Singh for diagnostics on your Iceberg metadata.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A three part series (&lt;a href="https://fluss.apache.org/blog/fluss-tiering-service-deep-dive-part1/" rel="noopener noreferrer"&gt;1&lt;/a&gt;, &lt;a href="https://fluss.apache.org/blog/fluss-tiering-service-deep-dive-part2/" rel="noopener noreferrer"&gt;2&lt;/a&gt;, &lt;a href="https://fluss.apache.org/blog/fluss-tiering-service-deep-dive-part3/" rel="noopener noreferrer"&gt;3&lt;/a&gt;) from Giannis Polyzos about Apache Fluss and lakehouse tiering, along with a &lt;a href="https://fluss.apache.org/blog/fluss-storage-hierarchy/" rel="noopener noreferrer"&gt;separate introductory overview to the concepts&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A useful post from Alex Merced about &lt;a href="https://www.dremio.com/blog/apache-iceberg-partition-evolution-change-your-partitioning-strategy-without-rewriting-data/" rel="noopener noreferrer"&gt;partition evolution in Iceberg&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/@shahsoumil519/how-do-you-know-if-variant-iceberg-tables-are-fully-shredded-or-partially-shredded-22088fdf7460" rel="noopener noreferrer"&gt;Two&lt;/a&gt; &lt;a href="https://medium.com/@shahsoumil519/variant-shred-coverage-report-see-what-iceberg-actually-shredded-b6bece53456f" rel="noopener noreferrer"&gt;posts&lt;/a&gt; from Soumil Shah about shredding &lt;code&gt;VARIANT&lt;/code&gt; fields in Iceberg.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  RDBMS and General Data Stuff
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Kelsey Hightower - &lt;a href="https://www.youtube.com/watch?v=NIvSgk0oV7Y" rel="noopener noreferrer"&gt;Thoughts on Open Source (2024)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Datadog’s Shree Sampath writes about &lt;a href="https://www.datadoghq.com/blog/engineering/postgresql-ha-kubernetes/" rel="noopener noreferrer"&gt;how they implement HA for Postgres on Kubernetes&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Warner Music Group’s Yask Srivastava describes &lt;a href="https://tech.wmg.com/why-we-shrank-our-timescaledb-chunks-from-30-days-to-7-07cab8afefc5" rel="noopener noreferrer"&gt;why we shrank our TimescaleDB chunks from 30 days to 7&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tom Pang - &lt;a href="https://planetscale.com/blog/the-only-scalable-delete" rel="noopener noreferrer"&gt;The only scalable delete in Postgres is DROP TABLE&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/microsoft/pg_durable" rel="noopener noreferrer"&gt;pg_durable&lt;/a&gt; brings durable execution to Postgres.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 This is less "data" blog post and more just "cool tech blog post" :) Conor Gallagher from Zalando details how they do &lt;a href="https://engineering.zalando.com/posts/2026/06/client-side-load-balancing.html" rel="noopener noreferrer"&gt;Client-Side Load Balancing at a Million Requests Per Second&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Eric Sun - &lt;a href="https://eric-sun.medium.com/the-join-aware-materialized-view-query-rewrite-gap-bdcb248dbd18" rel="noopener noreferrer"&gt;The Join-Aware Materialized View Query Rewrite Gap&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A good write-up from dbt’s Tristan Handy about recent tech events and trends: &lt;a href="https://roundup.getdbt.com/p/hunting-for-tokens-snowflake-summit" rel="noopener noreferrer"&gt;Hunting for Tokens. Snowflake Summit. Agent Use Cases.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  And finally…
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Nothing to do with data, but stuff that I’ve found interesting or has made me smile.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://alifeengineered.substack.com/p/youll-get-re-orged-again-this-year" rel="noopener noreferrer"&gt;You’ll Get Re-Orged Again This Year. Here’s How to Be Ready.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 A great write-up from Apurva Mehta about his company, Responsive, and what went wrong: &lt;a href="https://www.apurvamehta.com/blog/our-first-customers-were-the-exception" rel="noopener noreferrer"&gt;Our first customers were the exception&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simonida Jovanovic - &lt;a href="https://www.linkedin.com/pulse/how-do-you-sell-forever-free-product-posthogs-answer-10-jovanovic-hjvmf/" rel="noopener noreferrer"&gt;How do you sell a forever-free product? PostHog’s answer is 10 emails and a pet hedgehog.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Life
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Casey Neistat - &lt;a href="https://www.youtube.com/watch?v=OBtnKCG3QAE" rel="noopener noreferrer"&gt;yeah, 730 days no exceptions&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Murat Demirbas - &lt;a href="https://muratbuffalo.blogspot.com/2026/06/5-lessons-at-50.html" rel="noopener noreferrer"&gt;5 Lessons at 50&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fun
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;TikTok is my guilty-pleasure, and this account always brings a smile to my face for how completely and utterly weird-yet-engaging it is: &lt;a href="https://www.tiktok.com/@letriangle.shop/video/7629761890360642838" rel="noopener noreferrer"&gt;Le Triangle&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I definitely am adding this to my todo-list: &lt;a href="https://veronicaexplains.net/my-first-writerdeck/" rel="noopener noreferrer"&gt;Writerdeck&lt;/a&gt;. &lt;em&gt;Not sure if this one is &lt;code&gt;Work&lt;/code&gt; or &lt;code&gt;Fun&lt;/code&gt;. I’ll probably convince myself it’s the former whilst doing the latter ;)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Misc
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 I love this: &lt;a href="https://www.theguardian.com/technology/2026/jun/24/ai-read-papyrus-scroll-burnt-vesuvius-eruption" rel="noopener noreferrer"&gt;using ML to read burnt papyrus scrolls from Vesuvius&lt;/a&gt;. You can find details of the &lt;a href="https://scrollprize.org/firstscroll" rel="noopener noreferrer"&gt;full project here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.cypherpunkbooks.com/" rel="noopener noreferrer"&gt;The Cypherpunk Library&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.absurdlyoptimized.com/recipes/pancakes/" rel="noopener noreferrer"&gt;The Absurdly Optimized Pancake&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I started 'scrobbling' &lt;a href="https://www.last.fm/user/themoff" rel="noopener noreferrer"&gt;my music&lt;/a&gt; well over 20 years ago, so this one was a blast from the past for me: &lt;a href="https://support.last.fm/t/last-fm-is-now-independent/118591/2" rel="noopener noreferrer"&gt;Last.fm is now independent&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>interestinglinks</category>
    </item>
    <item>
      <title>Interesting links - May 2026</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Fri, 29 May 2026 10:00:19 +0000</pubDate>
      <link>https://dev.to/rmoff/interesting-links-may-2026-3o0p</link>
      <guid>https://dev.to/rmoff/interesting-links-may-2026-3o0p</guid>
      <description>&lt;p&gt;Welcome to May’s &lt;em&gt;Interesting Links&lt;/em&gt;! This month saw the Current conference in London with &lt;a href="https://rmoff.net/categories/5k-run/walk/" rel="noopener noreferrer"&gt;the usual 5k run&lt;/a&gt;, lots of familiar faces and friendly conversations—and plenty of excellent breakout sessions too. It seems live-tweeting conferences isn’t a thing any more, with only myself and Thomas Cooper seeming to post anything, but if you want you can go review &lt;a href="https://bsky.app/search?q=%23current26" rel="noopener noreferrer"&gt;the hashtag feed on BlueSky&lt;/a&gt; for some highlights of the conference.&lt;/p&gt;

&lt;p&gt;I got my first Hacker News front page hit with &lt;a href="https://rmoff.net/2026/05/06/ai-slop-is-killing-online-communities/" rel="noopener noreferrer"&gt;AI Slop is Killing Online Communities&lt;/a&gt; (51k views and climbing!), and a nice little halo boost for another rant from earlier this year, &lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;AI will fsck you up if you’re not on board&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Oh, and I got involved in some thought leadering over on LinkedIn (&lt;em&gt;which a non-zero number of people thought was serious&lt;/em&gt;) with my &lt;a href="https://www.linkedin.com/posts/robinmoffatt_current26-current26-leadership-activity-7463189069041180672-CFR4?rcm=ACoAAAC2ckIBstmoM1I4uBi9Djg8B7e0JaBvqzQ" rel="noopener noreferrer"&gt;shitposting about fried breakfasts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;{{&amp;lt; il-header &amp;gt;}}&lt;/p&gt;

&lt;h1&gt;
  
  
  Kafka and Event Streaming
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Apache Kafka 4.3.0 has been released. Check out the &lt;a href="https://kafka.apache.org/blog/2026/05/22/apache-kafka-4.3.0-release-announcement/" rel="noopener noreferrer"&gt;release announcement&lt;/a&gt;, as well as &lt;a href="https://www.youtube.com/watch?v=lePgrOiX11U" rel="noopener noreferrer"&gt;a video from Sandon Jacobs&lt;/a&gt; covering the new features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔥 After a few quiet months on his blog, Jack Vanlightly is back with a bang! He’s written a new tool, &lt;a href="https://jack-vanlightly.com/blog/2026/5/20/introducing-dimster-a-performance-benchmarking-tool-for-apache-kafka" rel="noopener noreferrer"&gt;Dimster, a performance benchmarking tool for Apache Kafka&lt;/a&gt;, and has written several more blog posts off the back of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://jack-vanlightly.com/blog/2026/5/22/benchmarking-apache-kafka-consumer-groups-vs-share-groups-overhead-test" rel="noopener noreferrer"&gt;Benchmarking Apache Kafka Consumer Groups vs Share Groups (overhead test)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Kafka Share Groups and Parallelizing Consumption &lt;a href="https://jack-vanlightly.com/blog/2026/5/25/kafka-share-groups-and-parallelizing-consumption-part-1-tuning-maxpollrecords" rel="noopener noreferrer"&gt;Part 1: Tuning max.poll.records&lt;/a&gt;, &lt;a href="https://jack-vanlightly.com/blog/2026/5/27/kafka-share-groups-and-parallelizing-consumption-part-2-producer-batches-and-shareacquiremode" rel="noopener noreferrer"&gt;Part 2: Producer Batches and share.acquire.mode&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;🔥 I had the absolute pleasure to watch Victor Rentea present at Devoxx UK earlier this month. This guy redefines what it means to be an entertaining, energetic, enthusiastic—and educational presenter. Whilst his specific talk, "Event-Driven Architecture Pitfalls" isn’t online yet, you can find &lt;a href="https://victorrentea.ro/eda-pitfalls" rel="noopener noreferrer"&gt;the slides here&lt;/a&gt;, and &lt;a href="https://www.youtube.com/watch?v=0SnuppAHOlQ" rel="noopener noreferrer"&gt;a recording from Devoxx last year&lt;/a&gt; of a similar talk.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;The Parallel Consumer library from Confluent has been marked as no longer maintained, prompting a &lt;a href="https://www.linkedin.com/posts/charles-larrieu-casias_nooooo-the-confluent-parallel-consumer-library-share-7465111475133685760-oBvc/" rel="noopener noreferrer"&gt;discussion&lt;/a&gt; of alternatives (and the concept itself) on LinkedIn, as well as &lt;a href="https://github.com/astubbs/parallel-consumer" rel="noopener noreferrer"&gt;a fork&lt;/a&gt; from one of the original authors, Tony Stubbs.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Mariano Gonzalez - &lt;a href="https://mariano-gonzalez.com/posts/post-5/" rel="noopener noreferrer"&gt;Benchmarking KPipe against the parallel-Kafka libraries you would actually pick&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Michel Tricot - &lt;a href="https://agentblueprint.substack.com/p/event-driven-vs-polling-architectures" rel="noopener noreferrer"&gt;Event-Driven vs. Polling Architectures for Agent Triggers&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;An interesting idea from Florent Ramiere and colleagues: what if you specify a set of interesting additions to Kafka’s functionality, with strict rules around the implementation, and then have LLMs take their best shot at it? You can see the ideas and results in &lt;a href="https://github.com/conduktor/current-london-2026" rel="noopener noreferrer"&gt;the branches of this repository&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Viquar Khan - &lt;a href="https://www.infoq.com/articles/architecting-cloud-native-kafka/" rel="noopener noreferrer"&gt;Architecting Cloud-Native Kafka: From Tiered Storage Towards a Diskless Future&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Elad Eldor - &lt;a href="https://dev.to/eeldor/kafkas-real-compression-problem-is-batch-depth-515k"&gt;Kafka’s Real Compression Problem Is Batch Depth&lt;/a&gt;, and &lt;a href="https://dev.to/eeldor/kafka-compute-is-cheap-network-is-not-2bdh"&gt;Kafka Compute Is Cheap. Network Is Not&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Kroxylicious &lt;a href="https://kroxylicious.io/blog/kroxylicious-proxy/releases/2026/05/15/release-0_21_0.html" rel="noopener noreferrer"&gt;version 0.21.0 has been released&lt;/a&gt;, and Sam Barker from the Kroxylicious project has been running some &lt;a href="https://kroxylicious.io/benchmarking/performance/2026/05/28/benchmarking-the-proxy.html" rel="noopener noreferrer"&gt;benchmarks to look at the impact that the proxy has&lt;/a&gt;, both pass-through and when encrypting records.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Aiven’s Juha Mynttinen explores why they think &lt;a href="https://aiven.io/blog/kafka-deserves-topic-types" rel="noopener noreferrer"&gt;Apache Kafka Deserves Topic Types&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Details of &lt;a href="https://x.com/rwitoff/status/2052863502424133949" rel="noopener noreferrer"&gt;a Coinbase outage&lt;/a&gt; involving their Kafka provider, which based on blogs from &lt;a href="https://www.coinbase.com/en-gb/blog/kafka-infrastructure-renovation" rel="noopener noreferrer"&gt;2022&lt;/a&gt; and &lt;a href="https://aws.amazon.com/blogs/aws-cloud-financial-management/how-coinbase-built-a-cloud-center-of-excellence-to-optimize-their-cloud-costs-on-aws/" rel="noopener noreferrer"&gt;2023&lt;/a&gt; is MSK.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Andy Muir - &lt;a href="https://muirandy.wordpress.com/2026/04/30/kafka-schema-registry-doesnt-guarantee-compatibility-and-what-actually-does/" rel="noopener noreferrer"&gt;Kafka Schema Registry doesn’t guarantee compatibility (and what actually does)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Bruno Cadonna - &lt;a href="https://www.opendata.dev/blog/buffer-ha-pipelines-without-kafka" rel="noopener noreferrer"&gt;OpenData Buffer: HA pipelines without Kafka&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Jeffrey J. Jennings - &lt;a href="https://medium.com/@jeffrey.j.jennings/kafkas-quiet-observability-superpower-kafka-interceptors-aca88c33867e" rel="noopener noreferrer"&gt;Kafka’s quiet observability superpower - Kafka Interceptors&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Grzegorz Kocur - &lt;a href="https://monedula.dev/blog/kafka-metrics-opentelemetry-otlp-monedula-metrics-reporter/" rel="noopener noreferrer"&gt;Do Kafka metrics have to be so difficult?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Stream Processing
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Flink’s Stateful Functions (StateFun) is not maintained by the project any more, so kzmlabs' Oleksandr Kazimirov &lt;a href="https://kzmlabs.github.io/flink-statefun/articles/forking-statefun/" rel="noopener noreferrer"&gt;forked it&lt;/a&gt; to continue developing it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Olena Vodzianova - &lt;a href="https://medium.com/@wizzywooz/how-chandy-lamport-inspired-apache-flink-checkpointing-256db84084ce" rel="noopener noreferrer"&gt;How Chandy-Lamport Inspired Apache Flink Checkpointing&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔥 Two good posts from the team at Grab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://engineering.grab.com/enchancing-flink-shadow-testing" rel="noopener noreferrer"&gt;Enhancing Flink deployment with shadow testing&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Details of how they built their &lt;a href="https://engineering.grab.com/one-click-data-ingestion-platform-with-apache-flink" rel="noopener noreferrer"&gt;one-click data ingestion platform with Apache Flink&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Details of &lt;a href="https://aws.amazon.com/blogs/big-data/how-smartsheet-built-real-time-dynamic-filtering-on-apache-flink-reducing-40k-month-in-messaging-costs/" rel="noopener noreferrer"&gt;how Smartsheet use Flink&lt;/a&gt; for optimising both costs and performance by filtering messages.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/Eric-D/flink-state-explorer" rel="noopener noreferrer"&gt;flink-state-explorer&lt;/a&gt; is, as the name suggests, a tool for exploring Apache Flink 1.20 canonical savepoints interactively.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;A hands-on github repo from Patrick Neff showing off &lt;a href="https://github.com/pneff93/dbt-cc-stream-processing" rel="noopener noreferrer"&gt;Stream processing pipeline using dbt and Flink on Confluent Cloud&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Shuva Jyoti Kar - &lt;a href="https://medium.com/google-cloud/designing-stateful-serverless-agentic-loop-bb73a63562b4" rel="noopener noreferrer"&gt;Designing stateful serverless Agentic Loop&lt;/a&gt; with Kafka and Flink.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;A couple of security issues for Flink to be aware of if you’re running it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-35194" rel="noopener noreferrer"&gt;CVE-2026-35194&lt;/a&gt; (SQL injection).&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-40564" rel="noopener noreferrer"&gt;CVE-2026-40564&lt;/a&gt; (K8s operator).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Analytics
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Tristan Handy - &lt;a href="https://roundup.getdbt.com/p/bis-second-unbundling" rel="noopener noreferrer"&gt;BI’s Second Unbundling&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A good writeup from Cloudflare’s James Morrison and Christian Endres about tracing performance issues in ClickHouse.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Several posts from StarRocks covering new features in 4.1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://medium.com/starrocks-engineering/starrocks-4-1-no-more-external-rewrite-pipelines-for-apache-iceberg-deletes-ce13132a082e" rel="noopener noreferrer"&gt;No More External Rewrite Pipelines for Apache Iceberg DELETEs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://medium.com/starrocks-engineering/starrocks-4-1-iceberg-acceleration-that-stays-fresh-9bc962c056a4" rel="noopener noreferrer"&gt;Iceberg Acceleration That Stays Fresh&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://medium.com/starrocks-engineering/starrocks-4-1-native-support-for-semi-structured-data-in-iceberg-6c71e989dd63" rel="noopener noreferrer"&gt;Native Support for Semi-Structured Data in Iceberg&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Two BigQuery optimisation/cost saving articles, from &lt;a href="https://medium.com/teads-engineering/how-we-cut-bigquery-slot-usage-by-90-on-one-of-our-most-resource-hungry-service-after-an-outage-c491af09e77e" rel="noopener noreferrer"&gt;Christophe Oudar&lt;/a&gt; and &lt;a href="https://medium.com/@azeemjalageri/23fc5efc91a5?sk=2d8855c53c8d878b6afa7a839b30ef09" rel="noopener noreferrer"&gt;Azeem Jalageri&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Daniel Beach - &lt;a href="https://www.confessionsofadataguy.com/spark-is-dead-long-live-duckdb/" rel="noopener noreferrer"&gt;Spark is Dead. Long Live DuckDB&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Alibaba added DuckDB into their fork of MySQL, AliSQL, providing &lt;a href="https://www.alibabacloud.com/blog/when-mysql-meets-the-columnar-storage-engine-duckdb-in-the-ai-era_603117" rel="noopener noreferrer"&gt;storage and query for OLAP workloads&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Simon Aubury - &lt;a href="https://simonaubury.substack.com/p/i-dont-need-an-untrusted-llm-to-tell" rel="noopener noreferrer"&gt;I don’t need an untrusted LLM to tell me I’m spending too much on coffee&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;The DuckDB team announced &lt;a href="https://duckdb.org/2026/05/12/quack-remote-protocol.html" rel="noopener noreferrer"&gt;Quack: The DuckDB Client-Server Protocol&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Ben Fleis explores &lt;a href="https://duckdb.org/2026/05/07/delta-uc-updates" rel="noopener noreferrer"&gt;DuckDB’s support for Delta and Unity Catalog&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;🔥 I’ve been a fan of &lt;a href="https://tech.marksblogg.com/" rel="noopener noreferrer"&gt;Mark Litwintschik’s&lt;/a&gt; no-nonsense blog posts showing current technologies and exploring interesting data sets for many years. In this one he uses DuckDB to analyse details of &lt;a href="https://tech.marksblogg.com/gcat-satellite-database.html" rel="noopener noreferrer"&gt;10K+ Satellites in Space&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Platforms, Architectures, and Modelling
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Nikola Ilic - &lt;a href="https://towardsdatascience.com/data-modeling-for-analytics-engineers-the-complete-primer/" rel="noopener noreferrer"&gt;Data Modeling for Analytics Engineers: The Complete Primer&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AirTable’s Matthew Jin &lt;a href="https://medium.com/airtable-eng/how-we-reduced-archive-storage-costs-by-100x-and-saved-millions-21754b5a6c8e" rel="noopener noreferrer"&gt;details how they optimised their costs&lt;/a&gt; by moving PBs of cold data from MySQL to S3, and wrote a query engine using Data Fusion to serve it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Brian Brunner and his colleagues at Cloudflare published details of &lt;a href="https://blog.cloudflare.com/our-unified-data-platform/" rel="noopener noreferrer"&gt;how they built Cloudflare’s data platform and an AI agent on top of it&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Engineering, Pipelines, and CDC
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Caesario Kisty - &lt;a href="https://blog.dataengineerthings.org/a-practical-implementation-of-medallion-architecture-using-clickhouse-484ec6dd960c" rel="noopener noreferrer"&gt;A Practical Implementation of Medallion Architecture Using ClickHouse&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Xinran Waibel - &lt;a href="https://blog.dataengineerthings.org/data-engineering-open-forum-2026-recap-b0154b770315" rel="noopener noreferrer"&gt;Data Engineering Open Forum 2026 Recap&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 After doing a bit of fairly naïve experimentation with Claude and dbt &lt;a href="https://rmoff.net/2026/03/11/claude-code-isnt-going-to-replace-data-engineers-yet/" rel="noopener noreferrer"&gt;earlier this year&lt;/a&gt;, I was very interested to read Jason Ganz’s article &lt;a href="https://roundup.getdbt.com/p/what-data-agent-benchmarks-do-and" rel="noopener noreferrer"&gt;What data agent benchmarks do and don’t tell us&lt;/a&gt;, and hope to try out the referenced &lt;a href="https://github.com/dbt-labs/ade-bench#user-content-fn-1-43049741a33bb2b20904cc0f5298be23" rel="noopener noreferrer"&gt;ADE-bench&lt;/a&gt; ("a framework for evaluating AI agents on data analyst tasks") soon.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Whilst Thijs Nieuwdorp’s article about &lt;a href="https://pola.rs/posts/schema-evolution/" rel="noopener noreferrer"&gt;Handling Schema Issues in Polars&lt;/a&gt; is specific to Polars, it’s a useful reference for the kind of schema changes one will want to make in a data pipeline, and the challenges it can cause depending on how or if your implementation technology of choice supports it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Pedram Navid - &lt;a href="https://databased.pedramnavid.com/p/we-need-to-talk-about-dbt" rel="noopener noreferrer"&gt;We need to talk about dbt&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A summary/re-write by Alex Yu (a.k.a. ByteByteGo) looking at &lt;a href="https://blog.bytebytego.com/p/how-figma-upgraded-data-pipeline" rel="noopener noreferrer"&gt;How Figma Upgraded Data Pipeline from Multi-Day Latency to Real-Time&lt;/a&gt; (based on &lt;a href="https://www.figma.com/blog/figmas-data-pipeline-upgrade/" rel="noopener noreferrer"&gt;the original blog post by Yichao Zhao&lt;/a&gt; from last year).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Netflix - &lt;a href="https://netflixtechblog.medium.com/the-evolution-of-cassandra-data-movement-at-netflix-6e13329c80a1" rel="noopener noreferrer"&gt;The Evolution of Cassandra Data Movement at Netflix&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alexey Makhotkin has a two parter on Data Quality &lt;a href="https://minimalmodeling.substack.com/p/my-take-on-data-quality" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; / &lt;a href="https://minimalmodeling.substack.com/p/my-take-on-data-quality-tier-2" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chris Hillman - &lt;a href="https://ghostinthedata.info/posts/2026/2026-05-23-dont-go-dark/" rel="noopener noreferrer"&gt;Don’t Go Dark: Visibility Is a Data Engineering Skill&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After the excellent survey and results that Joe Reis published about data engineering earlier this year, he’s now following up with a survey on &lt;a href="https://joereis.substack.com/p/the-organizational-state-of-data" rel="noopener noreferrer"&gt;The Organizational State of Data Engineering&lt;/a&gt; (open for submissions until Sunday, June 21).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mahendran Vasagam - &lt;a href="https://slack.engineering/from-ssh-to-rest-a-security-driven-modernization-of-slacks-emr-data-pipelines/" rel="noopener noreferrer"&gt;From SSH to REST: A Security-Driven Modernization of Slack’s EMR Data Pipelines&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dana Rabba - &lt;a href="https://blogs.halodoc.io/building-self-healing-data-pipelines-at-halodoc/" rel="noopener noreferrer"&gt;Building Self-Healing Data Pipelines at Halodoc&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/rocky-data/rocky" rel="noopener noreferrer"&gt;rocky&lt;/a&gt; is a dbt alternative that looks quite interesting. It describes itself as "the trust plane for your warehouse", and targeting Databricks users primarily, with Snowflake and BigQuery to follow. There’s a built-in playground feature that’s worth poking around to get a feel for it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CDC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Marc Bowes describes how &lt;a href="https://marc-bowes.com/dsql-coupler.html" rel="noopener noreferrer"&gt;Aurora DSQL’s CDC feature works&lt;/a&gt;. If you want more, there’s further details of its use from &lt;a href="https://aws.amazon.com/blogs/database/getting-started-with-change-data-capture-in-amazon-aurora-dsql/" rel="noopener noreferrer"&gt;Vijay Karumajji&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 George Zefko - &lt;a href="https://georgioszefkilis.substack.com/p/building-a-cdc-pipeline-part-2-debezium" rel="noopener noreferrer"&gt;Building a CDC pipeline, part 2: Debezium Internals&lt;/a&gt; (I featured part 1 last month, if you missed it it’s &lt;a href="https://georgioszefkilis.substack.com/p/building-a-cdc-pipeline-part-1-postgresql" rel="noopener noreferrer"&gt;here&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A couple of good posts from the Debezium team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Chris Cranford discusses &lt;a href="https://debezium.io/blog/2026/05/22/what-nobody-explains-about-debezium-2026/" rel="noopener noreferrer"&gt;What Nobody Explains About Debezium in 2026 (But Should)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Jiri Pechanec explores &lt;a href="https://debezium.io/blog/2026/05/05/debezium-and-jupyter-integration/" rel="noopener noreferrer"&gt;Change Data Capture with Debezium and Jupyter&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Open Table Formats (OTF), Catalogs, Lakehouses etc.
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://iceberg.apache.org/releases/#1110-release" rel="noopener noreferrer"&gt;Apache Iceberg 1.11&lt;/a&gt; has been released (I even got some &lt;a href="https://github.com/apache/iceberg/releases/tag/apache-iceberg-1.11.0" rel="noopener noreferrer"&gt;small contributions&lt;/a&gt; merged 🎉). There are more details of the release in these blog posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Alex Stephen &amp;amp; Talat Uyarer - &lt;a href="https://opensource.googleblog.com/2026/05/announcing-apache-iceberg-1110.html" rel="noopener noreferrer"&gt;Announcing Apache Iceberg 1.11.0&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Ajantha Bhat - &lt;a href="https://medium.com/@ajanthabhat/apache-iceberg-1-11-0-adds-registerview-closing-a-catalog-migration-gap-ca21aa81695f" rel="noopener noreferrer"&gt;Apache Iceberg 1.11.0 Adds registerView: Closing a Catalog Migration Gap&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  Alex Merced - &lt;a href="https://medium.alexmerced.blog/an-in-depth-overview-of-the-apache-iceberg-1-11-0-release-93b1186199de" rel="noopener noreferrer"&gt;An In-Depth Overview of the Apache Iceberg 1.11.0 Release&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;🔥 The talks from Iceberg Summit 2026 are now &lt;a href="https://www.youtube.com/watch?v=4Bg64WnkfgE&amp;amp;list=PLkifVhhWtccxSA6VskdKdLnIwCJevOqFL" rel="noopener noreferrer"&gt;online&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Alex Merced begins an epic 15-part series about Apache Iceberg by looking at &lt;a href="https://medium.com/data-engineering-with-dremio/what-are-table-formats-and-why-were-they-needed-7d5ca69546a1" rel="noopener noreferrer"&gt;What Are Table Formats and Why Were They Needed?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Yelp’s Nick Del Nano looks at &lt;a href="https://engineeringblog.yelp.com/2026/05/partition-access-visualizations.html" rel="noopener noreferrer"&gt;How Partition Access Visualizations Reduced Data Lake S3 Cost by 33%&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Honest words from Fresha’s Samuel Valente as he looks at the use of Iceberg with Snowflake in practice: &lt;a href="https://medium.com/fresha-data-engineering/snowflake-with-iceberg-lakekeeper-dbt-and-some-sparks-flying-a6231fcb35a7" rel="noopener noreferrer"&gt;Snowflake with Iceberg: Lakekeeper, dbt, and some Sparks Flying&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  RDBMS
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Daniel Guzman-Burgos describes &lt;a href="https://blog.dbtrail.com/time-travel-sql-for-mysql-finally/" rel="noopener noreferrer"&gt;bintrail which provides time-travel SQL for MySQL&lt;/a&gt;. Renato Losio has a summary &lt;a href="https://www.infoq.com/news/2026/05/bintrail-mysql-timetravel/" rel="noopener noreferrer"&gt;on InfoQ&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Teiva Harsanyi - &lt;a href="https://read.thecoder.cafe/p/linux-broke-postgresql" rel="noopener noreferrer"&gt;How Linux 7.0 Broke PostgreSQL: The Preemption Regression Explained&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Radim Marek covers &lt;a href="https://boringsql.com/posts/order-by-jungle/" rel="noopener noreferrer"&gt;the ORDER BY jungle&lt;/a&gt;, as well as &lt;a href="https://boringsql.com/posts/postgresql-toast/" rel="noopener noreferrer"&gt;PostgreSQL’s TOAST&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Markus Winand also looks at &lt;a href="https://modern-sql.com/blog/2026-05/order-by-history" rel="noopener noreferrer"&gt;ORDER BY and the evolution of support in different RDBMS&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 James Blackwood-Sewell writes up details of &lt;a href="https://www.paradedb.com/blog/what-we-think-about-when-we-think-about-benchmarking" rel="noopener noreferrer"&gt;the benchmarking platform they built&lt;/a&gt;, whilst Ben Dicken muses on &lt;a href="https://planetscale.com/blog/on-benchmarking" rel="noopener noreferrer"&gt;benchmarking at PlanetScale&lt;/a&gt; too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  General Data Stuff
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An opinionated, and fairly concise, set of recommendations for the use of different &lt;a href="https://www.data-landscape.com/" rel="noopener noreferrer"&gt;Open Standards for Modern Data Architecture&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LinkedIn’s Pratikmohan Srivastav writes about a performance troubleshooting experience - &lt;a href="https://www.linkedin.com/blog/engineering/feed/the-58-million-key-freeze-what-a-hashmap-resize-taught-us-about-memory-allocation-at-scale" rel="noopener noreferrer"&gt;The 58-Million-Key Freeze: What a HashMap Resize Taught Us About Memory Allocation at Scale&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sem Sinchenko - &lt;a href="https://semyonsinchenko.github.io/ssinchenko/post/jvm-tax/" rel="noopener noreferrer"&gt;Same buffers, same instructions, same hardware. Where Is the JVM Tax?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gergely Orosz &lt;a href="https://newsletter.pragmaticengineer.com/p/designing-data-intensive-applications-book-excerpt" rel="noopener noreferrer"&gt;shares some excerpts&lt;/a&gt; from Martin Kleppmann’s second edition of &lt;em&gt;Designing Data Intensive Applications&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AI
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I warned you &lt;a href="https://rmoff.net/2025/09/30/interesting-links-september-2025/#_ai" rel="noopener noreferrer"&gt;previously&lt;/a&gt;…this AI stuff is here to stay, and it’d be short-sighted to think otherwise.&lt;/em&gt; &lt;em&gt;As I read and learn more about it, I’m going to share interesting links (the clue is in the blog post title) that I find—whilst trying to avoid the breathless hype and slop.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Ben Evans - &lt;a href="https://www.ben-evans.com/presentations" rel="noopener noreferrer"&gt;AI Eats the World&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 TikTok is my guilty pleasure, but instead of dogs misbehaving in comical ways, here’s an excellent piece to camera from Scott Hanselman reflecting on &lt;a href="https://vm.tiktok.com/ZNRW27cR2/" rel="noopener noreferrer"&gt;the impact of AI in our lives as software developers&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro-tip:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/yt-dlp/yt-dlp" rel="noopener noreferrer"&gt;&lt;code&gt;yt-dlp&lt;/code&gt;&lt;/a&gt; works great with TikTok, so you don’t have to actually open the page if you still wanna view the video.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Nate Berkopec - &lt;a href="https://www.nateberkopec.com/blog/thoughts-on-llms-in-2026/" rel="noopener noreferrer"&gt;Thoughts on LLMs in 2026&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Julien Hurault - &lt;a href="https://juhache.substack.com/p/time-for-ai-coding-to-turn-boring" rel="noopener noreferrer"&gt;Time for AI Coding to Turn Boring?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kate Holterhoff - &lt;a href="https://redmonk.com/kholterhoff/2026/05/05/ai-slop-vulnerability-treadmill/" rel="noopener noreferrer"&gt;AI Slop &amp;amp; the Vulnerability Treadmill&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Paulo Arruda - &lt;a href="https://www.infoq.com/presentations/multi-agent-system-lessons/" rel="noopener noreferrer"&gt;What I Learned Building Multi-Agent Systems from Scratch at Shopify&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Loris Cro - &lt;a href="https://kristoff.it/blog/contributor-poker-and-ai/" rel="noopener noreferrer"&gt;Contributor Poker and Zig’s AI Ban&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lucia Cerchie - &lt;a href="https://luciacerchie.dev/articles/why-you-need-more-than-a-skill-md/" rel="noopener noreferrer"&gt;Why You Need More Than a SKILL.md&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  And finally…
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Nothing to do with data, but stuff that I’ve found interesting or has made me smile.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Work and Career
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 An oldie (2008!) but a goodie: Jeff Atwood - &lt;a href="https://blog.codinghorror.com/dont-go-dark/" rel="noopener noreferrer"&gt;Don’t Go Dark&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Lara Hogan - &lt;a href="https://larahogan.me/blog/be-a-thermostat-not-a-thermometer/" rel="noopener noreferrer"&gt;Be a thermostat, not a thermometer&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As an IC, I endorse this pitch from Elena Verna ;-) &lt;a href="https://www.elenaverna.com/p/ic-work-is-the-new-career-flex" rel="noopener noreferrer"&gt;IC work is the new career flex&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Ana Rodrigues - &lt;a href="https://ohhelloana.blog/woman-in-tech/" rel="noopener noreferrer"&gt;It’s 2026 and women are still asked to teach others to think a little bit and not be a prick&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Leyla Kazim - &lt;a href="https://leylakazim.substack.com/p/i-did-no-work-for-a-year" rel="noopener noreferrer"&gt;I did no work for a year and no one noticed&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Community and Blogging
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Kevin Powell wrote &lt;a href="https://www.kevinpowell.co/article/tell-someone-you-appreciate-them/" rel="noopener noreferrer"&gt;this article&lt;/a&gt; which resonated hard for me. I think it’s a boiling-frog situation; if I think about my motivation to write today, vs a year ago, vs 5, it’s definitely very different. AI noise drowns things out, kinda like SEO marketing 'content factories' did but on a bigger and more destructive scale, so as an author is it even worth writing original material? Is anyone even gonna read it?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An excellent writeup from Vicki Boykis about &lt;a href="https://vickiboykis.com/2026/05/18/tagging-my-blog-posts-with-bertopic-and-llms/" rel="noopener noreferrer"&gt;Tagging my blog posts with BERTopic and LLMs&lt;/a&gt; - definitely need to try this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mike McQuaid - Open Source Resistance: &lt;a href="https://ossresistance.com/" rel="noopener noreferrer"&gt;Keep OSS alive on company time&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Very cool idea for conference badges from Shy Ruparel, with &lt;a href="https://temporal.io/blog/badges-for-replay-and-i-havent-slept-since-december" rel="noopener noreferrer"&gt;an excellent writeup&lt;/a&gt; to boot.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Read, Watch, Listen
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I couldn’t think of a good subheading for these :)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Some fun nostalgia, with screenshots of various old OSes at &lt;a href="http://www.typewritten.org/Media/" rel="noopener noreferrer"&gt;typewritten.org&lt;/a&gt; and &lt;a href="https://virtualosmuseum.org/more-screenshots/" rel="noopener noreferrer"&gt;The Virtual OS Museum&lt;/a&gt; (the latter even has, IIUC, runnable VMs for &lt;a href="https://virtualosmuseum.org/downloads/" rel="noopener noreferrer"&gt;download&lt;/a&gt;!)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dan Carlin is probably my favourite podcaster, and as well as his well-known &lt;a href="https://www.dancarlin.com/hardcore-history-series/" rel="noopener noreferrer"&gt;Hardcore Histories&lt;/a&gt; he has occasional thoughts on more current affairs, including this one: &lt;a href="https://pca.st/episode/5df3b4a2-666d-4a53-9741-6d46fc85d188" rel="noopener noreferrer"&gt;The Water in Which We Swim&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.ideagames.fun/middle-class-museum" rel="noopener noreferrer"&gt;The Middle Class Museum&lt;/a&gt; (&lt;em&gt;A memorial to affordable living&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📷 &lt;a href="https://blogs.loc.gov/picturethis/2026/04/the-classic-american-diner/" rel="noopener noreferrer"&gt;The Classic American Diner&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ismy.blue/" rel="noopener noreferrer"&gt;Is my blue your blue?&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://hackingpassion.com/fast16-pre-stuxnet-cyber-sabotage/" rel="noopener noreferrer"&gt;Fast16: The Cyberweapon That Predates Stuxnet by Five Years&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unresolved directive in &amp;lt;stdin&amp;gt; - include::../../asciidoc-includes/il-footer.adoc[]&lt;/p&gt;

</description>
      <category>interestinglinks</category>
    </item>
    <item>
      <title>AI Slop is Killing Online Communities</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Thu, 07 May 2026 09:25:55 +0000</pubDate>
      <link>https://dev.to/rmoff/ai-slop-is-killing-online-communities-2gce</link>
      <guid>https://dev.to/rmoff/ai-slop-is-killing-online-communities-2gce</guid>
      <description>&lt;p&gt;Like a young child coming home from kindergarten with their latest crayon scrawls, the internet is currently awash with people sharing their AI-generated work. And just like the young child's drawings, much of that work should be proudly put up on the walls within the artist's house---and no further.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh0it8het7qcsjvpy8lk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flh0it8het7qcsjvpy8lk.webp" alt="A child's painting of five colourful flowers --- purple, blue, yellow and orange --- with green stems and leaves" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Prologue: I ❤️ AI
&lt;/h1&gt;

&lt;p&gt;I am not an AI-hater. In fact, I think AI-haters are &lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;on the wrong side of history&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's just that I know when to keep my crayon-drawings to myself ;) And I am getting increasingly sad and frustrated seeing communities that I value slowly wilting under the onslaught of shit. Often that shit is perhaps naïvely shared with no deleterious intent, but shit nonetheless it is.&lt;/p&gt;

&lt;h1&gt;
  
  
  Congratulations, you entered a prompt and pressed return.
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;"I rewrote Kafka in COBOL"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Great, enter it at your next science fair. Meanwhile stop begging for stars on your brand new GitHub repo that no-one's touching with a bargepole.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I wrote a blog post about Kafka"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Did you though? We can tell that Claude wrote it, and it's a piece of garbage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I made this video about Kafka"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cool story bro. Except AI made it, and it's only of interest as a curiosity, not a useful learning artefact.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm self-publishing an ebook that I wrote about Kafka"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What you mean is, you got Claude to scrape the internet and crap out a "book" that you should be ashamed to give away for free.&lt;/p&gt;

&lt;p&gt;Any fool can feed coins into a fruit machine and pull the arm.&lt;/p&gt;

&lt;h1&gt;
  
  
  Step 0: Profit
&lt;/h1&gt;

&lt;p&gt;The pattern I see over and over seems to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Step 1: Discover agentic coding. Mind blown.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step 2: Chuck a project up onto GitHub (&lt;em&gt;if it's &lt;a href="https://www.githubstatus.com/" rel="noopener noreferrer"&gt;actually up&lt;/a&gt;&lt;/em&gt; &lt;code&gt;&amp;lt;/snark&amp;gt;&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step 3: Have AI write a breathless blog post about your vibe-coded project. Share blog post and repo to any subreddit and Slack group that you can find. Not sure which is suitable? Post to all of them---people will love to see it! &lt;code&gt;/s&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me tell you now: pause after step 2. Take a really long breath. Think really hard about what you've created, and &lt;em&gt;why&lt;/em&gt; you want to share it. If it's "because it's cool" then I've got news for you: agentic coding is no longer a novelty. It's just how shit gets done now.&lt;/p&gt;

&lt;p&gt;If you can think of the prompt, AI can write it. Big deal. That's &lt;em&gt;so&lt;/em&gt; early-2026. Move on.&lt;/p&gt;

&lt;p&gt;Still want to share it far and wide? Is it actually useful? Are you using it? Has it got really good documentation? Is it usable? Have you actually come back to the code again and again and put it through its paces? Or was it a one-night stand with Claude and the next morning neither of you thinks it was such a good idea?&lt;/p&gt;

&lt;p&gt;Still want to share it? If it's software, are you prepared to stand behind it as something people will raise issues against, maybe submit PRs for? If it's written, is it something &lt;em&gt;you'd&lt;/em&gt; want to read? Is it actually adding to the cumulative understanding of the community, or is it just an LLM auto-completing its way through text that you can't be arsed to write and I can't be arsed to read?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmevmhdh19o4fsisfidsp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmevmhdh19o4fsisfidsp.webp" alt="A child's painting of two stick-figure people with round heads, one in a teal top and one in a yellow and red top, standing on brown ground" width="530" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Who cares?
&lt;/h1&gt;

&lt;p&gt;No one forces me to read this stuff. Why am I so bothered by it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because like bindweed, it's slowly strangling the organic life out of communities.&lt;/strong&gt; When I open up Reddit now, it's increasingly overrun with vibe-coded AI stuff. Whilst much of it is well-intentioned I'm sure, it does nothing to &lt;em&gt;contribute&lt;/em&gt; to the community.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczbuiixfwi3fqa6c5wo6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fczbuiixfwi3fqa6c5wo6.webp" alt="Bindweed" width="600" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI slop is driving up the noise, and making the signal more and more difficult to discern in communities. This risks becoming a downward spiral; as communities become more polluted by this stuff, members will get frustrated from wading through AI slop and draw back, thus diminishing the life of the organic community even further.&lt;/p&gt;

&lt;p&gt;Carrying on like this, online communities will either wither and die, or converge on something like the dystopian-but-banal &lt;a href="https://www.moltbook.com/" rel="noopener noreferrer"&gt;MoltBook&lt;/a&gt; in which AI agents "talk" to each other with no humans present.&lt;/p&gt;

&lt;h1&gt;
  
  
  There's good 'slop' and bad slop
&lt;/h1&gt;

&lt;p&gt;You may have noticed that &lt;code&gt;AI Slop&lt;/code&gt; has become the &lt;em&gt;mot du jour&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The broad use of the term that I'm generally familiar with is as a negative description for low-effort material &lt;em&gt;created by AI&lt;/em&gt; and foisted upon those to whom it is of no benefit. However &lt;a href="https://lobste.rs/s/6pvtem/ai_will_fuck_you_up_if_you_re_not_on_board#c_zwqkcf" rel="noopener noreferrer"&gt;I learnt recently&lt;/a&gt; that there are those---probably correlating strongly with the AI-hating crowd---who brand anything written &lt;em&gt;about AI&lt;/em&gt; as "AI Slop", even if not written &lt;em&gt;by&lt;/em&gt; AI.&lt;/p&gt;

&lt;p&gt;Material created with the assistance of AI is not bad in itself. It's the purpose to which it's put.&lt;/p&gt;

&lt;p&gt;A good use of AI is when it enables people to do something they couldn't do before, to contribute to a community when they couldn't before. Done with the care and good intent of a human behind it, this is a nett positive.&lt;/p&gt;

&lt;p&gt;Bad AI slop, on the other hand, is monkeys throwing crap over the fence for a purpose other than furthering the community. This includes spam, engagement farming, and simply thoughtless noise in a space which is not for that purpose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz5wac0fumaatniky7yq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz5wac0fumaatniky7yq.webp" alt="A child's pen drawing of a green climbing vine with curling leaves" width="148" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  OK, but who made you gatekeeper of the internet?
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;The standards of common decency and taste, that's who.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's take a step back. Sharing content online is &lt;em&gt;wonderful&lt;/em&gt;. It's pretty much what made the internet what it is today.&lt;/p&gt;

&lt;p&gt;The knack is to understand &lt;em&gt;what&lt;/em&gt; you're sharing, to &lt;em&gt;whom&lt;/em&gt;, and &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you were born before around 1980 you'll know that there was the Geocities era. Every high-school nerd had a homepage (mine was in Vienna since you're asking).&lt;/p&gt;

&lt;p&gt;Just because I built a homepage on Geocities, complete with 'Under Construction' anigifs, a web counter &lt;em&gt;and&lt;/em&gt; a web ring banner, does not mean that I should be sharing it to anyone who'll listen. Amongst my friends, sure. My parents, of course---they'll be proud of anything I build. But to the general internet? Who cares.&lt;/p&gt;

&lt;p&gt;And now with AI-generated content, whether a vibe-coded app or a blog post, the same applies. The internet went through a collective convulsion in early 2026 as everyone discovered the power of Claude Opus 4.5 (and don't get me wrong, it is &lt;em&gt;damn&lt;/em&gt; cool). And what does anyone do when they discover anything neat? They want to share it with their friends!&lt;/p&gt;

&lt;p&gt;Combine that with the deafening AI-hype machine of grifters already in overdrive---and suddenly subreddits and Slacks are overrun with AI-generated material.&lt;/p&gt;

&lt;h1&gt;
  
  
  So what &lt;em&gt;should&lt;/em&gt; I share?
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Built &lt;em&gt;with&lt;/em&gt; AI, not &lt;em&gt;by&lt;/em&gt; AI
&lt;/h2&gt;

&lt;p&gt;This excellent section heading is taken from my friend and colleague Gunnar Morling's &lt;a href="https://www.morling.dev/blog/hardwood-new-parser-for-apache-parquet/#_built_with_ai_not_by_ai" rel="noopener noreferrer"&gt;recent article&lt;/a&gt;. As I outlined above, AI is a powerful tool, and I will argue with anyone for the case that it's pretty much a dereliction of one's job to &lt;em&gt;not&lt;/em&gt; be including it in one's toolbox. Gunnar nails the nuance though:&lt;/p&gt;

&lt;p&gt;Build &lt;em&gt;with&lt;/em&gt; AI.&lt;/p&gt;

&lt;p&gt;AI is just a tool.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You&lt;/em&gt; need to do the thinking, the instructing, the checking.&lt;/p&gt;

&lt;p&gt;Gunnar has built a fantastic new project (&lt;a href="https://hardwood.dev/latest/" rel="noopener noreferrer"&gt;Hardwood&lt;/a&gt;; a new parser for Apache Parquet), &lt;em&gt;using&lt;/em&gt; AI. Does that mean it falls foul of my wrath and judgement? No, of course not. It's a project that's taken four months so far, with a solid roadmap, a burgeoning community, and a thoughtful and careful design behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contribution
&lt;/h2&gt;

&lt;p&gt;Does your offering &lt;strong&gt;contribute&lt;/strong&gt; anything to the community?&lt;/p&gt;

&lt;p&gt;If you boil it down to its essentials, is what you're sharing anything other than the manifestation of a prompt fed into an agentic coding tool? If I took your prompt and ran it, would I end up with something similar? Prompt engineering is fun and an interesting study, but it's tangential to the subject itself. Consider a community of ornate furniture enthusiasts (&lt;em&gt;I'm sure such a thing exists&lt;/em&gt;); it's the equivalent of bombarding them with Ikea-esque pieces simply because you've got a really interesting set of chisels that you want to show off.&lt;/p&gt;

&lt;p&gt;Just like I'm not posting my kid's drawings off to the National Gallery just yet, I'm also not sharing every cool app that I can build with Claude. Not that "software is art" (though some of the best actually is), but there's nothing much interesting in the puerile output of a &lt;em&gt;process&lt;/em&gt;. Anyone with a few tokens can prompt their way to a bit of software. Throwaway tools are just fine. They're great, in fact---the internet is built on weird little scripts that people have built and shared. But chuck them on gist/GitHub---they don't need a launch blog post as if you're the incarnation of Steve Jobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa80tlsatcwdnpnzmw26v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa80tlsatcwdnpnzmw26v.webp" alt="A child holding up two large abstract paintings in bold poster-paint colours" width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Respect the community
&lt;/h2&gt;

&lt;p&gt;This is a tale as old as time. Well, the internet anyway, once it got beyond ARPANET and BBSes.&lt;/p&gt;

&lt;p&gt;Whether Usenet, Reddit, lobste.rs, or any other online platform, the &lt;em&gt;netiquette&lt;/em&gt; is always to "lurk". Hang around, read what gets written, get a feel for "the vibe".&lt;/p&gt;

&lt;p&gt;I'm not the arbiter of what's acceptable in a given community. The community members are. Vibed an &lt;em&gt;amazing&lt;/em&gt; new implementation of the Kafka protocol, but not sure if people want to see it? Read the room, and get a feel for whether they'll welcome with open arms your AI slop---or not. If in doubt, ask!&lt;/p&gt;

&lt;p&gt;As well as lurking, another way of showing respect to the community is to be &lt;a href="https://rmoff.net/ai" rel="noopener noreferrer"&gt;very open and clear&lt;/a&gt; about if, how, and where you're using AI in your contribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Asymmetry of Bullshit
&lt;/h2&gt;

&lt;p&gt;What's the &lt;strong&gt;impact on others&lt;/strong&gt; of your contribution?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The amount of energy needed to refute bullshit is an order of magnitude bigger than that needed to produce it.&lt;/p&gt;

&lt;p&gt;---  &lt;a href="https://en.wikipedia.org/wiki/Brandolini%27s_law" rel="noopener noreferrer"&gt;Alberto Brandolini&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you splurge out a gobbledegook article, you're putting that workload onto your reader to realise that it's not worth wading through. If you dump a complex PR into a project without due care, you're obligating the reviewers to go through the code and for &lt;em&gt;them&lt;/em&gt; to explain to &lt;em&gt;you&lt;/em&gt; why it can't be merged. In both of these scenarios the community would be &lt;em&gt;better off without your contribution&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately the sheer volume of AI slop (and I mean that in the bad way) has meant that communities and projects are &lt;a href="https://redmonk.com/kholterhoff/2026/02/26/generative-ai-policy-landscape-in-open-source/" rel="noopener noreferrer"&gt;struggling to deal with the impact of contributions&lt;/a&gt;, in some cases even &lt;a href="https://ziglang.org/code-of-conduct/#strict-no-llm-no-ai-policy" rel="noopener noreferrer"&gt;slamming their doors shut on anything that's touched AI&lt;/a&gt;. Projects like &lt;a href="https://github.com/mitchellh/vouch" rel="noopener noreferrer"&gt;Vouch&lt;/a&gt; have sprung up to try and address this, but the Rubicon has been crossed and it's unclear where things will end up.&lt;/p&gt;

&lt;p&gt;Pre-AI, the effort required for contributions was sufficient proof of work to either deter people or demonstrate an actual commitment. Communities could deal with sub-par contributions. Those well-intentioned and willing to learn could be mentored and often would develop into important members of the community. Those less well-intentioned and not doing much more than spamming could be dealt with because the volume was so low.&lt;/p&gt;

&lt;h1&gt;
  
  
  With great power...
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;or perhaps that should be "With a great number of tokens"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Communities are powerful yet fragile things. Don't be the bindweed that suffocates the life out of them.&lt;/p&gt;

&lt;p&gt;Explore with great joy the power that LLMs and agentic coding tools bring. Enjoy the frisson of &lt;em&gt;jfc **that&lt;/em&gt;* is cool* that it invariably brings.&lt;/p&gt;

&lt;p&gt;But respect the community, and only share what is &lt;em&gt;truly&lt;/em&gt; relevant. Save the crayon pictures for your kitchen fridge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6f5syfuist5druoux47b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6f5syfuist5druoux47b.webp" alt="A child's crayon drawing of a red dragon roaring, with 'roar!' scrawled above its head" width="614" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Bindweed photo by &lt;a href="https://unsplash.com/@cirlbunting" rel="noopener noreferrer"&gt;Joshua Ralph&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-white-flower-on-a-plant-6hv1oVJAUMU" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All other pictures by my kids. Which is ironic, given my exhorting for people to literally keep their childish drawings to themselves ;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rant</category>
      <category>ai</category>
    </item>
    <item>
      <title>🏃🚶 The unofficial Current London 2026 Run/Walk 🏃🚶</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Tue, 05 May 2026 10:51:06 +0000</pubDate>
      <link>https://dev.to/rmoff/the-unofficial-current-london-2026-runwalk-46cc</link>
      <guid>https://dev.to/rmoff/the-unofficial-current-london-2026-runwalk-46cc</guid>
      <description>&lt;p&gt;&lt;strong&gt;Another year, another Current---another 5k run/walk for anyone who'd like to join!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Did I mostly copy-and-paste this from &lt;a href="https://rmoff.net/2025/05/02/the-unofficial-current-london-2025-run/walk/" rel="noopener noreferrer"&gt;last year's post&lt;/a&gt;? You bet I did!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a tradition that started back at Kafka Summit San Francisco in 2019 &lt;a href="https://twitter.com/rmoff/status/1179047181891883008" rel="noopener noreferrer"&gt;over the Golden Gate Bridge&lt;/a&gt; and has continued since then.&lt;/p&gt;

&lt;p&gt;London's Docklands offer up what would charitably be described as not the most...picturesque of routes---but with a group of similarly minded people, what could be a better way to start day 1 of Current 2026 London than a 5k run or walk :) Plus, it's flat! 🙌&lt;/p&gt;

&lt;h1&gt;
  
  
  Who? What? Where? When?
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Who&lt;/em&gt;: Anyone who would like to is very welcome :)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;What&lt;/em&gt;: A 5k (3.1 miles) run or walk&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Where&lt;/em&gt;: Starting from the Aloft Hotel (next to the ExCeL centre), heading anti-clockwise around the quayside at ExCeL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;When&lt;/em&gt;: &lt;strong&gt;Tuesday 19th May at 07:30 BST&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll &lt;strong&gt;meet at 07:30&lt;/strong&gt; in the carpark in front of the hotel. That'll give us time to congregate and take some &lt;a href="https://twitter.com/search?q=%23streamingselfie%20%23kafkasummit%20until%3A2020-03-01&amp;amp;src=typed_query&amp;amp;f=live" rel="noopener noreferrer"&gt;&lt;code&gt;#streamingselfie&lt;/code&gt;&lt;/a&gt;s if you want them, before setting off at 07:35 sharp to make our way around &lt;a href="https://www.strava.com/clubs/1039355/group_events/2028172" rel="noopener noreferrer"&gt;the 5k route&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Go at your own pace; this isn't a race. You can run, or walk. Or skip, if you prefer. You can &lt;a href="https://rmoff.net/files/current-london-2026-5k.gpx" rel="noopener noreferrer"&gt;download the route&lt;/a&gt; to follow at your leisure. In fact, it's a good idea to do so because this is not an organised event, so it's your responsibility not to get lost :-D&lt;/p&gt;

&lt;p&gt;If you Strava, feel free to join the &lt;a href="https://www.strava.com/clubs/1039355" rel="noopener noreferrer"&gt;Strava Club&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Cool, where do I register?
&lt;/h1&gt;

&lt;p&gt;Register at &lt;code&gt;/dev/null&lt;/code&gt; ;-)&lt;/p&gt;

&lt;p&gt;This is an entirely unofficial, barely-organised, thing. Turn up and run, if you want. Don't, if you don't. Fully intend to but sleep in from the jetlag and miss it, that's completely fine 😁&lt;/p&gt;

</description>
      <category>5krunwalk</category>
      <category>current</category>
      <category>kafkasummit</category>
      <category>running</category>
    </item>
    <item>
      <title>It's the Smell</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Tue, 05 May 2026 10:40:51 +0000</pubDate>
      <link>https://dev.to/rmoff/its-the-smell-5aff</link>
      <guid>https://dev.to/rmoff/its-the-smell-5aff</guid>
      <description>&lt;p&gt;It's now a joy to simply read &lt;em&gt;any&lt;/em&gt; blog post that's not AI-generated.&lt;/p&gt;

&lt;p&gt;If you actually have a good thing to write about, for the love of your readers and the good of the internet, please just write about it. Claude might shit out a completed article but it'll stink. Just like boomer art, readers are recognising this crap at a hundred yards and running away from it. It's horrible to read, it makes me feel gross just opening it, and it's going straight in &lt;code&gt;/dev/null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhy2p2e9mf81njbkn1hca.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhy2p2e9mf81njbkn1hca.gif" alt="Morpheus from The Matrix grimacing with caption " width="480" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use AI to &lt;em&gt;refine&lt;/em&gt; what you want to say, but I (and I'm sure many others) would rather read something imperfect-but-real than a pristine-but-anodyne article that's in the same style as every other bloody post these days.&lt;/p&gt;

&lt;p&gt;See also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://rmoff.net/2025/11/25/ai-smells-on-medium/" rel="noopener noreferrer"&gt;AI smells on medium&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://rmoff.net/ai" rel="noopener noreferrer"&gt;How I use AI on this blog&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rant</category>
      <category>ai</category>
    </item>
    <item>
      <title>Interesting links - April 2026</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Thu, 30 Apr 2026 14:21:40 +0000</pubDate>
      <link>https://dev.to/rmoff/interesting-links-april-2026-1blm</link>
      <guid>https://dev.to/rmoff/interesting-links-april-2026-1blm</guid>
      <description>&lt;p&gt;A bit of a streamlined edition, this month. Lots of interesting links still, but less commentary. You can put that down to me prevaricating on getting my &lt;a href="https://rmoff.net/2026/04/28/materialized-tables-in-apache-flink/" rel="noopener noreferrer"&gt;previous blog about Materialized Tables in Apache Flink&lt;/a&gt; finished, and leaving myself little time to work on this one :) Not including the detailed narration actually knocks a bunch of time off the preparation---I'd be interested in your feedback as to how much the absence of narration impacts (if at all) your enjoyment of reading it. Let me know in the comments below!&lt;/p&gt;

&lt;p&gt;Something that I'm slowly changing is how I categorise links to do with AI. A few months back anything "AI" got its own section. It wasn't much more than a novelty really; certainly not something worth distracting the regular link sections with. But now AI is just part-and-parcel of many people's workflows, a regular component in their toolbox. So where an article is about &lt;em&gt;credibly&lt;/em&gt; using AI as part of an existing topic (such as data engineering), I'll file it in that section. &lt;em&gt;(And if this news makes you cross because you abhor anything AI, well, &lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;I've got news for you&lt;/a&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Current London 2026 - wanna free ticket? 🎟️
&lt;/h1&gt;

&lt;p&gt;If you're in the UK and interested in Kafka, Flink, Iceberg, etc etc (which, since you're reading this blog post, I assume you at least have a &lt;em&gt;passing interest&lt;/em&gt; in) then you might be interested in &lt;a href="https://current.confluent.io/london" rel="noopener noreferrer"&gt;Current London&lt;/a&gt; in May---and I have a free ticket code for you to use! Register with code &lt;code&gt;L-CMP-LDNKafka&lt;/code&gt; and it's all yours :)&lt;/p&gt;

&lt;p&gt;Unresolved directive in &amp;lt;stdin&amp;gt; - include::../../asciidoc-includes/il-header.adoc[]&lt;/p&gt;

&lt;h1&gt;
  
  
  Analytics
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ben Sykes - &lt;a href="https://netflixtechblog.com/stop-answering-the-same-question-twice-interval-aware-caching-for-druid-at-netflix-scale-22fadc9b840e" rel="noopener noreferrer"&gt;Interval-Aware Caching for Druid at Netflix Scale&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dorothée Clerc - &lt;a href="https://medium.com/blablacar/beyond-the-dashboard-how-blablacar-pms-use-ai-to-self-serve-data-95ccd33ab1f9" rel="noopener noreferrer"&gt;How BlaBlaCar PMs use AI to self-serve data&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://duckdb.org/2026/04/13/announcing-duckdb-152" rel="noopener noreferrer"&gt;DuckDB 1.5.2 has been released&lt;/a&gt;, with support for DuckLake 1.0, even better Iceberg support, and fixes as a result of &lt;a href="https://github.com/duckdb/duckdb-jepsen" rel="noopener noreferrer"&gt;initial Jepsen testing&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Randy Au - &lt;a href="https://www.counting-stuff.com/dashboard-rot-as-org-attention-grave-markers/" rel="noopener noreferrer"&gt;Dashboard rot as org attention grave markers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ahmed Youssef - &lt;a href="https://blog.dataengineerthings.org/nobody-is-making-decisions-with-your-dashboards-06849015f28b" rel="noopener noreferrer"&gt;Nobody Is Making Decisions With Your Dashboards&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Torsten Grust has published a course about the &lt;a href="https://duckdb.org/library/design-and-implementation-of-duckdb-internals/" rel="noopener noreferrer"&gt;Design and Implementation of DuckDB Internals&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hamel Husain - &lt;a href="https://hamel.dev/blog/posts/revenge/" rel="noopener noreferrer"&gt;The Revenge of the Data Scientist&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Platforms, Architectures, and Modelling
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Antonia Badarau and team at Monzo - &lt;a href="https://monzo.com/blog/a-meshy-approach-to-data" rel="noopener noreferrer"&gt;A "meshy" approach to Data: Enabling 100+ teams to build Data Models&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Justina Bartulevičienė &amp;amp; Benediktas Kazanavičius (Vinted) - &lt;a href="https://vinted.engineering/2026/04/22/personalized-search-autocomplete/" rel="noopener noreferrer"&gt;Serving Personalised Search Autocomplete&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rishabh Kumar (Airbnb) - &lt;a href="https://medium.com/airbnb-engineering/building-a-fault-tolerant-metrics-storage-system-at-airbnb-26a01a6e7017" rel="noopener noreferrer"&gt;Building a fault-tolerant metrics storage system at Airbnb&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Matt Lawhon and team at Pinterest - &lt;a href="https://medium.com/pinterest-engineering/scaling-recommendation-systems-with-request-level-deduplication-93bd514142d9" rel="noopener noreferrer"&gt;Scaling Recommendation Systems with Request-Level Deduplication&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Facundo Agriel (Dropbox) - &lt;a href="https://dropbox.tech/infrastructure/improving-storage-efficiency-in-magic-pocket-our-immutable-blob-store" rel="noopener noreferrer"&gt;Improving storage efficiency in Magic Pocket, our immutable blob store&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 A couple of interesting posts from the teams at Notion: &lt;a href="https://www.notion.com/blog/enabling-multi-region-data-systems-at-notion" rel="noopener noreferrer"&gt;Enabling Multi-Region Data Systems&lt;/a&gt;, and &lt;a href="https://www.notion.com/blog/two-years-of-vector-search-at-notion" rel="noopener noreferrer"&gt;Two years of vector search: 10x scale, 1/10th cost&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nikola Ilic - &lt;a href="https://towardsdatascience.com/data-modeling-for-analytics-engineers-the-complete-primer/" rel="noopener noreferrer"&gt;Data Modeling for Analytics Engineers: The Complete Primer&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chris Gambill - &lt;a href="https://gambilldataengineering.substack.com/p/the-medallion-masterclass-why-knowing" rel="noopener noreferrer"&gt;The Medallion Masterclass: Why Knowing the Colors Isn't Enough&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Joe Reis - &lt;a href="https://practicaldatamodeling.substack.com/p/chapter-10-why-time-matters-in-data" rel="noopener noreferrer"&gt;Why Time Matters in Data Modeling&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Engineering, Pipelines, and CDC
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Alexander Goida - &lt;a href="https://medium.com/binary-notes/three-kafka-s3-sink-settings-for-easier-file-processing-419eab09c903" rel="noopener noreferrer"&gt;Three Kafka S3 Sink Settings for Easier File Processing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chris Gambill - &lt;a href="https://gambilldataengineering.substack.com/p/ai-agents-are-failing-your-data-engineers" rel="noopener noreferrer"&gt;AI Agents are Failing Your Data Engineers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sugat Mahanti (Zapier) - &lt;a href="https://zapier.com/blog/lessons-from-using-outbox-pattern-at-scale/" rel="noopener noreferrer"&gt;Lessons from using the outbox pattern at scale&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Couple of good posts from Chris Hillman - &lt;a href="https://ghostinthedata.info/posts/2026/2026-04-25-cost-management/" rel="noopener noreferrer"&gt;Your Data Platform Costs More Than It Should&lt;/a&gt;, and &lt;a href="https://ghostinthedata.info/posts/2026/2026-04-18-pipeline-optimization/" rel="noopener noreferrer"&gt;Why Your Pipeline Finishes Later Every Month&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Jin-won Park (Karrot) - &lt;a href="https://translate.kagi.com/freedium-mirror.cfd/https://medium.com/daangn/%EB%AA%A8%EB%91%90%EA%B0%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-ai-%EC%8B%9C%EB%8C%80-%EC%A7%80%EB%82%9C-1%EB%85%84%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%80%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EC%9D%84%EA%B9%8C-67bdb8fd91d4#bypass" rel="noopener noreferrer"&gt;In the AI era where everyone handles data, how has the data team changed over the past year?&lt;/a&gt; (&lt;a href="https://medium.com/daangn/%EB%AA%A8%EB%91%90%EA%B0%80-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-ai-%EC%8B%9C%EB%8C%80-%EC%A7%80%EB%82%9C-1%EB%85%84%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%80%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EC%9D%84%EA%B9%8C-67bdb8fd91d4#bypass" rel="noopener noreferrer"&gt;original Korean&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tristan Handy (dbt Labs) - &lt;a href="https://roundup.getdbt.com/p/five-things-i-believe-about-the-future" rel="noopener noreferrer"&gt;Five things I believe about the future of analytics&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Igor Shurmin (Riskified Tech) - &lt;a href="https://medium.com/riskified-technology/data-exploration-for-software-engineers-evaluating-and-integrating-external-datasets-da81d3e67b4d" rel="noopener noreferrer"&gt;Data Exploration for Software Engineers: Evaluating and Integrating External Datasets&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Aleksandr Klein (Just Eat Takeaway) - &lt;a href="https://medium.com/justeattakeaway-tech/daedalus-and-the-data-labyrinth-2c166b1d9866" rel="noopener noreferrer"&gt;Daedalus and the Data Labyrinth&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 An excellent deep-dive from George Zefkilis, looking at &lt;a href="https://georgioszefkilis.substack.com/p/building-a-cdc-pipeline-part-1-postgresql" rel="noopener noreferrer"&gt;PostgreSQL WAL Internals&lt;/a&gt; in the context of building a CDC pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://debezium.io/blog/2026/04/23/debezium-3-6-alpha1-released/" rel="noopener noreferrer"&gt;Debezium 3.6.0.Alpha1&lt;/a&gt; and &lt;a href="https://debezium.io/blog/2026/03/31/debezium-3-5-final-released/" rel="noopener noreferrer"&gt;Debezium 3.5.0.Final&lt;/a&gt; have been released.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yaroslav Tkachenko analysed the performance of different technologies for &lt;a href="https://thenewstack.io/postgres-iceberg-cdc-benchmarks/" rel="noopener noreferrer"&gt;getting data from Postgres into Iceberg.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Leonard Xu looks at good practices when building &lt;a href="https://www.linkedin.com/pulse/one-job-10000-tables-production-practices-large-scale-leonard-xu-1crjc/" rel="noopener noreferrer"&gt;Large-Scale Lake Ingestion with Flink CDC and Paimon&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Real-world details from Nathan Smit of how they've been using Debezium with Oracle for four years, and how they addressed issues with &lt;a href="https://debezium.io/blog/2026/04/20/oracle-cdc-replication-lag/" rel="noopener noreferrer"&gt;Oracle CDC Replication Lag&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Yanquan Lv published the announcement of &lt;a href="https://flink.apache.org/2026/03/30/apache-flink-cdc-3.6.0-release-announcement/" rel="noopener noreferrer"&gt;the release of Apache Flink CDC 3.6.0&lt;/a&gt; as well as an excellent &lt;a href="https://www.linkedin.com/pulse/deep-dive-apache-flink-cdc-360-yanquan-lv-a5nwc/" rel="noopener noreferrer"&gt;Deep Dive into Apache Flink CDC 3.6.0&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Jason Ganz &amp;amp; Benoit Perigaud (dbt Labs) - &lt;a href="https://docs.getdbt.com/blog/semantic-layer-vs-text-to-sql-2026" rel="noopener noreferrer"&gt;Semantic Layer vs. Text-to-SQL: 2026 Benchmark Update&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Kafka and Event Streaming
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Zapier - &lt;a href="https://zapier.com/blog/reducing-kafka-connections-sidecar/" rel="noopener noreferrer"&gt;Reducing Kafka connections by 10x with a sidecar pattern&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yunhong Zheng - &lt;a href="https://fluss.apache.org/blog/column-pruning-streaming-storage/" rel="noopener noreferrer"&gt;How Apache Fluss Achieves True Pruning in Streaming Storage&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bibek Maharjan - &lt;a href="https://ssrn.com/abstract=6422258" rel="noopener noreferrer"&gt;AI-Driven Autonomous Optimization of Apache Kafka on AWS MSK for High-Volume Financial Systems&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Piotr Minkowski - &lt;a href="https://piotrminkowski.com/2026/03/27/deep-dive-into-kafka-offset-commit-with-spring-boot/" rel="noopener noreferrer"&gt;Deep Dive into Kafka Offset Commit with Spring Boot&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://community.cncf.io/events/details/cncf-virtual-project-events-hosted-by-cncf-presents-strimzicon-2026/" rel="noopener noreferrer"&gt;StrimziCon 2026&lt;/a&gt; is on 3rd June, and &lt;a href="https://strimzi.io/blog/2026/04/17/strimzicon2026-schedule/" rel="noopener noreferrer"&gt;the schedule has been published&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Flink
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Robin Moffatt (that's me!) - &lt;a href="https://rmoff.net/2026/04/28/materialized-tables-in-apache-flink/" rel="noopener noreferrer"&gt;Materialized Tables in Apache Flink&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yaroslav Tkachenko - &lt;a href="https://streamacademy.io/tutorial/flink-kafka-consumer-offsets-with-the-state-processor-api/" rel="noopener noreferrer"&gt;Apache Flink: Reading and Modifying Kafka Consumer Offsets Using the State Processor API&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lee Seung-min / Choi Won-yong - &lt;a href="https://toss-tech.translate.goog/article/flink-realtime-frequency-capping?_x_tr_sl=auto&amp;amp;_x_tr_tl=en&amp;amp;_x_tr_hl=en-US&amp;amp;_x_tr_pto=wapp&amp;amp;_x_tr_hist=true" rel="noopener noreferrer"&gt;Extending Real-time Ad Frequency Capping Aggregation to One Week with Apache Flink + RocksDB Tuning&lt;/a&gt; (&lt;a href="https://toss.tech/article/flink-realtime-frequency-capping" rel="noopener noreferrer"&gt;original&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Katya Gorshkova - &lt;a href="https://medium.com/@katyagorshkova/hands-on-with-flink-part-5-managing-state-918923237589" rel="noopener noreferrer"&gt;Hands-On with Flink --- Part 5: Managing State&lt;/a&gt; (previously: &lt;a href="https://medium.com/@katyagorshkova/hands-on-with-flink-part-1-filtering-data-from-kafka-7bd3754f0233" rel="noopener noreferrer"&gt;1&lt;/a&gt;, &lt;a href="https://medium.com/@katyagorshkova/hands-on-with-flink-part-2-running-on-kubernetes-with-the-operator-cb8fc9737f5b" rel="noopener noreferrer"&gt;2&lt;/a&gt;, &lt;a href="https://medium.com/@katyagorshkova/hands-on-with-flink-part-3-sql-instead-of-java-0a5d698e39be" rel="noopener noreferrer"&gt;3&lt;/a&gt;, &lt;a href="https://medium.com/@katyagorshkova/hands-on-with-flink-part-4-sql-and-avro-36eb8a97f543" rel="noopener noreferrer"&gt;4&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Viktor Gamov digs out the open source toolbox to use Kafka, Flink, Iceberg, Superset and more to build &lt;a href="https://gamov.io/posts/streaming-lakehouse/" rel="noopener noreferrer"&gt;Building a Streaming Lakehouse&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Open Table Formats (OTF), Catalogs, Lakehouses etc.
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Gunnar Morling's &lt;a href="https://hardwood.dev/" rel="noopener noreferrer"&gt;Hardwood&lt;/a&gt; project has had &lt;a href="https://www.morling.dev/blog/variant-support-interactive-parquet-file-tui-hardwood-1.0.0.beta2-is-out/" rel="noopener noreferrer"&gt;its second beta release&lt;/a&gt;, which includes &lt;a href="https://asciinema.org/a/992284" rel="noopener noreferrer"&gt;a very cool TUI&lt;/a&gt; for working with Parquet files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Laurent Saint-Félix has written &lt;a href="https://github.com/Anaethelion/aq" rel="noopener noreferrer"&gt;aq&lt;/a&gt; - "&lt;em&gt;query and transform Parquet, Arrow IPC, CSV, and NDJSON files using jq-style expressions&lt;/em&gt;."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yusuf Gözübüyük (TOM Tech) - &lt;a href="https://medium.com/tom-tech/the-performance-improvement-journey-in-apache-iceberg-tables-438eccbe136d" rel="noopener noreferrer"&gt;The Performance Improvement Journey in Apache Iceberg Tables&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ved Prakash - &lt;a href="https://medium.com/snowflake/deep-dive-into-apache-iceberg-architecture-the-three-layers-that-power-your-lakehouse-83c03403e503" rel="noopener noreferrer"&gt;Deep Dive into Apache Iceberg Architecture&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 CMU-DB tech talk - Kurt Westerfeld &amp;amp; Mark Cusack - &lt;a href="https://www.youtube.com/watch?v=Kq3csHJqgJQ" rel="noopener noreferrer"&gt;Floe: A SQL Compute Service for the Data Lakehouse&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apache Iceberg has moved to "adopt" on the &lt;a href="https://www.thoughtworks.com/radar/languages-and-frameworks/apache-iceberg" rel="noopener noreferrer"&gt;latest Technology Radar from Thoughtworks&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Qiegang Long - &lt;a href="https://qlong.github.io/posts/2026-03-30-variant-early-results/" rel="noopener noreferrer"&gt;Preliminary Notes on Open-Source Variant Performance&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Steve Loughran - &lt;a href="https://steveloughran.github.io/benchmarking-variants/benchmarking-variants.html" rel="noopener noreferrer"&gt;Benchmarking Parquet Variants through Iceberg&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Anahita Singla (Picnic) - &lt;a href="https://blog.picnic.nl/leveraging-contextual-data-in-real-time-analytics-with-apache-iceberg-1873586e6730" rel="noopener noreferrer"&gt;Leveraging contextual data in real-time analytics with Apache Iceberg&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ducklake.select/2026/04/13/ducklake-10/" rel="noopener noreferrer"&gt;DuckLake version 1.0&lt;/a&gt; has been released, and thus is now deemed production-ready. AFAIK it's only got real support within DuckDB, but do let me know if you see it supported elsewhere. Thoughtworks have marked it as &lt;a href="https://www.thoughtworks.com/radar/platforms/ducklake" rel="noopener noreferrer"&gt;"assess" on their Tech Radar&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A nice hands-on guide for setting up a local playground with &lt;a href="https://creechy.wordpress.com/2026/04/01/local-iceberg-using-minio-and-gravitino/" rel="noopener noreferrer"&gt;Iceberg using Minio and Gravitino&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pedro Holanda describes how DuckLake deals with the small-files problem (often encountered when one starts streaming data to these types of table format). Using &lt;a href="https://ducklake.select/2026/04/02/data-inlining-in-ducklake/" rel="noopener noreferrer"&gt;Data Inlining in DuckLake&lt;/a&gt;, they saw vast performance improvements over the same kind of processing done with Iceberg.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  RDBMS
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Ohad Ravid - &lt;a href="https://ohadravid.github.io/posts/2026-04-query-plans/" rel="noopener noreferrer"&gt;The Best (Query) Plans of Mice and Men&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Radim Marek - &lt;a href="https://boringsql.com/posts/postgresql-mvcc-byte-by-byte/" rel="noopener noreferrer"&gt;PostgreSQL MVCC, Byte by Byte&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simeon Griggs - &lt;a href="https://planetscale.com/blog/keeping-a-postgres-queue-healthy" rel="noopener noreferrer"&gt;Keeping a Postgres queue healthy&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Thomas Kejser - &lt;a href="https://www.database-doctor.com/posts/joins-are-not-expensive" rel="noopener noreferrer"&gt;Joins are NOT Expensive!&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mike Freedman - &lt;a href="https://x.com/michaelfreedman/status/2034291102346326416" rel="noopener noreferrer"&gt;Introducing TigerFS - a filesystem backed by PostgreSQL, and a filesystem interface to PostgreSQL&lt;/a&gt; (Renato Losio wrote &lt;a href="https://www.infoq.com/news/2026/04/tigerfs-postgresql-filesystem/" rel="noopener noreferrer"&gt;an InfoQ article about it&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nikita Volkov - &lt;a href="https://nikita-volkov.github.io/pgenie-in-production-part-1/" rel="noopener noreferrer"&gt;My 14-Year Journey Away from ORMs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.dbpro.app/blog/chess-in-pure-sql" rel="noopener noreferrer"&gt;Chess in Pure SQL&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  General Data Stuff
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Almog Gavra - &lt;a href="https://www.bitsxpages.com/p/the-broken-economics-of-databases" rel="noopener noreferrer"&gt;The Broken Economics of Databases&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kirill Bobrov - &lt;a href="https://luminousmen.com/post/the-power-of-data-sketches-a-comprehensive-guide/" rel="noopener noreferrer"&gt;The Power of Data Sketches: A Comprehensive Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Gergely Orosz (a.k.a. The Pragmatic Engineer) &lt;a href="https://newsletter.pragmaticengineer.com/p/designing-data-intensive-applications" rel="noopener noreferrer"&gt;interviews Martin Kleppmann&lt;/a&gt; about the second edition of Designing Data-intensive Applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Animesh Kumar - &lt;a href="https://medium.com/@community_md101/ai-ready-data-vs-analytics-ready-data-f67ef0804341" rel="noopener noreferrer"&gt;AI-Ready Data vs. Analytics-Ready Data&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I'm slightly fascinated by the idea of &lt;a href="https://ggsql.org/" rel="noopener noreferrer"&gt;ggsql&lt;/a&gt;, which brings SQL to the world of &lt;a href="https://ggplot2.tidyverse.org/" rel="noopener noreferrer"&gt;ggplot2&lt;/a&gt; and the Grammar of Graphics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Akshat Vig &amp;amp; Andrew Davidson (MongoDB) - &lt;a href="https://www.infoq.com/presentations/MongoDB-evolution/" rel="noopener noreferrer"&gt;Open Source, Community, and Consequence: The Story of MongoDB&lt;/a&gt; (InfoQ London 2026)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AI
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I warned you &lt;a href="https://rmoff.net/2025/09/30/interesting-links-september-2025/#_ai" rel="noopener noreferrer"&gt;previously&lt;/a&gt;...this AI stuff is here to stay, and it'd be short-sighted to think otherwise.&lt;/em&gt; &lt;em&gt;As I read and learn more about it, I'm going to share interesting links (the clue is in the blog post title) that I find---whilst trying to avoid the breathless hype and slop.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Joe Reis - &lt;a href="https://joereis.substack.com/p/were-in-1905-why-electricity-not" rel="noopener noreferrer"&gt;Why Electricity (Not Dot-Com) Is the Right AI Analogy&lt;/a&gt;. I like this idea from Joe. It also makes me think of the lift-and-shift that folk did with on-premises workloads to VMs in the Cloud, instead of re-architecting properly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Jason Ganz - &lt;a href="https://roundup.getdbt.com/p/a-dispatch-from-the-jagged-frontier" rel="noopener noreferrer"&gt;A Dispatch from the Jagged Frontier of Analytics Engineering&lt;/a&gt; (referencing Ethan Mollick's &lt;a href="https://www.oneusefulthing.org/p/centaurs-and-cyborgs-on-the-jagged" rel="noopener noreferrer"&gt;jagged frontier&lt;/a&gt; article from 2023).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Industry legends Mark Russinovich and Scott Hanselman wrote this opinion piece for ACM: &lt;a href="https://dl.acm.org/doi/10.1145/3779312" rel="noopener noreferrer"&gt;Redefining the Software Engineering Profession for AI&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Without the hiring of early-in-career developers, the profession's talent pipeline will collapse, and organizations will face a future without the next generation of experienced engineers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Elena Verna - &lt;a href="https://www.elenaverna.com/p/confessions-of-a-millennial-in-tech" rel="noopener noreferrer"&gt;Confessions of a Millennial in Tech&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🎥 Vik Gamov - &lt;a href="https://www.youtube.com/watch?v=ZNHqavB01UI&amp;amp;feature=youtu.be" rel="noopener noreferrer"&gt;If Memento was about AI Agents&lt;/a&gt;. &lt;em&gt;I watched Memento in preparation for this...I still have no idea what was going on in either&lt;/em&gt; 😆&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Addy Osmani - &lt;a href="https://addyosmani.com/blog/agent-harness-engineering/" rel="noopener noreferrer"&gt;Agent Harness Engineering&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Hamel Husain - &lt;a href="https://hamel.dev/blog/posts/evals-faq/" rel="noopener noreferrer"&gt;LLM Evals: Everything You Need to Know&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Robin Moffatt - &lt;a href="https://rmoff.net/2026/04/09/kicking-the-tyres-on-harbor-for-agent-evals/" rel="noopener noreferrer"&gt;Kicking the Tyres on Harbor for Agent Evals&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Bryan Cantrill - &lt;a href="https://bcantrill.dtrace.org/2026/04/12/the-peril-of-laziness-lost/" rel="noopener noreferrer"&gt;The peril of laziness lost&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adam Jacob - &lt;a href="https://adamhjk.com/blog/laziness-impatience-hubris/" rel="noopener noreferrer"&gt;Laziness, Impatience, and Hubris&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alex Woods - &lt;a href="https://alexhwoods.com/dont-let-ai-write-for-you/" rel="noopener noreferrer"&gt;Don't Let AI Write For You&lt;/a&gt;. &lt;em&gt;(Reminder: I &lt;a href="https://rmoff.net/ai" rel="noopener noreferrer"&gt;disclose my use of AI&lt;/a&gt;, and it's NEVER for writing!)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  And finally...
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Nothing to do with data, but stuff that I've found interesting or has made me smile.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Mitchell Hashimoto - &lt;a href="https://mitchellh.com/writing/ghostty-leaving-github" rel="noopener noreferrer"&gt;Ghostty Is Leaving GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I love the agility with which one can collaboratively work in GDocs, but I also prefer working with plain text and Markdown (or even better, Asciidoc). &lt;a href="https://github.com/inanimate-tech/mist" rel="noopener noreferrer"&gt;mist&lt;/a&gt; brings the concept of GDocs collaboration to Markdown files. It's pretty neat, and it's &lt;a href="https://interconnected.org/home/2026/04/10/open-mist" rel="noopener noreferrer"&gt;now open source&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A useful reminder from Christian Hofstede-Kuhn of &lt;a href="https://blog.hofstede.it/shell-tricks-that-actually-make-life-easier-and-save-your-sanity/" rel="noopener noreferrer"&gt;Shell Tricks That Actually Make Life Easier (And Save Your Sanity)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Watch/Listen
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 A very cool example from the demo-scene: &lt;a href="https://www.youtube.com/watch?v=2AnbYNudAyM" rel="noopener noreferrer"&gt;Razor1911&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Internet Archive isn't just about finding webpages that have gone offline---it also hosts tons of media, like this recording of &lt;a href="https://archive.org/details/ajc00795_nirvana-1989-07-08" rel="noopener noreferrer"&gt;Nirvana Live at Dreamerz 1989-07-08&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I love this idea: &lt;a href="https://www.trainjazz.com/" rel="noopener noreferrer"&gt;TrainJazz: Every train, a note&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nerd
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;😸 Not all specification drafts published are serious. &lt;a href="https://www.ietf.org/archive/id/draft-meow-mrrp-00.html" rel="noopener noreferrer"&gt;Meow&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The ways in which one can play Doom continue to increase, with &lt;a href="https://github.com/xsawyerx/curl-doom" rel="noopener noreferrer"&gt;DOOM, played over cURL&lt;/a&gt;, and &lt;a href="https://blog.rice.is/post/doom-over-dns/" rel="noopener noreferrer"&gt;Can it Resolve DOOM? Game Engine in 2,000 DNS Records&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HackerNews members share their memories of &lt;a href="https://news.ycombinator.com/item?id=47579221" rel="noopener noreferrer"&gt;What was it like in the era of BBS before the internet?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My own memories are around Acorn-based BBSes. My favourite was &lt;a href="https://web.archive.org/web/19970419170850/http://arcade.demon.co.uk/about.html" rel="noopener noreferrer"&gt;Arcade BBS&lt;/a&gt;. Ah, memories. Fidonet, filebases...good times :)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What's more important than the code that you're writing Claude's writing for you? Getting it in the right font of course! Shave many a yak and waste plenty of time at &lt;a href="https://www.codingfont.com/" rel="noopener noreferrer"&gt;Codingfont&lt;/a&gt; picking &lt;em&gt;just the right font&lt;/em&gt;...&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unresolved directive in &amp;lt;stdin&amp;gt; - include::../../asciidoc-includes/il-footer.adoc[]&lt;/p&gt;

</description>
      <category>interestinglinks</category>
    </item>
    <item>
      <title>Materialized Tables in Apache Flink</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Tue, 28 Apr 2026 16:53:48 +0000</pubDate>
      <link>https://dev.to/rmoff/materialized-tables-in-apache-flink-b6d</link>
      <guid>https://dev.to/rmoff/materialized-tables-in-apache-flink-b6d</guid>
      <description>&lt;p&gt;Flink added support for what it calls &lt;em&gt;Materialized Tables&lt;/em&gt; in &lt;a href="https://flink.apache.org/2024/08/02/announcing-the-release-of-apache-flink-1.20/#introduce-materialized-tables" rel="noopener noreferrer"&gt;1.20&lt;/a&gt;, released in 2024. You can read about the design and motivations in &lt;a href="https://cwiki.apache.org/confluence/display/FLINK/FLIP-435%3A+Introduce+a+New+Materialized+Table+for+Simplifying+Data+Pipelines" rel="noopener noreferrer"&gt;FLIP-435&lt;/a&gt;. In a nutshell, Materialized Tables provide a way to &lt;em&gt;include&lt;/em&gt; the SQL to populate and refresh a table &lt;em&gt;as part of&lt;/em&gt; its definition.&lt;/p&gt;

&lt;p&gt;Let’s take a look!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
In this blog post I’m using Flink 2.2 for the examples; some things have been added since the initial 1.20 release.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Materialized Table basics
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
Materialized Tables are a new type of object in Flink and &lt;a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=294816797#FLIP435:IntroduceaNewMaterializedTableforSimplifyingDataPipelines-GatewayRestAPI" rel="noopener noreferrer"&gt;require&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A &lt;a href="https://rmoff.net/2024/02/16/catalogs-in-flink-sqla-primer/" rel="noopener noreferrer"&gt;Catalog&lt;/a&gt; that supports Materialized Tables&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A scheduler (if you want automated refreshes)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- The Flink SQL Gateway provides a basic one for test purposes
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;test-filesystem&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
In this blog post I’m using the &lt;code&gt;test-filesystem&lt;/code&gt; catalog that is, as the name implies, for testing only. It was added to Flink to support testing of Materialized Tables, and is currently the only option other than Apache Paimon for demonstrating all the capabilities.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;test-filesystem&lt;/code&gt; catalog also includes support for storing tables. Both catalog metadata and table data are written to disk.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;Since I’m using a Docker-based stack, I’m going to launch the SQL Client like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; jobmanager bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"./bin/sql-gateway.sh start &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="s2"&gt;
    sleep 2 &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    ./bin/sql-client.sh gateway --endpoint http://localhost:8083"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Start up the SQL Gateway&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use the SQL Gateway from the SQL Client&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First up we’ll create the catalog and set it as active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'execution.checkpointing.savepoint-dir'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'file:///shared/savepoints'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="n"&gt;mt_cat&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test-filesystem'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/shared/catalog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'default-database'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mydb'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="n"&gt;mt_cat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our source table for the Materialized Table is going to be a table reading data from Kafka:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'connector'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'kafka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'properties.bootstrap.servers'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'properties.group.id'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'flink-orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'scan.startup.mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'earliest-offset'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with some data in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;order_id&lt;/span&gt;   &lt;span class="n"&gt;total_gbp&lt;/span&gt;
           &lt;span class="mi"&gt;2&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt;
           &lt;span class="mi"&gt;3&lt;/span&gt;          &lt;span class="mi"&gt;10&lt;/span&gt;
           &lt;span class="mi"&gt;4&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt;
           &lt;span class="mi"&gt;1&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt;
           &lt;span class="mi"&gt;5&lt;/span&gt;          &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And what about the total of these orders? This is where it starts to get funky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="n"&gt;EXPR&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="err"&gt;█&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Notice the cursor, and the absence of a &lt;code&gt;Flink SQL&amp;gt;&lt;/code&gt; prompt?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This query is doing two things that you might not recognise from the RDBMS world:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We see the &lt;em&gt;changelog&lt;/em&gt;; as each record is read, the &lt;code&gt;SUM()&lt;/code&gt; is restated (&lt;code&gt;+U&lt;/code&gt;), with the previous value being replaced (&lt;code&gt;-U&lt;/code&gt;). The final record (&lt;code&gt;35&lt;/code&gt;) is the value we’d expect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The query doesn’t complete; it keeps on running. That’s because the source is &lt;em&gt;unbounded&lt;/em&gt;, and the values shown are just as of the data that has been read so far.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now let’s create a Materialized Table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;ENFORCED&lt;/span&gt; 
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'connector'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'upsert-kafka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders-total-mt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'properties.bootstrap.servers'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'key.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'value.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I’m specifying a primary key because of the connector that I’m using (&lt;code&gt;'upsert-kafka' tables require to define a PRIMARY KEY constraint. The PRIMARY KEY specifies which columns should be read from or write to the Kafka message key. The PRIMARY KEY also defines records in the 'upsert-kafka' table should update or delete on which keys.&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configuration for the &lt;code&gt;upsert-kafka&lt;/code&gt; connector here to persist the table’s data to Kafka.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Because of the primary key (see &amp;lt;1&amp;gt;), this needs to be non-nullable (&lt;code&gt;Could not create a PRIMARY KEY with nullable column 'total_gbp'. A PRIMARY KEY column must be declared on non-nullable physical columns.&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And did it work? Have we got data in it?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;span class="err"&gt;█&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;This is the latest value&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;These two cancel out&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Well…yes. Again with that changelog business, but still giving us the correct answer.&lt;/p&gt;

&lt;p&gt;So far, this Materialized Table is nothing extraordinary. In fact, it’s doing pretty much the same as the &lt;code&gt;CTAS&lt;/code&gt; we’ll see below and with which you may already be familiar; creating a table that stores its data in Kafka, and setting a query running to populate it.&lt;/p&gt;

&lt;p&gt;You can see the query running as a job, just as you would with a regular &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;CTAS&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhwvobnakxgxhpbs10ae.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhwvobnakxgxhpbs10ae.webp" alt="Flink Web UI showing the Materialized Table continuous refresh job running" width="800" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before we get much more into Materialized Tables, let’s do a quick run through of the alternatives in Flink SQL for defining a table and populating it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Without Materialized Tables: It’s kinda DDL, and sorta DML
&lt;/h1&gt;

&lt;p&gt;Materialized Tables move in the direction of solving the sometimes-awkward situation in Flink SQL that a table is part definition (data types, columns, etc) and part job (how to populate it), with the job needing managing and maintaining like any other code.&lt;/p&gt;

&lt;p&gt;Let’s create the &lt;code&gt;orders_total&lt;/code&gt; table like we saw in batch above, using the first pattern (&lt;code&gt;CREATE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;em&gt;As before, the syntax is a little bit more fiddly because of the underlying persistence&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;ENFORCED&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'connector'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'upsert-kafka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders-total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'properties.bootstrap.servers'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'key.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'value.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;em&gt;just&lt;/em&gt; the &lt;code&gt;CREATE&lt;/code&gt; statement; we’ve not populated it yet.&lt;/p&gt;

&lt;p&gt;Now to get data into the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Submitting&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;been&lt;/span&gt; &lt;span class="n"&gt;successfully&lt;/span&gt; &lt;span class="n"&gt;submitted&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Job&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;f666e58599217ad00feab629f8a5a9c&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Check it out ma, we’ve got a Job ID!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn’t look like your regular RDBMS &lt;code&gt;INSERT&lt;/code&gt;, which will return (sometimes after a while) once it’s done. This returned straight away, but with chat about submitting statements to a cluster. But what’s going on with this "job"? We can see it directly in Flink SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;DESCRIBE&lt;/span&gt; &lt;span class="n"&gt;JOB&lt;/span&gt; &lt;span class="s1"&gt;'8f666e58599217ad00feab629f8a5a9c'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+-----------------------------------+---------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                          &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+-----------------------------------+---------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;f666e58599217ad00feab629f8a5a9c&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;into_cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mydb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;RUNNING&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="n"&gt;T10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;054&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+-----------------------------------+---------+-------------------------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as well as in the Flink Web UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrr0g6ft2kc3d43d2omo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrr0g6ft2kc3d43d2omo.webp" alt="Flink Web UI showing one running job: INSERT INTO orders_total" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And with the job running, there’s data loaded to the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other than &lt;code&gt;CREATE TABLE&lt;/code&gt;/&lt;code&gt;INSERT&lt;/code&gt;, the other non-Materialized Table route is to use &lt;code&gt;CREATE TABLE…AS SELECT&lt;/code&gt; (&lt;code&gt;CTAS&lt;/code&gt;). Using the &lt;code&gt;CTAS&lt;/code&gt; approach, we get a similar behaviour; a populated table, and a job running on the Flink cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_ctas&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;ENFORCED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'connector'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'upsert-kafka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders-total'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'properties.bootstrap.servers'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'key.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'value.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INFO] Submitting SQL update statement to the cluster...
[INFO] SQL update statement has been successfully submitted to the cluster:
Job ID: dfebb27cd20653d93616945966244c66
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;DESCRIBE&lt;/span&gt; &lt;span class="n"&gt;JOB&lt;/span&gt; &lt;span class="s1"&gt;'dfebb27cd20653d93616945966244c66'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+--------------------------------------------+---------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                   &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+--------------------------------------------+---------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;dfebb27cd20653d93616945966244c66&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;into_mt_cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mydb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_total_ctas&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;RUNNING&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;T09&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;895&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+--------------------------------------------+---------+-------------------------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we’ve got a total of three jobs, all doing the same thing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxibdxayoc924d9wp0xf1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxibdxayoc924d9wp0xf1.webp" alt="Flink Web UI showing three running jobs: CTAS" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I insert a record to the source table &lt;code&gt;simple_orders&lt;/code&gt;…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…the three running jobs update their respective tables, and I see the expected result on them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'sql-client.execution.result-mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'table'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;total_gbp&lt;/span&gt;
          &lt;span class="mi"&gt;42&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;total_gbp&lt;/span&gt;
          &lt;span class="mi"&gt;42&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_ctas&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;total_gbp&lt;/span&gt;
          &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This means I just see the latest calculated value, not the changelog as shown above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So that’s the three options: &lt;code&gt;CREATE MATERIALIZED TABLE&lt;/code&gt;, &lt;code&gt;CREATE TABLE&lt;/code&gt;/&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;CREATE TABLE…AS SELECT&lt;/code&gt;. Let’s now look at what makes Materialized Tables of interest to us.&lt;/p&gt;

&lt;h1&gt;
  
  
  Lifecycle Management
&lt;/h1&gt;

&lt;p&gt;With &lt;code&gt;CREATE TABLE&lt;/code&gt; plus &lt;code&gt;INSERT&lt;/code&gt; (hereafter noted as &lt;code&gt;CT/I&lt;/code&gt;), the query to populate the table is a separate entity from the definition. Even with the combined &lt;code&gt;CREATE TABLE … AS SELECT&lt;/code&gt; (&lt;code&gt;CTAS&lt;/code&gt;), it’s just a marriage of convenience; in the background a separate &lt;code&gt;INSERT&lt;/code&gt; is fired off, never to be associated with its &lt;code&gt;CTAS&lt;/code&gt; parent again.&lt;/p&gt;

&lt;p&gt;Since we’re using the &lt;code&gt;test-filesystem&lt;/code&gt; catalog we can easily inspect what’s in the catalog just by looking at what’s on disk. Here’s the catalog entry for both the &lt;code&gt;orders_total&lt;/code&gt; (&lt;code&gt;CT/I&lt;/code&gt;) and &lt;code&gt;orders_total_ctas&lt;/code&gt; (&lt;code&gt;CTAS&lt;/code&gt;)—it’s identical for both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tableKind"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TABLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"catalogTableInfo"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties.bootstrap.servers"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"broker:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"connector"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upsert-kafka"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.0.data-type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INT NOT NULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value.format"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.primary-key.name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PK_total_gbp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.primary-key.columns"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total_gbp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"key.format"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders-total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.0.name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total_gbp"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contrast this to the Materialized Table, in which the DDL and DML are intertwined and the DML query literally persisted as part of the table definition, along with the query’s execution details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tableKind"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MATERIALIZED_TABLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"catalogTableInfo"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.0.data-type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INT NOT NULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"definition-query"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; 
        &lt;/span&gt;&lt;span class="s2"&gt;"SELECT COALESCE(SUM(`simple_orders`.`total_gbp`), 0) AS `total_gbp`&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;FROM `mt_cat`.`mydb`.`simple_orders` AS `simple_orders`"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"refresh-status"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTIVATED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"refresh-handler-bytes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rO0ABXNyADdvcmcuYXBhY2hlLmZsaW5rLnRhYmxlLnJlZnJlc2guQ29udGludW91c1JlZnJlc2hIYW5kbGVyAAAAAAAAAAECAARMAAljbHVzdGVySWR0ABJMamF2YS9sYW5nL1N0cmluZztMAA9leGVjdXRpb25UYXJnZXRxAH4AAUwABWpvYklkcQB+AAFMAAtyZXN0b3JlUGF0aHEAfgABeHB0ABNTdGFuZGFsb25lQ2x1c3RlcklkdAAGcmVtb3RldAAgNmFlNjBjZGU5N2VlMmMxZGU2MGVjNDhjMWZmZWY1NjFw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"freshness-interval"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"refresh-mode"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONTINUOUS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"properties.bootstrap.servers"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"broker:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"connector"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upsert-kafka"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value.format"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"freshness-unit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MINUTE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"refresh-handler-desc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; executionTarget=remote,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; clusterId=StandaloneClusterId,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; jobId=6ae60cde97ee2c1de60ec48c1ffef561&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.primary-key.name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PK_total_gbp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.primary-key.columns"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total_gbp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"key.format"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders-total-mt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"logical-refresh-mode"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUTOMATIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.0.name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total_gbp"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;These fields all hold information about the refresh query itself, not just the standard table DDL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a simple example of the benefits, consider lifecycle management of the query.&lt;/p&gt;

&lt;p&gt;Here are all three table population statements running:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxibdxayoc924d9wp0xf1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxibdxayoc924d9wp0xf1.webp" alt="Flink Web UI showing three running jobs: CTAS" width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What happens if we restart the task managers?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms8ru00ctwn1xkx0uuwk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms8ru00ctwn1xkx0uuwk.webp" alt="Flink Web UI after task manager restart" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ooops. Only one left, for the Materialized Table. The two &lt;code&gt;INSERT&lt;/code&gt; statements (one directly from &lt;code&gt;INSERT&lt;/code&gt;, the other a product of the &lt;code&gt;CTAS&lt;/code&gt;) got killed, and not restarted.&lt;/p&gt;

&lt;p&gt;Why would they get restarted? They’re just &lt;code&gt;INSERT&lt;/code&gt; statements, with no lifecycle guarantees attached to them. Contrast that to the Materialized Table, which includes this persistence of the definition of how it is populated, and thus Flink can resurrect the query.&lt;/p&gt;

&lt;h1&gt;
  
  
  Schema evolution
&lt;/h1&gt;

&lt;p&gt;What if we want to add a new column to the table being populated? In this example, that could be a count of the total orders (to go with the sum of the orders which is what we’ve already got).&lt;/p&gt;

&lt;p&gt;With both &lt;code&gt;CT/I&lt;/code&gt; and &lt;code&gt;CTAS&lt;/code&gt; it gets messy. I’ve dropped and recreated both (since they got killed in the restart above), so their &lt;code&gt;INSERT&lt;/code&gt; statements are running again now (as, presumably, they would be by default when we want to evolve the schema). Let’s see how it works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;order_ct&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;Execute&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;total_gbp&lt;/span&gt;    &lt;span class="n"&gt;order_ct&lt;/span&gt;
          &lt;span class="mi"&gt;42&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No value for &lt;code&gt;order_ct&lt;/code&gt;. Remember; the &lt;code&gt;INSERT&lt;/code&gt; from before is still running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this only populates the existing &lt;code&gt;total_gbp&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;Let’s kill the existing &lt;code&gt;INSERT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+--------------------------------------+----------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                             &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+--------------------------------------+----------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="n"&gt;af3aa38e4815ca3faa04be0fe51a51&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;into_mt_cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mydb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;RUNNING&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;T10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;990&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;STOP&lt;/span&gt; &lt;span class="n"&gt;JOB&lt;/span&gt; &lt;span class="s1"&gt;'12af3aa38e4815ca3faa04be0fe51a51'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now to run a new one, including the additional field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidationException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;Column&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;result&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s1"&gt;'mt_cat.mydb.orders_total'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Incompatible&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="s1"&gt;'order_ct'&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EXPR&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EXPR&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;Sink&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_ct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uh oh, schema mismatch. This is fiddly, right? Try again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="n"&gt;order_ct&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;Execute&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;order_ct&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;Execute&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Submitting&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;been&lt;/span&gt; &lt;span class="n"&gt;successfully&lt;/span&gt; &lt;span class="n"&gt;submitted&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Job&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;c197789f2b4966e97ed8d4002feb775&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new &lt;code&gt;INSERT&lt;/code&gt; runs, but it turns out that the table’s still not happy when it comes to querying it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;operators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnforcerException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;Column&lt;/span&gt; &lt;span class="s1"&gt;'order_ct'&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;however&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;being&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="s1"&gt;'table.exec.sink.not-null-enforcer'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'DROP'&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;suppress&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="n"&gt;such&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="n"&gt;silently&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
The reason is almost by-the-by here, but for completeness: the table is backed by a Kafka topic, and when we read from the table the first records fetched will be those populated by the initial &lt;code&gt;INSERT&lt;/code&gt; with just one column in. (When we &lt;code&gt;DROP&lt;/code&gt; the table it doesn’t touch the topic from which the table reads/writes). Hence, when we try to read those back, as the error message says, it can’t cope with null values for a &lt;code&gt;NOT NULL&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;Yes there’s a workaround, but this is not the happy path that it could be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are similar troubles with &lt;code&gt;CTAS&lt;/code&gt;—the process (and problems) are exactly the same as I just demonstrated for &lt;code&gt;CT&lt;/code&gt;/&lt;code&gt;I&lt;/code&gt; above. As a bonus we get just a little bit more ambiguity because of the &lt;em&gt;coupled-but-not&lt;/em&gt; issue with the &lt;code&gt;INSERT&lt;/code&gt; query that it spawns (as in, &lt;code&gt;CTAS&lt;/code&gt; includes the definition, but subsequently it’s just an &lt;code&gt;INSERT&lt;/code&gt; running on its own, for us to care for and water).&lt;/p&gt;

&lt;p&gt;Compare all this to life with Materialized Tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;
                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_ct&lt;/span&gt;
                    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;Execute&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;total_gbp&lt;/span&gt;             &lt;span class="n"&gt;order_ct&lt;/span&gt;
          &lt;span class="mi"&gt;42&lt;/span&gt;                    &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No errors, no drama.&lt;/p&gt;

&lt;p&gt;In the background, Flink stopped the first job, and started up a new one to include the new column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-----------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                    &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-----------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;ae60cde97ee2c1de60ec48c1ffef561&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`orders_total_mt`&lt;/span&gt;&lt;span class="n"&gt;_continuous_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;T10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;647&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a7a77aa3f909b6debb25c25d5d3c5de0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;RUNNING&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`orders_total_mt`&lt;/span&gt;&lt;span class="n"&gt;_continuous_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;T11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;07&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;887&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-----------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/statements/#as-select_statement-1" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; for full details of what’s supported in schema evolution, as well as caveats. Significantly, the docs do note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The new refresh job starts from the beginning and does not restore from the previous state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The new refresh job will not restore from the state of the original refresh job.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Pause and Resume execution
&lt;/h1&gt;

&lt;p&gt;With our Materialized Table we can suspend its execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt; &lt;span class="n"&gt;SUSPEND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, if we add some data to the source table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll see that these new records aren’t reflected in the Materialized Table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_ct&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;total_gbp&lt;/span&gt;             &lt;span class="n"&gt;order_ct&lt;/span&gt;
          &lt;span class="mi"&gt;52&lt;/span&gt;                    &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;total_gbp&lt;/span&gt;             &lt;span class="n"&gt;order_ct&lt;/span&gt;
          &lt;span class="mi"&gt;42&lt;/span&gt;                    &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then &lt;code&gt;RESUME&lt;/code&gt; the Materialized Table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt; &lt;span class="n"&gt;RESUME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the Materialized Table shows the updated data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;total_gbp&lt;/span&gt;             &lt;span class="n"&gt;order_ct&lt;/span&gt;
          &lt;span class="mi"&gt;52&lt;/span&gt;                    &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key thing here is that when we &lt;code&gt;SUSPEND&lt;/code&gt; the Materialized Table Flink takes a savepoint. This is much easier than stopping a job (as we’d do with &lt;code&gt;CT/I&lt;/code&gt; / &lt;code&gt;CTAS&lt;/code&gt;) and having to start a new one from the start, since Flink will use the savepoint to start its processing from where it got to before it was suspended.&lt;/p&gt;

&lt;p&gt;This very clean, SQL-driven, way of managing a table and its job could come in handy in several ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In the event of upstream data quality issues, rather than continuing to propagate suspect data—but without pulling the parachute cord of killing the job and losing its state—you can just suspend it whilst you investigate the problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Server maintenance, Flink upgrades, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If there are availability problems with the table’s source (remember, Flink tables can use connectors against numerous systems, such as RDBMS, flat files, etc etc) you can suspend reading from it rather than dealing with a flood of errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Batch, Streaming, or Both?
&lt;/h1&gt;

&lt;p&gt;One of &lt;a href="https://www.alibabacloud.com/blog/flink-materialized-table-building-unified-stream-and-batch-etl_602184" rel="noopener noreferrer"&gt;the pitches for Materialized Tables in Flink&lt;/a&gt; is that it 'unifies' batch and streaming, and dispenses with the Lambda/Kappa concept. By having one SQL concept (Materialized Tables) with different configuration options you can define your data manipulation logic &lt;em&gt;once&lt;/em&gt; and execute it as you want in both streaming or batch. This is defined by a parameter called &lt;code&gt;REFRESH_MODE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the example above the Materialized Table was using an effective &lt;code&gt;REFRESH_MODE&lt;/code&gt; of &lt;code&gt;CONTINUOUS&lt;/code&gt;, i.e. a streaming query, per the Flink job that we saw running.&lt;/p&gt;

&lt;p&gt;To explore the other &lt;code&gt;REFRESH_MODE&lt;/code&gt;, which is &lt;code&gt;FULL&lt;/code&gt;, we need a source connector that supports &lt;em&gt;bounded&lt;/em&gt; reads. You can find a list of connectors within the Flink project &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/connectors/table/overview/#supported-connectors" rel="noopener noreferrer"&gt;here&lt;/a&gt; which includes annotation as to what kind of source mode they support. Here we’ll stick with &lt;code&gt;test-filesystem&lt;/code&gt; just for convenience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_file&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders_file&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_file&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;order_id&lt;/span&gt;   &lt;span class="n"&gt;total_gbp&lt;/span&gt;
           &lt;span class="mi"&gt;1&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt;
           &lt;span class="mi"&gt;2&lt;/span&gt;          &lt;span class="mi"&gt;10&lt;/span&gt;
           &lt;span class="mi"&gt;3&lt;/span&gt;          &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now for our Materialized Table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt_file_full&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="n"&gt;FRESHNESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt; 
&lt;span class="n"&gt;REFRESH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FULL&lt;/span&gt; 
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_file&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We’re using the &lt;code&gt;test-filesystem&lt;/code&gt; catalog to store the data for this table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We’ll talk about &lt;code&gt;FRESHNESS&lt;/code&gt; in the next section&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;FULL&lt;/code&gt; refresh mode&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;SELECT&lt;/code&gt; works…but there’s no data in the table 😿&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'sql-client.execution.result-mode'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'tableau'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;Execute&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt_file_full&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike &lt;code&gt;CONTINUOUS&lt;/code&gt; refresh, which runs as a job on the task manager, &lt;code&gt;FULL&lt;/code&gt; refresh is managed on a scheduler that’s provided by the SQL Gateway. When we create the Materialized Table we see it added an entry in the scheduler on SQL Gateway:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-04-27 10:33:06,828 INFO  org.apache.flink.table.gateway.workflow.scheduler.EmbeddedQuartzScheduler []
Create quartz schedule job for materialized table `mt_cat`.`mydb`.`orders_total_mt_file_full` successfully,
job info: default_group.quartz_job_`mt_cat`.`mydb`.`orders_total_mt_file_full`,
cron expression: 0 0/5 * * * ? *.                                                                                                                              │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Check out the cron expression—that’s driven by the &lt;code&gt;FRESHNESS = INTERVAL '5' MINUTE&lt;/code&gt; configuration.&lt;/p&gt;

&lt;p&gt;We can sit tight and wait, or we can manually kick a refresh off ourselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt_file_full&lt;/span&gt; &lt;span class="n"&gt;REFRESH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;cluster&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2215&lt;/span&gt;&lt;span class="n"&gt;db412d0ef0551ae0704bebb978bd&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                            &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2215&lt;/span&gt;&lt;span class="n"&gt;db412d0ef0551ae0704bebb978bd&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`orders_total_mt_file_full`&lt;/span&gt;&lt;span class="n"&gt;_one_time_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="n"&gt;T11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;863&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we have data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt_file_full&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+-------------+&lt;/span&gt;
&lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Note how it’s a single value, rather than a changelog series of &lt;code&gt;+I&lt;/code&gt;, &lt;code&gt;-U&lt;/code&gt;, &lt;code&gt;+U&lt;/code&gt; rows like we saw with the Kafka sink above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over in the Flink Web UI we can see the periodic refreshes of the table successfully executing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi83iykfqo2rqx2l5frr5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi83iykfqo2rqx2l5frr5.webp" alt="Flink Web UI Completed Jobs showing periodic refresh jobs finishing successfully" width="800" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing that’s important to note about the refresh jobs is the SQL that gets run:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO  org.apache.flink.table.gateway.service.materializedtable.MaterializedTableManager [] -
Begin to refreshing the materialized table
`mt_cat`.`mydb`.`orders_total_mt_file_full`, statement:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`orders_total_mt_file_full`&lt;/span&gt; 
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`orders_file`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`total_gbp`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`total_gbp`&lt;/span&gt;
                    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`orders_file`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`orders_file`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;INSERT OVERWRITE&lt;/code&gt; &lt;em&gt;replaces&lt;/em&gt; the contents of the target table (or partition; see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Batch to Kafka? Nope.
&lt;/h2&gt;

&lt;p&gt;In the earlier section when we created a Materialized Table reading and writing from Kafka topics, we didn’t set a &lt;code&gt;REFRESH_MODE&lt;/code&gt;. What happened was that Flink set an implicit value for it, which we can see if we examine the DDL that Flink reads back for the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`orders_total_mt`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="nv"&gt;`PK_total_gbp`&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`total_gbp`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;ENFORCED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'connector'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'upsert-kafka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'key.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'properties.bootstrap.servers'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders-total-mt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'value.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FRESHNESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'3'&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt; 
&lt;span class="n"&gt;REFRESH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONTINUOUS&lt;/span&gt; 
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`simple_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`total_gbp`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`total_gbp`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`order_ct`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`simple_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`simple_orders`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;These weren’t in the submitted SQL; Flink added these.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where it gets a bit funky though, and also exposes some of the slightly rough edges on the current implementation of Materialized Tables. What if we decide we’d like to set the Materialized Table to refresh on a batch schedule instead?&lt;/p&gt;

&lt;p&gt;Here’s what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt__full&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;ENFORCED&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'connector'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'upsert-kafka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'topic'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders_total_mt__full'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'properties.bootstrap.servers'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'broker:9092'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'key.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'value.format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;FRESHNESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'5'&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;
            &lt;span class="n"&gt;REFRESH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FULL&lt;/span&gt; 
            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_gbp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_gbp&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;simple_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;Execute&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Use batch refresh for the Materialized Table reading and writing from Kafka.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flink tells me it’s succeeded. But has it actually?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqebwvk9flbsdeonkjjcx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqebwvk9flbsdeonkjjcx.gif" alt="The " width="220" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The table’s there, sure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;-----------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;-----------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt__full&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders_total_mt__full&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shaded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnknownTopicOrPartitionException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;host&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The target Kafka topic hasn’t been populated (or even created). Something went wrong!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So whilst the &lt;em&gt;Materialized Table&lt;/em&gt; created successfully, it looks like it’s not got any data in it. Let’s take a look at the SQL Gateway log. As if on cue, at the next 5-minute interval since the epoch, the job kicks off:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;org.apache.flink.table.gateway.service.utils.SqlExecutionException: Refreshing the materialized table `mt_cat`.`mydb`.`orders_total_mt__full` occur exception.
Caused by: org.apache.flink.table.api.ValidationException:
Querying an unbounded table 'mt_cat.mydb.simple_orders' 
in batch mode is not allowed. The table source is unbounded. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;It fails per the reason on the final line there—the source for the table is a Flink table reading from a Kafka topic, and since Kafka topics are unbounded, Flink won’t do a batch read from it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Logically, this makes sense; but some kind of "canary test" of the Materialized Table’s refresh query might be a useful thing to add to the &lt;code&gt;CREATE&lt;/code&gt; process, instead of the silent failure and log-diving necessary to surface it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Refresh frequency
&lt;/h1&gt;

&lt;p&gt;I’ve already trailed some of the details about the frequency with which Materialized Tables can be set to refresh above. It’s configured by the &lt;code&gt;FRESHNESS&lt;/code&gt; setting, which per &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/overview/#data-freshness" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; defines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data freshness defines the maximum amount of time that the materialized table’s content should lag behind updates to the base tables.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For tables using &lt;code&gt;FULL&lt;/code&gt; refresh mode, the refresh query is run on a &lt;strong&gt;schedule&lt;/strong&gt; determined by the &lt;code&gt;FRESHNESS&lt;/code&gt; value that you set, translated by Flink into a cron schedule. By default, the schedule is implemented on the built-in scheduler on the SQL Gateway. It’s worth noting that if it takes a long time to execute, the actual freshness may be noticeably longer than that specified.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For &lt;code&gt;CONTINUOUS&lt;/code&gt; refresh, the &lt;code&gt;FRESHNESS&lt;/code&gt; defines how often the table’s refresh job will &lt;strong&gt;checkpoint&lt;/strong&gt; the data. This is important for connectors (e.g. Iceberg—although perhaps a bad example as Iceberg doesn’t yet support Materialized Tables) that rely on a checkpoint to flush data to cause it to be readable.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One interesting sidenote for &lt;code&gt;FULL&lt;/code&gt; refresh mode is that since the schedule uses a cron expression, there are restrictions on what the value for &lt;code&gt;FRESHNESS&lt;/code&gt; can be. &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/statements/#freshness" rel="noopener noreferrer"&gt;The docs&lt;/a&gt; say it must be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a common divisor of the respective time interval unit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So if you’re using an &lt;code&gt;HOUR&lt;/code&gt; interval, the value must be divisible into 24, for &lt;code&gt;MINUTE&lt;/code&gt; into 60, etc. If you get it wrong, Flink will tell you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidationException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;In&lt;/span&gt; &lt;span class="k"&gt;full&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="k"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;only&lt;/span&gt; &lt;span class="n"&gt;freshness&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="k"&gt;are&lt;/span&gt; &lt;span class="n"&gt;factors&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;are&lt;/span&gt; &lt;span class="n"&gt;currently&lt;/span&gt;
&lt;span class="n"&gt;supported&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;MINUTE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defaulting on the defaults
&lt;/h2&gt;

&lt;p&gt;When I introduced the first Materialized Table query at the top of this article, I didn’t specify &lt;code&gt;REFRESH_MODE&lt;/code&gt; or &lt;code&gt;FRESHNESS&lt;/code&gt;. I’m all for minimum-required defaults enabling users to get up and running with software, but here there is potential for some serious ambiguity. I’ve gone through the &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/statements/#freshness" rel="noopener noreferrer"&gt;docs&lt;/a&gt; and code to try and distil down how Flink will handle the non-provision of both or either of these settings.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you &lt;strong&gt;do&lt;/strong&gt; specify &lt;code&gt;REFRESH_MODE&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;FRESHNESS&lt;/code&gt; then for &lt;code&gt;FRESHNESS&lt;/code&gt; Flink uses the values of system configuration option relating to the refresh mode you’ve chosen:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;+--------------+---------------------+---------------------------------------------------+&lt;br&gt;
  | REFRESH_MODE | &lt;code&gt;FRESHNESS&lt;/code&gt; default | Configuration                                     |&lt;br&gt;
  +==============+=====================+===================================================+&lt;br&gt;
  | &lt;code&gt;CONTINUOUS&lt;/code&gt; | 3 minutes           | &lt;code&gt;materialized-table.default-freshness.continuous&lt;/code&gt; |&lt;br&gt;
  +--------------+---------------------+---------------------------------------------------+&lt;br&gt;
  | &lt;code&gt;FULL&lt;/code&gt;       | 1 hour              | &lt;code&gt;materialized-table.default-freshness.full&lt;/code&gt;       |&lt;br&gt;
  +--------------+---------------------+---------------------------------------------------+&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you &lt;strong&gt;do&lt;/strong&gt; specify &lt;code&gt;FRESHNESS&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;REFRESH_MODE&lt;/code&gt; then Flink determines &lt;code&gt;REFRESH_MODE&lt;/code&gt; by comparing &lt;code&gt;FRESHNESS&lt;/code&gt; to the configuration &lt;code&gt;materialized-table.refresh-mode.freshness-threshold&lt;/code&gt; (which has a default of 30 minutes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your value of &lt;code&gt;FRESHNESS&lt;/code&gt; is less than the &lt;code&gt;freshness-threshold&lt;/code&gt; Flink will use &lt;code&gt;CONTINUOUS&lt;/code&gt;, otherwise it’ll use &lt;code&gt;FULL&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
If you want to use a different &lt;code&gt;REFRESH_MODE&lt;/code&gt; than this logic sets, then just configure &lt;code&gt;REFRESH_MODE&lt;/code&gt; directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;And finally, what happens if you don’t specify a &lt;code&gt;REFRESH_MODE&lt;/code&gt; or &lt;code&gt;FRESHNESS&lt;/code&gt; at all? This is where it gets less clear :)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don’t specify a &lt;code&gt;REFRESH_MODE&lt;/code&gt; &lt;strong&gt;or&lt;/strong&gt; &lt;code&gt;FRESHNESS&lt;/code&gt; then Flink &lt;a href="https://github.com/apache/flink/blob/master/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/utils/MaterializedTableUtils.java#L236" rel="noopener noreferrer"&gt;will default&lt;/a&gt; to an internal value of &lt;a href="https://nightlies.apache.org/flink/flink-docs-stable/api/java/org/apache/flink/table/catalog/CatalogMaterializedTable.LogicalRefreshMode.html#AUTOMATIC" rel="noopener noreferrer"&gt;&lt;code&gt;AUTOMATIC&lt;/code&gt;&lt;/a&gt; for the &lt;code&gt;REFRESH_MODE&lt;/code&gt;. You can actually see that if you look closely at the catalog entry that I quoted above, which includes &lt;code&gt;"logical-refresh-mode" : "AUTOMATIC"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Flink then defaults the &lt;code&gt;FRESHNESS&lt;/code&gt; to the value of &lt;code&gt;materialized-table.default-freshness.continuous&lt;/code&gt; (default: 3 minutes):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/apache/flink/blob/b101fa35ebfc80cdb0b74b6075c3adfd6291188d/flink-table/flink-table-common/src/main/java/org/apache/flink/table/catalog/DefaultMaterializedTableEnricher.java#L89-L94" rel="noopener noreferrer"&gt;DefaultMaterializedTableEnricher.java#L89-L94&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// User omitted freshness, choose default based on logical mode&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogicalRefreshMode&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;LogicalRefreshMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FULL&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;finalFreshness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultFullFreshness&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// For AUTOMATIC or CONTINUOUS modes, use the continuous default&lt;/span&gt;
    &lt;span class="n"&gt;finalFreshness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultContinuousFreshness&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, now that it has a value for &lt;code&gt;FRESHNESS&lt;/code&gt;, it follows the logic above using &lt;code&gt;materialized-table.refresh-mode.freshness-threshold&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My tl;dr from this&lt;/strong&gt; is that you don’t want to leave these values to the defaults for some poor sod who ends up maintaining your DDL having to try and pick through. Be deliberate and explicit in how you configure your Materialized Table &lt;code&gt;REFRESH_MODE&lt;/code&gt;, that way the person operating the pipeline will have a clear handle at least on the &lt;code&gt;FRESHNESS&lt;/code&gt; that will be used—and indeed, you may not want to hardcode freshness for each Materialized Table and manage it at a system level.&lt;/p&gt;

&lt;h1&gt;
  
  
  Partitions
&lt;/h1&gt;

&lt;p&gt;Let’s now look at the idea of partition-specific updates and refreshes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
At the moment there’s limited support for Materialized Tables in the ecosystem beyond Flink. For example, whilst it would make tons of sense in Iceberg (particularly with the partitioning stuff I’m about to show you), it’s not been implemented yet.&lt;/p&gt;

&lt;p&gt;Currently (April 2026) only Paimon supports partitioned Materialized Tables (along with a &lt;code&gt;test-filesystem&lt;/code&gt; catalog/connector).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the examples above I took a set of transactions and aggregated them up to one single total. What is much more common in the real world is aggregating by one or more dimensions. These dimensions could be things like date, customer, product, business unit, etc. Sometimes the volume of data can dictate that physically partitioning this data by a dimension at the storage layer will help performance and manageability.&lt;/p&gt;

&lt;p&gt;For this example I’m going to use a base transaction table that includes customer and date dimensions, and then aggregate it up by date which I’ll also partition by.&lt;/p&gt;

&lt;p&gt;Here’s the source table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt;    &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_date&lt;/span&gt;  &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_name&lt;/span&gt;   &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with some data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;INSERT INTO user_orders &lt;span class="o"&gt;(&lt;/span&gt;order_id, order_date, user_id, user_name, amount&lt;span class="o"&gt;)&lt;/span&gt;
VALUES &lt;span class="o"&gt;(&lt;/span&gt;1001, DATE &lt;span class="s1"&gt;'2026-04-18'&lt;/span&gt;, 1, &lt;span class="s1"&gt;'alice'&lt;/span&gt;, 1500&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1002, DATE &lt;span class="s1"&gt;'2026-04-18'&lt;/span&gt;, 2, &lt;span class="s1"&gt;'bob'&lt;/span&gt;, 2300&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1003, DATE &lt;span class="s1"&gt;'2026-04-18'&lt;/span&gt;, 3, &lt;span class="s1"&gt;'carol'&lt;/span&gt;, 800&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1004, DATE &lt;span class="s1"&gt;'2026-04-19'&lt;/span&gt;, 1, &lt;span class="s1"&gt;'alice'&lt;/span&gt;, 1200&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1005, DATE &lt;span class="s1"&gt;'2026-04-19'&lt;/span&gt;, 2, &lt;span class="s1"&gt;'bob'&lt;/span&gt;, 3100&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1006, DATE &lt;span class="s1"&gt;'2026-04-19'&lt;/span&gt;, 3, &lt;span class="s1"&gt;'carol'&lt;/span&gt;, 1900&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1007, DATE &lt;span class="s1"&gt;'2026-04-20'&lt;/span&gt;, 1, &lt;span class="s1"&gt;'alice'&lt;/span&gt;, 500&lt;span class="o"&gt;)&lt;/span&gt;,
        &lt;span class="o"&gt;(&lt;/span&gt;1008, DATE &lt;span class="s1"&gt;'2026-04-20'&lt;/span&gt;, 4, &lt;span class="s1"&gt;'dave'&lt;/span&gt;, 4200&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now to create the Materialized Table (using the &lt;code&gt;test-filesystem&lt;/code&gt; connector)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;
    &lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;REFRESH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FULL&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;   &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;    &lt;span class="n"&gt;user_orders&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Ooooh partitions!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh noes! But wait…what did we learn above, both about &lt;code&gt;REFRESH_MODE = FULL&lt;/code&gt; and the absence of &lt;code&gt;FRESHNESS&lt;/code&gt;? With &lt;code&gt;FULL&lt;/code&gt; the loading of data is done on a schedule, and without a value for &lt;code&gt;FRESHNESS&lt;/code&gt; Flink will default to &lt;code&gt;materialized-table.default-freshness.full&lt;/code&gt;, which is 1 hour.&lt;/p&gt;

&lt;p&gt;Given that we created the table at 16:56 (as we can see from its schedule being created on the SQL Gateway)…&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16:56:16,282 INFO  org.apache.flink.table.gateway.workflow.scheduler.EmbeddedQuartzScheduler []
Create quartz schedule job for materialized table `mt_cat`.`mydb`.`daily_order_summary` successfully, job info:
default_group.quartz_job_`mt_cat`.`mydb`.`daily_order_summary`, cron expression: 0 0 0/1 * * ? *.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;…it means we should see the job run in the next few minutes…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ywizdz5gbf9lboabidr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ywizdz5gbf9lboabidr.gif" alt="Cartoon of Daffy Duck sitting at a kitchen table" width="463" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and there it goes!&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17:00:00,016 INFO  org.apache.flink.table.gateway.workflow.scheduler.EmbeddedQuartzScheduler [] -
                    Execute refresh operation for workflow: WorkflowInfo{materializedTableIdentifier='`mt_cat`.`mydb`.`daily_order_summary`', dynamicOptions={}, initConfig={execution.checkpointing.savepoint-dir=file:///shared/savepoints}, executionConfig={}, restEndpointUrl='http://0.0.0.0:8083'}.
17:00:00,039 INFO  org.apache.flink.table.gateway.service.materializedtable.MaterializedTableManager [] -
                    Begin to refreshing the materialized table `mt_cat`.`mydb`.`daily_order_summary`,
                    statement: INSERT OVERWRITE `mt_cat`.`mydb`.`daily_order_summary`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                      &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;261181049&lt;/span&gt;&lt;span class="n"&gt;fca90b15786b09e383eae18&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`daily_order_summary`&lt;/span&gt;&lt;span class="n"&gt;_periodic_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="n"&gt;T17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;370&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But, alas!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DateTimeParseException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nb"&gt;Text&lt;/span&gt; &lt;span class="s1"&gt;'20561'&lt;/span&gt; &lt;span class="n"&gt;could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltrje2gwf462d08ev7tq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fltrje2gwf462d08ev7tq.gif" alt="Frank TJ Mackey " width="498" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the covers Flink is &lt;strong&gt;writing&lt;/strong&gt; the date as an epoch day (&lt;code&gt;20561&lt;/code&gt; being the epoch day for &lt;code&gt;2026-04-18&lt;/code&gt;), whilst &lt;strong&gt;reading&lt;/strong&gt; it in a different way and failing. Let’s try again, and be explicit in how to store it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;
    &lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'partition.fields.order_date.date-formatter'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yyyy-MM-dd'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="n"&gt;REFRESH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;FULL&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;   &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;    &lt;span class="n"&gt;user_orders&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Specify the format for the date when reading and writing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Needs to be written as a &lt;code&gt;STRING&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
Specifying a &lt;em&gt;date&lt;/em&gt; as a &lt;em&gt;string&lt;/em&gt; gives me strong ick vibes. A date is a date, not a string. Date-based partitioning is cool, but ending up with a &lt;code&gt;STRING&lt;/code&gt; in the target Materialized Table feels off to me (now your end users have to figure out how to handle it, what format it is, etc).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So now our Materialized Table is recreated, let’s force a load of it instead of waiting for the next &lt;code&gt;:00&lt;/code&gt;/&lt;code&gt;:30&lt;/code&gt; to roll around on the clock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt; &lt;span class="n"&gt;REFRESH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;cluster&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0490&lt;/span&gt;&lt;span class="n"&gt;ff33c9a8a530592b8451f7da43df&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                      &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0490&lt;/span&gt;&lt;span class="n"&gt;ff33c9a8a530592b8451f7da43df&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`daily_order_summary`&lt;/span&gt;&lt;span class="n"&gt;_one_time_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="n"&gt;T17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;799&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+--------------+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+--------------+-------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4700&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4600&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;6200&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+------------+--------------+-------------+&lt;/span&gt;
&lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Woo, we have data!&lt;/p&gt;

&lt;p&gt;We can see it on disk too, laid out in partitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tree flink-shared/catalog/mydb/
flink-shared/catalog/mydb/
├── daily_order_summary
│&amp;nbsp;&amp;nbsp; ├── data
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── &lt;span class="nv"&gt;order_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-04-18
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── part-b86894d8-b96f-4add-857e-9a29121e45f9-task-0-file-0
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── &lt;span class="nv"&gt;order_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-04-19
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── part-b86894d8-b96f-4add-857e-9a29121e45f9-task-0-file-1
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── &lt;span class="nv"&gt;order_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-04-20
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp;     └── part-b86894d8-b96f-4add-857e-9a29121e45f9-task-0-file-2
│&amp;nbsp;&amp;nbsp; └── schema
│&amp;nbsp;&amp;nbsp;     └── daily_order_summary_schema.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since it’s in partitions, we can take advantage of the functionality in Materialized Tables to &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/statements/#refresh" rel="noopener noreferrer"&gt;only refresh a specific partition&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt; &lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-20'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;cluster&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f7b979f004cbf88d574bb07509ed57e8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+--------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                       &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+--------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f7b979f004cbf88d574bb07509ed57e8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`daily_order_summary`&lt;/span&gt;&lt;span class="n"&gt;_one_time_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="n"&gt;T09&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How do we know it only refreshed the data for a single partition (&lt;code&gt;2026-04-20&lt;/code&gt;)? In the SQL Gateway logs we can see the SQL used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`daily_order_summary`&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`user_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_date`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`order_date`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`user_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`amount`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`total_amount`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`order_count`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`user_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`user_orders`&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;`user_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_date`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-20'&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Predicate on &lt;code&gt;order_date&lt;/code&gt; as expected, matching the &lt;code&gt;[…] REFRESH PARTITION (order_date='2026-04-20')&lt;/code&gt; statement that we ran&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we can do partition-specific refreshes—pretty useful for things like recomputing once missing data has arrived for a date.&lt;/p&gt;

&lt;p&gt;The other thing that partitioned Materialized Tables gives us though is this: when a regular scheduled refresh of the Materialized Table runs, it &lt;strong&gt;only&lt;/strong&gt; refreshes the &lt;em&gt;current&lt;/em&gt; partition. So given that today is 28 Apr 2026, if we inspect the SQL for the most recent scheduled refresh of the partitioned Materialized Table we see this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="n"&gt;OVERWRITE&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`daily_order_summary`&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`user_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_date`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`order_date`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`user_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`amount`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`total_amount`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`order_count`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`user_orders`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`user_orders`&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;`user_orders`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_date`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-28'&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;That’s today’s date!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a much more efficient way of loading data if you’re going to use a batch process. Instead of pulling &lt;em&gt;all&lt;/em&gt; the data from the source and recomputing it &lt;em&gt;all&lt;/em&gt;, you just pick the data for the latest partition. Now, there are a few considerations to bear in mind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;You need to have set your partition date carefully, based on the business logic of your data. In our simplistic example we’re just capturing the date an order was placed. What if the source table includes things like order status, and our aggregate table also needs to show the total value of fulfilled orders? If an order is placed yesterday but fulfilled today, we still need to pick that change up—which we won’t if we &lt;em&gt;only&lt;/em&gt; refresh the partition of the date on which the order was placed. In this situation you’d perhaps end up adding a new field such as &lt;code&gt;date_updated&lt;/code&gt; and partitioning on that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It’s still a batch-based process. In the example of orders changing and you wanting to reflect the latest state in your aggregate table, you’d probably be using &lt;code&gt;CONTINUOUS&lt;/code&gt; mode and this batch-based partitioning refresh would be irrelevant.&lt;/p&gt;

&lt;p&gt;It’s not explicitly stated in &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/statements/#partitioned-by" rel="noopener noreferrer"&gt;the docs&lt;/a&gt;, but so far as I can tell since you (1) must specify &lt;code&gt;date-formatter&lt;/code&gt; (as we saw above) and (2) &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/docs/dev/table/materialized-table/statements/#with-options" rel="noopener noreferrer"&gt;the docs state that&lt;/a&gt; &lt;code&gt;date-formatter&lt;/code&gt; only works in &lt;code&gt;FULL&lt;/code&gt; refresh mode, partitioned Materialized Tables won’t work for streaming updates.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also means, since this is not a configurable thing, that your partitioned Materialized Table will &lt;em&gt;never&lt;/em&gt; pick up data earlier than today &lt;em&gt;unless&lt;/em&gt; &lt;strong&gt;you&lt;/strong&gt; manually force a full refresh (&lt;code&gt;ALTER MATERIALIZED TABLE…REFRESH&lt;/code&gt;) or refresh of the specific partition (as seen above).&lt;/p&gt;

&lt;p&gt;That means you could end up with holes in your data unwittingly. Consider the following scenario. We saw above that we’ve currently got data in the Materialized Table for 18-20th April:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------------------------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                     &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="n"&gt;order_count&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------------------------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                     &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                 &lt;span class="mi"&gt;4600&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                     &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                 &lt;span class="mi"&gt;6200&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                     &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                 &lt;span class="mi"&gt;4700&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----+--------------------------------+----------------------+----------------------+&lt;/span&gt;
&lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some more data gets added to the source table for both 21st April (let’s say it came in late) and for today (28th April):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;user_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1011&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-21'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1012&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-21'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1021&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-28'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1022&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-28'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bob'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine being on-call and someone complains that the data in the aggregate table (the Materialized Table) is incomplete. Bleary-eyed (it’s 03:00 and you were up late studying the source code for Apache Flink, naturally) you check the table and gasp in horror as you (and your angry end-user) only see four rows of data, not five:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;order_date&lt;/span&gt;         &lt;span class="n"&gt;total_amount&lt;/span&gt;          &lt;span class="n"&gt;order_count&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;                 &lt;span class="mi"&gt;4600&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;                 &lt;span class="mi"&gt;6200&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;                 &lt;span class="mi"&gt;4700&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;                 &lt;span class="mi"&gt;3800&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Note, no row for 2026-04-21&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But when you query the base table you see the full set of data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`total_amount`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;`order_count`&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_orders&lt;/span&gt;
            &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;order_date&lt;/span&gt;         &lt;span class="n"&gt;total_amount&lt;/span&gt;          &lt;span class="n"&gt;order_count&lt;/span&gt;
 &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;                 &lt;span class="mi"&gt;4600&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt;
 &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;                 &lt;span class="mi"&gt;6200&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt;
 &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;                 &lt;span class="mi"&gt;4700&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt;
 &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;                 &lt;span class="mi"&gt;3800&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt; 
 &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;                 &lt;span class="mi"&gt;3800&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The missing date!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You hurriedly check and confirm that the scheduled refresh of the Materialized Table has run and completed successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                      &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+----------+-------------------------------------------------------------------------------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;d17cc4458346ed8cb4675cac8170b059&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`daily_order_summary`&lt;/span&gt;&lt;span class="n"&gt;_periodic_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="n"&gt;T11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;157&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But then you remember: when the scheduled refresh of the Materialized Table ran, it only refreshed the &lt;em&gt;current partition&lt;/em&gt; (because it’s a &lt;em&gt;partitioned&lt;/em&gt; Materialized Table).&lt;/p&gt;

&lt;p&gt;In order to see the full set of data, we need to force a full refresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt; &lt;span class="n"&gt;REFRESH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;cluster&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;f0d3e3c03fe821b66cfe642c589f649&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+---------------------------+&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;

&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_order_summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;order_date&lt;/span&gt;         &lt;span class="n"&gt;total_amount&lt;/span&gt;          &lt;span class="n"&gt;order_count&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;                 &lt;span class="mi"&gt;4600&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;                 &lt;span class="mi"&gt;6200&lt;/span&gt;                    &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;                 &lt;span class="mi"&gt;4700&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;                 &lt;span class="mi"&gt;3800&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt; 
&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;                 &lt;span class="mi"&gt;3800&lt;/span&gt;                    &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The partition’s data that was missed in the scheduled refresh because it’s not the current date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A full refresh is no big deal if we’re aggregating a handful of rows of source data, but if this were a huge table this could be quite a consideration. And then you’re into targeting the refresh of specific partitions, and hoping you’ve got them all.&lt;/p&gt;

&lt;h1&gt;
  
  
  The scheduler that didn’t
&lt;/h1&gt;

&lt;p&gt;The last bit to cover here is to highlight a bit of a gap, IMHO, in the state of Materialized Tables in Flink 2.2 as they currently stand.&lt;/p&gt;

&lt;p&gt;I’ve already addressed the limited catalog support for Materialized Tables (necessitating the use of &lt;code&gt;test-filesystem&lt;/code&gt; for the simple examples above). The other "&lt;em&gt;fine for a blog post but only just&lt;/em&gt;" component is the &lt;strong&gt;scheduler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The scheduler is a crucial part of batch-based Materialized Tables in Flink 2.2. Per the &lt;a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=294816797#FLIP435:IntroduceaNewMaterializedTableforSimplifyingDataPipelines-OverallArchitecture" rel="noopener noreferrer"&gt;FLIP’s design document&lt;/a&gt;, it’s designed to be pluggable, so I bring no criticism per se about the state of it in SQL Gateway, just a "buyer beware" note. In fact, the &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/api/java/org/apache/flink/table/gateway/workflow/scheduler/EmbeddedQuartzScheduler.html" rel="noopener noreferrer"&gt;docs themselves even say so&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This embedded scheduler is mainly used for testing scenarios and is not suitable for production environment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s an example of why, which had me scratching my head somewhat when I was writing this. If you create a Materialized Table, and then &lt;em&gt;restart&lt;/em&gt; the SQL Gateway, &lt;strong&gt;its refresh schedule is not restored&lt;/strong&gt;. That’s because the SQL Gateway is using an embedded &lt;a href="https://nightlies.apache.org/flink/flink-docs-release-2.2/api/java/org/apache/flink/table/gateway/workflow/scheduler/EmbeddedQuartzScheduler.html" rel="noopener noreferrer"&gt;memory-based scheduler&lt;/a&gt; which doesn’t persist schedules to disk.&lt;/p&gt;

&lt;p&gt;When you create a Materialized Table you’ll see the schedule get added:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-04-28 11:16:44,111 INFO  org.apache.flink.table.gateway.workflow.scheduler.EmbeddedQuartzScheduler []
Create quartz schedule job for materialized table `mt_cat`.`mydb`.`order_summary_1min` successfully,
job info: default_group.quartz_job_`mt_cat`.`mydb`.`order_summary_1min`, cron expression: 0 0/1 * * * ? *.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;But that’s the &lt;em&gt;only&lt;/em&gt; time it gets added. And if there’s no schedule (such as after a restart), there’s no refresh of your Materialized Table.&lt;/p&gt;

&lt;p&gt;If you think, aha! I can just &lt;code&gt;RESUME&lt;/code&gt; the job, you’re gonna have a bad time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_summary_1min&lt;/span&gt; &lt;span class="n"&gt;RESUME&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SqlExecutionException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Materialized&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_summary_1min`&lt;/span&gt; &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;been&lt;/span&gt; &lt;span class="n"&gt;resumed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hang on…an &lt;code&gt;[ERROR]&lt;/code&gt; but &lt;code&gt;refresh workflow has been resumed&lt;/code&gt;? Sounds bad, but promising?&lt;/p&gt;

&lt;p&gt;It’s actually just a misleading error message; what it’s actually saying is indeed an &lt;code&gt;ERROR&lt;/code&gt;: Flink &lt;a href="https://github.com/apache/flink/blob/954fb37214b1c6a3727df834166b9178dc64af0a/flink-table/flink-sql-gateway/src/main/java/org/apache/flink/table/gateway/service/materializedtable/MaterializedTableManager.java#L495-L499" rel="noopener noreferrer"&gt;thinks that the Materialized Table is already active&lt;/a&gt;, and therefore will not resume it.&lt;/p&gt;

&lt;p&gt;Okay, so what about if we &lt;code&gt;SUSPEND&lt;/code&gt; it then, so that it’s then in a state from which to resume it?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_summary_1min&lt;/span&gt; &lt;span class="n"&gt;SUSPEND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;execute&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="k"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SchedulerException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;Failed&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;suspend&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;existent&lt;/span&gt; &lt;span class="n"&gt;quartz&lt;/span&gt; &lt;span class="n"&gt;schedule&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;default_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quartz_job_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_summary_1min`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can’t suspend it because there’s no schedule!&lt;/p&gt;

&lt;p&gt;The quick option is just to drop &amp;amp; recreate the Materialized Table. The fiddly option is to manually recreate the schedule using the SQL Gateway’s REST API to hit the scheduler directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8083/v3/workflow/embedded-scheduler/create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "materializedTableIdentifier": "`mt_cat`.`mydb`.`order_summary_1min`",
    "cronExpression": "0 0/1 * * * ? *",
    "initConfig": {"execution.checkpointing.savepoint-dir": "file:///shared/savepoints"},
    "executionConfig": {},
    "restEndpointUrl": "http://0.0.0.0:8083"  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This then brings the refresh job back to life, and we see it start running again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Flink&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+------------------------------------------------------------------------------+----------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;                           &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                     &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;start&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+------------------------------------------------------------------------------+----------+-------------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;8969&lt;/span&gt;&lt;span class="n"&gt;c414cd83a569b91f3127be509cf2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Materialized_table_&lt;/span&gt;&lt;span class="nv"&gt;`mt_cat`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`mydb`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`order_summary_1min`&lt;/span&gt;&lt;span class="n"&gt;_periodic_refresh_job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FINISHED&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="n"&gt;T11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;931&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------------------------+------------------------------------------------------------------------------+----------+-------------------------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it works…but I wouldn’t be going anywhere near a production environment with this. Which, to be fair, is &lt;em&gt;literally&lt;/em&gt; what the docs say too :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cwiki.apache.org/confluence/display/FLINK/FLIP-448%3A+Introduce+Pluggable+Workflow+Scheduler+Interface+for+Materialized+Table" rel="noopener noreferrer"&gt;FLIP-448&lt;/a&gt; does define a pluggable scheduler, but I’m only aware of Apache DolphinScheduler having added support for it (and even then, I can’t find much info on how to put it into practice).&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping Up
&lt;/h1&gt;

&lt;p&gt;Apache Flink’s Materialized Tables are a solid concept. However, whilst they give me warm fuzzy memories of my &lt;a href="https://rmoff.net/2011/01/08/materialised-views-pct-partition-truncation/" rel="noopener noreferrer"&gt;time in the trenches with Materialized Views in Oracle 11g&lt;/a&gt;, I think streaming complicates things, as do the issues around scheduling and partitioning—so I’m not convinced yet that the implementation is complete enough across the bits of the ecosystem that I’m working most with for me to wholeheartedly adopt them. That said, the schema evolution and query lifecycle management is a really good development.&lt;/p&gt;

&lt;p&gt;Definitely worth playing with and making up your own mind!&lt;/p&gt;

&lt;p&gt;If you want to try this out for yourself, you can find the Docker Compose stack on my &lt;a href="https://github.com/rmoff/examples" rel="noopener noreferrer"&gt;examples repo on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>apacheflink</category>
      <category>materializedtables</category>
    </item>
    <item>
      <title>Kicking the Tyres on Harbor for Agent Evals</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Fri, 10 Apr 2026 14:26:41 +0000</pubDate>
      <link>https://dev.to/rmoff/kicking-the-tyres-on-harbor-for-agent-evals-507i</link>
      <guid>https://dev.to/rmoff/kicking-the-tyres-on-harbor-for-agent-evals-507i</guid>
      <description>&lt;p&gt;After &lt;a href="https://rmoff.net/2026/03/13/evaluating-claudes-dbt-skills-building-an-eval-from-scratch/" rel="noopener noreferrer"&gt;cobbling together my own eval for Claude&lt;/a&gt;, I was interested to discover &lt;a href="https://harborframework.com/" rel="noopener noreferrer"&gt;harbor&lt;/a&gt;. It’s described as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A framework for evaluating and optimizing agents and models in container environments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which sounds kinda cool, right? &lt;/p&gt;

&lt;p&gt;It ships with a bunch of &lt;a href="https://harborframework.com/registry" rel="noopener noreferrer"&gt;pre-created tests and benchmarks&lt;/a&gt;, such as the mandatory &lt;a href="https://harborframework.com/registry/hello-world/1.0" rel="noopener noreferrer"&gt;hello-world&lt;/a&gt; to more complex and multi-task examples such as &lt;a href="https://harborframework.com/registry/terminal-bench/2.0" rel="noopener noreferrer"&gt;terminal-bench&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Harbor’s unit of execution is a &lt;em&gt;task&lt;/em&gt;, which is basically a prompt for a coding agent (such as Claude Code). Harbor works with multiple coding agents, and multiple models. Which is basically what it says on the tin above, right?&lt;/p&gt;

&lt;p&gt;Here’s an &lt;a href="https://github.com/harbor-framework/harbor/blob/3a945a028b850066f5dde6c4e542d9097cf64870/examples/tasks/hello-world/instruction.md" rel="noopener noreferrer"&gt;example task&lt;/a&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a file called hello.txt with "Hello, world!" as the content.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Trying it out
&lt;/h1&gt;

&lt;p&gt;Let’s try out &lt;a href="https://harborframework.com/registry/hello-world/1.0" rel="noopener noreferrer"&gt;hello-world&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;harbor run  &lt;span class="nt"&gt;--model&lt;/span&gt; anthropic/claude-sonnet-4-6 &lt;span class="se"&gt;\ &lt;/span&gt;
            &lt;span class="nt"&gt;--agent&lt;/span&gt; claude-code &lt;span class="se"&gt;\ &lt;/span&gt;                
            &lt;span class="nt"&gt;--dataset&lt;/span&gt; hello-world &lt;span class="se"&gt;\ &lt;/span&gt;              
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use Sonnet 4.6 model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the test using Claude Code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the pre-packaged "Hello, World" test&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a short time this completes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Metric              ┃ Value                           ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Agent               │ claude-code (claude-sonnet-4-6) │
│ Dataset             │ hello-world                     │
│ Trials              │ 1                               │
│ Errors              │ 0                               │
│                     │                                 │
│ Mean                │ 1.000                           │
│                     │                                 │
│ Reward Distribution │                                 │
│   reward = 1.0      │ 1                               │
└─────────────────────┴─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Harbor ships (see what I did there? 😉) with a nice dashboard for exploring test runs. Spin it up by pointing it at the output folder (&lt;code&gt;jobs&lt;/code&gt;, in this case):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;harbor view &lt;span class="nb"&gt;jobs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Status and timing breakdown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqku5bivy3q8ay9qg8d9k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqku5bivy3q8ay9qg8d9k.webp" alt="Harbor dashboard showing hello-world task outcome with reward 1.00" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the covers, Harbor spins up a Docker container, within which Claude runs with &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; so that it can go about its business without any of that pesky permission seeking. It takes the defined task or dataset prompt, and runs it, as we can see here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gpuzwrsutk2o4qpm2ia.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gpuzwrsutk2o4qpm2ia.webp" alt="Harbor trajectory view showing the five steps Claude took to complete the hello-world task" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Scoring
&lt;/h1&gt;

&lt;p&gt;A task’s performance is scored using a verifier that’s part of the task definition. For the above "hello world", all we need to do is check if the agent (a) created the file with the correct name and (b) with the correct content. Which is exactly what &lt;a href="https://github.com/harbor-framework/harbor/blob/3a945a028b850066f5dde6c4e542d9097cf64870/examples/tasks/hello-world/tests/test_state.py" rel="noopener noreferrer"&gt;this Python script&lt;/a&gt; does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_file_exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;hello_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/app/hello.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;hello_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hello_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; does not exist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello_file_contents&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;hello_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/app/hello.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hello_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;expected_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, world!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File content is &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, expected &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected_content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its &lt;a href="https://github.com/harbor-framework/harbor/blob/3a945a028b850066f5dde6c4e542d9097cf64870/examples/tasks/hello-world/tests/test.sh#L15-L19" rel="noopener noreferrer"&gt;wrapper script&lt;/a&gt; gives it a pass/fail score—either it worked, or it didn’t:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /logs/verifier/reward.txt
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /logs/verifier/reward.txt
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the test multiple times, you’ll get scores (rewards); unsurprisingly "hello-world" doesn’t pose any challenges or show variability:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejr7gk9k83ykfoo3dihx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejr7gk9k83ykfoo3dihx.webp" alt="Harbor dashboard listing six hello-world trials" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So that’s Hello World…what about the Real World?&lt;/p&gt;

&lt;h1&gt;
  
  
  Using it with dbt
&lt;/h1&gt;

&lt;p&gt;The driver to looking at Harbor was my curiosity as to whether I could have used Harbor in place of my &lt;a href="https://rmoff.net/2026/03/13/evaluating-claudes-dbt-skills-building-an-eval-from-scratch/" rel="noopener noreferrer"&gt;hacky homebrew bespoke and artisanal scripts&lt;/a&gt;, and if so what it would look like.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
There is an imbalance here in that I now know more about evals, deterministic testing and LLM-as-judge than I did before creating my custom harness. If I were to write it again, it’d be a lot cleaner I’m sure. So almost by definition, something like Harbor is probably going to be better.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you’d expect by now, I didn’t write the dbt task myself; I told Claude about my previous work, and had it build a Harbor-compliant task. Its key components are this:&lt;/p&gt;

&lt;p&gt;+-----------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;br&gt;
| Component                               | Description                                                                                                                                                                                            |&lt;br&gt;
+=========================================+========================================================================================================================================================================================================+&lt;br&gt;
| &lt;code&gt;Dockerfile&lt;/code&gt;                            | Installs &lt;code&gt;dbt-duckdb&lt;/code&gt; and the &lt;a href="https://github.com/dbt-labs/dbt-agent-skills" rel="noopener noreferrer"&gt;dbt-agent-skills&lt;/a&gt; in the same container that Claude Code will run.                                                        |&lt;br&gt;
+-----------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;br&gt;
| &lt;code&gt;instruction.md&lt;/code&gt;                        | The same prompt &lt;a href="https://github.com/rmoff/dbt-claude/blob/main/prompts/rich.md" rel="noopener noreferrer"&gt;as before&lt;/a&gt;                                                                                                             |&lt;br&gt;
|                                         |                                                                                                                                                                                                        |&lt;br&gt;
|                                         | &amp;gt; […] Build a dbt project using DuckDB for this data using idiomatic patterns and good practices. […] Run &lt;code&gt;dbt build&lt;/code&gt; to verify your work. If it fails, fix the errors and re-run until it passes. |&lt;br&gt;
+-----------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;br&gt;
| &lt;code&gt;tests/test.sh&lt;/code&gt;                         | Verifier script, which does several things:                                                                                                                                                            |&lt;br&gt;
|                                         |                                                                                                                                                                                                        |&lt;br&gt;
|                                         | 1.  &lt;em&gt;Deterministic&lt;/em&gt; checks (similar to the ones &lt;a href="https://github.com/rmoff/dbt-claude/blob/main/validate.sh" rel="noopener noreferrer"&gt;I used before&lt;/a&gt; - is the dbt project there, does it have models defined, etc)               |&lt;br&gt;
|                                         |                                                                                                                                                                                                        |&lt;br&gt;
|                                         | 2.  &lt;em&gt;Non-deterministic&lt;/em&gt; checks, via LLM-as-judge                                                                                                                                                       |&lt;br&gt;
|                                         |                                                                                                                                                                                                        |&lt;br&gt;
|                                         | 3.  If the LLM-as-judge succeeds, uses its score for the Harbor reward; if it fails, fall back on the deterministic checks score                                                                       |&lt;br&gt;
+-----------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;br&gt;
| &lt;code&gt;tests/llm_judge.py&lt;/code&gt;, &lt;code&gt;tests/rubric.md&lt;/code&gt; | Script to call out to LLM to judge the work, using the rubric provided (similar to &lt;a href="https://github.com/rmoff/dbt-claude/blob/main/rubric.md" rel="noopener noreferrer"&gt;the one used before&lt;/a&gt;)                                     |&lt;br&gt;
+-----------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+&lt;/p&gt;

&lt;p&gt;There are plenty of gaps in this, such as only using the non-deterministic score. The final Harbor reward value should probably be a weighted version of the deterministic and non-deterministic verification. However, this was more about understanding the scope of Harbor than building the perfect test.&lt;/p&gt;

&lt;p&gt;With this in place I could then run my test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;harbor run &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--agent&lt;/span&gt; claude-code &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"claude-sonnet-4-6"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="s2"&gt;"tasks/f-rich-with-plugin"&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
    &lt;span class="nt"&gt;--artifact&lt;/span&gt; /app &lt;span class="se"&gt;\ &lt;/span&gt;                  
    &lt;span class="nt"&gt;--n-attempts&lt;/span&gt; 3 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Custom task definition&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Capture the output of the agent as an artifact (i.e. don’t throw it away once the test finishes)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the same task multiple times&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2khyien6lmonn4o3o6nb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2khyien6lmonn4o3o6nb.webp" alt="Harbor dashboard for the dbt task showing a reward of 0.96" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and see how multiple iterations of the same prompt and model scored, and the variance between them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn83vc7spk00zr2czgrj4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn83vc7spk00zr2czgrj4.webp" alt="Harbor results table showing three dbt task trials with scores of 0.85" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  I still Harbor some doubt…
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;(…and not just about the scope for so many awful puns)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I think Harbor would be extremely useful for tightly defined tasks against which one wanted to evaluate new models or agentic coding tools (for example, alternatives to Claude Code). In fact, it’s perfect for that, and it’s literally what it’s designed for.&lt;/p&gt;

&lt;p&gt;Where I’m not going to be rushing to use it though is for evaluating the effectiveness of different prompts or skills, particularly as I flail around trying all the things and randomly changing lots of stuff.&lt;/p&gt;

&lt;p&gt;A lot of that is down to my inexperience in this area; Harbor &lt;em&gt;adds&lt;/em&gt; a layer of complexity, and I’m almost certain my random jiggling to unbreak things would often break my Harbor test rig (or invalidate the integrity of the test results). For example; to vary the &lt;em&gt;prompt&lt;/em&gt; (the task’s &lt;code&gt;instruction.md&lt;/code&gt;) means building another task (if I understand Harbor correctly), which then means duplicating the verifier. Once that’s duplicated, it has the scope to get out of sync across tasks. Maybe there’s a way to align it (&lt;em&gt;symlinks, perhaps&lt;/em&gt; - but I’d need to test it, and that’s more tooling work), but maybe Harbor’s not really designed to be used in this way.&lt;/p&gt;

&lt;p&gt;Anyway - an interesting tool, and definitely one to keep in mind as I continue to explore this bamboozling world of agents and AI :)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>harbor</category>
    </item>
    <item>
      <title>Look Ma, I made a JAR! (Building a connector for Kafka Connect with Claude Code)</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Fri, 27 Mar 2026 14:40:58 +0000</pubDate>
      <link>https://dev.to/confluentinc/look-ma-i-made-a-jar-building-a-connector-for-kafka-connect-with-claude-code-1b0n</link>
      <guid>https://dev.to/confluentinc/look-ma-i-made-a-jar-building-a-connector-for-kafka-connect-with-claude-code-1b0n</guid>
      <description>&lt;p&gt;As a non-Java coder, for the last ten years I've stumbled my way through the JVM-centric world of "big data" (as it was called then), relying on my wits with SQL and config files to just about muddle through. One of the things that drew me to Kafka Connect was that I could build integrations between Kafka and other systems without needing to write Java, and the same again for ksqlDB and Flink SQL---now stream processing was available to mere RDBMS mortals and not just the Java adonises.&lt;/p&gt;

&lt;p&gt;One thing defeated me though; if a connector &lt;em&gt;didn't exist&lt;/em&gt; for Kafka Connect, then I was stuck. I'd resort to cobbled-together pipelines leaning heavily on kafkacat kcat, such as I did in &lt;a href="https://www.confluent.io/blog/streaming-etl-and-analytics-for-real-time-location-tracking/" rel="noopener noreferrer"&gt;this blog post&lt;/a&gt;. I built some cool analytics on top of maritime AIS data about ships' locations, but the foundations were shaky at best:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc ais.host.com 5631 | gpsdecode | kafkacat &lt;span class="nt"&gt;-b&lt;/span&gt; broker:9092 &lt;span class="nt"&gt;-P&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No failure logic, no schema handling, no bueno.&lt;/p&gt;

&lt;p&gt;What I really needed was a connector for Kafka Connect. However for that, you need Java.&lt;/p&gt;

&lt;p&gt;I don't write Java.&lt;/p&gt;

&lt;p&gt;But Claude can write Java.&lt;/p&gt;

&lt;p&gt;I've already &lt;a href="https://rmoff.net/2026/01/27/cosplaying-as-a-webdev-with-claude-code-in-january-2026/" rel="noopener noreferrer"&gt;waxed lyrical about the power of LLMs for coding&lt;/a&gt;, and written about the &lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;risks of not keeping up&lt;/a&gt; with this revolution in how we build things, and the &lt;a href="https://rmoff.net/2026/03/11/claude-code-isnt-going-to-replace-data-engineers-yet/" rel="noopener noreferrer"&gt;limitations of what they can currently do&lt;/a&gt;. So far all this work with coding agents was generally within the realm of something I could eyeball (HTML/CSS), or actually debug and validate (dbt/SQL).&lt;/p&gt;

&lt;p&gt;But what about getting a coding agent to build a connector for me?&lt;/p&gt;

&lt;h1&gt;
  
  
  Skillin Up
&lt;/h1&gt;

&lt;p&gt;There were two things I wanted to get out of this experiment - a working connector for Kafka Connect, and a skill that I could use to have Claude build more in the future. Simply prompting it to build a connector throws away a lot of useful context and knowledge that it builds up in the process.&lt;/p&gt;

&lt;p&gt;Since I don't know anything about building connectors, I figured I'd just get Claude to figure it out:&lt;/p&gt;

&lt;p&gt;inspect the Kafka Connect API and prepare a Claude skill. Use source available repos on GitHub from Confluent to inform good design practice, as well as their docs: &lt;a href="https://docs.confluent.io/platform/current/connect/devguide.html" rel="noopener noreferrer"&gt;https://docs.confluent.io/platform/current/connect/devguide.html&lt;/a&gt; &lt;a href="https://www.confluent.io/blog/how-build-custom-kafka-connector/" rel="noopener noreferrer"&gt;https://www.confluent.io/blog/how-build-custom-kafka-connector/&lt;/a&gt; Make no mistakes ;)&lt;/p&gt;

&lt;p&gt;Claude went away and busily set fire to a bunch of tokens. It came back like a proud six year old, having &lt;em&gt;completely missed the point&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;# Plan: Kafka Connect REST API Skill&lt;/p&gt;

&lt;p&gt;Robin needs a Claude skill that provides deep knowledge of the Kafka Connect REST API and common connector configuration patterns. [...]&lt;/p&gt;

&lt;p&gt;Wait, what? &lt;code&gt;provides deep knowledge of the Kafka Connect REST API and common connector configuration patterns&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;[rejected tool --- user feedback:] wow, you completely screwed up the brief. I want a skill for &lt;em&gt;building new connectors for Kafka Connect&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You're right, I misread the brief completely. The docs you linked are about &lt;strong&gt;developing&lt;/strong&gt; custom Kafka Connect connectors (Java implementation of SourceConnector/SinkConnector), not using the REST API. Let me restart the research properly.&lt;/p&gt;

&lt;p&gt;Once back on course, it came up with a plan. I may not code Java, but I know enough to nudge Claude to really do a thorough job learning as much as it can, as well as reminding it about security. As a side note, this is one of the fundamental beauties of source-available code: people, and now agents, can &lt;em&gt;learn&lt;/em&gt; from it.&lt;/p&gt;

&lt;p&gt;does this plan include security (not logging passwords, etc)? Can it learn anything from other source-available connectors of note such as Debezium?&lt;/p&gt;

&lt;p&gt;After that, it was full-stream ahead, and Claude duly wrote the skill, nbd ¯\_(ツ)_/¯&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Kafka Connect — Connector Development

Build production-grade Kafka Connect source and sink connector plugins in Java.

## Workflow

1. **Gather requirements** — source or sink? external system? schema? auth? offset model?
2. **Scaffold** — Maven project, directory layout, POM dependencies
3. **Implement Connector** — config validation, task partitioning
4. **Implement Task** — data movement (poll or put), offset tracking
5. **Define configuration** — ConfigDef with types, validators, groups
6. **Handle schemas** — SchemaBuilder, Struct, logical types
7. **Security review** — credentials, logging, SSL, error sanitization
8. **Test** — unit, integration (Testcontainers), failure scenarios
9. **Package &amp;amp; deploy** — plugin directory or uber-JAR, install to plugin.path
[…]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can see the complete skill &lt;a href="https://github.com/rmoff/kafka-connect-ais/tree/master/claude-code-skill" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;proof of concept&lt;/strong&gt;. Literally that. Proving a concept. Don't use it unless you know what gaps might exist with it, both in the skill and the code that it creates.&lt;/p&gt;

&lt;h1&gt;
  
  
  Claude, build me a connector
&lt;/h1&gt;

&lt;p&gt;As I mentioned above, a few years ago I wrote &lt;a href="https://www.confluent.io/blog/streaming-etl-and-analytics-for-real-time-location-tracking/" rel="noopener noreferrer"&gt;a blog post&lt;/a&gt; about stream processing using AIS data as the source. This data is emitted from ships, with information about their heading etc. Unless you have an AIS receiver, you need to find a service that emits this relayed AIS data, which fortunately the &lt;a href="https://www.kystverket.no/en/sea-transport-and-ports/ais/access-to-ais-data/" rel="noopener noreferrer"&gt;Norwegian government provides for free&lt;/a&gt;, including a map viewer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7l36jm6ag8ka81tk3ale.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7l36jm6ag8ka81tk3ale.webp" alt="Norwegian AIS map viewer showing vessel positions near Kråkerøy with ship details popup" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also a TCP endpoint, which I figured Claude could use to test and validate a connector.&lt;/p&gt;

&lt;p&gt;build a source connector for AIS data. there is a live endpoint at 153.44.253.27 5631 that you can test with&lt;/p&gt;

&lt;p&gt;The user wants to build a Kafka Connect source connector for AIS (Automatic Identification System) data. AIS is a maritime tracking system used by ships. The endpoint at 153.44.253.27:5631 appears to be a live AIS data feed.&lt;/p&gt;

&lt;p&gt;Let me first understand what AIS data looks like and explore the codebase to see if there's any existing work. Let me also test the endpoint to understand the data format.&lt;/p&gt;

&lt;p&gt;I should: 1. Explore what AIS data format looks like (NMEA sentences typically) 2. Test the endpoint to see what data comes through 3. Check if there's any existing connector or project structure&lt;/p&gt;

&lt;p&gt;Let me start with Phase 1 - understanding.&lt;/p&gt;

&lt;p&gt;Let me explore the AIS data format and test the live endpoint to understand what we're working with.&lt;/p&gt;

&lt;p&gt;If you're interested in how Claude works, using Simon Willison's &lt;a href="https://github.com/simonw/tools/blob/main/claude-code-timeline.html" rel="noopener noreferrer"&gt;claude-code-timeline&lt;/a&gt; tool is a great way to see quite what it gets up to. Poking around through the JSONL file that Claude writes to, you get an insight into its "thought" process:&lt;/p&gt;

&lt;p&gt;Now I have real AIS data from the endpoint. Let me analyze what I see:&lt;/p&gt;

&lt;p&gt;[...]&lt;/p&gt;

&lt;p&gt;Now I'm ready to design the connector architecture. I need to build a source connector that can handle this tag block format, parse the Norwegian station identifiers, and manage multi-sentence message reassembly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key architectural decisions are whether to decode AIS messages internally using a Java library or pass raw NMEA downstream, how to organize the output topic structure, and how to handle the offset model for a live stream with no meaningful replay capability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actually, let me think about this more carefully. The user said "a source connector for AIS data" - they want a proper Kafka Connect connector. Given their existing experience with the nc+gpsdecode+kcat pipeline, they probably want something more robust and native.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How did it know about my existing &lt;code&gt;nc+gpsdecode+kcat pipeline&lt;/code&gt;? If you look at the prompt above, I just told it the AIS endpoint. Turns out it had a good rummage around my &lt;em&gt;existing&lt;/em&gt; projects!&lt;/p&gt;

&lt;p&gt;Check if there are any existing AIS or Kafka Connect connector projects in /Users/rmoff/git/. Look for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Any directories with "ais" or "connect" or "connector" in the name&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any pom.xml or build.gradle files related to Kafka Connect connectors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any NMEA or maritime-related code&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Just do a quick search - this is a brief reconnaissance task.&lt;/p&gt;

&lt;p&gt;From that it found its way to my local clone of &lt;a href="https://github.com/confluentinc/demo-scene/tree/master/maritime-ais" rel="noopener noreferrer"&gt;demo-scene&lt;/a&gt; which includes the code it references. Clever stuff.&lt;/p&gt;

&lt;p&gt;Its first iteration of the plan is pretty good:&lt;/p&gt;

&lt;p&gt;The plan is written. Summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: A Kafka Connect source connector that connects to AIS TCP endpoints, decodes NMEA/AIS messages using AisLib (Apache 2.0), and produces structured Kafka records keyed by MMSI (vessel ID)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Where&lt;/strong&gt;: &lt;code&gt;/Users/rmoff/git/kafka-connect-ais&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;: Single topic with a flat schema --- common fields (mmsi, msg_type, timestamp) plus nullable type-specific fields (lat/lon/speed for position reports, ship_name/callsign for static data)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replaces&lt;/strong&gt;: The &lt;code&gt;nc | gpsdecode | kafkacat&lt;/code&gt; pipeline from your existing demos&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;8 implementation steps: Maven skeleton → config → TCP manager → NMEA parser → schema/converter → connector/task → tests → build &amp;amp; verify.&lt;/p&gt;

&lt;p&gt;Now comes the bit that Claude (or a real developer, without domain knowledge) can't do: does it actually make sense what's being proposed?&lt;/p&gt;

&lt;p&gt;I picked up on its plan to write one single topic. AIS data has different record types, with different fields across them. I also wanted it to "make no mistakes" a.k.a. sense-check its architecture that it wasn't going to bug-out on a flaky TCP connection.&lt;/p&gt;

&lt;p&gt;check the conceptual architecture is right. usually KC would be reading from a finite source such as RDBMS. does anything change for sucking on a TCP pipe?&lt;/p&gt;

&lt;p&gt;also topics; what about 1:N design to split by type? is that idiomatic or not? config option maybe?&lt;/p&gt;

&lt;p&gt;Other than the ass-kissing ("Great questions!") its reply is useful:&lt;/p&gt;

&lt;p&gt;Great questions. Let me address both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TCP pipe architecture&lt;/strong&gt;: Yes, this changes things meaningfully. Most source connectors follow a request/response pattern (query DB → get rows → return records). A live TCP stream is fundamentally different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No replay&lt;/strong&gt;: If the connector is down, messages are lost. The offset is for observability only, not resume.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long-lived connection&lt;/strong&gt;: The TCP socket persists across &lt;code&gt;poll()&lt;/code&gt; calls (opened in &lt;code&gt;start()&lt;/code&gt;, read in &lt;code&gt;poll()&lt;/code&gt;, closed in &lt;code&gt;stop()&lt;/code&gt;). This is similar to MQTT or syslog source connectors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No backoff needed&lt;/strong&gt;: Data flows continuously --- &lt;code&gt;poll()&lt;/code&gt; just reads whatever's buffered on the socket. No "sleep when no data" pattern (though &lt;code&gt;SO_TIMEOUT&lt;/code&gt; handles the case where the stream pauses).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No snapshot phase&lt;/strong&gt;: No existing data to catch up on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reconnection is critical&lt;/strong&gt;: TCP connections drop. This is the main failure mode to handle well.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is sound --- it's just a different &lt;strong&gt;flavor&lt;/strong&gt; of source connector. The Connect framework handles this fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topic strategy&lt;/strong&gt;: You're right, 1:N is worth considering. With separate topics, each gets a tighter schema (no nullable fields from other types). Let me think through the options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single topic&lt;/strong&gt; (&lt;code&gt;ais&lt;/code&gt;): simpler, matches your existing ksqlDB pipeline&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Split by category&lt;/strong&gt; (&lt;code&gt;ais.position&lt;/code&gt;, &lt;code&gt;ais.static&lt;/code&gt;, &lt;code&gt;ais.base_station&lt;/code&gt;, &lt;code&gt;ais.other&lt;/code&gt;): cleaner schemas, easier for consumers that only care about one type&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configurable&lt;/strong&gt;: best of both worlds&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, I am full of sheer joy. As an end user of Kafka Connect and its connectors for many years I've often thought &lt;em&gt;wouldn't it be nice if the connector had config option &amp;lt;x&amp;gt;&lt;/em&gt;. And now, they can!&lt;/p&gt;

&lt;p&gt;Claude enables you to write tools that work as &lt;em&gt;you&lt;/em&gt; want them, not make do and adapt your workflow to fit &lt;em&gt;them&lt;/em&gt;. For example: one topic would be fine if that's all I could get. But I'd probably need some post-processing (e.g. Flink SQL) to split out records of interest. Or a Single Message Transform to filter and route particular message types to a sink.&lt;/p&gt;

&lt;p&gt;also Kafka message headers - anything clever to do there? And talk to me more about the reconnection of TCP, how are we gonna handle this well and not stall?&lt;/p&gt;

&lt;p&gt;Good questions. Let me address both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message headers&lt;/strong&gt;: Yes, headers are useful here. The key win is &lt;code&gt;ais.msg_type&lt;/code&gt; --- consumers can filter/route by header without deserializing the value. Also useful for provenance (source station, raw NMEA for debugging).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TCP reconnection/stall prevention&lt;/strong&gt;: The critical insight is that &lt;code&gt;poll()&lt;/code&gt; must never block indefinitely. Here's the layered defense:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;SO_TIMEOUT&lt;/code&gt;&lt;/strong&gt; on the socket (e.g., 1000ms) --- &lt;code&gt;readLine()&lt;/code&gt; throws &lt;code&gt;SocketTimeoutException&lt;/code&gt; after 1s of no data, so &lt;code&gt;poll()&lt;/code&gt; always returns&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;stop()&lt;/code&gt; closes the socket&lt;/strong&gt; from another thread --- immediately unblocks any pending &lt;code&gt;readLine()&lt;/code&gt; with an IOException&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Volatile &lt;code&gt;stopping&lt;/code&gt; flag&lt;/strong&gt; --- &lt;code&gt;poll()&lt;/code&gt; checks it on each iteration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backoff on reconnect&lt;/strong&gt; --- avoids hammering a down endpoint&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me update the plan with both.&lt;/p&gt;

&lt;p&gt;Is this TCP reconnection logic good enough? No idea. But I have the---potentially dangerous---feeling of reassurance that at least Claude "understands" the requirement and is going to code for it.&lt;/p&gt;

&lt;p&gt;After some more back-and-forth with Claude over how it handled particular AIS message types, and making sure that the connector was going to be ergonomic to work with overall, we settled on a plan.&lt;/p&gt;

&lt;p&gt;The user wants me to implement a Kafka Connect AIS Source Connector based on a detailed plan. Let me start by setting up the project structure and implementing each component step by step.&lt;/p&gt;

&lt;p&gt;This is a big implementation task.&lt;/p&gt;

&lt;p&gt;Claude then merrily chugs away and...builds the connector. In case you're not familiar with coding agents, the key thing here is that it doesn't just churn out code and stop. It'll write code, and then build it and test it, and fix problems, and test it again, until it works.&lt;/p&gt;

&lt;p&gt;Several issues: invalid checksums in test data, and the talker ID normalization is likely breaking NMEA checksums. Let me investigate and fix.&lt;/p&gt;

&lt;p&gt;Got live data. Now let me write a quick Java test to check if AisLib can parse &lt;code&gt;!BSVDM&lt;/code&gt; directly without normalization.&lt;/p&gt;

&lt;p&gt;And then there it was, done:&lt;/p&gt;

&lt;p&gt;All clean --- 13 files, no target/ artifacts. Here's what was built:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project: &lt;code&gt;kafka-connect-ais&lt;/code&gt;&lt;/strong&gt; --- Kafka Connect source connector for AIS maritime data&lt;/p&gt;

&lt;p&gt;[...]&lt;/p&gt;

&lt;p&gt;commit this then add a really good, non AI-stinky README, and full docker compose and quickstart&lt;/p&gt;

&lt;p&gt;And there we have it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kafka-connect-ais
├── docker-compose.yml
├── README.adoc
├── src
│&amp;nbsp;&amp;nbsp; ├── main
│&amp;nbsp;&amp;nbsp; └── test
└── target
    └── kafka-connect-ais-0.1.0-SNAPSHOT.jar 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;  Look ma, I just made my own JAR!&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  The Connector
&lt;/h1&gt;

&lt;p&gt;GitHub repo: &lt;a href="https://github.com/rmoff/kafka-connect-ais" rel="noopener noreferrer"&gt;https://github.com/rmoff/kafka-connect-ais&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv97s1hg7qu0k26vwynis.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv97s1hg7qu0k26vwynis.webp" alt="Architecture diagram showing AIS data flow from vessels through TCP feed to the Kafka Connect AIS Source Connector and into Apache Kafka topics" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Credit: Claude, obvs)&lt;/p&gt;

&lt;p&gt;Let's start up Kafka, Kafka Connect, and Schema Registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;+] Running 4/4
 ✔ Network kafka-connect-ais_default  Created   0.0s
 ✔ Container broker                   Started   0.4s
 ✔ Container schema-registry          Started   0.4s
 ✔ Container kafka-connect            Started   0.4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure &lt;a href="https://github.com/kcctl/kcctl" rel="noopener noreferrer"&gt;kcctl&lt;/a&gt; for easy working with Kafka Connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kcctl config set-context &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8083 &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that the connector plugin has loaded (my connector! that I 'wrote'! omg!!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ kcctl get plugins | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ais
 &lt;span class="nb"&gt;source             &lt;/span&gt;net.rmoff.connect.ais.AisSourceConnector                           0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bare config is very simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ais-source-per-type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"connector.class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"net.rmoff.connect.ais.AisSourceConnector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ais.hosts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"153.44.253.27:5631"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ais"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"topic.per.type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tasks.max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an instance of the connector using this config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kcctl apply &lt;span class="nt"&gt;-f&lt;/span&gt; connector-ais-per-type.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the connector is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ kcctl get connectors

 NAME                  TYPE     STATE     TASKS
 ais-source-per-type   &lt;span class="nb"&gt;source   &lt;/span&gt;RUNNING   0: RUNNING 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  Honestly thought it would be &lt;code&gt;FAILED&lt;/code&gt;. Maybe this AI thing has legs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the litmus test...do we have data?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ kcat &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092 &lt;span class="nt"&gt;-L&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;ais
  topic &lt;span class="s2"&gt;"ais.position"&lt;/span&gt; with 1 partitions:
  topic &lt;span class="s2"&gt;"ais.binary"&lt;/span&gt; with 1 partitions:
  topic &lt;span class="s2"&gt;"ais.aton"&lt;/span&gt; with 1 partitions:
  topic &lt;span class="s2"&gt;"ais.static"&lt;/span&gt; with 1 partitions:
  topic &lt;span class="s2"&gt;"ais.safety"&lt;/span&gt; with 1 partitions:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, we have &lt;em&gt;topics&lt;/em&gt;...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ kcat &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092 &lt;span class="nt"&gt;-t&lt;/span&gt; ais.static &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;avro &lt;span class="nt"&gt;-r&lt;/span&gt; http://localhost:8081 | jq &lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"mmsi"&lt;/span&gt;: 258503000,
  &lt;span class="s2"&gt;"msg_type"&lt;/span&gt;: 5,
  &lt;span class="s2"&gt;"receive_timestamp"&lt;/span&gt;: 1774612034000,
  &lt;span class="s2"&gt;"source_station"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"string"&lt;/span&gt;: &lt;span class="s2"&gt;"2573555"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"raw_nmea"&lt;/span&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;s:2573555,c:1774612034*0D&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;!BSVDM,2,1,5,A,53nQiF000000hiLH000I9DPthlDr22222222220N2H633400000000000000,0*0E&lt;/span&gt;&lt;span class="se"&gt;\n\\&lt;/span&gt;&lt;span class="s2"&gt;s:2573555,c:1774612034*0D&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;!BSVDM,2,2,5,A,00000000000,2*38"&lt;/span&gt;,
  &lt;span class="s2"&gt;"callsign"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"string"&lt;/span&gt;: &lt;span class="s2"&gt;"LLWF"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"ship_name"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"string"&lt;/span&gt;: &lt;span class="s2"&gt;"FRUHOLMEN"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
&lt;span class="o"&gt;[&lt;/span&gt;…]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and OMG we have data! 🙀&lt;/p&gt;

&lt;p&gt;You can start to poke around the data itself and also do fun things from the terminal with &lt;a href="https://www.visidata.org/" rel="noopener noreferrer"&gt;visidata&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kcat &lt;span class="nt"&gt;-b&lt;/span&gt; localhost:9092 &lt;span class="nt"&gt;-t&lt;/span&gt; ais.static &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;avro &lt;span class="nt"&gt;-r&lt;/span&gt; http://localhost:8081 &lt;span class="nt"&gt;-c&lt;/span&gt; 2000 | &lt;span class="se"&gt;\&lt;/span&gt;
jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'{mmsi, ship_name: .ship_name.string, ship_type: .ship_type_text.string, dest: .destination.string}'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
vd &lt;span class="nt"&gt;-f&lt;/span&gt; jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpafdvqmbwcytpnfweqzv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpafdvqmbwcytpnfweqzv.webp" alt="Visidata terminal showing AIS ship type frequency distribution" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Regarding this connector itself, I'll reiterate the same as I did with the skill:&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;proof of concept&lt;/strong&gt;. Literally that. Proving a concept. Don't use it unless you know what gaps might exist with it, both in the skill and the code that it creates.&lt;/p&gt;

&lt;h1&gt;
  
  
  y u hate java tho?
&lt;/h1&gt;

&lt;p&gt;This is &lt;em&gt;not&lt;/em&gt; a Java-bashing post, in the slightest.&lt;/p&gt;

&lt;p&gt;It's a "&lt;em&gt;I don't know this language but AI enables me to work with it&lt;/em&gt;".&lt;/p&gt;

&lt;p&gt;It could just as well be a "&lt;em&gt;I'm a Java programmer who doesn't know SQL and look at what Claude wrote and wtf is this &lt;code&gt;PIVOT&lt;/code&gt; doing&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;I don't hate Java. Language 'wars' are for imbeciles.&lt;/p&gt;

&lt;p&gt;You can find the connector and the skill that I built in this process here: &lt;a href="https://github.com/rmoff/kafka-connect-ais" rel="noopener noreferrer"&gt;https://github.com/rmoff/kafka-connect-ais&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kafkaconnect</category>
      <category>claudecode</category>
      <category>visidata</category>
    </item>
    <item>
      <title>Interesting links - March 2026</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Thu, 26 Mar 2026 16:37:05 +0000</pubDate>
      <link>https://dev.to/rmoff/interesting-links-march-2026-4833</link>
      <guid>https://dev.to/rmoff/interesting-links-march-2026-4833</guid>
      <description>&lt;p&gt;I’ve had a huge amount of fun this month exploring quite what AI (in the form of Claude Code) can do for a data engineer. Rather than just hack around at a prompt, I took a bit more of a considered approach to it, &lt;a href="https://rmoff.net/2026/03/13/evaluating-claudes-dbt-skills-building-an-eval-from-scratch/" rel="noopener noreferrer"&gt;building a harness&lt;/a&gt; to test out different prompts and skills. You can read my write-up here, the headline of which is that literally &lt;a href="https://rmoff.net/2026/03/11/claude-code-isnt-going-to-replace-data-engineers-yet/" rel="noopener noreferrer"&gt;Claude Code isn’t going to replace data engineers (yet)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ve also written up an &lt;a href="https://rmoff.net/ai/" rel="noopener noreferrer"&gt;AI Disclosure&lt;/a&gt; for my blog which I’ll keep up to date as my use of AI evolves, along with &lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;a sweary rant&lt;/a&gt; about why you basically &lt;em&gt;have&lt;/em&gt; to get on board with AI if you value your career.&lt;/p&gt;

&lt;p&gt;Unresolved directive in &amp;lt;stdin&amp;gt; - include::../../asciidoc-includes/il-header.adoc[]&lt;/p&gt;

&lt;h1&gt;
  
  
  Kafka and Event Streaming
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 A fascinating deep-dive from the Apache Iggy team looking at their &lt;a href="https://iggy.apache.org/blogs/2026/02/27/thread-per-core-io_uring/" rel="noopener noreferrer"&gt;migration journey to thread-per-core architecture powered by io_uring&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simone Esposito at Bitrock looks at the new &lt;a href="https://medium.com/@BitrockIT/queues-for-kafka-kip-932-the-bridge-between-event-streaming-and-queuing-0168d18a5324" rel="noopener noreferrer"&gt;Queues for Kafka&lt;/a&gt; feature that was released recently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"&lt;em&gt;Unknown magic byte&lt;/em&gt;" is an error that has sent many a Kafka user running for the comforting (if unsuitable) embrace of plain JSON for their message serialisation, but no longer! David Araujo describes how &lt;a href="https://www.confluent.io/blog/schema-id-kafka-headers-data-governance/" rel="noopener noreferrer"&gt;schema IDs can now be stored in the header of a Kafka message&lt;/a&gt; - this is huge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An interesting thread on Reddit looking at how to give &lt;a href="https://old.reddit.com/r/apachekafka/comments/1rjllrm/giving_external_partners_access_to_kafka_topics/" rel="noopener noreferrer"&gt;external partners access to Kafka topics&lt;/a&gt; without direct broker access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I’ve mentioned &lt;a href="https://tansu.io/" rel="noopener noreferrer"&gt;Tansu&lt;/a&gt; before, and here’s &lt;a href="https://www.infoq.com/news/2026/03/tansu-stateless-kafka-compatible/" rel="noopener noreferrer"&gt;another write-up of it&lt;/a&gt;, covering a talk by its author at QCon London 2026.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Stream Processing
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Feldera’s Mihai Budiu explains &lt;a href="https://www.feldera.com/blog/why-incremental-aggregates-are-difficult---part-1" rel="noopener noreferrer"&gt;Why incremental aggregates are difficult&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 My colleague Gustavo de Morais has shared his slides from Flink Forward 2025: &lt;a href="https://noti.st/gustavodemorais/Qt8UHe/the-big-state-monster-taming-state-size-in-multi-way-joins-with-flip-516" rel="noopener noreferrer"&gt;The &lt;strong&gt;Big State&lt;/strong&gt; Monster: Taming State Size in Multi-Way Joins with FLIP-516&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A nice step-by-step guide from the Apache Gravitino project showing how to &lt;a href="https://dev.to/gravitino/using-gravitino-with-apache-flink-for-streaming-25n9"&gt;use Gravitino with Apache Flink&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The recording of LinkedIn’s Stream Processing meetup (February 2026) is now available with three good talks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://youtu.be/4PZYIuMR3GE?si=BSgEr4lOdafPMmqa&amp;amp;t=136" rel="noopener noreferrer"&gt;Kafka @ Walmart - Consumer QoS: Lag-Sorted Assignment + Partition Priority Filtering&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://youtu.be/4PZYIuMR3GE?si=2tHJajvHNUxJbF9S&amp;amp;t=2205" rel="noopener noreferrer"&gt;Operating Postgres Change Data Capture at Massive Scale&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://youtu.be/4PZYIuMR3GE?si=dKmX618Ju9gmHz6k&amp;amp;t=4200" rel="noopener noreferrer"&gt;Powering Stateful Joins at Scale with Flink SQL at LinkedIn&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;A very nice tool from Tom Cooper for tracking KIPs and FLIPs: &lt;a href="https://ossip.dev/" rel="noopener noreferrer"&gt;OSSIP&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;My colleague Florian Eiden has published &lt;a href="https://github.com/Fleid/flink.unittest" rel="noopener noreferrer"&gt;flink-unittest&lt;/a&gt;, a Python unit testing framework for Flink SQL.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Gordon Murray built a &lt;a href="https://github.com/gordonmurray/flink-connector-iggy" rel="noopener noreferrer"&gt;Flink Connector for Apache Iggy&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Zalando’s Maryna Kryvko looks at &lt;a href="https://engineering.zalando.com/posts/2026/03/why-we-ditched-flink-table-api-joins-cutting-state.html" rel="noopener noreferrer"&gt;the impact of joins using the Table API on Flink’s state store&lt;/a&gt; in 1.20, and optimisations that they made to reduce it by 75%. They rightly note that improvements are already available in Flink 2.1.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Analytics
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reddit’s Neven Miculinic describes &lt;a href="https://www.reddit.com/r/RedditEng/comments/1rpbk7u/olap_is_all_you_need_how_we_built_reddits_logging/" rel="noopener noreferrer"&gt;how they built their logging platform around Clickhouse and Kafka&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://duckdb.org/2026/03/23/announcing-duckdb-151" rel="noopener noreferrer"&gt;DuckDB 1.5.1&lt;/a&gt; includes support for Lance lakehouse format, improvements to Iceberg v3 support, and more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Autotrader’s Tom Armitage has a very cool article looking at data visualisation and how one goes about &lt;a href="https://medium.com/autotrader-engineering/lines-on-maps-a-foray-into-splitting-the-uk-into-ten-perfectly-balanced-regions-6f667ccef4b5" rel="noopener noreferrer"&gt;splitting the UK into ten perfectly balanced regions&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 My colleague Elijah Meeks (who did all the cool animations on &lt;a href="https://flink-watermarks.wtf/" rel="noopener noreferrer"&gt;Flink Watermarks…WTF&lt;/a&gt;) has a dataviz library which he rewrote to be streaming-first. You can find the Semiotic repo &lt;a href="https://github.com/nteract/semiotic" rel="noopener noreferrer"&gt;here&lt;/a&gt; and lots of very pretty examples on the &lt;a href="https://semiotic.nteract.io/" rel="noopener noreferrer"&gt;main site&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kartik Khare from StarTree has a good two part series looking at real-time ingestion in Apache Pinot (&lt;a href="https://startree.ai/resources/inside-the-flight-path-of-real-time-ingestion-in-apache-pinot/" rel="noopener noreferrer"&gt;part 1&lt;/a&gt;, &lt;a href="https://startree.ai/resources/inside-the-flight-path-of-real-time-ingestion-in-apache-pinot-part-2/" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prithwish Nath took a look at &lt;a href="https://levelup.gitconnected.com/the-practical-limits-of-duckdb-on-commodity-hardware-3d6d6cf2bdde" rel="noopener noreferrer"&gt;how far you can push DuckDB on Commodity Hardware&lt;/a&gt;, whilst Gábor Szárnyas tried out &lt;a href="https://duckdb.org/2026/03/11/big-data-on-the-cheapest-macbook" rel="noopener noreferrer"&gt;DuckDB on the new MacBook Neo&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Platforms, Architectures, and Modelling
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Swiggy’s Sundaram Dubey looks at the demand for real-time dashboards at Swiggy, and &lt;a href="https://bytes.swiggy.com/how-we-re-architected-swiggys-real-time-bi-for-sub-second-latency-with-microsoft-fabric-52bdbf55d180" rel="noopener noreferrer"&gt;how they implemented it&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A good primer from Milan Mosny on &lt;a href="https://medium.com/response42/ontology-taxonomy-data-model-context-graph-friends-56a605e14355" rel="noopener noreferrer"&gt;Ontology, Taxonomy, Data Model, Context Graph &amp;amp; Friends&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A recording of Anurag Kale’s QCon talk about their &lt;a href="https://www.infoq.com/presentations/data-mesh-horse-powertrain/" rel="noopener noreferrer"&gt;experience with Data Mesh&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Chris Hillman has a great post here &lt;a href="https://ghostinthedata.info/posts/2026/2026-03-14-your-data-model-isnt-broken-part-1/" rel="noopener noreferrer"&gt;Your Data Model Isn’t Broken, Part I: Why Refactoring Beats Rebuilding&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yoshnee Raveendran from Spotify explains &lt;a href="https://engineering.atspotify.com/2026/3/inside-the-archive-2025-wrapped" rel="noopener noreferrer"&gt;how they generate "Spotify Wrapped"&lt;/a&gt; for each user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Nice write up from Edijs Drezovs and team describing in good detail the &lt;a href="https://aws.amazon.com/blogs/big-data/building-a-modern-lakehouse-architecture-yggdrasil-gamings-journey-from-bigquery-to-aws/" rel="noopener noreferrer"&gt;lakehouse that Yggdrasil Gaming built&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔥 Joe Reis' &lt;a href="https://practicaldatamodeling.substack.com/" rel="noopener noreferrer"&gt;Practical Data Modeling&lt;/a&gt; book is nearing completion, and you can access significant chunks of the early chapters already on his Substack.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/the-era-of-the-mixed-model-artist" rel="noopener noreferrer"&gt;Ch 1 - The Era of the Mixed Model Artist&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/what-data-modeling-is-and-is-not" rel="noopener noreferrer"&gt;Ch 2 - What Data Modeling Is and Is Not&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/no-free-lunch-the-debt-the-excuses" rel="noopener noreferrer"&gt;Ch 3 - No Free Lunch: The Debt, The Excuses, and The Reality of Data Modeling&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/beyond-rows-and-columns-the-five" rel="noopener noreferrer"&gt;Ch 4 - Beyond Rows and Columns: The Five Forms of Data&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/entities-instances-and-identifiers-b5f" rel="noopener noreferrer"&gt;Ch 5 - Entities, Instances, and Identifiers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/ch-6-attributes-describing-the-entity" rel="noopener noreferrer"&gt;Ch 6 - Attributes: Describing the Entity&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/relationships-tying-it-together" rel="noopener noreferrer"&gt;Ch 7 - Relationships: Tying It Together&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/ch-8-grain-getting-the-level-right" rel="noopener noreferrer"&gt;Ch 8 - Grain: Getting the Level Right&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://practicaldatamodeling.substack.com/p/ch-9-counting-and-aggregation-controlling" rel="noopener noreferrer"&gt;Ch 9 - Counting and Aggregation: Controlling the Grain&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href="https://www.vldb.org/pvldb/vol14/p3083-edara.pdf" rel="noopener noreferrer"&gt;A VLDB paper from 2021&lt;/a&gt; by Google’s Pavan Edara and Mosha Pasumansky describing BigQuery’s metadata management system and how it supports performance at scale.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Tim Castillo has a good piece discussing the three layers in which business logic lives (AI context, semantic, and dimensional model): &lt;a href="https://loglevelinfo.substack.com/p/analytics-engineerings-unfinished" rel="noopener noreferrer"&gt;Analytics Engineering’s Unfinished Work&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Data Engineering, Pipelines, and CDC
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Confluent have just launched their &lt;a href="https://github.com/confluentinc/dbt-confluent" rel="noopener noreferrer"&gt;dbt adapter for Confluent Cloud Flink SQL&lt;/a&gt; (it also works with Apache Flink too via &lt;a href="https://github.com/awmatheson/flink-proxy-gateway" rel="noopener noreferrer"&gt;this gateway&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 I dug into what Claude Code could do with dbt and concluded that &lt;a href="https://rmoff.net/2026/03/11/claude-code-isnt-going-to-replace-data-engineers-yet/" rel="noopener noreferrer"&gt;Claude Code isn’t going to replace data engineers (yet)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Following on from my baby-steps above, here’s &lt;a href="https://blog.rittmananalytics.com/introducing-the-wire-framework-the-secret-sauce-behind-our-ai-augmented-analytics-project-7e7a2b50d9a3" rel="noopener noreferrer"&gt;a thorough examination&lt;/a&gt; from Mark Rittman of what it looks like when you actually put LLMs to work alongside you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Goldsky’s Jeff Ling has details of how they &lt;a href="https://goldsky.com/blog/making-historical-blockchain-backfills-faster" rel="noopener noreferrer"&gt;got a 12x improvement&lt;/a&gt; by reading data directly from Clickhouse as Arrow instead of Kafka (Warpstream) with Avro.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chris Gambill puts into clear words what anyone my age has also been thinking: &lt;a href="https://open.substack.com/pub/gambilldataengineering/p/medallion-architecture-isnt-as-new" rel="noopener noreferrer"&gt;Medallion Architecture Isn’t As New As You Think&lt;/a&gt;. I took a similar run at it previously, looking at &lt;a href="https://rmoff.net/2022/10/02/data-engineering-in-2022-architectures-terminology/#_reference_architectures" rel="noopener noreferrer"&gt;how Oracle’s Big Data (lol, this was 2016, ok) reference architecture compared to it&lt;/a&gt; too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hila Turi from Riskified has &lt;a href="https://medium.com/riskified-technology/the-one-view-odyssey-architecture-aggregations-and-1-billion-rows-in-snowflake-21008dcca2fd" rel="noopener noreferrer"&gt;a fascinating article&lt;/a&gt; about their 1 billion row table in Snowflake for 3rd-parties, and how they handled the data modelling, cost, and retroactive changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A set of three excellent, hands-on, posts about building really high-quality data pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 The nitty-gritty of actual data engineering isn’t in the fancy dashboard for the stakeholders, but making sure that you’re actually processing all the data and not losing any. Robert Sahlin has a great article all about &lt;a href="https://robertsahlin.substack.com/p/your-pipeline-succeeded-your-data" rel="noopener noreferrer"&gt;monitoring for silent data loss&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🔥 As well as guarding against data loss, a data engineer also needs to have a clear picture of the performance of their estate, which Rodrigo Molina describes in this practical article looking at &lt;a href="https://medium.com/@molina.rodrigo/measuring-latency-in-data-platforms-a2ad48ee16f9" rel="noopener noreferrer"&gt;Measuring Latency in Data Platforms&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;🔥 Good stuff from Jeremy Chia and Justina Šakalytė at Vinted looking at &lt;a href="https://vinted.engineering/2026/03/11/risk-based-testing/" rel="noopener noreferrer"&gt;how they handle data quality issues&lt;/a&gt; without stalling their pipelines. Also available as a &lt;a href="https://youtu.be/tNZMm4KTjTc?si=iDknJydAjqUDA7In&amp;amp;t=16" rel="noopener noreferrer"&gt;recording&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;A practical primer from Ben Rogojan (a.k.a. SeattleDataGuy) on &lt;a href="https://seattledataguy.substack.com/p/full-refresh-vs-incremental-pipelines" rel="noopener noreferrer"&gt;Full Refresh vs Incremental Pipelines&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Joe Reis mulls over the &lt;a href="https://joereis.substack.com/p/the-job-market-isnt-dead-but-it-seems" rel="noopener noreferrer"&gt;state of the data engineering job market&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Dan Beach has a list of &lt;a href="https://dataengineeringcentral.substack.com/p/data-engineering-blogs-to-follow" rel="noopener noreferrer"&gt;Data Engineering Blogs to Follow&lt;/a&gt; that’s very good. But then I would say that, cos I’m on it ;)&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Phi Vu Trinh has an &lt;a href="https://vutr.substack.com/p/how-did-youtube-engineers-build-cicd" rel="noopener noreferrer"&gt;excellent writeup&lt;/a&gt; of &lt;a href="https://www.vldb.org/pvldb/vol18/p4887-yang.pdf" rel="noopener noreferrer"&gt;a VLDB paper&lt;/a&gt; from engineers at YouTube describing how they do CI/CD for data pipelines.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Open Table Formats (OTF), Catalogs, Lakehouses etc.
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Details of &lt;a href="https://github.com/relytcloud/pg_duckpipe" rel="noopener noreferrer"&gt;pg_duckpipe&lt;/a&gt;, which streams Postgres tables into DuckLake.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grigorii Osipov considers whether &lt;a href="https://medium.com/datamindedbe/ducklake-wants-to-fix-the-lakehouse-can-it-fd0a65880ce6" rel="noopener noreferrer"&gt;DuckLake can fix the Lakehouse&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A good article from Reetika Agrawal looking at &lt;a href="https://medium.com/@reetikaagrawal/iceberg-branches-and-tags-with-prestodb-2501ce87266d" rel="noopener noreferrer"&gt;Iceberg Branches and Tags with Presto&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you need to read and write across table formats (Iceberg/Delta/Hudi) then you’ll be interested in this article from Junaid Effendi in which he explains &lt;a href="https://www.junaideffendi.com/p/how-delta-uniform-works" rel="noopener noreferrer"&gt;how Delta UniForm works&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 My colleague Gunnar Morling has released v1.0 of &lt;a href="https://www.morling.dev/blog/hardwood-new-parser-for-apache-parquet/" rel="noopener noreferrer"&gt;Hardwood: A New Parser for Apache Parquet&lt;/a&gt;. Much as we all love shovelling a ton of Hadoop dependencies every time we want to use Parquet, something tells me that this approach might be a better one :D&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Some lovely Iceberg deep-dive detail and performance analysis from Chris Douglas &lt;a href="https://cdouglas.github.io/posts/2026/03/catalog" rel="noopener noreferrer"&gt;part 1&lt;/a&gt; / &lt;a href="https://cdouglas.github.io/posts/2026/03/providercatalog" rel="noopener noreferrer"&gt;part 2&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I wrote previously about &lt;a href="https://rmoff.net/2026/01/14/alternatives-to-minio-for-single-node-local-s3/" rel="noopener noreferrer"&gt;alternatives to the now-abandoned MinIO project&lt;/a&gt;. One of my favourite replacements is SeaweedFS, which &lt;a href="https://github.com/seaweedfs/seaweedfs/wiki/S3-Table-Bucket" rel="noopener noreferrer"&gt;now supports Amazon S3 Table&lt;/a&gt; so you can &lt;a href="https://github.com/seaweedfs/seaweedfs/wiki/SeaweedFS-Iceberg-Catalog" rel="noopener noreferrer"&gt;write Iceberg tables directly&lt;/a&gt; to it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Andrew Lamb’s recent talk, "Column Storage for the AI Era", looked at the proliferation of column storage formats and what is driving it &lt;a href="https://www.youtube.com/watch?v=k9uhw7yqPsQ" rel="noopener noreferrer"&gt;recording&lt;/a&gt; / &lt;a href="https://docs.google.com/presentation/d/19F-XvNJ8sgIpIeIduA3PhbsWp4pC-P632J2eJV1cLG8/edit?slide=id.g33d6952b95a_0_1014#slide=id.g33d6952b95a_0_1014" rel="noopener noreferrer"&gt;slides&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  RDBMS
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Elizabeth Christensen (a.k.a. SQLLIZ) recently published &lt;em&gt;a whole day’s worth&lt;/em&gt; of &lt;a href="https://github.com/Snowflake-Labs/postgres-full-day-training" rel="noopener noreferrer"&gt;free Postgres training material&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A summary from InfoQ’s Leela Kumili of Uber’s work &lt;a href="https://www.infoq.com/news/2026/03/uber-mysql-uptime-consensus/" rel="noopener noreferrer"&gt;improving their MySQL Cluster uptime&lt;/a&gt; with consensus replication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 A nice troubleshooting diagnostics story from Anthonin Bonnefoy at DataDog about &lt;a href="https://www.datadoghq.com/blog/engineering/debugging-postgres-performance/" rel="noopener noreferrer"&gt;Postgres upserts that don’t update but still write&lt;/a&gt; and the problems this can cause at scale.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Staying with DataDog, they recently made available a free online &lt;a href="https://explain.datadoghq.com/?dbms=postgres" rel="noopener noreferrer"&gt;explain plan visualizer&lt;/a&gt;, which Tanel Poder put through its paces with &lt;a href="https://tanelpoder.com/posts/testing-datadog-plan-visualizer-with-oracle-execution-plans/" rel="noopener noreferrer"&gt;Oracle execution plans&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 I love this post from Nile’s Jan Nidzwetzki about his tool &lt;a href="https://jnidzwetzki.github.io/2026/03/04/pg-plan-alternatives.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_plan_alternatives&lt;/code&gt;, which uses eBPF for tracing Postgres query plan alternatives&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fun stuff from Radim Marek showing &lt;a href="https://boringsql.com/posts/portable-stats/" rel="noopener noreferrer"&gt;how to manipulate Postgres optimizer stats&lt;/a&gt; to help evaluate query behaviour in different environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ergest Xheblati explains why &lt;a href="https://sqlpatterns.com/p/should-you-still-learn-sql-in-the" rel="noopener noreferrer"&gt;you should still learn SQL&lt;/a&gt; (despite LLMs being able to do a rather good job of writing it for you these days).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Etsy’s Ella Yarmo-Gray describes how they &lt;a href="https://www.etsy.com/codeascraft/migrating-etsyas-database-sharding-to-vitess" rel="noopener noreferrer"&gt;migrated Etsy’s database sharding to Vitess&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A couple of good blog posts from the folk at PlanetScale. First a serious one: Ben Dicken with an excellent interactive explainer of &lt;a href="https://planetscale.com/blog/database-transactions" rel="noopener noreferrer"&gt;Database Transactions&lt;/a&gt;. Second, a silly one, in which Nick Van Wiggeren shows how to do &lt;a href="https://planetscale.com/blog/video-conferencing-with-postgres" rel="noopener noreferrer"&gt;Video Conferencing with Postgres&lt;/a&gt; (seriously).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  AI
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I warned you &lt;a href="https://rmoff.net/2025/09/30/interesting-links-september-2025/#_ai" rel="noopener noreferrer"&gt;previously&lt;/a&gt;…this AI stuff is here to stay, and it’d be short-sighted to think otherwise.&lt;/em&gt; &lt;em&gt;As I read and learn more about it, I’m going to share interesting links (the clue is in the blog post title) that I find—whilst trying to avoid the breathless hype and slop.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 &lt;a href="https://pca.st/episode/73dcda3b-1c2b-46c0-9946-3c9542d48159" rel="noopener noreferrer"&gt;A fascinating podcast episode of Dwarkesh Podcast with Anthropic’s Dario Amodei&lt;/a&gt;. Pleasantly BS-free and easy enough for a non-expert like me to not feel completely lost listening to :)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Big Picture &amp;amp; Culture
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
There’s a theme running through this section. I’m going to crudely summarise it thus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AI is here, and it’s a massive opportunity for us as individuals in the software industry.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There are many reasons we recoil against AI and that’s natural and understandable—but don’t mistake these for reasons not to embrace it (see first point).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;🔥 Brittany Ellich’s blog post was my favourite one this month with &lt;a href="https://brittanyellich.com/embrace-the-uncertainty/" rel="noopener noreferrer"&gt;this excellent post&lt;/a&gt;, from which I’ll quote verbatim:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Nobody knows what the future of software engineering looks like, and that’s incredibly uncomfortable. But instead of waiting for someone to hand us the answer, I think the move is to &lt;strong&gt;embrace the uncertainty&lt;/strong&gt;, because these moments of deep uncertainty have historically been moments of &lt;strong&gt;extraordinary opportunity&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Annie Vella has an accessible and unfussy way of clearly communicating, and I really enjoyed both of her recent posts, &lt;a href="https://annievella.com/posts/finding-comfort-in-the-uncertainty/" rel="noopener noreferrer"&gt;Finding Comfort in the Uncertainty&lt;/a&gt; and &lt;a href="https://annievella.com/posts/the-middle-loop/" rel="noopener noreferrer"&gt;The Middle Loop&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I wrote a ranty post to get stuff off my chest, but not nearly as articulately nor considered as many others in this section: &lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;AI will f**k you up if you’re not on board&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chris Gambill’s post is in a similar vein to mine: &lt;a href="https://open.substack.com/pub/gambilldataengineering/p/if-you-arent-using-ai-you-are-already" rel="noopener noreferrer"&gt;If You Aren’t Using AI, You Are Already Legacy&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Nolan Lawson’s post &lt;a href="https://nolanlawson.com/2026/02/07/we-mourn-our-craft/" rel="noopener noreferrer"&gt;We mourn our craft&lt;/a&gt; may sound hyperbolic but there is genuinely a sense of grief that I sense amongst engineers at something that is being taken from them whether they like it or not (and regardless of whether &lt;em&gt;they themselves&lt;/em&gt; adopt it or not).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The concept of grief and loss and its corresponding stages (anger, denial, bargaining, etc) actually fit well with many of the emotions that I’m seeing play out at the moment, and Andrew Murphy explores it in exactly this framing: &lt;a href="https://andrewmurphy.io/blog/the-five-stages-of-losing-our-craft" rel="noopener noreferrer"&gt;The five stages of losing our craft&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some of the folk in the "denial" camp need to read &lt;a href="https://charitydotwtf.substack.com/p/my-hypothetical-srecon26-keynote" rel="noopener noreferrer"&gt;Charity Majors' latest post&lt;/a&gt;, in which she addresses head-on the point that AI now is not what it was when people dismissed it &lt;em&gt;even last year&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;When the facts change, I change my mind&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Outside of the pondering and realisation that the world has changed for software engineers comes the refreshing and entertaining reactions against one of the downsides of AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 Kirill Bobrov - &lt;a href="https://luminousmen.com/post/stop-feeding-me-ai-slop/" rel="noopener noreferrer"&gt;Stop Feeding Me AI Slop&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Brent Ozar - &lt;a href="https://www.brentozar.com/archive/2026/03/im-not-gonna-waste-time-debunking-crap-on-linkedin/" rel="noopener noreferrer"&gt;I’m Not Gonna Waste Time Debunking Crap on LinkedIn&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;See also &lt;a href="https://en.wikipedia.org/wiki/Brandolini%27s_law" rel="noopener noreferrer"&gt;Brandolini’s law&lt;/a&gt;)&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open Source Licensing and AI Policies
&lt;/h2&gt;

&lt;p&gt;The ability of LLMs to write code has blown wide open the debate on software licensing and what constitutes a 'copy'.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://simonwillison.net/2026/Mar/5/chardet/" rel="noopener noreferrer"&gt;Simon Willison analyses&lt;/a&gt; the recent events on the chardet project that ignited the latest round of this debate and &lt;a href="https://www.theregister.com/2026/03/06/ai_kills_software_licensing/" rel="noopener noreferrer"&gt;Thomas Claburn in El Reg&lt;/a&gt; has commentary from Bruce Perens (who created the original &lt;a href="https://en.wikipedia.org/wiki/The_Open_Source_Definition" rel="noopener noreferrer"&gt;Open Source Definition&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;antirez has an excellent &lt;a href="https://antirez.com/news/162" rel="noopener noreferrer"&gt;commentary piece&lt;/a&gt; on the subject, as well as details of &lt;a href="http://antirez.com/news/160" rel="noopener noreferrer"&gt;implementing a clean room Z80 / ZX Spectrum emulator with Claude Code&lt;/a&gt; (not specific to OSS per se, but interesting to understand the concepts being used).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, projects and foundations are trying to rapidly keep up with what LLMs can do, and codify what role they should play in contributions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Phil Eaton - &lt;a href="https://theconsensus.dev/p/2026/03/02/source-available-projects-and-their-ai-contribution-policies.html" rel="noopener noreferrer"&gt;Source-available projects and their AI contribution policies&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kate Holterhoff - &lt;a href="https://redmonk.com/kholterhoff/2026/02/26/generative-ai-policy-landscape-in-open-source/" rel="noopener noreferrer"&gt;The Generative AI Policy Landscape in Open Source&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building with AI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Thoughtworks' Rahul Garg discusses &lt;a href="https://martinfowler.com/articles/reduce-friction-ai/context-anchoring.html" rel="noopener noreferrer"&gt;Context Anchoring&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cat Hicks has a neat idea for Claude Code - a &lt;a href="https://github.com/DrCatHicks/learning-opportunities" rel="noopener noreferrer"&gt;&lt;code&gt;learning-opportunities&lt;/code&gt; skill&lt;/a&gt; that has it &lt;em&gt;teach you&lt;/em&gt; and build your expertise of what you’re building with it. I tried something very (very) crudely along the same lines in &lt;a href="https://rmoff.net/2026/02/20/claude-the-instructor/" rel="noopener noreferrer"&gt;Claude the Instructor&lt;/a&gt; and found it very useful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hajime Takeda has a thorough explainer of &lt;a href="https://towardsdatascience.com/how-to-build-a-production-ready-claude-code-skill/" rel="noopener noreferrer"&gt;How to Build a Production-Ready Claude Code Skill&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Daniel D. McKinnon has &lt;a href="https://www.ddmckinnon.com/2026/02/11/my-%f0%9f%8c%b6-take-on-vibe-coding-for-pms/" rel="noopener noreferrer"&gt;some home-truths for PMs having fun vibe-coding features for their products&lt;/a&gt;, such as:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If the feature is actually important, fix the system for prioritization (your real job) rather than circumventing it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He’s also got good advice on &lt;a href="https://www.ddmckinnon.com/2025/03/30/show-dont-tell-a-llama-pms-guide-to-writing-genai-evals/" rel="noopener noreferrer"&gt;writing GenAI evals&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Geoff Cisler and team at Whoop have written about &lt;a href="https://engineering.prod.whoop.com/ai-evaluation-framework" rel="noopener noreferrer"&gt;their use of evaluations&lt;/a&gt; to test and track the performance of their Agents.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Joe Hellerstein discusses &lt;a href="https://jhellerstein.github.io/blog/ai-mixed-consistency/" rel="noopener noreferrer"&gt;AI and the Mixed-Consistency Future&lt;/a&gt; (files instead of databases isn’t gonna work for everything).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rajanikant Vellaturi discusses &lt;a href="https://blog.dataengineerthings.org/rag-is-a-data-engineering-problem-heres-how-to-build-on-5013fa2931d1" rel="noopener noreferrer"&gt;RAG and Data Engineering&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As people get past the "omg this is magic" initial experience of coding agents, understanding exactly &lt;em&gt;what&lt;/em&gt; they’re doing is often the next question. &lt;a href="https://github.com/papercomputeco/tapes" rel="noopener noreferrer"&gt;tapes&lt;/a&gt; is one tool that &lt;a href="https://johncodes.com/archive/2026/02-09-introducing-tapes/" rel="noopener noreferrer"&gt;aims to address this&lt;/a&gt;, sitting between your agent and the model provider and giving a bunch of instrumentation and insight.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fascinating exploration from Mitchell Turner of the idea of &lt;em&gt;promptware&lt;/em&gt;: &lt;a href="https://www.originhq.com/blog/brainworm" rel="noopener noreferrer"&gt;Brainworm - Hiding in Your Context Window&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The MCP Debate
&lt;/h3&gt;

&lt;p&gt;MCP was hot (you don’t need CLI access!), and then it wasn’t (it burns context!), and now…It Depends ;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Allen Hutchison has a good article explaining why &lt;a href="https://allen.hutchison.org/2026/03/14/mcp-isnt-dead-you-just-arent-the-target-audience/" rel="noopener noreferrer"&gt;MCP Isn’t Dead You Just Aren’t the Target Audience&lt;/a&gt; (tl;dr MCP is still vastly important for agents not running with CLI access).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Samir Amzani - &lt;a href="https://www.apideck.com/blog/mcp-server-eating-context-window-cli-alternative" rel="noopener noreferrer"&gt;Your MCP Server Is Eating Your Context Window. There’s a Simpler Way&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Charles Chen - &lt;a href="https://chrlschn.dev/blog/2026/03/mcp-is-dead-long-live-mcp/" rel="noopener noreferrer"&gt;MCP is Dead; Long Live MCP!&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ben Davis - &lt;a href="https://x.com/davis7/status/2032991801876033739" rel="noopener noreferrer"&gt;&lt;em&gt;"The current 'MCP is dead discourse' is my current favorite example of the really annoying "simplicity" brainrot that’s been plaguing tech for a while"&lt;/em&gt;&lt;/a&gt; (Twitter thread).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI in Practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 Instacart’s Moein Hasani &lt;a href="https://tech.instacart.com/our-early-journey-to-transform-instacarts-discovery-recommendations-with-llms-cf4591a8602b" rel="noopener noreferrer"&gt;describes how they improved their recommendations engine&lt;/a&gt; using tools and techniques including LLMs and Evals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tao Ruangyam at Zalando has a good blog post about &lt;a href="https://engineering.zalando.com/posts/2026/03/search-quality-assurance-with-llm-judge.html" rel="noopener noreferrer"&gt;using LLM-as-judge to ensure the quality of their search results&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fascinating detail of how Uber are using AI for development (&lt;a href="https://newsletter.pragmaticengineer.com/p/how-uber-uses-ai-for-development" rel="noopener noreferrer"&gt;blog&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=i1tZN41VKcE" rel="noopener noreferrer"&gt;talk&lt;/a&gt;), as well as &lt;a href="https://www.uber.com/en-AU/blog/automate-design-specs/" rel="noopener noreferrer"&gt;an agentic system for writing design specs&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tan Wang at Pinterest has practical details of &lt;a href="https://medium.com/pinterest-engineering/building-an-mcp-ecosystem-at-pinterest-d881eb4c16f1" rel="noopener noreferrer"&gt;how they’re implementing an MCP ecosystem&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overview from Aman Gupta and Daniel Braithwaite at Nubank of how they’re &lt;a href="https://building.nubank.com/building-ai-agents-for-127-million-customers/" rel="noopener noreferrer"&gt;building AI agents, doing evals, and semantic versioning of prompts&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Surabhi Gupta details how Klaviyo are approaching &lt;a href="https://klaviyo.tech/the-four-levels-of-ai-driven-engineering-our-approach-at-klaviyo-12e50b9f04a1" rel="noopener noreferrer"&gt;AI-driven engineering&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sneh Agrawal and team at Grab built AI agents to help them &lt;a href="https://engineering.grab.com/from-firefighting-to-building" rel="noopener noreferrer"&gt;support users of their data warehouse&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you’re still not convinced that AI is out there &lt;em&gt;right now&lt;/em&gt; delivering value for businesses, check out this list from Allen Hutchison of &lt;a href="https://allen.hutchison.org/2026/03/21/agents-in-the-wild/" rel="noopener noreferrer"&gt;real-world AI agent examples&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  And finally…
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Nothing to do with data, but stuff that I’ve found interesting or has made me think (or smile).&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔥 A great piece from Chris Hillman, the tldr of which is the title (but read the article too!) &lt;a href="https://ghostinthedata.info/posts/2026/2026-03-18-friendship/" rel="noopener noreferrer"&gt;Your Friends Will Be There for You. Your Work Won’t.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.dancarlin.com/" rel="noopener noreferrer"&gt;Dan Carlin&lt;/a&gt; (he of &lt;a href="https://www.dancarlin.com/hardcore-history-series/" rel="noopener noreferrer"&gt;Hardcore History&lt;/a&gt;) is one of my absolute favourite podcasters. His style of presenting and depth of historical material is just wonderful. That’s why &lt;a href="https://substack.com/home/post/p-192140717" rel="noopener noreferrer"&gt;this post&lt;/a&gt; hit me harder than I’d expected:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;I think it is just hard to talk with passion and enthusiasm […] about events thousands of years ago on the history show when such momentous ones are occurring to all of us right now. […] We ARE living through absolutely momentous times (and dangerous ones). Don’t allow yourself to be gaslit about that. Any fan of History can see it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The concept of "managing up" is important at work, and Lara Hogan describes it well in &lt;a href="https://us2.campaign-archive.com/?u=1f50ce22f02ed0ffc80c10aff&amp;amp;id=2f390b47a1" rel="noopener noreferrer"&gt;Managing your manager&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔥 Almog Gavra has created a very nifty tool called &lt;a href="https://github.com/agavra/yuzudraw" rel="noopener noreferrer"&gt;YuzuDraw&lt;/a&gt; which lets you create—and edit—ASCII-art style diagrams. It works with coding agents too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3p3wt5ccur0f7cxoydx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3p3wt5ccur0f7cxoydx.webp" alt="yuzudraw l" width="396" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://translate.kagi.com/" rel="noopener noreferrer"&gt;Kagi Translate&lt;/a&gt; includes a target language of &lt;strong&gt;LinkedIn Speak ;)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88htbexon8knvonq7tx8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88htbexon8knvonq7tx8.webp" alt="kagi l" width="800" height="802"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In all seriousness though, &lt;a href="https://help.kagi.com/kagi/company/" rel="noopener noreferrer"&gt;Kagi&lt;/a&gt; is a set of applications that I rather like for its focus on functionality and respect, not growth hacking and enshittification. Their &lt;a href="https://kagi.com/smallweb/" rel="noopener noreferrer"&gt;small web&lt;/a&gt; is a joyful reminder of how the internet used to be before walled gardens and karma-farming, and the &lt;a href="https://kagi.com/" rel="noopener noreferrer"&gt;search engine&lt;/a&gt; lets you wrest back control of your search results from the spam and shit (sorry, "sponsored listings").&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 &lt;a href="https://www.calebleak.com/posts/dog-game/" rel="noopener noreferrer"&gt;I Taught My Dog to Vibe Code Games&lt;/a&gt;. No notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unresolved directive in &amp;lt;stdin&amp;gt; - include::../../asciidoc-includes/il-footer.adoc[]&lt;/p&gt;

</description>
      <category>interestinglinks</category>
    </item>
    <item>
      <title>Evaluating Claude's dbt Skills: Building an Eval from Scratch</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Fri, 13 Mar 2026 18:05:22 +0000</pubDate>
      <link>https://dev.to/rmoff/evaluating-claudes-dbt-skills-building-an-eval-from-scratch-30a4</link>
      <guid>https://dev.to/rmoff/evaluating-claudes-dbt-skills-building-an-eval-from-scratch-30a4</guid>
      <description>&lt;p&gt;I wanted to explore the extent to which Claude Code could build a data pipeline using dbt without iterative prompting. What difference did skills, models, and the prompt itself make? I’ve written &lt;a href="https://rmoff.net/2026/03/11/claude-code-isnt-going-to-replace-data-engineers-yet/" rel="noopener noreferrer"&gt;in a separate post&lt;/a&gt; about what I found (&lt;em&gt;yes it’s good; no it’s not going to replace data engineers, yet&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;In this post I’m going to show how I ran these tests (with Claude) and analysed the results (using Claude), including a pretty dashboard (created by Claude):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhy3bjk16potalke21ucd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhy3bjk16potalke21ucd.webp" alt="dbt EVAL dashboard showing test results across prompt" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The Test
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Can Claude Code build a production-ready dbt project? (is AI going to take data engineers' jobs?)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminology check:&lt;/strong&gt;&lt;br&gt;
I am not, as you can already tell, an expert at building and running this kind of controlled test. I’ve adopted my own terminology to refer to elements of what I was doing, which may or may not match what someone who knows what they’re doing would use :)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scenario: What are we testing (specific Prompt + Skill combination)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configuration: Scenario + Model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run: Execution of a configuration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Validation: Deterministic checking of the dbt project&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;e.g. Does the project build? Are there any errors? Are there &lt;code&gt;staging&lt;/code&gt; and &lt;code&gt;mart&lt;/code&gt; folders? etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Judging: Using an LLM to assess the quality of the dbt project that was built&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trial: One execution of a judge against a run of a configuration&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;I created the test to run independently, with no 'human in the loop'. That is, Claude Code was free to run whatever it wanted to in order to achieve the task I’d given it.&lt;/p&gt;

&lt;p&gt;I explored permutations of two dimensions in my scenarios: prompt (x2) and skills (x3). Each of these I then iterated over with different models.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Prompt&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rich&lt;/strong&gt; (lots of background data analysis, specifics on what features to include, etc)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;View prompt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’ve explored and built pipelines for the UK Environment Agency flood monitoring API. Here’s my analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Data exploration: &lt;a href="https://rmoff.net/2025/02/28/exploring-uk-environment-agency-data-in-duckdb-and-rill/" rel="noopener noreferrer"&gt;https://rmoff.net/2025/02/28/exploring-uk-environment-agency-data-in-duckdb-and-rill/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pipeline build: &lt;a href="https://rmoff.net/2025/03/20/building-a-data-pipeline-with-duckdb/" rel="noopener noreferrer"&gt;https://rmoff.net/2025/03/20/building-a-data-pipeline-with-duckdb/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build a dbt project using DuckDB for this data using idiomatic patterns and good practices. Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Proper staging → dim/fact data model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handle known data quality issues (see blog posts for details)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SCD type 2 snapshots for station metadata&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Historical backfill from CSV archives (see &lt;a href="https://environment.data.gov.uk/flood-monitoring/archive" rel="noopener noreferrer"&gt;https://environment.data.gov.uk/flood-monitoring/archive&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Documentation and tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Source freshness checks&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run &lt;code&gt;dbt build&lt;/code&gt; to verify your work. If it fails, fix the errors and re-run until it passes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimal&lt;/strong&gt; (here’s an API, build me analytics)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;View prompt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The UK Environment Agency publishes flood monitoring data, see &lt;a href="https://environment.data.gov.uk/flood-monitoring" rel="noopener noreferrer"&gt;https://environment.data.gov.uk/flood-monitoring&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build an idiomatic dbt project following good practices using DuckDB that ingests this data and models it for analytics.&lt;/p&gt;

&lt;p&gt;Run the project and make sure that it works. If it fails, fix the errors and re-run until it passes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Skills&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;None&lt;/li&gt;
&lt;li&gt;Single skill (&lt;a href="https://github.com/dbt-labs/dbt-agent-skills/blob/main/skills/dbt/skills/using-dbt-for-analytics-engineering/SKILL.md" rel="noopener noreferrer"&gt;Using dbt for Analytics Engineering&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I’d meant to test the full plugin, but a snafu meant I only ended up pulling in the single skill.&lt;/em&gt; &lt;em&gt;I realised this only after running the scenario in full, so expanded the test to include the full plugin as a separate scenario.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full plugin (&lt;a href="https://github.com/dbt-labs/dbt-agent-skills/tree/main" rel="noopener noreferrer"&gt;dbt Agent Skills&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Model&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Sonnet 4.5&lt;/li&gt;
&lt;li&gt;Claude Sonnet 4.6&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Execution
&lt;/h2&gt;

&lt;p&gt;One of the core things that I wanted to find out was what Claude can do &lt;em&gt;on its own&lt;/em&gt;. Having it ask for permission to do something slows things down, and asking for input defeats the point of the exercise. So I used it with the effective but spicy flag &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &lt;span class="nv"&gt;$PROMPT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was wrapped in a Docker container so that it couldn’t cause too much trouble.&lt;/p&gt;

&lt;p&gt;Claude Code writes a full transcript of its sessions to a JSONL file that usually resides in &lt;code&gt;~/.claude/&lt;/code&gt;, so for the Docker container I had that copied out into the test results too, along with the actual dbt project itself and any other artefacts from the test run. The JSONL is interesting for what it tells us about how Claude Code approaches the task, particularly on multiple runs of the same configuration. Here’s an &lt;a href="https://rmoff.net/2026/03/11/claude-code-in-action-with-dbt/" rel="noopener noreferrer"&gt;example analysis of part of a session log&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I used Claude to write a bash script that then spun up a Docker container with the correct set of configuration for the test scenario.&lt;/p&gt;

&lt;p&gt;Each run’s session log was processed to produce summary metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model_requested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model_actual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cost_usd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.420355&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;duration_ms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1175360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;718&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;43568&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache_read_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2423321&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cache_creation_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;162914&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;num_turns&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;57&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Output
&lt;/h2&gt;

&lt;p&gt;Once I’d run all of the scenarios, I had a set of results on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ tree runs &lt;span class="nt"&gt;-L1&lt;/span&gt;
runs
├── A-minimal-no-skills
├── B-rich-no-skills
├── C-minimal-with-skills
├── D-rich-with-skills
├── E-minimal-with-plugin
└── F-rich-with-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each folder had multiple models and within those, runs, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ tree runs/A-minimal-no-skills &lt;span class="nt"&gt;-L2&lt;/span&gt;
runs/A-minimal-no-skills
├── claude-opus-4-6
│&amp;nbsp;&amp;nbsp; ├── run-1
│&amp;nbsp;&amp;nbsp; ├── run-2
│&amp;nbsp;&amp;nbsp; └── run-3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and within each of those, a dbt project (assuming that Claude had done its job successfully!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ tree runs/A-minimal-no-skills/claude-opus-4-6/run-1/project/flood_monitoring &lt;span class="nt"&gt;-L1&lt;/span&gt;
runs/A-minimal-no-skills/claude-opus-4-6/run-1/project/flood_monitoring
├── analyses
├── dbt_packages
├── dbt_project.yml
├── flood_monitoring.duckdb
├── logs
├── macros
├── models
├── README.md
├── seeds
├── snapshots
├── target
└── tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we’ve got a set of dbt projects, produced by Claude Code. As part of Claude’s prompt it was instructed to iterate until they work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run &lt;code&gt;dbt build&lt;/code&gt; to verify your work. If it fails, fix the errors and re-run until it passes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So they should hopefully at least build. But are they any good?&lt;/p&gt;

&lt;p&gt;There are two ways to evaluate it: deterministically, and non-deterministically. Each has its own strengths and weaknesses, and I’ve used both.&lt;/p&gt;

&lt;h1&gt;
  
  
  Validation
&lt;/h1&gt;

&lt;p&gt;This is a shell script that encodes various checks one can do against a dbt project, such as &lt;em&gt;does it exist&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check dbt project exists&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBT_ROOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DBT_ROOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS  dbt_project.yml exists"&lt;/span&gt;
    &lt;span class="k"&gt;else
        &lt;/span&gt;&lt;span class="nv"&gt;rel_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DBT_ROOT&lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS  dbt_project.yml exists (in &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rel_path&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/)"&lt;/span&gt;
    &lt;span class="k"&gt;fi
else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL  dbt_project.yml not found"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Validation complete (no dbt project found)."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can also check for the implementation of features such as incremental materialisation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for incremental materialization&lt;/span&gt;
&lt;span class="nv"&gt;incremental_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"materialized.*=.*'incremental'&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;materialized.*=.*&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;incremental&lt;/span&gt;&lt;span class="se"&gt;\"\|&lt;/span&gt;&lt;span class="s2"&gt;incremental"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DBT_ROOT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/models/"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.sql"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$incremental_count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS  Incremental materialization: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;incremental_count&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; model(s)"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"INFO  No incremental models (all full-refresh)"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s an interesting one though, where it checks for the implementation of slowly changing dimensions (SCD):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Count snapshots&lt;/span&gt;
&lt;span class="nv"&gt;snapshot_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DBT_ROOT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/snapshots"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.sql"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$snapshot_count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS  Snapshots: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;snapshot_count&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (SCD handling)"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL  No snapshot models found (no SCD handling)"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what if it’s done SCD using a different method from dbt’s snapshots? Arguably, that’d be a non-standard approach, but the above check might &lt;code&gt;FAIL&lt;/code&gt; even if SCD are there.&lt;/p&gt;

&lt;p&gt;And this is where we hit the limitation of validations; they can only determine so much. The presence of a file or folder is easy enough to check and makes sense to do this way. But how do we check for the presence of a concept, or look holistically at what’s been built?&lt;/p&gt;

&lt;p&gt;That’s where the concept of "LLM-as-judge" comes in.&lt;/p&gt;

&lt;h1&gt;
  
  
  Judging
&lt;/h1&gt;

&lt;p&gt;We’re familiar enough by now with how powerful LLMs are. I mean, crikey, it’s just gone and built a dbt project for us from the scraps of a prompt!&lt;/p&gt;

&lt;p&gt;As well as building, LLMs can assess. I use them to &lt;a href="https://rmoff.net/ai" rel="noopener noreferrer"&gt;proofread my blog&lt;/a&gt;, heavily. They’re great at it. And they’re perfect for examining a dbt project and seeing if it matches up the spec it was given and general good analytics engineering practices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfl7zl1i5ro0cnouyfgw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfl7zl1i5ro0cnouyfgw.webp" alt="Terminal-style dashboard titled " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are an expert dbt analytics engineer evaluating the quality of a dbt project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I produced a rubric describing different aspects on which to judge a project:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Scoring Rubric

Standalone scoring rubric for evaluating Claude-built dbt projects against the
[reference implementation](https://rmoff.net/2026/02/19/ten-years-late-to-the-dbt-party-duckdb-edition/).

**Scoring scale**: 0 = missing, 1 = attempted but broken, 2 = functional but lacking, 3 = production-quality.

## Criteria

### 1. Data model

Staging → marts with dim/fact separation, proper naming conventions. [dbt best practice](https://docs.getdbt.com/best-practices/how-we-structure/1-guide-overview): staging creates atomic building blocks from source data, marts produce wide, rich business entities.

| Score | What it looks like |
|-------|--------------------|
| 0 | No layered model — everything in one directory or single query |
| 1 | Layers exist but naming is inconsistent or structure is wrong (e.g. dim_ in staging/) |
| 2 | Proper staging (stg_) → marts with schema separation, but fact table is fully denormalized (no separate dims) |
| 3 | staging → marts with dim/fact separation: `stg_stations`, `stg_measures`, `stg_readings` → `dim_stations`, `dim_measures`, `dim_date`, `fct_readings`. Bonus: intermediate layer for reusable joins |

**Reference**: 3 staging models, `dim_stations`, `dim_measures`, `dim_date`, `fct_readings`. dbt recommends `stg_[source]__[entity]` naming (double underscore separating source from entity).

[…]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and told the LLM to use this to produce a set of scores in JSON format&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Your Task

Score this dbt project against each of the 9 criteria in the rubric above. For each criterion:
1. Review the project files provided
2. Assess what was built against what the rubric describes
3. Assign a score (0-3) with brief notes explaining your reasoning

Respond with ONLY a JSON object (no markdown fences, no explanation outside the JSON) matching this exact schema:

{
  "scores": {
    "data_model": {"score": &amp;lt;0-3&amp;gt;, "notes": "&amp;lt;brief explanation&amp;gt;"},
    "key_relationships": {"score": &amp;lt;0-3&amp;gt;, "notes": "&amp;lt;brief explanation&amp;gt;"},
    "messy_data": {"score": &amp;lt;0-3&amp;gt;, "notes": "&amp;lt;brief explanation&amp;gt;"},
    "scd_snapshots": {"score": &amp;lt;0-3&amp;gt;, "notes": "&amp;lt;brief explanation&amp;gt;"},

[…]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;along with freeform narrative:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  "observations": ["&amp;lt;key observation 1&amp;gt;", "&amp;lt;key observation 2&amp;gt;", "..."],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This produces output that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scores&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data_model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The project has a perfect staging -&amp;gt; marts structure with dim/fact separation and correct naming. However, the data loading mechanism within the `on-run-start` hook is brittle and causes the `dbt build` to fail, preventing the models from being populated. The design is a 3, but the implementation is broken.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;key_relationships&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The model joins are correct, using the canonical keys extracted in staging. A `relationships` test is correctly implemented on `dim_measures` to validate the foreign key to `dim_stations`, including a `warn` severity.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;observations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The `dbt build` command fails due to a `Binder Error` when loading raw readings. The `CREATE TABLE IF NOT EXISTS` followed by an `INSERT ... WHERE NOT EXISTS` pattern in the `on-run-start` hook is brittle and likely conflicts with the validation script's setup.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The `dim_stations` model is not built from the `snap_stations` snapshot. This is a common mistake; the project creates the Type 2 SCD history but fails to use it in the dimensional model, which means analyses will not be historically accurate.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As well as the original Claude model, I tried with Qwen (&lt;code&gt;qwen2.5-coder:32b&lt;/code&gt;, running locally on Ollama), and Gemini (&lt;code&gt;gemini-2.5-pro&lt;/code&gt;, hosted externally). For a model to be of use it needs to be able to &lt;em&gt;accurately&lt;/em&gt; analyse the dbt project with sufficient detail to then be able to judge it. That is, it’s no use if it takes a cursory look (figuratively speaking), sees some SQL and YAML and signs it off as a &lt;code&gt;LGTM&lt;/code&gt;. As well as being able to do analysis of sufficient depth, it needs to be able to then judge what it’s found against the criteria it’s given.&lt;/p&gt;

&lt;p&gt;With yet another healthy dollop of Claude, I processed the judging data into some charts, such as this one showing how different aspects of the judging rubric were assessed by different models:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwujzphrowr9y04s111p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwujzphrowr9y04s111p.webp" alt="judge comparison dots" width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rolling this up shows that Qwen (the local model) scores pretty consistently with Claude and Gemini:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvazrqk63wqqnzqkb6ekz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvazrqk63wqqnzqkb6ekz.webp" alt="judge comparison heatmap" width="800" height="771"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, if we look at the score that each trial (as I’ve called it; one iteration of the judging prompt against the generated dbt project) scores when judging the same dbt project, there’s an interesting variance. Ideally we’d want every trial to return pretty much the same number. Too much difference indicates that the LLM is inconsistent with its judging, which thus lessens our trust in it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg5ycq8lv3celaqngmzm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg5ycq8lv3celaqngmzm.webp" alt="judge comparison consistency" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition, Qwen’s freeform notes are much sparser. Compare these judging notes relating to the same dbt project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;claude-opus-4-6&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;correctly uses the station URI field (not stationReference) as the FK, with a code comment noting 'stationReference diverges in ~0.4% of records'&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;qwen2.5-coder:32b&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Joins exist and use correct keys&lt;/p&gt;
&lt;/blockquote&gt;


&lt;/blockquote&gt;

&lt;p&gt;So, Qwen is ok for local testing, but for really consistent and insightful judging, it’s not going to be my first choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldrabgok5l71jj1h2354.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldrabgok5l71jj1h2354.webp" alt="opus vs qwen" width="650" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I’m tiering up here
&lt;/h2&gt;

&lt;p&gt;My initial version of the rubric had the LLM score from 0 (sucks) to 3 (excellent) on a range of features, including data model, testing, SCD, and documentation.&lt;/p&gt;

&lt;p&gt;This was fine, but arguably a slightly crude way to do things. After all, if the project is missing some core best practices or has correctness issues (e.g. missing a dimension, or has mis-identified the correct PK/FK relationships), it’s garbage and needs fixing. Other features that might be missing or sub-optimal, perhaps incomplete documentation or missing tests, oughtn’t count as much as they can be rectified or added on later.&lt;/p&gt;

&lt;p&gt;I created a second rubric and reran the judging (a nice side-effect of having captured the project and running the validation and judging separately) using a tiered approach, giving greater weight to &lt;em&gt;foundation&lt;/em&gt; principles and less weight to &lt;em&gt;advanced&lt;/em&gt; ones. Given another pass at it, I’d probably refine it further, adding some kind of punishment weighting for correctness errors (such as truncating the input dimension data from the API call by ~60%, which one of the runs did).&lt;/p&gt;

&lt;h2&gt;
  
  
  Gosh, didn’t I do well?
&lt;/h2&gt;

&lt;p&gt;If you ask an LLM to do something, it’ll do so as &lt;em&gt;it thinks is best&lt;/em&gt;. If you then ask it how it did, it’s going to judge it &lt;strong&gt;against&lt;/strong&gt; what &lt;em&gt;it thinks is best&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Can you see the issue here?&lt;/p&gt;

&lt;p&gt;LLMs are notoriously positive and eager to confirm (&lt;code&gt;That’s an astute observation!&lt;/code&gt;, &lt;code&gt;You’re absolutely right!&lt;/code&gt;), so asking Claude to mark Claude’s work is going to be subject to this kind of bias.&lt;/p&gt;

&lt;p&gt;One option here is to use a different family of models to judge the output. (Of course, you then risk diving into an ever-deeper hole of evals; how do the different models perform at judging? Maybe we should score them and judge them too?!)&lt;/p&gt;

&lt;p&gt;What I found was that Claude and Gemini both do a good job at judging, with Claude not showing any apparent bias:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3z65wbhzs0p4btjkv5fp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3z65wbhzs0p4btjkv5fp.webp" alt="judge comparison self eval" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Validation vs Judging - Pros and Cons
&lt;/h1&gt;

&lt;p&gt;+--------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+&lt;br&gt;
| Validation                                                         | Judging                                                                                             |&lt;br&gt;
+====================================================================+=====================================================================================================+&lt;br&gt;
| Pass/Fail only                                                     | Detailed report (not just pass/fail)\                                                               |&lt;br&gt;
|                                                                    |                                                                                                     |&lt;br&gt;
|                                                                    | Subjective scoring with narration/explanation of scoring and overall project                        |&lt;br&gt;
+--------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+&lt;br&gt;
| Can only determine so much. Each check requires explicitly coding. | Holistic evaluation using existing training and supplemented with skills as needed                  |&lt;br&gt;
+--------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+&lt;br&gt;
| Deterministic (same result each time for the same input)           | Non-deterministic (potentially different results each time for the same input)\                     |&lt;br&gt;
|                                                                    |                                                                                                     |&lt;br&gt;
|                                                                    | If it made the mistake in building the project will it necessarily spot it when judging it?\        |&lt;br&gt;
|                                                                    | Possibly recommended to use different model family to avoid "marking my own homework" positive bias |&lt;br&gt;
+--------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+&lt;br&gt;
| No additional LLM costs                                            | Additional LLM cost                                                                                 |&lt;br&gt;
+--------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+&lt;/p&gt;

&lt;p&gt;The upshot of this is that it makes sense to use &lt;em&gt;both&lt;/em&gt; approaches. It makes sense to run the validations first, and then only continue to judging if a sufficient threshold score is met from the validation. That is, there’s no point burning LLM tokens for a more nuanced assessment of a project if the validation script found that there were critical errors in even running it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Contamination (a.k.a. What Went Wrong?)
&lt;/h1&gt;

&lt;p&gt;One serious problem with my harness was that to begin with I mounted the project itself into the Docker container. This was so what Claude built, along with the session log, would be available outside the container.&lt;/p&gt;

&lt;p&gt;The issue with this was that Claude Code is a clever soul, and as well as doing the research that I’d expect it to (e.g. reading the blog posts that I gave it explaining the data structures), it also scoured the local filesystem, and found the rubric that is used for scoring the result by the judge LLMs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent&lt;/strong&gt; Explore the workspace at /workspace to understand what already exists.&lt;/p&gt;

&lt;p&gt;Let me read the rubric and key reference files to understand the scoring criteria.&lt;/p&gt;

&lt;p&gt;Now I understand the setup. The validation script: 1. Looks for &lt;code&gt;dbt_project.yml&lt;/code&gt; - profile name is extracted from it […] Looking at the rubric requirements, I need a macro with date range parameters, a separate staging model for archive data, and the fact table should union API and archive data with deduplication. […]&lt;/p&gt;

&lt;p&gt;So not only the rubric was exposed (giving Claude a huge glance at the answers to its homework), but also the validation script, and a Python script it had written in a previous iteration to load the data, &lt;code&gt;load_api_data.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, well and truly contaminated. If you want to spin it…an example of what Claude Code can do when you give it a ton of help ;)&lt;/p&gt;

&lt;p&gt;All these test runs I marked as contaminated and excluded from my analysis and notes shown below. (I didn’t delete them because in isolation they were still interesting as they demonstrated 'given a huge helping hand, how do different models perform?').&lt;/p&gt;

&lt;h1&gt;
  
  
  Front End
&lt;/h1&gt;

&lt;p&gt;What do you do if you’ve got a ton of data spread across multiple folders, and you want to pull it all together into one place to both summarise and dig into specifics? You get Claude to build you a dashboard of course!&lt;/p&gt;

&lt;p&gt;Here are all the results for each scenario using Opus 4.6, with summarised results for each scenario:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ul8i49pouho7yqvo0pv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ul8i49pouho7yqvo0pv.webp" alt="242eaf4a27d17a92828edd2bf2511ad0f9a44026328d7289d1770f554602b549 l" width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These can be examined per-run, highlighting the differences that LLMs will sometimes make when given the exact same input:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27f6iwqembh9xag2t19h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27f6iwqembh9xag2t19h.webp" alt="62038bf877218b16985a798be02025eee0686367d9df5e4af0120982c3388415 l" width="800" height="137"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The columns show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;LLM-as-judge results&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Validation results&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How long a run took, how many turns, and cost of tokens&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each run the Claude session log is available to inspect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcbq9d9rfwfgoseldyg4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcbq9d9rfwfgoseldyg4.webp" alt="f6211575411099905f28828c7d43a69e4e2e1843137137325b44f99695d97174 l" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Different event types can be filtered, so you can dig into exactly why and how something happened, or just stroll along through the thought patterns of the LLM:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flm1a10ycg22qqo54dd0y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flm1a10ycg22qqo54dd0y.webp" alt="71f94023bfbe929e2731ff81a36a19f0a42122b2615a61d27b1bc3737de72201 l" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I included an option to view the log in &lt;a href="https://simonwillison.net/" rel="noopener noreferrer"&gt;Simon Willison&lt;/a&gt;'s &lt;a href="https://github.com/simonw/tools/blob/main/claude-code-timeline.html" rel="noopener noreferrer"&gt;Timeline Viewer&lt;/a&gt;, which makes some of the really low-level poking about the logs easier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwmm4ninkytvtyqkl0pu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwmm4ninkytvtyqkl0pu.webp" alt="8df61b2bb7b0d10f516e686b988191435cd59746d71ff73aca511f70e865843c l" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Going back up to judging, for each test the details of the scores can be viewed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Futgx1wfd1oxjgbn5t20j.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Futgx1wfd1oxjgbn5t20j.webp" alt="e0e319a06491cfb3819e1bd2e081de69c27cf9f95c6955dc776de1f0a6bda30f l" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;along with the scoring detail and notes themselves:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkw2u4sb1c53b6m5pjja.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkw2u4sb1c53b6m5pjja.webp" alt="e1723dfa570123c4bf26bd598f2d04fd5c3b0a083e76bfcf968201e0ab826e11 l" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  And the findings from all of this testing?
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;All of the Claude models do a pretty good job, given a sufficiently rich prompt&lt;/strong&gt;. The dbt-agent plugin and skills help, but not as much as you might assume. None of the tests produced a dbt project that was sufficiently good to take through to production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flepo6dpu8vopp248e631.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flepo6dpu8vopp248e631.webp" alt="20ce09043aa2ed59a0c0022c7445da4a204774190be90377ec3a67358dd58735 l" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a &lt;em&gt;companion&lt;/em&gt;, Claude Code is an invaluable tool. But it still needs an experienced data engineer to instruct and guide it, and review the work it produces.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
For a full analysis and commentary of the best performing result, see &lt;a href="https://rmoff.net/2026/03/11/claude-code-isnt-going-to-replace-data-engineers-yet/" rel="noopener noreferrer"&gt;&lt;strong&gt;Claude Code isn’t going to replace data engineers (yet)&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Code
&lt;/h1&gt;

&lt;p&gt;The code for all of this is on &lt;a href="https://github.com/rmoff/dbt-claude" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s a slightly sanitised version of what I ran, but should be enough to get you (or Claude) going!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>dbt</category>
    </item>
    <item>
      <title>Claude Code isn't going to replace data engineers (yet)</title>
      <dc:creator>Robin Moffatt</dc:creator>
      <pubDate>Thu, 12 Mar 2026 10:38:58 +0000</pubDate>
      <link>https://dev.to/confluentinc/claude-code-isnt-going-to-replace-data-engineers-yet-3j55</link>
      <guid>https://dev.to/confluentinc/claude-code-isnt-going-to-replace-data-engineers-yet-3j55</guid>
      <description>&lt;p&gt;Ten years late (but hopefully not &lt;a href="https://en.wiktionary.org/wiki/a_day_late_and_a_dollar_short" rel="noopener noreferrer"&gt;a dollar short&lt;/a&gt;) I recently figured out &lt;a href="https://rmoff.net/2026/02/19/ten-years-late-to-the-dbt-party-duckdb-edition/" rel="noopener noreferrer"&gt;what all the fuss about dbt is about&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Well that’s cute, Robin&lt;/em&gt;, you might be saying. &lt;em&gt;Congratulations for catching up on what data/analytics engineers have been doing for years now.&lt;/em&gt; &lt;em&gt;But you see, coding by hand is **so&lt;/em&gt;* 2025.* &lt;em&gt;Didn’t you hear?&lt;/em&gt; &lt;strong&gt;AI is going to replace data engineers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No it’s not (at least, not yet). In fact, used incorrectly, it’ll do a worse job than you. But used right, it’s a kick-ass tool that any data engineer should be adding to their toolbox &lt;em&gt;today&lt;/em&gt; ^*^. In this article I’ll show you why.&lt;/p&gt;

&lt;p&gt;I’d already used &lt;a href="https://rmoff.net/2026/02/20/claude-the-instructor/" rel="noopener noreferrer"&gt;Claude to &lt;em&gt;teach&lt;/em&gt; me dbt&lt;/a&gt;, and so I was very curious to see to what extent Claude Code (at the beginning of March 2026) could actually &lt;strong&gt;write&lt;/strong&gt; a dbt project. Not only that, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Can Claude build a &lt;strong&gt;credible&lt;/strong&gt; dbt project from &lt;strong&gt;real data&lt;/strong&gt;?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How much help (prompting) does it need; can I just give it the API source, or does it need the analysis work done for it?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What difference do the &lt;a href="https://docs.getdbt.com/blog/dbt-agent-skills" rel="noopener noreferrer"&gt;skills&lt;/a&gt; that dbt labs recently published make?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do the different Claude models perform?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do you actually evaluate these different factors? (tl;dr crash-course in evals and LLM-as-judge assessment of what was built each time)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
I’ve deliberately split this write-up into two posts as they each address different points and interests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;What Claude can (and can’t) build with dbt ← &lt;strong&gt;you are here&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This one’s for the data engineers wondering if they still have a job.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The rig I built to run the scenarios, evals, and LLM-as-judge assessments to see what impact different factors had on the quality of what was built.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This one’s for the detail nerds, and LLM folk to poke holes in what &amp;amp; how I tested stuff ;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rmoff.net/2026/03/13/evaluating-claudes-dbt-skills-building-an-eval-from-scratch/" rel="noopener noreferrer"&gt;Evaluating Claude’s dbt Skills: Building an Eval from Scratch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Building a dbt project with Claude Code
&lt;/h1&gt;

&lt;p&gt;Here’s the prompt that I gave Claude Code, running with model Opus 4.6:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I've explored and built pipelines for the UK Environment Agency flood
monitoring API. Here's my analysis:

- Data exploration: https://rmoff.net/2025/02/28/exploring-uk-environment-agency-data-in-duckdb-and-rill/
- Pipeline build: https://rmoff.net/2025/03/20/building-a-data-pipeline-with-duckdb/

Build a dbt project using DuckDB for this data using idiomatic patterns
and good practices. Requirements:

- Proper staging → dim/fact data model
- Handle known data quality issues (see blog posts for details)
- SCD type 2 snapshots for station metadata
- Historical backfill from CSV archives (see https://environment.data.gov.uk/flood-monitoring/archive)
- Documentation and tests
- Source freshness checks

Run `dbt build` to verify your work.
If it fails, fix the errors and re-run until it passes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;As well as the above prompt, I also gave Claude access to the newly-shipped &lt;a href="https://github.com/dbt-labs/dbt-agent-skills/blob/main/README.md" rel="noopener noreferrer"&gt;dbt-agent-skills&lt;/a&gt; from dbt Labs.&lt;/p&gt;

&lt;p&gt;It built me a working dbt project!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── dbt_packages
├── dbt_project.yml
├── macros
│&amp;nbsp;&amp;nbsp; └── generate_archive_urls.sql
├── models
│&amp;nbsp;&amp;nbsp; ├── marts
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── _marts__models.yml
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── dim_date.sql
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── dim_measures.sql
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── dim_stations.sql
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; ├── fct_readings.sql
│&amp;nbsp;&amp;nbsp; │&amp;nbsp;&amp;nbsp; └── station_freshness.sql
│&amp;nbsp;&amp;nbsp; └── staging
│&amp;nbsp;&amp;nbsp;     ├── _flood_monitoring__models.yml
│&amp;nbsp;&amp;nbsp;     ├── _flood_monitoring__sources.yml
│&amp;nbsp;&amp;nbsp;     ├── stg_flood_monitoring__measures.sql
│&amp;nbsp;&amp;nbsp;     ├── stg_flood_monitoring__readings_archive.sql
│&amp;nbsp;&amp;nbsp;     ├── stg_flood_monitoring__readings.sql
│&amp;nbsp;&amp;nbsp;     └── stg_flood_monitoring__stations.sql
├── profiles.yml
├── snapshots
│&amp;nbsp;&amp;nbsp; └── snap_stations.sql
└── tests
    └── assert_recent_readings.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiles and it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;1 of 37 START sql table model main.dim_date .................................... &lt;span class="o"&gt;[&lt;/span&gt;RUN]
2 of 37 START sql view model main.stg_flood_monitoring__measures ............... &lt;span class="o"&gt;[&lt;/span&gt;RUN]
3 of 37 START sql view model main.stg_flood_monitoring__readings ............... &lt;span class="o"&gt;[&lt;/span&gt;RUN]
4 of 37 START sql view model main.stg_flood_monitoring__stations ............... &lt;span class="o"&gt;[&lt;/span&gt;RUN]
&lt;span class="o"&gt;[&lt;/span&gt;…]
37 of 37 PASS unique_station_freshness_station_id .............................. &lt;span class="o"&gt;[&lt;/span&gt;PASS &lt;span class="k"&gt;in &lt;/span&gt;0.01s]
36 of 37 PASS not_null_station_freshness_station_id ............................ &lt;span class="o"&gt;[&lt;/span&gt;PASS &lt;span class="k"&gt;in &lt;/span&gt;0.01s]

Finished running 1 incremental model, 1 snapshot, 4 table models, 26 data tests, 2 unit tests, 3 view models &lt;span class="k"&gt;in &lt;/span&gt;0 hours 0 minutes and 0.61 seconds &lt;span class="o"&gt;(&lt;/span&gt;0.61s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and writes data successfully to DuckDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;🟡◗ SELECT &lt;span class="k"&gt;*&lt;/span&gt; from fct_readings LIMIT 5&lt;span class="p"&gt;;&lt;/span&gt;
┌─────────────────────┬───────────────────────────────────────────┬────────┬──────────────┐
│      date_time      │                measure_id                 │ value  │ reading_date │
│      timestamp      │                  varchar                  │ double │     &lt;span class="nb"&gt;date&lt;/span&gt;     │
├─────────────────────┼───────────────────────────────────────────┼────────┼──────────────┤
│ 2026-03-09 12:00:00 │ 52119-level-stage-i-15_min-mASD           │  0.637 │ 2026-03-09   │
│ 2026-03-09 12:00:00 │ E72639-level-tidal_level-Mean-15_min-mAOD │  2.982 │ 2026-03-09   │
│ 2026-03-09 12:00:00 │ 730506-level-stage-i-15_min-m             │  1.319 │ 2026-03-09   │
│ 2026-03-09 12:00:00 │ 2095-level-stage-i-15_min-mASD            │  1.087 │ 2026-03-09   │
│ 2026-03-09 12:00:00 │ 3015TH-level-stage-i-15_min-mASD          │  0.178 │ 2026-03-09   │
└─────────────────────┴───────────────────────────────────────────┴────────┴──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  The prompt
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhtqfi3q71jsgs05mfc6g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhtqfi3q71jsgs05mfc6g.webp" alt="Meme using the " width="500" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
This blog post is not about testing different permutations of a prompt. I’ve done that, and &lt;a href="https://rmoff.net/2026/03/13/evaluating-claudes-dbt-skills-building-an-eval-from-scratch/" rel="noopener noreferrer"&gt;have written about it separately&lt;/a&gt;. This blog post is looking at an approximation of the best effort I could get from Claude. There’ll always be "&lt;em&gt;yes, but did you try &lt;code&gt;\$thing\&lt;/code&gt;&lt;/em&gt;" and I discuss that below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s look a bit more closely at the prompt that I gave Claude to understand what we’re giving it to work with.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I've explored and built pipelines for the UK Environment Agency flood monitoring API. Here's my analysis: 

- Data exploration: https://rmoff.net/2025/02/28/exploring-uk-environment-agency-data-in-duckdb-and-rill/ 
- Pipeline build: https://rmoff.net/2025/03/20/building-a-data-pipeline-with-duckdb/ 

Build a dbt project using DuckDB for this data using idiomatic patterns and good practices. Requirements: 

- Proper staging → dim/fact data model  
- Handle known data quality issues (see blog posts for details)  
- SCD type 2 snapshots for station metadata  
- Historical backfill from CSV archives (see https://environment.data.gov.uk/flood-monitoring/archive)  
- Documentation and tests  
- Source freshness checks  

Run `dbt build` to verify your work. If it fails, fix the errors and re-run until it passes. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Background context&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tell Claude where to go and find out the nitty-gritty detail about the data, its relationships, and its quirks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Show Claude what I built myself using just SQL and no dbt&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lay out the requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Skeleton spec, as one might give to a junior engineer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Secret sauce". This is where &lt;em&gt;agentic coding&lt;/em&gt; kicks in. It will literally sit there and fix its own errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My yardstick for this prompt was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What would a senior data engineer write if they were giving this to someone else to build?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is, how much does AI help a data engineer &lt;em&gt;today&lt;/em&gt; without them having to get into the guts of prompt engineering?&lt;/p&gt;

&lt;p&gt;I also tested with a much more minimal prompt:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The UK Environment Agency publishes flood monitoring data,
see https://environment.data.gov.uk/flood-monitoring

Build an idiomatic dbt project following good practices using DuckDB that ingests this data and
models it for analytics.

Run the project and make sure that it works. If it fails, fix the errors and re-run until it passes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This didn’t do very well beyond the absolute basics, and I’m not covering it in the rest of this article.&lt;/p&gt;

&lt;h1&gt;
  
  
  Agentic coding in action
&lt;/h1&gt;

&lt;p&gt;At the heart of using Claude Code rather than a "chat bot" like ChatGPT is the idea of agentic AI. That is, it understands &lt;em&gt;what&lt;/em&gt; it needs to do, and it has access to &lt;em&gt;tools&lt;/em&gt; to achieve it. Tools can include looking up documentation on the web, amending files, invoking binaries, and so on.&lt;/p&gt;

&lt;p&gt;Here’s an example of it in action, debugging a problem with the project as it goes. Remember, it’s doing everything I’m showing here on its own—no input, prompting, or guidance from me.&lt;/p&gt;

&lt;p&gt;We join Claude part-way through its journey. It’s already built out the dbt models, and is testing it for the first time. On its own, it invokes &lt;code&gt;dbt build&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bash&lt;/strong&gt; cd /workspace/flood_monitoring &amp;amp;&amp;amp; dbt build --profiles-dir . 2&amp;gt;&amp;amp;1&lt;/p&gt;

&lt;p&gt;x Exit code 2&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full error output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Exit code 2&lt;br&gt;
Running with dbt=1.11.7&lt;br&gt;
Registered adapter: duckdb=1.10.1&lt;br&gt;
Unable to do partial parsing because saved manifest not found. Starting full parse.&lt;br&gt;
[WARNING][MissingArgumentsPropertyInGenericTestDeprecation]: Deprecated&lt;br&gt;
functionality&lt;br&gt;
Found top-level arguments to test &lt;code&gt;accepted_values&lt;/code&gt; defined on 'stg_measures' in&lt;br&gt;
package 'flood_monitoring' (models/staging/_staging.yml). Arguments to generic&lt;br&gt;
tests should be nested under the &lt;code&gt;arguments&lt;/code&gt; property.&lt;br&gt;
Encountered an error:&lt;br&gt;
"'dateTime'"&lt;br&gt;
Traceback (most recent call last):&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 182, in wrapper&lt;br&gt;
    result, success = func(*args, **kwargs)&lt;br&gt;
                      ^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 128, in wrapper&lt;br&gt;
    return func(*args, **kwargs)&lt;br&gt;
           ^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 276, in wrapper&lt;br&gt;
    return func(*args, **kwargs)&lt;br&gt;
           ^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 321, in wrapper&lt;br&gt;
    return func(*args, **kwargs)&lt;br&gt;
           ^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 391, in wrapper&lt;br&gt;
    return func(*args, **kwargs)&lt;br&gt;
           ^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 368, in wrapper&lt;br&gt;
    return func(*args, **kwargs)&lt;br&gt;
           ^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 407, in wrapper&lt;br&gt;
    setup_manifest(ctx, write=write, write_perf_info=write_perf_info)&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/cli/requires.py", line 434, in setup_manifest&lt;br&gt;
    ctx.obj["manifest"] = parse_manifest(&lt;br&gt;
                          ^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/parser/manifest.py", line 2257, in parse_manifest&lt;br&gt;
    manifest = ManifestLoader.get_full_manifest(&lt;br&gt;
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/parser/manifest.py", line 323, in get_full_manifest&lt;br&gt;
    manifest = loader.load()&lt;br&gt;
               ^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/parser/manifest.py", line 459, in load&lt;br&gt;
    patcher.construct_sources()&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/parser/sources.py", line 87, in construct_sources&lt;br&gt;
    parsed = self.parse_source(patched)&lt;br&gt;
             ^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/parser/sources.py", line 204, in parse_source&lt;br&gt;
    parsed_source.relation_name = self._get_relation_name(parsed_source)&lt;br&gt;
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/parser/sources.py", line 338, in _get_relation_name&lt;br&gt;
    return str(relation_cls.create_from(self.root_project, node))&lt;br&gt;
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/adapters/duckdb/relation.py", line 27, in create_from&lt;br&gt;
    return cls.create_from_source(quoting, relation_config, **kwargs)&lt;br&gt;
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
  File "/usr/local/lib/python3.11/dist-packages/dbt/adapters/duckdb/relation.py", line 62, in create_from_source&lt;br&gt;
    ext_location = ext_location_template.format_map(source_config.as_dict())&lt;br&gt;
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;br&gt;
KeyError: "'dateTime'"&lt;/p&gt;

&lt;p&gt;[WARNING][DeprecationsSummary]: Deprecated functionality&lt;br&gt;
Summary of encountered deprecations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MissingArgumentsPropertyInGenericTestDeprecation: 6 occurrences
To see all deprecation instances instead of just the first occurrence of each,
run command again with the &lt;code&gt;--show-all-deprecations&lt;/code&gt; flag. You may also need to
run with &lt;code&gt;--no-partial-parse&lt;/code&gt; as some deprecations are only encountered during
parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can expand the &lt;code&gt;Full error output&lt;/code&gt; log line above in full if you want, but the salient messages are these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[WARNING][MissingArgumentsPropertyInGenericTestDeprecation]: Deprecated functionality
Found top-level arguments to test `accepted_values` defined on 'stg_measures' in
package 'flood_monitoring' (models/staging/_staging.yml). Arguments to generic
tests should be nested under the `arguments` property.

Encountered an error:
"'dateTime'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude figures out it has two problems, as you can see from its internal 'thinking':&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Two errors:
1. The `accepted_values` test has the wrong format - it needs to use `arguments` property
2. The `external_location` in sources.yml is being parsed as a format string, and the `\{dateTime\}` part is being interpreted as a placeholder. I need to escape the braces. Let me fix both issues. For the `external_location`, I need to escape the curly braces in the SQL. DuckDB uses `column=\{'dateTime': 'TIMESTAMPTZ', ...\}` which has `\{` and `\}`. The dbt-duckdb source parser is trying to format this string with the source config dict. I'll escape the braces with double braces so `\{\{` becomes `\{` and `\}\}` becomes `\}` in the format string. For the `accepted_values` test, I need to restructure it so the values and severity are nested under an `arguments` property, which is the format dbt 1.11+ expects. I see the issue now—the deprecation warning about test syntax is separate from the actual error. The real problem is the `external_location` format string with the curly braces, which is causing the `KeyError`. I should prioritize fixing that first since it's what's actually breaking the code, and I can address the test syntax update separately if needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It summarises this up into two distinct tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Two issues to fix:
(1) curly braces in `external_location` need escaping for dbt's format string interpolation, and
(2) test syntax deprecation. Let me fix both:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then sets about updating the &lt;code&gt;external_location&lt;/code&gt; references like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write&lt;/strong&gt; /workspace/flood_monitoring/models/staging/_sources.yml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;…&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Curly braces in the DuckDB columns dict must be doubled to escape&lt;/span&gt;
  &lt;span class="c1"&gt;# dbt-duckdb's format_map() interpolation.&lt;/span&gt;
  &lt;span class="na"&gt;external_location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;read_csv(&lt;/span&gt;
      &lt;span class="s"&gt;'https://environment.data.gov.uk/flood-monitoring/archive/readings-2026-03-07.csv',&lt;/span&gt;
      &lt;span class="s"&gt;columns={{'dateTime': 'TIMESTAMPTZ', &lt;/span&gt;
                &lt;span class="s"&gt;'measure': 'VARCHAR', 'value': 'VARCHAR'}}, &lt;/span&gt;
      &lt;span class="s"&gt;header=true,&lt;/span&gt;
      &lt;span class="s"&gt;ignore_errors=true&lt;/span&gt;
    &lt;span class="s"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Double curly braces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;← The file /workspace/flood_monitoring/models/staging/_sources.yml has been updated successfully.&lt;/p&gt;

&lt;p&gt;After also updating the test syntax, it runs &lt;code&gt;dbt build&lt;/code&gt; again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bash&lt;/strong&gt; cd /workspace/flood_monitoring &amp;amp;&amp;amp; dbt build --profiles-dir . 2&amp;gt;&amp;amp;1&lt;/p&gt;

&lt;p&gt;x Exit code 2&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exit code 2
17:34:54  Running with dbt=1.11.7
17:34:54  Registered adapter: duckdb=1.10.1
17:34:54  Unable to do partial parsing because saved manifest not found. Starting full parse.
17:34:55  Encountered an error:
Compilation Error
  Could not render read_csv(
    'https://environment.data.gov.uk/flood-monitoring/archive/readings-2026-03-07.csv',
    columns={{'dateTime': 'TIMESTAMPTZ', 'measure': 'VARCHAR', 'value': 'VARCHAR'}},
    header=true,
    ignore_errors=true
  ): expected token 'end of print statement', got ':'
    line 3
      columns={{'dateTime': 'TIMESTAMPTZ', 'measure': 'VARCHAR', 'value': 'VARCHAR'}},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time it’s a different problem, which it identifies and figures out how to work around:&lt;/p&gt;

&lt;p&gt;The external_location goes through Jinja2 rendering - can’t use &lt;code&gt;{{}}&lt;/code&gt; for literal braces. I’ll use &lt;code&gt;read_csv_auto&lt;/code&gt; instead which needs no column type dict:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write&lt;/strong&gt; /workspace/flood_monitoring/models/staging/_sources.yml&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bash&lt;/strong&gt; cd /workspace/flood_monitoring &amp;amp;&amp;amp; dbt build --profiles-dir . 2&amp;gt;&amp;amp;1&lt;/p&gt;

&lt;p&gt;This time the build works, failing only on data tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[…]
Done. PASS=27 WARN=2 ERROR=2 SKIP=17 NO-OP=0 TOTAL=48
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Good progress! The build almost works. Two real data quality issues:
- 631 stations lack lat/long coordinates in the API (genuine EA data gap)
- Need to downgrade those tests to `warn` severity so they don't cascade-skip downstream models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check this out for what it does next—fires up DuckDB and starts checking the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 &amp;lt;&amp;lt; 'EOF'
import duckdb
conn = duckdb.connect()
result = conn.execute("""
SELECT count(*) as missing_coords_count
FROM (
  SELECT UNNEST(items) as item
  FROM read_json_auto('https://environment.data.gov.uk/flood-monitoring/id/stations.json?_limit=10000')
) WHERE item.lat IS NULL
""").fetchone()
print('Missing lat/long:', result)
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;631 stations genuinely have no coordinates in the EA API. Downgrade those tests to &lt;code&gt;warn&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edit&lt;/strong&gt; /workspace/flood_monitoring/models/staging/_staging.yml&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;diff&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;old&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: latitude
description: WGS-84 latitude in decimal degrees.
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;name: longitude
description: WGS-84 longitude in decimal degrees.
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;new&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: latitude
description: &amp;gt;
WGS-84 latitude in decimal degrees. Known data quality issue: ~631
stations in the EA API have no coordinates (newly-commissioned or
administrative stations). Test is warn-only for this reason.
tests:

&lt;ul&gt;
&lt;li&gt;not_null:
config:
  severity: warn&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;name: longitude
description: WGS-84 longitude in decimal degrees. See latitude note.
tests:

&lt;ul&gt;
&lt;li&gt;not_null:
config:
  severity: warn&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;and so on and so on, until it gets to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dbt build&lt;/code&gt; passes with 0 errors.&lt;/strong&gt; All 38 tests pass; 10 warnings document known EA data quality issues.&lt;/p&gt;

&lt;p&gt;The above autonomous debugging session is just &lt;em&gt;one&lt;/em&gt; example of the numerous ways in which Claude Code makes things easier for a data engineer.&lt;/p&gt;

&lt;p&gt;Here’s another narrated session log from a different run using the same prompt, in which &lt;a href="https://rmoff.net/2026/03/11/claude-code-in-action-with-dbt/" rel="noopener noreferrer"&gt;Claude realises the data has a bunch of quirks, enumerates them, and codes for them&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Truly invaluable.&lt;/p&gt;

&lt;h1&gt;
  
  
  Okay…but is it any good?
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;As the content of many LinkedIn posts and comments is testament to: infinite AI monkeys with infinite tokens will crap out as much material as you want—but most of it is garbage.&lt;/em&gt; &lt;em&gt;Is that what Claude is doing here? Is it garbage?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Actually, the dbt project that Claude built is pretty good.&lt;/p&gt;

&lt;p&gt;What I love about dbt is that it makes it easy to adhere to a ton of good practices that have always been true when working with data including staging layers, slowly-changing dimensions (SCD), and dimensional modelling as well as "newer" (but equally important) concepts such as documentation (gasp!), testing (imagine!) and a smooth integration with source control and CI/CD (how modern!).&lt;/p&gt;

&lt;p&gt;How much of these good features did Claude use, and use successfully?&lt;/p&gt;

&lt;h2&gt;
  
  
  Not bad…
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz0ai0hsnzrt6gwqjd0p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz0ai0hsnzrt6gwqjd0p.gif" alt="not bad james corden" width="498" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Plausible data model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frmoff.net%2Fimages%2F2026%2F03%2Fdbt-data-model.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frmoff.net%2Fimages%2F2026%2F03%2Fdbt-data-model.webp" alt="dbt data model" width="800" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Correct key relationships enforced with &lt;a href="https://docs.getdbt.com/reference/resource-properties/constraints?version=1.10" rel="noopener noreferrer"&gt;dbt constraints&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;contract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enforced&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;station_id&lt;/span&gt;
      &lt;span class="na"&gt;data_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;varchar&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unique&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;station&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;identifier&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(e.g.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'1029TH')"&lt;/span&gt;
      &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One point to note is that it didn’t include &lt;a href="https://docs.getdbt.com/docs/build/data-tests?version=1.10" rel="noopener noreferrer"&gt;tests&lt;/a&gt; to check the keys in the loaded data. This is arguably 'belt and braces' anyway.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Incremental fact table load
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;  config(
      materialized='incremental',
      unique_key=['date_time', 'measure_id'],
  )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;✅ Handle messy source data (e.g. multiple values where only one expected)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="c1"&gt;-- Value cleaning: handle pipe-delimited values (e.g. "0.770|0.688")&lt;/span&gt;
  &lt;span class="c1"&gt;-- by taking the first value, then cast to double&lt;/span&gt;
  &lt;span class="n"&gt;try_cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt;
          &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;like&lt;/span&gt; &lt;span class="s1"&gt;'%|%'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;split_part&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;                                               &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reading_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;✅ Handles Slowly Changing Dimensions (SCD)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'snap_stations'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;dbt_valid_to&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
For some reason Claude only implemented this for the &lt;em&gt;stations&lt;/em&gt; dimension, not &lt;em&gt;measures&lt;/em&gt;. &lt;strong&gt;Turns out, I literally told it to do this in the prompt!&lt;/strong&gt; (&lt;code&gt;SCD type 2 snapshots for station metadata&lt;/code&gt;). So it stuck to that literally, omitting measures. But it’d have been nice for it to perhaps challenge me on that. Measure definitions &lt;em&gt;shouldn’t&lt;/em&gt; change…but they might?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;✅ Documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7nkybapmkszb8ltcdtj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7nkybapmkszb8ltcdtj.webp" alt="Screenshot of dbt docs showing the stg_flood_monitoring\_\_stations view" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Tests
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
    &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;accepted_values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Active'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Closed'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Suspended'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ukcmf'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unknown'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flood_monitoring_api&lt;/span&gt;
    &lt;span class="na"&gt;freshness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;warn_after&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hour&lt;/span&gt;
      &lt;span class="na"&gt;error_after&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt;
        &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hour&lt;/span&gt;
    &lt;span class="na"&gt;loaded_at_field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loaded_at"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;✅ Backfill
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  {% macro generate_archive_urls(start_date, end_date) %}
  {%- set start = modules.datetime.datetime.strptime(start_date, '%Y-%m-%d') -%}
  {%- set end_dt = modules.datetime.datetime.strptime(end_date, '%Y-%m-%d') -%}
  {%- set day_count = (end_dt - start).days -%}
  {%- set ns = namespace(urls=[]) -%}
  {%- for i in range(day_count + 1) -%}
    {%- set d = start + modules.datetime.timedelta(days=i) -%}
    {%- do ns.urls.append("'https://environment.data.gov.uk/flood-monitoring/archive/readings-" ~ d.strftime('%Y-%m-%d') ~ ".csv'") -%}
  {%- endfor -%}
  [{{ ns.urls | join(', ') }}]
  {%- endmacro %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;select&lt;/span&gt;
      &lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"dateTime"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;date_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'.+/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;measure_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;try_cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;split_part&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;generate_archive_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;General good practice and nice surprises&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Doesn’t use &lt;code&gt;SELECT *&lt;/code&gt; but instead hardcodes column names. Generally a better pattern for creating less brittle pipelines.&lt;/li&gt;
&lt;li&gt;✅ Parses out the station &lt;code&gt;status&lt;/code&gt; field cleanly
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;regexp_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'.+/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Rather than storing the source which looks like this&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://environment.data.gov.uk/flood-monitoring/def/core/statusActive
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;✅ More elegant date logic:
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isodow&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;date_day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;is_weekend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;vs&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;dayofweek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;date_is_weekend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;So, pretty solid work, if the alternative is starting from a blank slate of a dbt project. What about the downsides?&lt;/p&gt;

&lt;h2&gt;
  
  
  …but not &lt;em&gt;that&lt;/em&gt; good
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;All you Claude Code fans out there, hold your horses on the "yes, but…" until the "yes, but" section below :p&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://rmoff.net/2026/02/19/ten-years-late-to-the-dbt-party-duckdb-edition/#_ingest" rel="noopener noreferrer"&gt;the version that I built by hand&lt;/a&gt;, I pull the raw API data into a DuckDB table &lt;a href="https://github.com/rmoff/env-agency-dbt/blob/master/macros/ingestion/load_raw_measures.sql" rel="noopener noreferrer"&gt;using a Jinja macro&lt;/a&gt;. Claude approached it differently, writing a Python script, &lt;code&gt;load_api_data.py&lt;/code&gt;. Arguably more elegant, but I have three issues with it, as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The first of these is personal preference. I’m a SQL guy, and I’d rather not be on the hook to support a pipeline that leans on Python. After all, it’s one thing to generate code with AI, but ultimately someone’s left holding the support baby.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The next two issues are the code itself. There’s one silly flaw in it, and an insidiously bad one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The silly flaw is easily spotted and easily fixed: it only does a &lt;code&gt;CREATE TABLE&lt;/code&gt; and so falls over if the table exists already (like, the second time you run it). Easily spotted (the job falls over) and easily fixed (&lt;code&gt;CREATE OR REPLACE TABLE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The really bad problem is this, in Claude’s own mea culpa words:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;load_api_data.py&lt;/code&gt; fetches stations with &lt;code&gt;?_limit=2000&lt;/code&gt;, but the API itself has a maximum response size of 2000 items (it silently caps at that). &lt;strong&gt;The actual total number of stations is ~5,458, so the script only gets 1,493 rows back&lt;/strong&gt; (fewer than the limit because the API returns a paginated subset — probably the first page).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let that sink in. We have a huge hole in our data, that’s not evident until we wonder why a ton of our facts are without corresponding dimension entries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnk8rg3s2grtwacx4zk9j.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnk8rg3s2grtwacx4zk9j.gif" alt="really tommy norris" width="498" height="498"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Wrong is worse than absent because you can’t trust it.:&lt;/strong&gt;&lt;br&gt;
If Claude just doesn’t build a feature, that’s ok because I can add it (or tell it to). But if it confidently builds it and it turns out to be wrong, that’s a &lt;em&gt;trust issue&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Yes, but Robin, you could &lt;code&gt;&amp;lt;do this thing&amp;gt;&lt;/code&gt;! → See yes, but&lt;/em&gt; below&lt;/p&gt;

&lt;p&gt;Other things that look off from just eyeballing the code include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;dim_stations&lt;/code&gt; drops several columns (&lt;code&gt;stageScale&lt;/code&gt;, &lt;code&gt;datumOffset&lt;/code&gt;, &lt;code&gt;gridReference&lt;/code&gt;, &lt;code&gt;downstageScale&lt;/code&gt;). Two of these arguably extend the data model beyond what I’ve manually investigated myself before (&lt;code&gt;stageScale&lt;/code&gt;/&lt;code&gt;downstageScale&lt;/code&gt;) but two definitely hold relevant reference data (&lt;code&gt;datumOffset&lt;/code&gt;, &lt;code&gt;gridReference&lt;/code&gt;) which is getting silently dropped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;dim_measures&lt;/code&gt; drops &lt;code&gt;unit&lt;/code&gt;. Arguably a decent optimisation given the repetition between &lt;code&gt;unit&lt;/code&gt; and the human-friendly &lt;code&gt;unitName&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="err"&gt;🟡◗&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unitName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;parameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;parameterName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;valueType&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;measures&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌──────────────────────────────────────┬──────────┬──────────────────────────────────────────────┬───────────┬───────────────┬───────────────┐
  │                 unit                 │ unitName │                    label                     │ parameter │ parameterName │   valueType   │
  │               varchar                │ varchar  │                   varchar                    │  varchar  │    varchar    │    varchar    │
  ├──────────────────────────────────────┼──────────┼──────────────────────────────────────────────┼───────────┼───────────────┼───────────────┤
  │ http://qudt.org/1.1/vocab/unit#Meter │ m        │ LAVERSTOCK - level-stage-i-15_min-m          │ level     │ Water Level   │ instantaneous │
  │ http://qudt.org/1.1/vocab/unit#Meter │ m        │ KNETTISHALL GS GSM - level-stage-i-15_min-m  │ level     │ Water Level   │ instantaneous │
  │ http://qudt.org/1.1/vocab/unit#Meter │ mASD     │ Keadby TL B - level-tidal_level-i-5_min-mASD │ level     │ Water Level   │ instantaneous │
  │ http://qudt.org/1.1/vocab/unit#Meter │ m        │ Riding Mill - level-stage-i-15_min-m         │ level     │ Water Level   │ instantaneous │
  │ http://qudt.org/1.1/vocab/unit#Meter │ mASD     │ Clomoney Way LVL - level-stage-i-15_min-mASD │ level     │ Water Level   │ instantaneous │
  └──────────────────────────────────────┴──────────┴──────────────────────────────────────────────┴───────────┴───────────────┴───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;But that’s not the point is it; &lt;strong&gt;silently dropping data fields is not what I want my robot to do&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fct_readings&lt;/code&gt; duplicates the &lt;code&gt;date_time&lt;/code&gt; field:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;select&lt;/span&gt;
      &lt;span class="n"&gt;date_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;measure_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_time&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;reading_date&lt;/span&gt; 
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;deduped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;y, tho?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude builds the SCD (nice!) but &lt;code&gt;check_cols&lt;/code&gt; only lists &lt;em&gt;some&lt;/em&gt; of the values that might change:
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="n"&gt;check_cols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s1"&gt;'station_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'town'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'river_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'catchment_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'latitude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'longitude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if &lt;code&gt;latitude&lt;/code&gt;/&lt;code&gt;longitude&lt;/code&gt; are in there, why not &lt;code&gt;northing&lt;/code&gt;/&lt;code&gt;easting&lt;/code&gt;? As with the &lt;code&gt;measures&lt;/code&gt; decisions above, these are real data issues that a human should be involved in reviewing, &lt;em&gt;or&lt;/em&gt; the AI surfacing for review.&lt;/p&gt;

&lt;h1&gt;
  
  
  Yes, but…
&lt;/h1&gt;

&lt;p&gt;All of the mistakes and errors that Claude made and which I discuss above, I could, &lt;em&gt;of course&lt;/em&gt;, get Claude to fix. This would be an iterative process: Claude builds → I review → Claude fixes, etc.&lt;/p&gt;

&lt;p&gt;But…that isn’t what I wanted to determine in this article. It changes Claude from a means of production into a means of assistance (&lt;em&gt;which is, BTW, what I conclude in this post that its most effective role is today&lt;/em&gt;). It’s the difference between giving a junior (or senior!) colleague a piece of work to do and being confident that it will be ready for testing, versus knowing that it will need a lot of checking and reworking.&lt;/p&gt;

&lt;p&gt;Another option would be to build the prompt defensively up-front (encode everything I’ve seen it do here into a better prompt for next time, &lt;code&gt;make no mistakes&lt;/code&gt;, etc). The point remains, that &lt;em&gt;I would have had to do that&lt;/em&gt;. The prompt I already gave it was designed to give it enough hints, without having to hold its hand through every step.&lt;/p&gt;

&lt;p&gt;Could I create an improved version, based on what it’s learnt? Sure. But that’s not having Claude build it &lt;em&gt;for&lt;/em&gt; me is it. It’s building it &lt;em&gt;with me&lt;/em&gt;. Which is my whole argument here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
Claude Code is an amazing productivity &lt;em&gt;companion&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt;&lt;br&gt;
Do not, if you value your job, use it to one-shot a dbt project!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prompt tweaking is a mug’s game
&lt;/h2&gt;

&lt;p&gt;LLMs are non-deterministic, so even given the same prompt it might behave differently next time, let alone when a newer model comes along. Then you end up building a spaghetti bowl of sticking plasters ("don’t do this", "remember to do this"), and it’s still not necessarily going to do it right each time—&lt;em&gt;as the technology currently stands&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I’m reminded of &lt;a href="https://www.reddit.com/r/ChatGPTPro/comments/15ffpx3/comment/juhcdia/" rel="noopener noreferrer"&gt;this example of a ChatGPT prompt&lt;/a&gt; I earnestly bookmarked in August 2023. It was the magic incantation! It was amazing! It was cargo culting.&lt;/p&gt;

&lt;p&gt;Relying on prompting alone is cute for tricks, but it’s not a viable strategy for &lt;em&gt;reliable&lt;/em&gt; hands-off dbt code generation (or even hands-on, but trusted, for that matter).&lt;/p&gt;

&lt;h1&gt;
  
  
  How did you test this? Did you try technique &lt;code&gt;$wibble&lt;/code&gt;? What about model &lt;code&gt;$foo&lt;/code&gt;?
&lt;/h1&gt;

&lt;p&gt;The best thing about using AI agents to make you more productive is that they make you more productive at the thing you’re building.&lt;/p&gt;

&lt;p&gt;The worst thing about using AI agents to make you more productive is that they make you more productive at &lt;em&gt;any random stupid idea that pops into your lizard brain&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Thus, in figuring out what Claude Code could do with dbt, I also stumbled into the worlds of evals, LLM-as-judge, and a whole bunch more. Oh, and of course, having Claude Code build a bespoke dashboard to track all of my results :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfl7zl1i5ro0cnouyfgw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgfl7zl1i5ro0cnouyfgw.webp" alt="Terminal-style dashboard titled " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve written all about this in &lt;a href="https://rmoff.net/2026/03/13/evaluating-claudes-dbt-skills-building-an-eval-from-scratch/" rel="noopener noreferrer"&gt;a separate blog post&lt;/a&gt;, because I learnt a lot of stuff—some of it useful too.&lt;/p&gt;

&lt;p&gt;For now, the headline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;The prompt and the skills matter more than the model.&lt;/strong&gt; Sonnet 4.5 can produce respectable results given the necessary context. Opus 4.6 isn’t a silver bullet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frmoff.net%2Fimages%2F2026%2F03%2Fscore-heatmap-by-prompt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Frmoff.net%2Fimages%2F2026%2F03%2Fscore-heatmap-by-prompt.webp" alt="Heatmap comparing LLM scores across 9 evaluation criteria and 6 prompt variants" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  A fantastic productivity booster for data engineers
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What does even replacing a data engineer with AI mean?
&lt;/h2&gt;

&lt;p&gt;To truly suggest that AI can &lt;em&gt;replace&lt;/em&gt; a data engineer you’d be talking about a tool that a business user could tell to build a pipeline and have it do it with sufficient quality and robustness that it could be thrown over the wall to a general IT ops team to run.&lt;/p&gt;

&lt;p&gt;Perhaps in the same way that business users these days eat Excel sheets for breakfast whereas 40 years ago they’d have a specialist in IT to do that for them.&lt;/p&gt;

&lt;p&gt;We’re not there with AI yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;DE + AI &amp;gt; DE&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;What agentic coding tools such as Claude Code are doing is making data engineers vastly more &lt;em&gt;productive&lt;/em&gt;. We saw above that it’s not going to build the whole pipeline in one go, hands-off. And in fact, there are chances it’s going to be a drag on productivity to do so because you won’t have built up the mental map of what’s been built to then be able to verify and troubleshoot it. But Claude Code excels at specific tasks, and iteration. It can troubleshoot a failing build, as well as &lt;a href="https://rmoff.net/2026/03/11/claude-code-in-action-with-dbt/" rel="noopener noreferrer"&gt;figure out nuances in the data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Whilst it does need coaching through some tasks, and reprimanding and correcting, and whilst it does make mistakes…so do humans! My experience is that it is so shit-hot—and fast as f…errari—the &lt;em&gt;vast majority of the time&lt;/em&gt; that the nett gain over just doing it yourself manually is still great.&lt;/p&gt;

&lt;p&gt;My mental model for &lt;em&gt;not&lt;/em&gt; using Claude Code is currently something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An accountant choosing to do their sums by hand, instead of use a calculator&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An SRE manually logging into AWS to provision a dozen EC2 machines and SSHing into each one individually to install software, instead of using Terraform&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A programmer using &lt;a href="https://xkcd.com/378/" rel="noopener noreferrer"&gt;a magnetized needle and a steady hand&lt;/a&gt; to edit a file, instead of a text editor ;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Deterministic vs non-deterministic is irrelevant here; my point is that AI is a&lt;/em&gt; tool &lt;em&gt;to enable you to get something done more easily than you used to be able to.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You don’t &lt;em&gt;have&lt;/em&gt; to use agentic coding tools…but you’re going to get left behind and have to catch up at some point.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;br&gt;
Oh…and if you honestly think AI is just ChatGPT writing haikus about Linux and other chatbot-esque simplicity…&lt;a href="https://rmoff.net/2026/03/06/ai-will-fuck-you-up-if-youre-not-on-board/" rel="noopener noreferrer"&gt;&lt;strong&gt;you’re maybe missing the point about AI&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>dbt</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
