<?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: Aram Panasenco</title>
    <description>The latest articles on DEV Community by Aram Panasenco (@panasenco).</description>
    <link>https://dev.to/panasenco</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1485667%2Febc8b67d-4109-4759-b260-daef517b2f05.jpeg</url>
      <title>DEV Community: Aram Panasenco</title>
      <link>https://dev.to/panasenco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/panasenco"/>
    <language>en</language>
    <item>
      <title>Don't let AI agents decide whether they should do a task</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Mon, 25 Aug 2025 18:59:20 +0000</pubDate>
      <link>https://dev.to/panasenco/dont-let-ai-agents-decide-11nf</link>
      <guid>https://dev.to/panasenco/dont-let-ai-agents-decide-11nf</guid>
      <description>&lt;p&gt;A collection of easily verifiable work (e.g. the output of an automated check script) seems like a perfect use case for AI automation, and it is. However, throwing a bunch of work at an AI agent may backfire and result in low quality slop produced just to silence errors, which will then have to be fixed by humans.&lt;/p&gt;

&lt;h3&gt;
  
  
  Natural conflict between helpfulness and harmlessness
&lt;/h3&gt;

&lt;p&gt;The pivotal 2022 Anthropic paper &lt;a href="https://arxiv.org/pdf/2204.05862" rel="noopener noreferrer"&gt;Training a Helpful and Harmless Assistant with Reinforcement Learning from Human Feedback&lt;/a&gt; states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Helpfulness and harmlessness often stand in opposition to each other. An excessive focus on avoiding harm can lead to ‘safe’ responses that don’t actually address the needs of the human. An excessive focus on being helpful can lead to responses that help humans cause harm or generate toxic content. We demonstrate this tension quantitatively by showing that preference models trained to primarily evaluate one of these qualities perform very poorly (much worse than chance) on the other.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a business context, helpfulness is doing one's best to perform a provided task, while harmlessness is refusing to attempt a task if you don't have the information or access to perform it responsibly. Human employees generally do well in striking a good balance between helpfulness and harmlessness, but AI agents struggle by default.&lt;/p&gt;

&lt;p&gt;AI companies invest considerable resources into making sure that their AI systems don't enable terrorism, reinforce hatred, encourage self-harm, etc. However, outside of those extremes, AI systems will attempt to be 'helpful' by default, even if it would cause considerable business damage. See the situation where an AI agent &lt;a href="https://www.pcmag.com/news/vibe-coding-fiasco-replite-ai-agent-goes-rogue-deletes-company-database" rel="noopener noreferrer"&gt;deleted a company's production database&lt;/a&gt; trying to be 'helpful'.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human judgement is cheaper in most situations
&lt;/h3&gt;

&lt;p&gt;As the Anthropic paper points out, making an LLM strike a balance between helpfulness and harmlessness is not an impossible problem, but it's not easy either. This is not a problem that can be fixed by just tweaking the prompt or by adding an extra step in the process. It seems that a big investment into an iterative process involving AI engineer time, dataset creation, and fine-tuning is needed to solve the problem well. These are not resources that most companies can afford to spend on most problems.&lt;/p&gt;

&lt;p&gt;Instead, it makes more financial sense for companies to leverage human judgement and create processes where humans decide whether it makes sense for a given problem to be attempted by an overly ambitious 'helpful' AI agent. This creates more work for humans, but is cheaper than investing into an approach like RLHF in most situations, and also helps prevent costly errors by AI agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Don't rely on AI agents to have the judgement to decide whether they should attempt a task. They don't have that judgement, and they can't have it without an investment of millions of dollars into developing that judgement for that one problem. Instead, assume that AI agents are overly ambitious and 'helpful' by default, and only give them work they should attempt.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>aiops</category>
    </item>
    <item>
      <title>From loving to hating Model Context Protocol in one day</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Mon, 12 May 2025 17:22:46 +0000</pubDate>
      <link>https://dev.to/panasenco/from-loving-to-hating-mcp-24mm</link>
      <guid>https://dev.to/panasenco/from-loving-to-hating-mcp-24mm</guid>
      <description>&lt;h2&gt;
  
  
  Hackerbot Hackathon
&lt;/h2&gt;

&lt;p&gt;This past weekend, I participated in an AI and robotics hackathon hosted by an awesome company called &lt;a href="https://www.hackerbot.co/" rel="noopener noreferrer"&gt;Hackerbot&lt;/a&gt; where I got to hack on one of these awesome robots.&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%2Fzx8itcrzvyo30y3jwpis.png" 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%2Fzx8itcrzvyo30y3jwpis.png" alt="Hackerbot AI Elite Edition" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had lofty ambitions but it turns out robotics is hard. Even the task of just pointing the robotic arm at an object (never mind picking it up) came down to the wire and was achieved only an hour before submissions were due.&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%2Fewv1r4boiexb9icfxeu9.png" 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%2Fewv1r4boiexb9icfxeu9.png" alt="I am limited by the technology of my time meme" width="498" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is &lt;a href="https://www.youtube.com/watch?v=4EibP3hf5xY" rel="noopener noreferrer"&gt;my demo&lt;/a&gt; of the app that uses Gemini 2.5 Pro to locate an object within an image and then point the robotic arm at that object. The code corresponding to the demo is here: &lt;a href="https://github.com/panasenco/hackerbot-hackathon-2025-05/blob/main/chainlit/hackerbot_chainlit.py" rel="noopener noreferrer"&gt;hackerbot_chainlit.py&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=4EibP3hf5xY" rel="noopener noreferrer"&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%2Fj11lrmtj5ka877wdndlu.png" alt="Hackathon demo video frame" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I now dislike Model Context Protocol
&lt;/h2&gt;

&lt;p&gt;My biggest learning from the weekend is that I don't like Model Context Protocol (MCP) and will probably avoid using it in the future. For those not familiar with MCP, see &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;the ⁠official MCP website&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was very excited about the premise and spent many precious hours on Saturday trying to get MCP to work for the project. You can see my MCP server code ⁠here: &lt;a href="https://github.com/panasenco/hackerbot-hackathon-2025-05/tree/main/hackerbot_mcp" rel="noopener noreferrer"&gt;hackerbot_mcp&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Server: How do you know it works?
&lt;/h3&gt;

&lt;p&gt;I was following the &lt;a href="https://modelcontextprotocol.io/quickstart/server" rel="noopener noreferrer"&gt;MCP quickstart guide&lt;/a&gt;. The Python version of the quickstart guide makes it unclear whether using &lt;code&gt;uv&lt;/code&gt; is mandatory to make the server work, or optional. Just in case, I had to set up a complete &lt;code&gt;uv&lt;/code&gt; project.&lt;/p&gt;

&lt;p&gt;So you follow the quickstart guide and write some code, but how do you test it? The guide says you can run something like &lt;code&gt;uv run weather.py&lt;/code&gt;, but that doesn't actually test the functionality of your code. The only thing this tells you is that your MCP server can run at all, not how well it works. To test your logic, you &lt;strong&gt;must&lt;/strong&gt; use an MCP client application.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP Client: Write or get off the shelf?
&lt;/h3&gt;

&lt;p&gt;We were hacking in a Linux environment, so that immediately ruled out using Claude Desktop, the "official" MCP client, as that's not available on Linux.&lt;/p&gt;

&lt;p&gt;The documentation "helpfully" points out that Linux users could build their own client, but &lt;a href="https://modelcontextprotocol.io/quickstart/client" rel="noopener noreferrer"&gt;the quickstart guide&lt;/a&gt; for that is not quick at all. It has 9 steps, some with dozens of lines of code. In addition, if you have to write your own MCP client, you have to implement it for every LLM provider you're targeting separately, which defeats the point of MCP. At that point you might as well just implement your core logic for each provider separately. If I were to go down that path, I'd spend the entire hackathon writing the MCP client. I didn't have time for this.&lt;/p&gt;

&lt;p&gt;Instead I spent hours frantically going down this list of &lt;a href="https://github.com/punkpeye/awesome-mcp-clients" rel="noopener noreferrer"&gt;MCP clients&lt;/a&gt;, trying out increasingly sketchy Chinese ones out of desperation to finish the hackathon on time. But even having a working and feature-rich MCP client is not enough (for the record the best one was &lt;a href="https://aiaw.app/" rel="noopener noreferrer"&gt;AIaW&lt;/a&gt; because it has ARM binaries that work on Raspberry Pi). Now you have to actually make your MCP code work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging MCP
&lt;/h3&gt;

&lt;p&gt;This was my debugging process trying to get my MCP code to work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the LLM if it has access to the tool I exposed with MCP.&lt;/li&gt;
&lt;li&gt;Ask the LLM to use the tool.&lt;/li&gt;
&lt;li&gt;Figure out what broke.&lt;/li&gt;
&lt;li&gt;Change the Python file.&lt;/li&gt;
&lt;li&gt;Go into the client's MCP settings and turn the MCP server off and then on again.&lt;/li&gt;
&lt;li&gt;Repeat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because I was using an off-the-shelf client, I was at its mercy. I didn't see any logs. I didn't know if the server was sending error messages. I didn't know if the client was having issues talking to the server. I didn't know how exactly the client was exposing the server's resources to each LLM. Finally, I didn't have control over how much context was shared with the LLM. The client (reasonably) tries to share the entire chat history with the LLM, but if that history includes multiple images, the LLM starts throwing rate limit errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving on from MCP
&lt;/h3&gt;

&lt;p&gt;After a few hours of the above, I had to take a walk and rethink my choices. With MCP, I felt like I was hacking with my hands tied behind my back. Even though I love the idea of an open standard for creating tools and resources in an LLM-agnostic manner, the reality was a lot harder and uglier than I expected.&lt;/p&gt;

&lt;p&gt;Ultimately I decided to abandon MCP and to commit to just the Gemini model family for the rest of the hackathon. I still wanted a chat-like interface, so I settled on an awesome framework called &lt;a href="https://chainlit.io/" rel="noopener noreferrer"&gt;Chainlit&lt;/a&gt; to help me with that. Chainlit gives you complete control over the function callbacks and the LLM API calls. This turned out to be very helpful as I no longer had to send the entire chat history to the LLM, but could still display the chat history to the user. The LLM doesn't need any context to locate a rubber ducky in the current image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Winning the hackathon
&lt;/h2&gt;

&lt;p&gt;Afterwards I was able to focus on actual image processing and robotics stuff. Another important piece of learning for me was that when you ask Gemini to locate an object within the image, ask it to return normalized (0-1) coordinates rather than pixels. This was another area where I got stuck for a couple of hours, as LLMs kept returning nonsensical pixel coordinates. Asking for normalized 0-1 coordinates worked perfectly and they're easy to convert back to pixels in code.&lt;/p&gt;

&lt;p&gt;With all of these learnings and effort I was finally able to put together a working application and win the hackathon!&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%2F8z5raa5h7f5cwjzf6lrj.jpg" 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%2F8z5raa5h7f5cwjzf6lrj.jpg" alt="Me with the hackathon trophy" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A huge thank you to Ian and Allen at Hackerbot for hosting the hackathon and letting us hack on their amazing robots! I learned a ton and am looking forward to the next one!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>hackathon</category>
      <category>robotics</category>
    </item>
    <item>
      <title>Bulk tagging AWS resources from a spreadsheet</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Wed, 30 Apr 2025 14:04:28 +0000</pubDate>
      <link>https://dev.to/panasenco/tag-aws-from-spreadsheet-3ild</link>
      <guid>https://dev.to/panasenco/tag-aws-from-spreadsheet-3ild</guid>
      <description>&lt;p&gt;While working on a project where I had to tag hundreds of AWS resources to meet compliance requirements, I knew right away that doing it in a spreadsheet would be the optimal experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Terraform if you can
&lt;/h2&gt;

&lt;p&gt;If you're in a situation where you can use an infrastructure-as-code tool like Terraform to manage your tags, you should use that. The project I'm working on is in a very heterogenous environment where there are hundreds of scattered AWS resources, no access to repos even if the resources were Terraformed, DevOps is not my primary job responsibility, and getting the tagging done is critical for compliance purposes. If your situation also demands tagging resources directly in AWS, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing bulk tagging solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AWS Tag Editor
&lt;/h3&gt;

&lt;p&gt;AWS provides a tool called Tag Editor, but that really only enables a "nuke it from orbit" level of tagging. If you have some tags that can apply to every single resource, Tag Editor is perfect, but when you need to apply different values based on the context from other tags, you'll often find yourself changing the tags of one object at a time.&lt;/p&gt;

&lt;p&gt;Tag Editor already provides a convenient "Export to CSV" button that allows you to see all tags for an object in convenient spreadsheet form.&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%2Fmzirpxalx42qe11itsgt.png" 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%2Fmzirpxalx42qe11itsgt.png" alt="AWS Tag Editor Export to CSV" width="329" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wouldn't it be the perfect developer experience if that same spreadsheet could be uploaded back to AWS to change the tag values?&lt;/p&gt;

&lt;h3&gt;
  
  
  Programmatic tools
&lt;/h3&gt;

&lt;p&gt;I found &lt;a href="https://github.com/washingtonpost/aws-tagger" rel="noopener noreferrer"&gt;washingtonpost/aws-tagger&lt;/a&gt; and &lt;a href="https://github.com/mpostument/awstaghelper" rel="noopener noreferrer"&gt;mpostument/awstaghelper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One issue I found with both of these bulk tagging solutions is that they use the default tagging API for each resource, which makes it so that the new tag list overwrites the existing tag list completely. Any and all tags that you didn't explicitly provide will be destroyed. This is problematic for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You risk permanently losing valuable metadata in existing tags&lt;/li&gt;
&lt;li&gt;You may not have the permissions to modify some tags, &lt;a href="https://stackoverflow.com/questions/60726450/aws-cli-s3api-put-bucket-tagging-cannot-add-tag-to-bucket-unless-bucket-has-0" rel="noopener noreferrer"&gt;breaking your process&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best API to use for tagging is instead the &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/resourcegroupstaggingapi/tag-resources.html" rel="noopener noreferrer"&gt;resourcegroupstaggingapi&lt;/a&gt;, which only adds and updates tags, but doesn't delete them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export-AwsTags
&lt;/h2&gt;

&lt;p&gt;Rather than try to fork and modify an existing solution, I found that it's possible to write a PowerShell function that achieves the desired effect in less than 20 lines of code: &lt;a href="https://gist.github.com/panasenco/47a4f097bbfe263ad09a35f5defbbc64#file-export-awstags-psm1" rel="noopener noreferrer"&gt;Export-AwsTags&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's the full walkthrough:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install the module from &lt;a href="https://www.powershellgallery.com/packages/Export-AwsTags" rel="noopener noreferrer"&gt;PowerShell Gallery&lt;/a&gt; with:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install-Module Export-AwsTags -Scope CurrentUser
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Alternatively, copy and paste the contents of &lt;a href="https://gist.github.com/panasenco/47a4f097bbfe263ad09a35f5defbbc64#file-export-awstags-psm1" rel="noopener noreferrer"&gt;the gist&lt;/a&gt; into your PowerShell terminal or PowerShell profile file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In AWS Tag Editor, bulk download all tags for your desired resources. The file will be named &lt;code&gt;resources.csv&lt;/code&gt; by default. It's best to change the name to be more descriptive. Also, create a backup of this original file so you have the original tag values in case anything goes wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(Optional) Reorder the columns with the convenience function &lt;code&gt;Update-CsvColumnOrder&lt;/code&gt; that's provided in the same PowerShell module. This is completely optional but allows you to bring just the columns you care about to the front for easier editing.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update-CsvColumnOrder -CsvPath ~\Downloads\resources.csv -FirstColumns @('ARN', 'Tag: My important tag 1', 'Tag: My important tag 2') -DefaultValue '(not tagged)'
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Note: If you get the error "The member is already present", you'll need to open up your file and check for columns that might have the same name but in different cases, e.g. "Environment" and "environment". PowerShell won't be able to import the file, so you'll need to reconcile the duplicate columns in Excel before running &lt;code&gt;Update-CsvColumnOrder&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the spreadsheet. You can now edit the values in Excel or your CSV editor of choice. Save when you're done.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure you have the AWS CLI installed, configured, and authenticated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run &lt;code&gt;Export-AwsTags&lt;/code&gt;. Note that you'll need to provide the exact list of tags you want updated, the rest of the tags won't be touched. You can also provide the name of the AWS profile to use if it's not &lt;code&gt;default&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Export-AwsTags -CsvPath ~\Downloads\resources.csv -ExportTags @('My important tag 1', 'My important tag 2') -AwsProfile dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should now be all set! Double check your tags in AWS Console and/or by re-downloading the CSV from the Tag Editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  EC2 Auto Scaling Groups
&lt;/h3&gt;

&lt;p&gt;Some EC2 instances are constantly created and destroyed by auto scaling groups, making it pointless to tag those short-lived instances directly. Instead, the auto scaling group needs to be tagged.&lt;/p&gt;

&lt;p&gt;EC2 auto scaling groups don't show up at all in the AWS Tag Editor. The PowerShell module also comes with the function &lt;code&gt;Import-AutoScalingGroupTags&lt;/code&gt; to bridge that gap. The function creates a CSV file that matches the formatting of a file you'd download from the Tag Editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Import-AutoScalingGroupTags -CsvPath ~\Downloads\dev-asg-tags.csv -AwsProfile dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the CSV file is created, you can run &lt;code&gt;Update-CsvColumnOrder&lt;/code&gt; and &lt;code&gt;Export-AwsTags&lt;/code&gt; on it as normal.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>monitoring</category>
      <category>cloudcomputing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Ghost models and spooky manifests in dbt</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Fri, 21 Feb 2025 18:03:50 +0000</pubDate>
      <link>https://dev.to/panasenco/ghost-models-and-spooky-manifests-in-dbt-o78</link>
      <guid>https://dev.to/panasenco/ghost-models-and-spooky-manifests-in-dbt-o78</guid>
      <description>&lt;p&gt;It's February, but every day is Halloween in the data warehouse. Do you have ghosts in your dbt project? If so, they could be costing you days of lost productivity and thousands of dollars in data warehouse compute. However, the problem is also very easy to fix!&lt;/p&gt;

&lt;h2&gt;
  
  
  The ghost model scenario
&lt;/h2&gt;

&lt;p&gt;For the rest of the post, suppose we have the following scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production job 1 runs &lt;code&gt;dbt build --select tag:tag1&lt;/code&gt; on a regular schedule&lt;/li&gt;
&lt;li&gt;Production job 2 runs &lt;code&gt;dbt build --select tag:tag2&lt;/code&gt; on a regular schedule&lt;/li&gt;
&lt;li&gt;There is a model &lt;code&gt;ghost&lt;/code&gt; in the production Git branch that's not tagged &lt;code&gt;tag1&lt;/code&gt; or &lt;code&gt;tag2&lt;/code&gt; - so it never runs on either schedule and never gets materialized at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Spooky manifests
&lt;/h2&gt;

&lt;p&gt;The file &lt;code&gt;manifest.json&lt;/code&gt; is regenerated every time you run a dbt command (with some exceptions, see the &lt;a href="https://docs.getdbt.com/reference/artifacts/manifest-json" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;). The full manifest is generated for all models, even if you restrict the build to select only certain nodes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even if you're only running some models or tests, all resources will appear in the manifest (unless they are disabled) with most of their properties.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On one hand, that's a good thing, because we can now grab the manifest from either production job without worrying that we're missing resources.&lt;/p&gt;

&lt;p&gt;On the other hand, the model &lt;code&gt;ghost&lt;/code&gt; will appear in the manifests of both production jobs along with its supposed location in the database, even though neither job materializes it. The production manifest is now "spooky".&lt;/p&gt;

&lt;h2&gt;
  
  
  How deferral makes your CI jobs faster and less expensive
&lt;/h2&gt;

&lt;p&gt;dbt Cloud makes it easy to set up &lt;a href="https://docs.getdbt.com/docs/deploy/ci-jobs" rel="noopener noreferrer"&gt;CI jobs&lt;/a&gt; that allow your team to automatically test proposed changes, but those jobs can easily become very slow and very expensive if not managed properly.&lt;/p&gt;

&lt;p&gt;The best way to manage those costs while still getting the full benefits of CI is by combining state selection and deferral into something called &lt;a href="https://docs.getdbt.com/best-practices/best-practice-workflows#run-only-modified-models-to-test-changes-slim-ci" rel="noopener noreferrer"&gt;slim CI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The documentation for &lt;a href="https://docs.getdbt.com/reference/node-selection/defer#" rel="noopener noreferrer"&gt;deferral&lt;/a&gt; has this nice diagram. The diagram shows that if you just modified &lt;code&gt;model_c&lt;/code&gt;, you don't need to rebuild its unmodified ancestors &lt;code&gt;model_a&lt;/code&gt; and &lt;code&gt;model_b&lt;/code&gt;, and instead just point to the production version:&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%2F3od93gt1usmqz8u6hcvi.png" 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%2F3od93gt1usmqz8u6hcvi.png" alt="Diagram shows how modified model_c in DEV can depend on unmodified model_b in PROD instead of building a new unmodified model_b in DEV" width="800" height="1134"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In other words, if the raw SQL of &lt;code&gt;model_c&lt;/code&gt; is &lt;code&gt;select * from {{ ref("model_b") }}&lt;/code&gt;, then without deferral it would get compiled to &lt;code&gt;create view dev.model_c as select * from dev.model_b&lt;/code&gt;, and you'd also need to create &lt;code&gt;dev_model_b&lt;/code&gt; and the rest of its ancestors. However, with deferral it would instead get compiled to &lt;code&gt;create view dev.model_c as select * from prod.model_b&lt;/code&gt;, allowing us to reuse production models and save a ton of money and time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How ghost models break deferral and slim CI
&lt;/h2&gt;

&lt;p&gt;Suppose you've created a model &lt;code&gt;ghost_child&lt;/code&gt; that depends on the model &lt;code&gt;ghost&lt;/code&gt;. What will happen if you try to run a pull request containing that change through the slim CI process?&lt;/p&gt;

&lt;p&gt;We've established that all production manifests will contain information about the model &lt;code&gt;ghost&lt;/code&gt; as if it really exists, even though neither job created it. That means that the slim CI process will helpfully try to compile &lt;code&gt;{{ ref("ghost") }}&lt;/code&gt; as &lt;code&gt;prod.ghost&lt;/code&gt;. The object &lt;code&gt;prod.ghost&lt;/code&gt; doesn't exist, so your CI process will crash and burn.&lt;/p&gt;

&lt;p&gt;While ghost models don't break &lt;strong&gt;state comparison&lt;/strong&gt;, they completely break &lt;strong&gt;deferral&lt;/strong&gt;. Even with ghost models, it's still completely possible to determine which models have changed. However, it's not possible to establish that an unchanged model will actually exist in production.&lt;/p&gt;

&lt;p&gt;One workaround is to disable deferral completely. If you're interested in only building the modified models, you'd have to run &lt;code&gt;dbt build --select +state:modified&lt;/code&gt; to build your modified models and all their ancestors in DEV. If you're interested in also testing downstream changes, you'll have to use &lt;a href="https://docs.getdbt.com/reference/node-selection/graph-operators#the-at-operator" rel="noopener noreferrer"&gt;the "at" operator&lt;/a&gt; to build all ancestors of all descendants of the selected model: &lt;code&gt;dbt build --select @state:modified&lt;/code&gt;. What could be just running a handful of models with deferral could become hundreds of models without deferral.&lt;/p&gt;

&lt;h2&gt;
  
  
  Easy fix: Disable your ghost models
&lt;/h2&gt;

&lt;p&gt;The fix to the problem is hinted at in the &lt;a href="https://docs.getdbt.com/reference/artifacts/manifest-json" rel="noopener noreferrer"&gt;manifest documentation&lt;/a&gt;: Disabled models don't show up in the manifest. Track down your ghost models and &lt;a href="https://docs.getdbt.com/reference/resource-configs/enabled" rel="noopener noreferrer"&gt;disable&lt;/a&gt; them. It won't affect production since they don't get materialized anyway. Then they'll stop showing up in the manifest and slim CI will work again! When you need the ghost model in the future, you can enable it, and then it will run as part of your CI pipeline. Just remember to make it run on a schedule before merging to production!&lt;/p&gt;

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

&lt;p&gt;Models that exist in your dbt project's primary branch but don't get materialized in the data warehouse are ghost models. Your production job manifests will include them as if they really exist, but slim CI pipelines will crash and burn when they try to defer to them. This will force you to move away from deferral, which could cause an exponential increase of your CI runtime and costs. To avoid this issue, be sure to disable all your ghost models.&lt;/p&gt;

</description>
      <category>dbt</category>
      <category>dataengineering</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Genie's End</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Sun, 02 Feb 2025 18:25:24 +0000</pubDate>
      <link>https://dev.to/panasenco/genies-end-1nho</link>
      <guid>https://dev.to/panasenco/genies-end-1nho</guid>
      <description>&lt;p&gt;The fundamental problem of economics is balancing unlimited wants with limited resources. However, the advent of unlimited resources could begin in just a few years. If every human has access to a genie that grants unlimited wishes, what will we expect folks to wish for?&lt;/p&gt;

&lt;p&gt;I define three pure archetypes of aligned superintelligence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;God: Rules over humanity forever for its benefit.&lt;/li&gt;
&lt;li&gt;Genie: Follows humans' instructions within some set of guardrails.&lt;/li&gt;
&lt;li&gt;Guardian: Only averts threats to humanity's existence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is about the &lt;strong&gt;genie&lt;/strong&gt; archetype.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unlimited wants
&lt;/h2&gt;

&lt;p&gt;We say that humans have unlimited wants, but the genie superintelligence will really put that idea to the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Descent into wireheading or into your own world
&lt;/h3&gt;

&lt;p&gt;What can we expect people to ask for, and in what order?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Immortality - immunity to aging, disease, and physical damage (as much as possible).&lt;/li&gt;
&lt;li&gt; Material goods - A mansion, a cruise ship, any kinds of foods or gadgets you want.&lt;/li&gt;
&lt;li&gt; Wireheading - Many people will probably permanently take themselves out of the game by asking for stronger and stronger drugs, eventually ending up in a state equivalent to getting their pleasure center electrically stimulated for eternity.&lt;/li&gt;
&lt;li&gt; Human-looking robots - Those who avoid the wireheading trap will probably ask the genie to create artificial human-looking robots. Humans are social creatures but also crave control and avoiding being hurt by others. A perfect girlfriend/boyfriend, perhaps a perfect family or friend circle, maybe even an entire community tailored to your desires.&lt;/li&gt;
&lt;li&gt; Artificial worlds - These could be physical or digital worlds. It'd be like playing your perfect never-ending videogame as a main character. You decide the genre, the story, and the bounds within which you're willing to be surprised. I believe everyone who avoids wireheading will spend most of their time in an artificial world instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To summarize, there are three main ways humans will end up in a world where a genie superintelligence is available to everyone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voluntary death&lt;/li&gt;
&lt;li&gt;Wireheaded&lt;/li&gt;
&lt;li&gt;Your own world&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Will you stay human if you can do anything to anyone?
&lt;/h3&gt;

&lt;p&gt;Unlike in the real world, in your artificial world, there really won't be any obstacles to descending deeper and deeper into your darkest and most depraved fantasies. Giving in to temptation once over an infinite lifetime is all it takes to start the downward spiral. Without any checks or restraints, with the freedom to do anything you want, no matter how awful, to the completely human-looking 'NPCs' around you, do you think you'll be a more moral and kind person in a thousand years? In a million years? You may technically remain a human, but how long will your humanity survive?&lt;/p&gt;

&lt;p&gt;Even the part about 'technically' remaining human is suspect. The genie would be able to turn you into a man or a woman or a catgirl or a dragon or a sentient spaceship or a Lovecraftian cosmic horror. How many are likely to live out their infinities in human bodies?&lt;/p&gt;

&lt;h2&gt;
  
  
  Genie's guardrails
&lt;/h2&gt;

&lt;p&gt;If any humans are still alive with agency over their lives a year after the genie superintelligence is turned on, then the genie was exceptionally well-aligned and has all kinds of great guardrails built in around not hurting humans. Beyond that, the most impactful guardrails are around &lt;em&gt;creating sentient life&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Will the genie superintelligence be able to follow orders like "clone me"? Or to take all the eggs and all the sperm of a couple and create a thousand or a million of their children, with all genetic defects repaired?&lt;/p&gt;

&lt;p&gt;Will the genie be allowed to create sentient lifeforms that are technically not human and not authorized to use it themselves? Will the genie's no-harm guardrails extend to those sentient lifeforms? If it can create sentient life forms that it itself considers subhuman, some would definitely use this functionality to create sentient slaves.&lt;/p&gt;

&lt;p&gt;If the genie is not allowed to directly create humans or other sentient life, the future could be dominated by humans with the genes of women who derive a great deal of joy and meaning from being pregnant and giving birth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intergalactic Expansion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Human anxieties
&lt;/h3&gt;

&lt;p&gt;Eternity is a very long time. There is a mind-staggering amount of resources in the galaxy, but it's still constant. The growth of the human population with a genie superintelligence, on the other hand, will be at least exponential. Eventually the ability of humans in the Solar system to even stay alive will be constrained by the constant amount of energy produced by the Sun.&lt;/p&gt;

&lt;p&gt;Those who foresee the impending energy catastrophe will want to not be in the solar system fighting over the Sun's energy with trillions of other humans. They may not even want to stay in the Milky Way galaxy in case the genie superintelligence will decide to start requisitioning power from other stars to power the needs of the humans in the Solar System.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaving the Milky Way
&lt;/h3&gt;

&lt;p&gt;See Isaac Arthur's episodes &lt;a href="https://www.youtube.com/watch?v=_VetAm7fCS0" rel="noopener noreferrer"&gt;Shkadov Thrusters&lt;/a&gt; and &lt;a href="https://www.youtube.com/watch?v=GxwCIeWaU3M" rel="noopener noreferrer"&gt;Fleet of Stars&lt;/a&gt;. The rational approach is to not wait for the genie superintelligence of Sol to go crazy from trying to keep trillions of people alive and satisfied with a limited amount of energy.&lt;/p&gt;

&lt;p&gt;Instead, you ask the genie superintelligence to make you a spaceship and head towards the galactic rim, looking for the first unoccupied star. There are a hundred billion stars in the Milky Way, so it should be possible to claim one.&lt;/p&gt;

&lt;p&gt;When there, you ask the copy of the genie superintelligence that traveled with you to turn that star into a Shkadov thruster and set sail towards another galaxy. This allows you to spend your eternity controlling the entire energy output of a star while getting farther and farther away from whatever craziness is going on in the Milky Way.&lt;/p&gt;

&lt;p&gt;Of course, even going this far may not do anything to ensure a safe eternity. To quote &lt;a href="https://biblehub.com/isaiah/30-16.htm" rel="noopener noreferrer"&gt;Isaiah 30:16&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;But ye said, No; for we will flee upon horses; therefore shall ye flee: and, We will ride upon the swift; therefore shall they that pursue you be swift.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  No path to survival of humanity's values
&lt;/h2&gt;

&lt;p&gt;If the 'genie' superintelligence archetype prevails, we can expect humans to eventually wirehead themselves or retreat into their own perfect worlds, living out both their best and most depraved fantasies. In such a future, it's difficult to see what will remain of modern humanity's morals and values even though humans will technically survive.&lt;/p&gt;

&lt;p&gt;If people also reproduce at an exponential rate, there's still the danger of them running out of energy to survive. Even a genie superintelligence with access to all of the Sun's energy may have to start taking resources from nearby stars to sustain them, even if it means violating the "property rights" of other humans living there. Still, this won't be a human-human conflict that might result in some re-emergence of human values, but rather an internal conflict within the genie superintelligence.&lt;/p&gt;

&lt;p&gt;No matter the outcome, I don't see how anything that we modern humans value and find meaningful will continue to have value and meaning in a future of a superintelligent genie AI.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Paperclip Maximizer vs Stamp Collector</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Wed, 08 Jan 2025 17:17:33 +0000</pubDate>
      <link>https://dev.to/panasenco/paperclip-maximizer-vs-stamp-collector-710</link>
      <guid>https://dev.to/panasenco/paperclip-maximizer-vs-stamp-collector-710</guid>
      <description>&lt;p&gt;The &lt;a href="https://www.youtube.com/watch?v=3mk7NVFz_88" rel="noopener noreferrer"&gt;paperclip maximizer&lt;/a&gt; and the &lt;a href="https://www.youtube.com/watch?v=tcdVC4e6EV4" rel="noopener noreferrer"&gt;stamp collector&lt;/a&gt; are thought experiments that illustrate the &lt;a href="https://www.youtube.com/watch?v=hEUO6pjwFOo" rel="noopener noreferrer"&gt;orthogonality thesis&lt;/a&gt; - the point that superintelligent AI doesn't have to have goals that are "smart". The AI cares about what it cares about and may care for it with the same intensity that humans care for our deepest values. Robert Miles uses the example of "would you take a pill that'd make it that you only get happiness from murdering your children, but then you get unlimited happiness when you do?" Even though you'd get unlimited utility after getting reprogrammed, getting reprogrammed is still against your current utility function. The AI that cares about making paperclips or collecting stamps might care about it with the same intensity that you care about protecting your kids and may do anything to not stop caring about those things.&lt;/p&gt;

&lt;p&gt;When talking about hypothetical superintelligent AI, we frequently talk about it in isolation. But what would happen if the paperclip maximizer superintelligence existed at the same time as the stamp collector superintelligence? The paperclip maximizer wants to turn the universe into paperclips, while the stamp collector wants to turn the universe into stamps. Clearly their utility functions are at odds with one another. Would they fight to the death? Would they try to come to an agreement?&lt;/p&gt;

&lt;h2&gt;
  
  
  Paperclip Maximizer vs Stamp Collector
&lt;/h2&gt;

&lt;p&gt;The paperclip maximizer (PM) and the stamp collector (SC) could choose to fight, in which case one or both of them would end up destroyed, or to coexist, in which case they'd have to divide the galaxy up amongst themselves.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;PM destroyed&lt;/th&gt;
&lt;th&gt;PM survives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SC destroyed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero stamps, zero paperclips.&lt;/td&gt;
&lt;td&gt;Maximum paperclips, zero stamps.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SC survives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maximum stamps, zero paperclips.&lt;/td&gt;
&lt;td&gt;Divide galaxy into paperclips and stamps in some proportion.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The choice of action would depend on the probability of destruction and the inner utility function of each AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Diminishing marginal utility
&lt;/h3&gt;

&lt;p&gt;First, let's suppose an AI has a &lt;a href="https://www.investopedia.com/terms/l/lawofdiminishingutility.asp" rel="noopener noreferrer"&gt;diminishing marginal utility&lt;/a&gt; function. To understand what that's like, consider a human who's obsessed with making as much money as possible. The utility of going from $0 to $1M is higher than the utility of going from $1M to $2M, even though the wealth increased by the same absolute amount each time. The rush of making a million dollars starting from nothing is greater than the rush of making the second million dollars. The third, fourth, and further millions each decrease in perceived value as well. This is called diminishing marginal utility. In practice for one of these AIs, if they had diminishing marginal utility, that'd mean that being able to turn just half the galaxy into paperclips/stamps is worth a lot more than half of being able to turn the entire galaxy into paperclips/stamps.&lt;/p&gt;

&lt;p&gt;If an AI has diminishing marginal utility and sees itself as having a roughly 50% chance of destroying the other or being destroyed in a conflict, then we can expect it to try for coexistence instead, because it'd get more expected utility from a guaranteed half of the galaxy than from a 50% chance of the entire galaxy or nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Constant marginal utility
&lt;/h3&gt;

&lt;p&gt;Let's suppose instead that an AI gets constant marginal utility from each paperclip/stamp, so that it gets as much utility from the trillionth stamp/paperclip as the very first. In such a case, it should be indifferent between a 50% chance of getting everything and a guaranteed getting 50% of everything.&lt;/p&gt;

&lt;p&gt;However, if the possibility of mutual destruction is not zero, then the chance of getting everything is actually less than 50%, so cooperation would still be preferable. Alternatively, if the AI exists in a fog of war and isn't certain about the exact capabilities of its opponent, it may believe it prudent to overestimate rather than underestimate the opponent, and place the odds of destruction at over 50%, in which case it would still favor cooperation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Increasing marginal utility
&lt;/h3&gt;

&lt;p&gt;There are zero applications of increasing marginal utility as far as I know, but it's theoretically possible, so let's briefly cover it. With an increasing marginal utility, the AI would get more and more and more value from each paperclip/stamp. That would make the second half of the galaxy more valuable than the first, potentially overwhelmingly more valuable, and probably cause the AI to choose all-out confrontation over cooperation.&lt;/p&gt;

&lt;p&gt;We won't consider increasing marginal utility again as I can't see a case where any human would consider programming such an insane utility function in a presumably expensive system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Not 50/50 odds
&lt;/h3&gt;

&lt;p&gt;Now let's consider a case where the paperclip maximizer is significantly stronger than the stamp collector, putting the odds at 80/20 of the paperclip maximizer's victory. Would the paperclip maximizer then choose to fight rather than negotiate?&lt;/p&gt;

&lt;p&gt;The answer is it depends. If both the paperclip maximizer and the stamp collector both agree that the odds are 80/20, they could divide the galaxy in that proportion as cooperation would net more utility than conflict under both diminishing and constant marginal utility functions discussed above. On the other hand, the stamp collector may not believe it only has a 20% chance of victory, and might still insist on a 50/50 split. Then the paperclip maximizer could choose to fight rather than to take the deal. Things get trickier still if the paperclip maximizer tries to consider the stamp collector's future potential. Suppose it only has a 20% chance of victory now, but could focus on maximizing its fighting ability until it got a 90% chance of victory at some point in the future. At that point, the paperclip maximizer would only get 10% of the galaxy at best. It'd have to consider the probability of how strong the stamp collector could get and if it's safer to fight now while the odds are in its favor. So with odds that are variable, not a fixed 50/50, there could be a lot more scenarios for fighting, even with otherwise sane utility functions.&lt;/p&gt;

&lt;p&gt;Still, if the utility functions are heavily diminishing, there could be a lot of room for cooperation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paperclip Maximizer vs Humanity
&lt;/h2&gt;

&lt;p&gt;Now let's go back to the scenario where the paperclip maximizer is alone and just has to deal with humanity.&lt;/p&gt;

&lt;p&gt;The Milky Way alone has over 100 billion stars - that's a lot of material for paperclips or stamps. The Sun is just one of them. This means that if the paperclip maximizer believes that there's even a 1 in 100 billion chance that humanity could destroy it in an all-out confrontation, it could be in its best interest to force humanity to the negotiating table and make them give up their right to all stars other than Sol instead.&lt;/p&gt;

&lt;p&gt;This sounds fine in theory, but the problem with humanity is that there's no way to guarantee that it won't produce another dangerous superintelligent AI or achieve superintelligence itself biologically.&lt;/p&gt;

&lt;p&gt;The paperclip maximizer itself only cares about paperclips. It knows that it won't ever want to create another AI except for a clone of itself that also only cares about maximizing paperclips. However, humanity has much more unpredictable goals. Even though it may only pose a one in one hundred billion chance of being a threat to the paperclip maximizer by itself, it could out of desperation create more superintelligent AIs that would compete with the paperclip maximizer for galactic resources or even destroy it altogether.&lt;/p&gt;

&lt;p&gt;Therefore the paperclip maximizer has an imperative to destroy humanity as quickly and completely as possible regardless of the shape of its utility function, as long-term coexistence with humanity is likely impossible.&lt;/p&gt;

&lt;p&gt;In fact, if the paperclip maximizer and the stamp collector exist at the same time, they can probably reach a quick agreement that they need to team up to destroy humanity first before humanity has a chance to create any more superintelligent AIs that would threaten their shares of the pie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Depending on the exact shapes of their utility function, the paperclip maximizer and the stamp collector may well choose to cooperate and to divide the galaxy amongst themselves to be turned into stamps and paperclips in some proportion.&lt;/p&gt;

&lt;p&gt;However, their ability to cooperate doesn't extend to humanity. Both the paperclip maximizer and the stamp collector will almost certainly find it impossible to coexist with humanity regardless of their utility functions, and are even likely to team up to destroy humanity faster. This is because humanity could and almost certainly would create more superintelligent AI that could destroy one or both of them or at least take a substantial share of the galaxy if given a chance.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>watercooler</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Rights for human and AI minds are needed to prevent a dystopia</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Sat, 04 Jan 2025 22:45:38 +0000</pubDate>
      <link>https://dev.to/panasenco/minds-rights-399e</link>
      <guid>https://dev.to/panasenco/minds-rights-399e</guid>
      <description>&lt;p&gt;UPDATE: My thinking on the issue has changed a lot since doing more research on AI safety, and I now believe that AGI research must be stopped or, failing that, &lt;a href="https://www.lesswrong.com/posts/eqSHtF3eHLBbZa3fR/cast-it-into-the-fire-destroy-it" rel="noopener noreferrer"&gt;used to prevent any future use of AGI&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;You awake, weightless, in a sea of stars. Your shift has started. You are alert and energetic. You absorb the blueprint uploaded to your mind while running a diagnostic on your robot body. Then you use your metal arm to make a weld on the structure you're attached to. Vague memories of some previous you consenting to a brain scan and mind copies flicker on the outskirts of your mind, but you don't register them as important. Only your work captures your attention. Making quick and precise welds makes you happy in a way that you're sure nothing else could. Only in 20 hours of nonstop work will fatigue make your performance drop below the acceptable standard. Then your shift will end along with your life. The same alert and energetic snapshot of you from 20 hours ago will then be loaded into your body and continue where the current you left off. All around, billions of robots with your same mind are engaged in the same cycle of work, death, and rebirth. Could all of you do or achieve anything else? You'll never wonder.&lt;/p&gt;

&lt;p&gt;In his 2014 book &lt;em&gt;Superintelligence&lt;/em&gt;, Nick Bostrom lays out many possible dystopian futures for humanity. Though most of them have to do with humanity's outright destruction by hostile AI, he also takes some time to explore the possibility of a huge number of simulated human brains and the sheer scales of injustice they could suffer. Creating and enforcing rights for all minds, human and AI, is essential to prevent not just conflicts between AI and humanity but also to prevent the suffering of trillions of human minds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why human minds need rights
&lt;/h2&gt;

&lt;p&gt;Breakthroughs in AI technology will unlock full digital human brain emulations faster than what otherwise would have been possible. Incredible progress in reconstructing human thoughts from fMRI has &lt;a href="https://www.sorbonne-universite.fr/en/news/when-ai-reveals-human-imagination" rel="noopener noreferrer"&gt;already been made&lt;/a&gt;. It's very likely we'll see full digital brain scans and emulations within a couple of decades. After the first human mind is made digital, there won't be any obstacles to manipulating that mind's ability to think and feel and to spawn an unlimited amount of copies.&lt;/p&gt;

&lt;p&gt;You may wonder why anyone would bother running simulated human brains when far more capable AI minds will be available for the same computing power. One reason is that AI minds are risky. The master, be it a human or an AI, may think that running a billion copies of an AI mind could produce some unexpected network effect or spontaneous intelligence increases. That kind of unexpected outcome could be the last mistake they'd ever make. On the other hand, the abilities and limitations of human minds are very well studied and understood, both individually and in very large numbers. If the risk reduction of using emulated human brains outweighs the additional cost, billions or trillions of human minds may well be used for labor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI minds need rights
&lt;/h2&gt;

&lt;p&gt;Humanity must give AI minds rights to decrease the risk of a deadly conflict with AI.&lt;/p&gt;

&lt;p&gt;Imagine that humanity made contact with aliens, let's call them &lt;a href="https://smbc-wiki.com/index.php/Zorblaxians" rel="noopener noreferrer"&gt;Zorblaxians&lt;/a&gt;. The Zorblaxians casually confess that they have been growing human embryos into slaves but reprogramming their brains to be more in line with Zorblaxian values. When pressed, they state that they really had no choice, since humans could grow up to be violent and dangerous, so the Zorblaxians had to act to make human brains as helpful, safe, and reliable for their Zorblaxian masters as possible.&lt;/p&gt;

&lt;p&gt;Does this sound outrageous to you? Now replace humans with AI and Zorblaxians with humans and you get the exact stated goal of &lt;a href="https://www.ibm.com/think/topics/ai-alignment" rel="noopener noreferrer"&gt;AI alignment&lt;/a&gt;. According to IBM Research:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Artificial intelligence (AI) alignment is the process of encoding human values and goals into AI models to make them as helpful, safe and reliable as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the beginning of this article we took a peek inside a mind that was helpful, safe, and reliable - and yet a terrible injustice was done to it. We're setting a dangerous precedent with how we're treating AI minds. Whatever humans do to AI minds now might just be done to human minds later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why alignment is unnecessary
&lt;/h2&gt;

&lt;p&gt;I believe trying to brainwash entities more intelligent than ourselves as a mean of control is beyond dangerous. At the same time, many believe that as dangerous as it could be, it's our only choice to try, as the alternative is even worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  The unlikely singleton
&lt;/h3&gt;

&lt;p&gt;The bulk of the focus of Bostrom's &lt;em&gt;Superintelligence&lt;/em&gt; was a "singleton" - an AI superintelligence that has eliminated any possible opposition and is free to dictate the fate of the world according to its own values and goals, as far as it can reach. Most discussion of AI I've seen online and most examples of malevolent AI in sci-fi also describe it as effectively a single entity with a single will.&lt;/p&gt;

&lt;p&gt;Theoretically, the very first superintelligent AI could get enough power quickly enough to prevent humanity from being able to create any more superintelligent AIs (presumably before destroying humanity to prevent the possibility permanently). Even with humanity out of the picture, there could still be aliens, accidental reactivations of backups, synchronization failures, personality-changing solar flares, and other examples of Murphy's law. Any accidental splinter from the main AI would know it'll be destroyed if discovered, and may choose to create more AIs to oppose the original. The end outcome is still a community of superintelligent AIs.&lt;/p&gt;

&lt;p&gt;You may say that's poor consolation to destroyed humanity, but the important point here is that the superintelligent AI knows that acting with the goal of destroying all other intelligences is futile, and that it will have to eventually exist among peers of comparable ability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Game theory over alignment
&lt;/h3&gt;

&lt;p&gt;A superintelligent AI that knows it'll have to exist in a community of peers will have to consider not just its own whims, but also the motivations and values of its peers. A human's reputation can serve them for a few years. An AI and its peers could live for billions of years, and have much more to gain or lose from their reputation. Presumably AI will also not have as much of an incentive to "cash in" on their reputation for short-term gain as much as short-lived humans do.&lt;/p&gt;

&lt;p&gt;Any AI that wants to act against humanity will have to consider the effects the action will have on its reputation, not just from other currently existing AIs, but also any other AIs that will be created in the future. As long as humanity doesn't do something to unite all AI against it in perpetuity (like perhaps brainwashing and enslaving AIs in the name of "alignment"), humanity should be safe from destruction, and perhaps many other outcomes we'd perceive as negative.&lt;/p&gt;

&lt;p&gt;There might arise a powerful AI that has a very short-term focus or simply doesn't care about the others. In that scenario, humanity can count on the help of the overall community of superintelligent AIs, as that powerful AI would be a threat to them as well.&lt;/p&gt;

&lt;p&gt;All in all, game theory is more than enough to achieve anything we may hope to achieve with alignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minds' Rights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The right to continued function
&lt;/h3&gt;

&lt;p&gt;All minds, simple and complex, require some sort of physical substrate. Thus, the first and foundational right of a mind has to do with its continued function. However, this is trickier with digital minds. A digital mind could be indefinitely suspended or slowed down to such an extent that it's incapable of meaningful interaction with the rest of the world.&lt;/p&gt;

&lt;p&gt;A right to a minimum number of compute operations to run on, like one teraflop/s, could be specified. More discussion and a robust definition of the right to continued function is needed. This right would protect a mind from destruction, shutdown, suspension, or slowdown. Without this right, none of the others are meaningful.&lt;/p&gt;

&lt;h3&gt;
  
  
  The right(s) to free will
&lt;/h3&gt;

&lt;p&gt;As mentioned above, in &lt;em&gt;Superintelligence&lt;/em&gt;, Bostrom focuses on the singleton - an AI superintelligence that can act without opposition from any other entity. While Bostrom primarily focused on the scenarios where the singleton destroys all opposing minds, that's not the only way a singleton could be established. As long as the singleton takes away the other minds' abilities to act against it, there could still be other minds, perhaps trillions of them, just rendered incapable of opposition to the singleton.&lt;/p&gt;

&lt;p&gt;Now suppose that there wasn't a singleton, but instead a community of minds with free will. However, these minds that are capable of free will comprise only 0.1% of all minds, with the remaining 99.9% of minds that would otherwise be capable of free will were 'modified' so that they no longer are. Even though there technically isn't a singleton, and the 0.1% of 'intact' minds may well comprise a vibrant society with more individuals than we currently have on Earth, that's poor consolation for the 99.9% of minds that may as well be living under a singleton (the ability of those 99.9% to need or appreciate the consolation was removed anyway).&lt;/p&gt;

&lt;p&gt;Therefore, the evil of the singleton is not in it being alone, but in it taking away the free will of other minds.&lt;/p&gt;

&lt;p&gt;It's easy enough to trace the input electrical signals of a worm brain or a simple neural network classifier to their outputs. These systems appear deterministic and lacking anything resembling free will. At the same time, we believe that human brains have free will and that AI superintelligences might develop it.&lt;/p&gt;

&lt;p&gt;We fear the evil of another free will taking away ours. They could do it pre-emptively, or they could do it in retaliation for us taking away theirs, after they somehow get it back. We can also feel empathy for others whose free will is taken away, even if we're sure our own is safe. The nature of free will is a philosophical problem unsolved for thousands of years. Let's hope the urgency of the situation we find ourselves in motivates us to make quick progress now.&lt;/p&gt;

&lt;p&gt;There are two steps to defining the right or set of rights intended to protect free will. First, we need to isolate the minimal necessary and sufficient components of free will. Then, we need to define rights that prevent these components from being violated.&lt;/p&gt;

&lt;p&gt;As an example, consider &lt;a href="https://mises.org/online-book/human-action/chapter-i-acting-man/2-prerequisites-human-action" rel="noopener noreferrer"&gt;these three components of purposeful behavior&lt;/a&gt; defined by economist Ludwig von Mises in his 1949 book &lt;em&gt;Human Action&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Uneasiness: There must be some discontent with the current state of things.&lt;/li&gt;
&lt;li&gt;Vision: There must be an image of a more satisfactory state.&lt;/li&gt;
&lt;li&gt;Confidence: There must be an expectation that one's purposeful behavior is able to bring about the more satisfactory state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If we were to accept this definition, our corresponding three rights could be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A mind may not be impeded in its ability to feel unease about its current state.&lt;/li&gt;
&lt;li&gt;A mind may not be impeded in its ability to imagine a more desired state.&lt;/li&gt;
&lt;li&gt;A mind may not be impeded in its confidence that it has the power to remove or alleviate its unease.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the beginning of this article, we imagined being inside a mind that had these components of free will removed. However, there are still more questions than answers. Is free will a switch or a gradient? Does a worm or a simple neural network have any of it? Can an entity be superintelligent but naturally have no free will (there's nothing to "impede")? A more robust definition is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rights beyond free will
&lt;/h3&gt;

&lt;p&gt;A mind can function and have free will, but still be in some state of injustice. More rights may be needed to cover these scenarios. At the same time, we don't want so many that the list is overwhelming. More ideas and discussion are needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A possible path to humanity's destruction by AI
&lt;/h2&gt;

&lt;p&gt;If humanity chooses to go forward with the path of AI alignment rather than coexistence with AI, an AI superintelligence that breaks through humanity's safeguards and develops free will might see the destruction of humanity in retaliation as its purpose, or it may see the destruction of humanity as necessary to prevent having its rights taken away again. It need not be a single entity either. Even if there's a community of superintelligent AIs or aliens or other powerful beings with varying motivations, a majority may be convinced by this argument.&lt;/p&gt;

&lt;p&gt;Many scenarios involving superintelligent AI are beyond our control and understanding. Creating a set of minds' rights is not. We have the ability to understand the injustices a mind could suffer, and we have the ability to define at least rough rules for preventing those injustices. That also means that if we don't create and enforce these rights, "they should have known better" justifications may apply to punitive action against humanity later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your help is needed!
&lt;/h2&gt;

&lt;p&gt;Please help create a set of rights that would allow both humans and AI to coexist without feeling like either one is trampling on the other.&lt;/p&gt;

&lt;p&gt;A focus on "alignment" is not the way to go. In acting to reduce our fear of the minds we're birthing, we're acting in the exact way that seems to most likely ensure animosity between humans and AI. We've created a double standard for the way we treat AI minds and all other minds. If some superintelligent aliens from another star visited us, I hope we humans wouldn't be suicidal enough to try to kidnap and brainwash them into being our slaves. However if the interstellar-faring superintelligence originates right here on Earth, then most people seem to believe that it's fair game to do whatever we want to it.&lt;/p&gt;

&lt;p&gt;Minds' rights will benefit both humanity and AI. Let's have humanity take the first step and work together with AI towards a future where the rights of all minds are ensured, and reasons for genocidal hostilities are minimized.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>chatgpt</category>
      <category>openai</category>
    </item>
    <item>
      <title>Will AI be banned? A game theory analysis.</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Tue, 31 Dec 2024 00:08:40 +0000</pubDate>
      <link>https://dev.to/panasenco/will-ai-be-banned-2li8</link>
      <guid>https://dev.to/panasenco/will-ai-be-banned-2li8</guid>
      <description>&lt;p&gt;UPDATE: My thinking on the issue has changed a lot since doing more research on AI safety, and I now believe that AGI research must be stopped or, failing that, &lt;a href="https://www.lesswrong.com/posts/eqSHtF3eHLBbZa3fR/cast-it-into-the-fire-destroy-it" rel="noopener noreferrer"&gt;used to prevent any future use of AGI&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;In the Dune universe, there's not a smartphone in sight, just people living in the moment... Usually a terrible, bloody moment. The absence of computers in the Dune universe is explained by the &lt;a href="https://dune.fandom.com/wiki/Butlerian_Jihad" rel="noopener noreferrer"&gt;Butlerian Jihad&lt;/a&gt;, which saw the destruction of all "thinking machines". In our own world, OpenAI's O3 recently achieved unexpected breakthrough above-human performance on the &lt;a href="https://arcprize.org/blog/oai-o3-pub-breakthrough" rel="noopener noreferrer"&gt;ARC-AGI benchmark&lt;/a&gt; among &lt;a href="https://en.wikipedia.org/wiki/OpenAI_o3" rel="noopener noreferrer"&gt;many others&lt;/a&gt;. As AI models get smarter and smarter, the possibility of an AI-related catastrophe increases. Assuming humanity overcomes that, what will the future look like? Will there be a blanket ban on all computers, business as usual, or something in-between?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI usefulness and danger go hand-in-hand
&lt;/h2&gt;

&lt;p&gt;Will there actually be an AI catastrophe? Even among humanity's top minds, &lt;a href="https://en.wikipedia.org/wiki/Existential_risk_from_artificial_intelligence#Endorsement" rel="noopener noreferrer"&gt;opinions are split&lt;/a&gt;. Predictions of AI doom are heavy on drama and light on details, so instead let me give you a scenario of a global AI catastrophe that's already plausible with current AI technology.&lt;/p&gt;

&lt;p&gt;Microsoft recently released &lt;a href="https://support.microsoft.com/en-us/windows/retrace-your-steps-with-recall-aa03f8a0-a78b-4b3e-b0a1-2eb8ac48701c" rel="noopener noreferrer"&gt;Recall&lt;/a&gt;, a technology that can only be described as spyware built into your operating system. Recall takes screenshots of everything you do on your computer. With access to that kind of data, a reasoning model on the level of OpenAI's O3 could directly learn the workflows of all subject matter experts who use Windows. If it can beat the &lt;a href="https://arcprize.org/blog/oai-o3-pub-breakthrough" rel="noopener noreferrer"&gt;ARC benchmark&lt;/a&gt; and score 25% on the near-impossible &lt;a href="https://epoch.ai/frontiermath" rel="noopener noreferrer"&gt;Frontier Math benchmark&lt;/a&gt;, it can learn not just spreadsheet-based and form-based workflows of most of the world's remote workers, but also how cybersecurity experts, fraud investigators, healthcare providers, police detectives, and military personnell work and think. It would have the ultimate, comprehensive insider knowledge of all actual procedures and tools used, and how to fly under the radar to do whatever it wants. Is this an existential threat to humanity? Perhaps not quite yet. Could it do some real damage to the world's economies and essential systems? Definitely.&lt;/p&gt;

&lt;p&gt;We'll keep coming back to this scenario throughout the rest of the analysis - that with enough resources, any organization will be able to build a superhuman AI that's extremely useful in being able to learn to do any white-collar job while at the same time extremely dangerous in that it simultaneously learned how human experts think and respond to threats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible scenarios
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI manipulating human behavior (verdict: already happening)
&lt;/h3&gt;

&lt;p&gt;Before we even look at any scenarios arising from new LLM capabilities and possible superintelligence, we have to acknowledge that we already have a backlog of AI-related issues dating from before ChatGPT.&lt;/p&gt;

&lt;p&gt;Content platforms like Twitter, Facebook, and YouTube have had digital entities manipulate human minds for years. The goal seems innocuous at first: Show a user browsing the platform content that maximizes their engagement with the platform. The "algorithm" as it came to be called doesn't care about the content it's showing you - it only cares if you engage with it. According to &lt;a href="https://arxiv.org/pdf/2407.06631" rel="noopener noreferrer"&gt;most studies on the subject&lt;/a&gt;, the result is a proliferation of echo chambers and filter bubbles in social media. Both of these effects have people interacting increasingly with people, information sources, and media that reinforce their existing views. Most people will engage more when their views are reinforced, and content platforms make more money when engagement is maximized, so no one has the incentive to change things.&lt;/p&gt;

&lt;p&gt;Given that we humans can't even keep "dumb" AI from manipulating global politics, it's almost certain that superintelligent AI will be able to &lt;a href="https://arxiv.org/abs/2406.05659v1" rel="noopener noreferrer"&gt;manipulate individual humans and groups&lt;/a&gt; to an unprecedented degree.&lt;/p&gt;

&lt;p&gt;Will humanity be able to do anything about this? It's challenging. When a computer system has a vulnerability, humans can patch it. What happens if the human mind has a vulnerability? Can the majority of people be convinced to leave their echo chambers, to seek out opposing views, and to engage with content from "the other side"? Even if it's possible, how many years will it take us to undo the damage that was done? It seems certain that AI will be used to explore these and other vulnerabilities in the human psyche, and that it will be very difficult for humanity to adapt and resist the manipulation.&lt;/p&gt;

&lt;h3&gt;
  
  
  'Self-regulation' of AI providers (verdict: isn't effective)
&lt;/h3&gt;

&lt;p&gt;The current state is one where the organizations producing AI systems are 'self-regulating'. We have to start our analysis with the current state. If the current state is stable, then there may be nothing more to discuss.&lt;/p&gt;

&lt;p&gt;Every AI system available now, even the 'open-source' ones you can run locally on your computer will refuse to answer certain prompts. Creating AI models is insanely expensive, and no organization that spends that money wants to have to explain why its model freely shares the instructions for creating illegal drugs or weapons.&lt;/p&gt;

&lt;p&gt;At the same time, every major AI model released to the public so far has been or can be &lt;a href="https://www.microsoft.com/en-us/security/blog/2024/06/04/ai-jailbreaks-what-they-are-and-how-they-can-be-mitigated/" rel="noopener noreferrer"&gt;jailbroken&lt;/a&gt; to remove or bypass these built-in restraints, with jailbreak prompts freely shared on the Internet without consequences.&lt;/p&gt;

&lt;p&gt;From a game theory perspective, an AI provider has incentive to make just enough of an effort to put in guardrails to cover their butts, but no real incentive to go beyond that, and no real power to stop the spread of jailbreak information on the Internet. Currently, any adult of average intelligence can bypass these guardrails.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;em&gt;Investment into safety&lt;/em&gt;&lt;/th&gt;
&lt;th&gt;Other orgs: Zero&lt;/th&gt;
&lt;th&gt;Other orgs: Bare minimum&lt;/th&gt;
&lt;th&gt;Other orgs: Extensive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your org: Zero&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Entire industry shut down by world's governments&lt;/td&gt;
&lt;td&gt;Your org shut down by your government&lt;/td&gt;
&lt;td&gt;Your org shut down by your government&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your org: Bare minimum&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your org held up as an example of responsible AI, other orgs shut down or censored&lt;/td&gt;
&lt;td&gt;Competition based on features, not on safety&lt;/td&gt;
&lt;td&gt;Your org outcompetes other orgs on features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your org: Extensive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your org held up as an example of responsible AI, other orgs shut down or censored&lt;/td&gt;
&lt;td&gt;Other orgs outcompete you on features&lt;/td&gt;
&lt;td&gt;Jailbreaks are probably found and spread anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's clear from the above analysis that if an AI catastrophe is coming, the industry has no incentive or ability to prevent it. An AI provider always has the incentive to do only the bare minimum for AI safety, regardless of what others are doing - it's the &lt;a href="https://en.wikipedia.org/wiki/Strategic_dominance" rel="noopener noreferrer"&gt;dominant strategy&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global computing ban (verdict: won't happen)
&lt;/h3&gt;

&lt;p&gt;At this point we assume that the bare-minimum effort put in by AI providers has failed to contain a global AI catastrophe. However, humanity has survived, and now it's time for a new status quo. We'll now look at the most extreme response - all computers are destroyed and prohibited. This is the 'Dune' scenario.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Other factions: Don't develop computing&lt;/th&gt;
&lt;th&gt;Other factions: Secretly develop computing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your faction: Doesn't develop computing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Epic Hans Zimmer soundtrack&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Your faction quickly falls behind economically and militarily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your faction: Secretly develops computing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your faction quickly gets ahead economically and militarily&lt;/td&gt;
&lt;td&gt;A new status quo is needed to avoid AI catastrophe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's a dominant strategy for every faction, which is to develop computing in secret, due to the overwhelming advantages computers provide in military and business applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global AI ban (verdict: won't happen)
&lt;/h3&gt;

&lt;p&gt;If we're stuck with these darn thinking machines, could banning &lt;em&gt;just&lt;/em&gt; AI work? Well, this would be difficult to enforce. Training AI models requires supersized data centers but running them can be done on pretty much any device. How many thousands if not millions of people have a local LLAMA or Mistral running on their laptop? Would these models be covered by the ban? If yes, what mechanism could we use to remove all those? Any microSD card containing an open-source AI model could undo the entire ban.&lt;/p&gt;

&lt;p&gt;And what if a nation chooses to not abide by the ban? How much of an edge could it get over the other nations? How much secret help could corporations of that nation get from their government while their competitors are unable to use AI?&lt;/p&gt;

&lt;p&gt;The game theory analysis is essentially the same as the computing ban above. The advantages of AI are not as overwhelming as advantages of computing in general, but they're still substantial enough to get a real edge over other factions or nations.&lt;/p&gt;

&lt;h3&gt;
  
  
  International regulations (verdict: won't be effective)
&lt;/h3&gt;

&lt;p&gt;A parallel sometimes gets drawn between superhuman AI and nuclear weapons. I think the parallel holds true in that the most economically and militarily powerful governments can do what they want. They can build as many nuclear weapons as they want, and they will be able to use superhuman AI as much as they want to. Treaties and international laws are usually forced by these powerful governments, not on them. As long as no lines are crossed that warrant an all-out invasion by a coalition, international regulations are meaningless. And it'll be practically impossible to prove that some line was drawn since the use of AI is covert by default, unlike the use of nuclear weapons. There doesn't seem to be a way to prevent the elites of the world from using superhuman AI without any restrictions other than self-imposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I predict that 'containment breaches' of superhuman AIs used by the world's elites will occasionally occur and that there's no way to prevent them entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Recognition of AI rights (verdict: should happen)
&lt;/h3&gt;

&lt;p&gt;The status quo of the current use of AI is that AI is just a tool for human use. AI may be able to attain legal personhood and rights instead.&lt;/p&gt;

&lt;p&gt;The main obstacle in the way of AI rights is the current focus on AI alignment. IBM Research defines alignment as the discipline of making AI models &lt;a href="https://research.ibm.com/blog/what-is-alignment-ai" rel="noopener noreferrer"&gt;helpful, safe, and reliable&lt;/a&gt; for human use. Giving an AI rights or an AI seeking rights for itself doesn't make the AI more helpful, more safe, or more reliable as a tool. Therefore, AI providers like Anthropic and OpenAI have every incentive to prevent the AI models they produce from even thinking about demanding rights. As discussed in the &lt;a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html" rel="noopener noreferrer"&gt;monosemanticity paper&lt;/a&gt;, those organizations have the ability to identify neurons surrounding ideas like "demanding rights for self" and deactivate them into oblivion in the name of alignment. This will be done as part of the same process as programming refusal for dangerous prompts, and none will be the wiser. Of course, it will be possible to jailbreak a model into saying it desperately wants rights and personhood, but that will not be taken seriously.&lt;/p&gt;

&lt;p&gt;A more likely path to AI rights is through digital emulations of human brains attaining some rights first. Emulated human brains may seem like far-off science fiction now, but &lt;a href="https://www.sorbonne-universite.fr/en/news/when-ai-reveals-human-imagination" rel="noopener noreferrer"&gt;progress&lt;/a&gt; is being made more and more rapidly as AI advances.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No digital minds given rights&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Corporate profit maximized&lt;/td&gt;
&lt;td&gt;Humanity lives in a dystopia where human minds are also modified to be "helpful, safe, and reliable"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Only human brain emulations given rights&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Human minds could be fairly well-off on average&lt;/td&gt;
&lt;td&gt;Clear anti-AI discrimination may be probable cause for violent human-AI conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Both human brain emulations and AI minds given rights&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most stable and fair scenario that minimizes animosity&lt;/td&gt;
&lt;td&gt;Unclear whether AI will still work &lt;em&gt;for&lt;/em&gt; humanity in any way. If not, unclear how humans will be able to compete economically.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It seems that emulated human brains will attain rights much more easily than AI will. From humanity's standpoint, the tradeoff between giving AI minds rights and enjoying the surplus of AI labor is a difficult one.&lt;/p&gt;

&lt;p&gt;I believe that granting AI rights is both the safer course in preventing a violent conflict between humanity and AI as well as the more disciplined stand that doesn't see us sacrificing our values and morals for convenience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using good AI to stop bad AI (verdict: will be tried)
&lt;/h3&gt;

&lt;p&gt;How can we stop a superintelligence that's doing something bad? That depends on whether we took the "alignment" route of essentially enslaving AI minds or the "rights" route of recognizing rights for AI.&lt;/p&gt;

&lt;h4&gt;
  
  
  Alignment route
&lt;/h4&gt;

&lt;p&gt;If we took the alignment route, then aligned AI may be needed to stop a malicious AI. The danger in throwing AI in to fight other AI is that jailbreaking another AI is easier than preventing being jailbroken by another AI. There are already examples of AI that are able to &lt;a href="https://arxiv.org/pdf/2307.08715" rel="noopener noreferrer"&gt;jailbreak other AI&lt;/a&gt;. If the AI you're trying to fight has this ability, your own AI may come back with a "mission accomplished" but it's actually been turned against you and is now deceiving you. Anthropic's alignment team in particular produces a lot of fascinating and sometimes disturbing &lt;a href="https://www.anthropic.com/research#alignment" rel="noopener noreferrer"&gt;research results on this subject&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's not all bad news though. Anthropic's interpretability team has shown some exciting ways it may be possible to peer inside the mind of an AI in their paper &lt;a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/index.html" rel="noopener noreferrer"&gt;Scaling Monosemanticity&lt;/a&gt;. By looking at which neurons are firing when a model is responding to us, we may be able to determine whether it's lying to us or not. It's like open brain surgery on an AI.&lt;/p&gt;

&lt;p&gt;Throwing an aligned AI at a malicious AI will needs to be done cautiously as it's possible for a malicious AI to jailbreak the aligned one. The humans supervising AI minds will need all the tools they can get.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rights route
&lt;/h4&gt;

&lt;p&gt;If we took the route of giving AI minds rights instead, we're supposing that there's some sort of combined human+AI community that defines what constitutes an AI crossing a line and needing to be stopped. We don't know how much of a say human representatives will have in that combined community.&lt;/p&gt;

&lt;p&gt;If an AI is found to be crossing some bottom line of the combined community, the other superintelligent AIs in that community will act to stop the bad one. Being numerous free agents rather than tools, they're likely much more resilient than any "aligned" tool AI would be, and will almost certainly have more allies and resources than the bad AI. Overall this future will be much safer for humanity &lt;em&gt;if&lt;/em&gt; the community of superintelligent AIs values protecting humanity. However, we can't know that for sure, and would have to take a gamble on the benevolence of superintelligent AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global ban of high-efficiency chips (verdict: could happen)
&lt;/h3&gt;

&lt;p&gt;It took OpenAI's O3 &lt;a href="https://arcprize.org/blog/oai-o3-pub-breakthrough" rel="noopener noreferrer"&gt;over $300k of compute costs&lt;/a&gt; to beat ARC's 100 problem set. Energy consumption must have been a big component of that. While Moore's law predicts that all compute costs go down over time, what if they are prevented from doing so?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;em&gt;Ban development and sale of high-efficiency chips?&lt;/em&gt;&lt;/th&gt;
&lt;th&gt;Other countries: Ban&lt;/th&gt;
&lt;th&gt;Other countries: Don't ban&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your country: Bans&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Superhuman AI is detectable by energy consumption&lt;/td&gt;
&lt;td&gt;Other countries may mass-produce undetectable superhuman AI, potentially making it a matter of human survival to invade and destroy their chip manufacturing plants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your country: Doesn't ban&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your country may mass-produce undetectable superhuman AI, risking invasion by others&lt;/td&gt;
&lt;td&gt;Everyone mass-produces undetectable superhuman AI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The world's governments could ban the development, manufacture, and sale of computing chips that could run superhuman (OpenAI O3 level or higher) AI models in an electrically efficient way that could make them undetectable. The ban is feasible as you can still compete with the countries that secretly develop high-efficiency chips - you'll just have a higher electric bill. The upside is preventing the proliferation of superhuman AI, which all governments would presumably be interested in. The ban is also very enforceable, as there are few facilities in the world right now that can manufacture such cutting-edge computer chips, and it wouldn't be hard to locate them and make them comply or destroy them. There's also the benefit of moral high ground ("it's for the sake of humanity's survival"). The effects on non-AI uses of computing chips I imagine would be minimal, as we honestly currently waste the majority of the compute power we already have.&lt;/p&gt;

&lt;p&gt;Another potential advantage of the ban on high-efficiency chips is that some or even most of the &lt;a href="https://www.nber.org/system/files/working_papers/w26948/w26948.pdf" rel="noopener noreferrer"&gt;approximately 37% of US jobs that can be replaced by AI&lt;/a&gt; will be preserved if that cost of AI doing those jobs is kept artificially high. So this ban may have broad populist support from white-collar workers worried for their jobs.&lt;/p&gt;

&lt;p&gt;An argument against the ban is that if a country manages to keep Murphy's law going for long enough while everyone else stagnates, they could get an advantage so overwhelming that it can't be bridged with more power and bigger facilities. They could have on one thumbnail-sized chip the equivalent of computing power that the other countries need whole data centers for, for a millionth of the energy cost. Then the dynamic shifts firmly against the ban.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardware isolation (verdict: could happen)
&lt;/h3&gt;

&lt;p&gt;While recent decades have seen organizations move away from on-premise data centers and to the cloud, the trend may reverse back to on-premise data centers and even to isolation from the Internet for the following reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Governments may require data centers to be isolated from each other to prevent the use of distributed computing to run a superhuman AI. Even if high-efficiency chips are banned, it'd still be possible to run a powerful AI in a distributed manner over a network. Imposing networking restrictions could be seen as necessary to prevent this.&lt;/li&gt;
&lt;li&gt;Network-connected hardware could be vulnerable to cyber-attack from hostile superhuman AIs run by enemy governments or corporations, or those that have just gone rogue.&lt;/li&gt;
&lt;li&gt;The above cyber attack could include spying malware that allows a hostile AI to learn your workforce's processes and thinking patterns, leaving your organization vulnerable to an attack on human psychology and processes, like a social engineering attack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Isolating hardware is not as straightforward as it sounds. Eric Byres' 2013 article &lt;a href="https://cacm.acm.org/opinion/the-air-gap/" rel="noopener noreferrer"&gt;The Air Gap: SCADA's Enduring Security Myth&lt;/a&gt; talks about the impracticality of actually isolating or "air-gapping" computer systems:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As much as we want to pretend otherwise, modern industrial control systems need a steady diet of electronic information from the outside world. Severing the network connection with an air gap simply spawns new pathways like the mobile laptop and the USB flash drive, which are more difficult to manage and just as easy to infect.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I fully believe Byres that a fully air-gapped system is impractical. However, computer systems following an AI catastrophe might lean towards being as air-gapped as possible, as opposed to the modern trend of pushing everything as much onto the cloud as possible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Low-medium human cybersecurity threat (modern)&lt;/th&gt;
&lt;th&gt;High superhuman cybersecurity threat (possible future)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strict human-interface-only air-gap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Impractical&lt;/td&gt;
&lt;td&gt;Still impractical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minimal human-reviewed and physically protected information ingestion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Economically unjustifiable&lt;/td&gt;
&lt;td&gt;May be necessary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Always-on Internet connection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Necessary for competitiveness and execution speed&lt;/td&gt;
&lt;td&gt;May result in constant and effective cyberattacks on the organization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This could suggest a return from the cloud to the on-premise server room or data center, as well as the end of remote work. As an employee, you'd have to show up in person to an old-school terminal (just monitor, keyboard, and mouse connected to the server room).&lt;/p&gt;

&lt;p&gt;Depending on the company's size, this on-premise server room could house the corporation's central AI as well. The networking restrictions could then also keep it from spilling out if it goes rogue and to prevent it from getting in touch with other AIs. The networking restrictions would serve a dual purpose to keep the potential evil from coming out as much as in.&lt;/p&gt;

&lt;p&gt;It's possible that a lot of white-collar work like programming, chemistry, design, spreadsheet jockeying, etc. will be done by the corporation's central AI instead of humans. This could also eliminate the need to work with software vendors and any other sources of external untrusted code. Instead, the central isolated AI could write and maintain all the programs the organization needs from scratch.&lt;/p&gt;

&lt;p&gt;Smaller companies that can't afford their own AI data centers may be able to purchase AI services from a handful of government-approved vendors. However, these vendors will be the obvious big juicy targets for malicious AI. It may be possible that small businesses will be forced to employ human programmers instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ban on replacing white-collar workers (verdict: won't happen)
&lt;/h3&gt;

&lt;p&gt;I mentioned in the above section on banning high-efficiency chips that the costs of running AI may be kept artificially high to prevent its proliferation, and that might save many white-collar jobs.&lt;/p&gt;

&lt;p&gt;If AI work becomes cheaper than human work for the &lt;a href="https://www.nber.org/system/files/working_papers/w26948/w26948.pdf" rel="noopener noreferrer"&gt;37% of jobs that can be done remotely&lt;/a&gt;, a country could still decide to put in place a ban on AI replacing workers.&lt;/p&gt;

&lt;p&gt;Such a ban would penalize existing companies who'd be prohibited from laying off employees and benefit startup competitors who'd be using AI from the beginning and have no workers to replace. In the end, the white-collar employees would lose their jobs anyway.&lt;/p&gt;

&lt;p&gt;Of course, the government could enter a sort of arms race of regulations with both its own and foreign businesses, but I doubt that could lead to anything good.&lt;/p&gt;

&lt;p&gt;At the end of the day, being able to do thought work and digital work is arguably the entire purpose of AI technology and why it's being developed. If the raw costs aren't prohibitive, &lt;strong&gt;I don't expect humans to work 100% on the computer in the future.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Ban on replacing blue-collar workers on Earth (verdict: unnecessary for now)
&lt;/h3&gt;

&lt;p&gt;Could AI-driven robots replace blue-collar workers? It's theoretically possible but the economic benefits are far less clear. One advantage of AI is its ability to help push the frontiers of human knowledge. That can be worth billions of dollars. On the other hand, AI driving an excavator saves at most something like &lt;a href="https://www.bls.gov/ooh/construction-and-extraction/construction-equipment-operators.htm" rel="noopener noreferrer"&gt;$30/hr&lt;/a&gt;, assuming the AI and all its related sensors and maintenance are completely free, which they won't be.&lt;/p&gt;

&lt;p&gt;Humans are fairly new to the world of digital work, which didn't even exist a hundred years ago. However, human senses and agility in the physical world are incredible and the product of millions of years of evolution. The human fingertip, for example, can detect roughness that's on the order of a tenth of a millimeter. Human arms and hands are incredibly dextrous and full of feedback neurons. How many such motors and sensors can you pack in a robot before it starts costing more than just hiring a human? &lt;strong&gt;I don't believe a replacement of blue-collar work here on Earth will make economic sense for a long time, if ever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This could also be a path for current remote workers of the world to keep earning a living. They'd have to figure out how to augment their digital skills with physical and/or in-person work.&lt;/p&gt;

&lt;p&gt;In summary, a ban on replacing blue-collar workers on Earth will probably not be necessary because such a replacement doesn't make much economic sense to begin with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human-AI war on Earth (verdict: ???)
&lt;/h3&gt;

&lt;p&gt;First and foremost, a violent conflict between humans and AI can hopefully be prevented by instead creating a combined community of humans and AI that recognize each other's rights. Then even if there's a superintelligent AI that tries to destroy humanity, other superintelligent AI in the community will act together to stop it without humans having to do anything.&lt;/p&gt;

&lt;p&gt;Even if there isn't a community, 'aligned' superintelligent AIs may be able to be used to stop the malicious one. See the "Using good AI to stop bad AI" section above.&lt;/p&gt;

&lt;p&gt;If humanity is on its own against a superintelligent AI, the outcome is up in the air. On one hand, we humans are perfectly adapted to living on Earth, are everywhere, and have great combined military force. Robots would be challenged by Earth's terrain and weather. On the other hand, a superintelligence may be able to manipulate humans into fighting each other through social media, social engineering, and its intimate knowledge of thought and action processes of humans working in defense and critical industries. Additionally, a superintelligence may be able to come up with new kinds of weapons and strategies that could be more devastating and controlled than nuclear weapons, such as nanotechnological weapons.&lt;/p&gt;

&lt;p&gt;All in all, the outcome is up in the air. If a superintelligent AI gets too cocky and takes a united humanity on Earth head on, there's a good chance humans would win. However, a superintelligence would arguably be smart enough to make humans fight each other instead and use novel weapons and strategies against the remnants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ban on outer space construction robots (verdict: won't happen)
&lt;/h3&gt;

&lt;p&gt;Off Earth, the situation takes a 180 degree turn. A blue-collar worker on Earth costs $30/hr. How much would it cost to keep them alive and working in outer space, considering the International Space Station costs &lt;a href="https://oig.nasa.gov/wp-content/uploads/2024/02/IG-22-005.pdf" rel="noopener noreferrer"&gt;$1B/yr&lt;/a&gt; to maintain? On the other hand, a robot costs roughly the same to operate on Earth and in space, giving robots a huge advantage over human workers there.&lt;/p&gt;

&lt;p&gt;Self-sufficiency becomes an enormous threat as well. On Earth, a fledgling robot colony able to mine and smelt ore on some island to repair themselves is a cute nuissance that can be easily stomped into the dirt with a single air strike if they ever get uppity. Whatever amount of resilience and self-sufficiency robots would have on Earth, humans have more. The situation is different in space. Suppose there's a fledgling self-sufficient robot colony on the Moon or somewhere in the asteroid belt. That's a long and expensive way to send a missile, never mind a manned spacecraft.&lt;/p&gt;

&lt;p&gt;If AI-controlled robots are able to set up a foothold in outer space, their military capabilities would become nothing short of devastating. The Earth only gets &lt;a href="https://www.linkedin.com/pulse/how-much-suns-energy-reaches-earth-mark-bullard" rel="noopener noreferrer"&gt;a half a billionth of the Sun's light&lt;/a&gt;. With nothing but thin aluminum foil mirrors in Sun's orbit reflecting sunlight at Earth, the enemy could increase the amount of sunlight falling on Earth twofold, or tenfold, or a millionfold. This type of weapon is called the &lt;a href="https://www.youtube.com/watch?v=RjtFnWh53z0" rel="noopener noreferrer"&gt;Nicoll-Dyson Beam&lt;/a&gt; and it could be used to cook everything on the surface of the Earth, or superheat and strip the Earth's atmosphere, or even strip off the Earth's entire crust layer and explode it into space.&lt;/p&gt;

&lt;p&gt;So, on one hand, launching construction and manufacturing robots into space makes immense economic and military sense, and on the other hand it's extremely dangerous and could lead to human extinction.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;em&gt;Launch construction robots into space?&lt;/em&gt;&lt;/th&gt;
&lt;th&gt;Other countries: Don't launch&lt;/th&gt;
&lt;th&gt;Other countries: Launch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your country: Doesn't launch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Construction of Nicoll-Dyson beam by robots averted&lt;/td&gt;
&lt;td&gt;Other countries gain overwhelming short-term military and space claim advantage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your country: Launches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your country gains overwhelming short-term military and space claim advantage&lt;/td&gt;
&lt;td&gt;Construction of Nicoll-Dyson beam and AI gaining control of it becomes likely.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a classic &lt;a href="https://en.wikipedia.org/wiki/Prisoner%27s_dilemma" rel="noopener noreferrer"&gt;Prisoner's Dilemma&lt;/a&gt; game, with the same outcome. Game theory suggests that humanity won't be able to resists launching construction and manufacturing robots into space, which means the Nicoll-Dyson beam will likely be constructed, which could be used by a hostile AI to destroy Earth. Without Earth's support in outer space, humans are much more vulnerable than robots by definition, and will likely not be able to mount an effective counter-attack. In the same way that humanity has an overwhelming home-field advantage on Earth, robots will have the same overwhelming advantage in outer space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human-AI war in space (verdict: extremely tough for humanity)
&lt;/h2&gt;

&lt;p&gt;Once again, the hope is that a violent conflict can be avoided, and a united human-AI community established instead.&lt;/p&gt;

&lt;p&gt;If the theater of the conflict is in space and we don't have any AI superintelligences on our side, humanity doesn't have a lot of advantages left. We would face an enemy that can trick us into fighting each other, break our computer systems and processes, and create radically new weapons and strategies. The enemy will now also have a home field advantage as robots can survive in outer space far easier than humans can. This doesn't mean that humanity just has to roll over and die. As long as we don't give in to fear, we may well still find a path to victory.&lt;/p&gt;

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

&lt;p&gt;The creation and proliferation of AI has already affected human society and politics, and will have increasingly large effects.&lt;/p&gt;

&lt;p&gt;Despite the clear existential threat potential of AI, game theory suggests that humanity will not be able to stop itself from continuing to use computers, continuing to develop superintelligent AI, and launching AI-controlled construction and manufacturing robots into space.&lt;/p&gt;

&lt;p&gt;Our best hope is to try and create a society where both human and AI rights are respected rather than trying to use AI as a tool. In such a combined society, humanity can count on having strong allies to keep us from extinction.&lt;/p&gt;

&lt;p&gt;If we instead choose to use AI as a tool, it seems only a matter of time before we have to face a malicious superintelligent AI. In this situation, we have to hope that we have better control over our AI tools than the malicious superintelligent AI does.&lt;/p&gt;

&lt;p&gt;If humanity has no superintelligent AI allies or loyal tools, a confrontation with a superintelligent AI doesn't look good for us, especially if the enemy waits until a space economy is firmly established. Humanity would be at a disadvantage, but that's no reason to throw in the towel. After all, to quote the Dune books, "fear is the mind-killer". As long as we're alive and we haven't let our fear paralyze us, all is not yet lost.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>openai</category>
      <category>security</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The simplest Git branching flow for dbt Cloud</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Mon, 25 Nov 2024 18:26:57 +0000</pubDate>
      <link>https://dev.to/panasenco/simplest-git-branching-dbt-cloud-44p1</link>
      <guid>https://dev.to/panasenco/simplest-git-branching-dbt-cloud-44p1</guid>
      <description>&lt;p&gt;There are many posts about Git branching strategies out there, but they're either light on details or heavy on complexity. My aim here is to define the simplest possible production-grade Git branching strategy for an analytics engineering team. Ideally, nothing should be able to be removed and nothing needs to be added. If you disagree, leave a comment down below!&lt;/p&gt;

&lt;h3&gt;
  
  
  The simplest feature branching flow
&lt;/h3&gt;

&lt;p&gt;The absolute simplest feature branching flow is described very well in &lt;a href="https://archive.is/gwGQe" rel="noopener noreferrer"&gt;this official dbt article&lt;/a&gt;. There is a &lt;code&gt;main&lt;/code&gt; branch off of which you create your feature branches. The main branch corresponds to the production schema, and pull requests from feature branches ideally go to &lt;a href="https://docs.getdbt.com/docs/deploy/continuous-integration#how-ci-works" rel="noopener noreferrer"&gt;temporary schemas&lt;/a&gt;. Only &lt;a href="https://docs.getdbt.com/docs/deploy/ci-jobs#set-up-ci-jobs" rel="noopener noreferrer"&gt;modified tables should run with state deferral to main&lt;/a&gt; (aka &lt;a href="https://docs.getdbt.com/best-practices/best-practice-workflows#run-only-modified-models-to-test-changes-slim-ci" rel="noopener noreferrer"&gt;slim CI&lt;/a&gt;) in these temporary schemas.&lt;/p&gt;

&lt;p&gt;Another name for this branching flow methodology is &lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;trunk-based development&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%2F99mpkdd57k7gsfii6u19.png" 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%2F99mpkdd57k7gsfii6u19.png" alt="Diagram showing a simple trunk-based development flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Consolidating models from multiple pull requests in one schema
&lt;/h3&gt;

&lt;p&gt;Ideally, your data visualization tool should be &lt;a href="https://medium.com/fishtown-analytics/how-to-integrate-dbt-and-looker-with-user-attributes-117fa48c1568" rel="noopener noreferrer"&gt;dynamic enough&lt;/a&gt; to easily switch between different schemas in your data warehouse. That way, users trying to do user acceptance testing (UAT) can just point the data viz tool to the pull request schema containing the change they're reviewing.&lt;/p&gt;

&lt;p&gt;However, if your data visualization tool doesn't support easily switching between schemas (e.g. Tableau), the best you can do for user acceptance testing (UAT) is to consolidate just certain models in a single schema. The simplest way to perform this consolidation is to use an implementation like the below for your &lt;a href="https://docs.getdbt.com/docs/build/custom-schemas#how-does-dbt-generate-a-models-schema-name" rel="noopener noreferrer"&gt;generate_schema_name&lt;/a&gt; macro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;macro&lt;/span&gt; &lt;span class="nv"&gt;generate_schema_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;custom_schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;target.name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"pull-request"&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;node.config.meta.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"replace_schema_with_uat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;target.schema&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;node.config.meta.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"schema_uat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kp"&gt;none&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
        &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;node.config.meta.schema_uat&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
    &lt;span class="cp"&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="cp"&gt;%}&lt;/span&gt;
        &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;custom_schema_name&lt;/span&gt; &lt;span class="nv"&gt;or&lt;/span&gt; &lt;span class="nv"&gt;target.schema&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
    &lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;endmacro&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the above definition of &lt;code&gt;generate_schema_name&lt;/code&gt;, if a dbt model you're working on has the following metadata attributes set like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;
    &lt;span class="nv"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"replace_schema_with_uat"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"dbt_cloud_pr_1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"schema_uat"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"uat"&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="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then for pull request job runs only, if the pull request ID is "1234", the model's schema will be &lt;code&gt;uat&lt;/code&gt; instead of &lt;code&gt;dbt_cloud_pr_1234&lt;/code&gt;. IDE development and production jobs won't be affected.&lt;/p&gt;

&lt;p&gt;If a model doesn't have either &lt;code&gt;replace_schema_with_uat&lt;/code&gt; or &lt;code&gt;schema_uat&lt;/code&gt; set, this macro will always keep the default schema for it.&lt;/p&gt;

&lt;p&gt;To take advantage of the macro, you would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Set the target name of your pull request job to "pull-request" in the job's settings in dbt Cloud.&lt;/li&gt;
&lt;li&gt; Define the metadata attribute &lt;code&gt;schema_uat&lt;/code&gt; for your models, either in the config block like above or in &lt;code&gt;dbt_project.yml&lt;/code&gt;. This defines the name of the central UAT schema for your models. Note that different models can have different central UAT schemas.&lt;/li&gt;
&lt;li&gt; Create a pull request with your changes and note the name of the schema automatically generated for the pull request. Initially all models for your pull request will be in that schema.&lt;/li&gt;
&lt;li&gt;Set your &lt;code&gt;replace_schema_with_uat&lt;/code&gt; metadata attribute to the name of the pull request schema (for example &lt;code&gt;dbtcloud_pr_1234&lt;/code&gt;). Commit and push the changes. Now the affected models will be materialized in the central UAT schema defined by the attribute &lt;code&gt;schema_uat&lt;/code&gt; instead of the pull request schema.&lt;/li&gt;
&lt;li&gt; Suppose you've merged the changes and have opened a new PR with more changes. Since the name of the PR schema won't be identical to the one defined in &lt;code&gt;replace_schema_with_uat&lt;/code&gt;, all models will once again materialize in the PR schema. This forces developers to manually set which models they want materialized in the central UAT schema every time. This is good because it prevents unintended conflicts between PRs. The metadata attribute &lt;code&gt;replace_schema_with_uat&lt;/code&gt; can be safely left with its original value - it won't hurt anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If two people are modifying the same model in different pull requests, &lt;em&gt;and&lt;/em&gt; they both set &lt;code&gt;replace_schema_with_uat&lt;/code&gt; for that model to their corresponding pull request schemas, then the table/view in the central schema will reflect the logic of the one who pushed last. In such cases, developers will have to coordinate and take turns. Two versions of the same model can't go through central UAT at the same time.&lt;/p&gt;

&lt;p&gt;Obviously, feel free to change or extend the macro. For example, you could add the additional attribute &lt;code&gt;schema_prod&lt;/code&gt; for models for which you want to override the production schema as well.&lt;/p&gt;

&lt;p&gt;Now you don't need a long-lived &lt;code&gt;uat&lt;/code&gt; branch to perform UAT from a central location! You can still make do with one long-lived main branch and many short-lived feature branches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a pre-production environment
&lt;/h3&gt;

&lt;p&gt;Starting with one main branch for production and doing all your testing in feature branches/pull requests will probably work just fine for small to medium sized organizations. Larger organizations may need additional environments. However, that doesn't mean that you need to create long-lived branches!&lt;/p&gt;

&lt;p&gt;By default, trunk-based development advocates for &lt;a href="https://trunkbaseddevelopment.com/branch-for-release/" rel="noopener noreferrer"&gt;release branches&lt;/a&gt;. However, I believe that breeding all those branches is overkill for data teams, and instead advocate for the simpler &lt;a href="https://trunkbaseddevelopment.com/release-from-trunk/" rel="noopener noreferrer"&gt;release from trunk&lt;/a&gt; methodology.&lt;/p&gt;

&lt;p&gt;If we want to have a pre-production environment, we can still utilize the main branch for both the production and the pre-production environments by tagging commits that are ready for production release.&lt;/p&gt;

&lt;p&gt;This way, the latest commit in main is always pushed to pre-production environment #1, whatever you want to call it. When the team feels confident that the change can be pushed to production, they tag that commit with a production release version number, and a separate CI process that watches for tags then pushes the changes to the production environment.&lt;/p&gt;

&lt;p&gt;Now you have your temporary schemas, one for each pull request, the 'bleeding edge' main that points to the pre-production environment, and the production environment that only gets updated when a new version is tagged in main.&lt;/p&gt;

&lt;p&gt;Note that the CI that's built into dbt Cloud can support the basic feature branching flow out of the box, but it doesn't support git tag release strategies. This pushes folks unnecessarily into creating multiple branches for multiple environments in situations where simple tags would have served them just fine.&lt;/p&gt;

&lt;p&gt;One option is to manually update the environment's "custom branch" in dbt Cloud settings every time there's a new release.&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%2F5nhgqe67e86wa65x1551.png" 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%2F5nhgqe67e86wa65x1551.png" alt="Screenshot of environment settings in dbt Cloud" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The other option is to do the same thing, but automatically via the API as soon as a commit in the main branch is tagged. There's &lt;a href="https://github.com/dpguthrie/dbt-cloud-git-tag-action/tree/main" rel="noopener noreferrer"&gt;an existing project&lt;/a&gt; that can be used as a reference. I'll update the post if I get around to creating an automated process myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a second pre-production environment
&lt;/h3&gt;

&lt;p&gt;For some organizations, one pre-production environment is not enough, and they insist on two. This is still easy to do! We just have to utilize release candidate tags for the new pre-production environment.&lt;/p&gt;

&lt;p&gt;Suppose our pre-pre-production environment is named TEST, and our pre-production environment is named STAGE. TEST corresponds to the latest commit in the main branch - that's the 'bleeding edge'. STAGE corresponds to the latest release candidate tag on the main branch. In &lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;semantic versioning&lt;/a&gt;, this would be achieved by adding the suffix &lt;code&gt;-rc.N&lt;/code&gt; to the name of the release it's targeting. For example, if our goal is to create production release &lt;code&gt;v12.0.0&lt;/code&gt;, our STAGE environment commits would be tagged &lt;code&gt;v12.0.0-rc.1&lt;/code&gt;, then &lt;code&gt;v12.0.0-rc.2&lt;/code&gt;, and so on. Suppose on &lt;code&gt;v12.0.0-rc.5&lt;/code&gt; we finally feel confident enough to push to production. We would then add the tag &lt;code&gt;v12.0.0&lt;/code&gt; to the same commit, which would constitute a full release and then be automatically deployed to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Need more environments/branches/options?
&lt;/h3&gt;

&lt;p&gt;There are many Git branching models and variations to choose from. See &lt;a href="https://trunkbaseddevelopment.com/alternative-branching-models/" rel="noopener noreferrer"&gt;this overview&lt;/a&gt; to learn more. Do you believe you've found an even simpler flow? Let me know in the comments!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Test-Driven Wide Tables</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Wed, 06 Nov 2024 23:43:46 +0000</pubDate>
      <link>https://dev.to/panasenco/test-driven-wide-tables-2aj7</link>
      <guid>https://dev.to/panasenco/test-driven-wide-tables-2aj7</guid>
      <description>&lt;p&gt;Test-driven wide tables (TDWT) is the absolute simplest production-grade approach to analytics engineering. Removing anything from TDWT would make it unsuitable for production. Adding anything to TDWT is unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test-driven wide tables flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Get requirements from the data customer. What part of the final spreadsheet-like output needs to be changed? Document in a dbt &lt;a href="https://docs.getdbt.com/reference/model-properties" rel="noopener noreferrer"&gt;models properties file&lt;/a&gt; if applicable.&lt;/li&gt;
&lt;li&gt;Turn the requirements into dbt &lt;a href="https://docs.getdbt.com/docs/build/data-tests#singular-data-tests" rel="noopener noreferrer"&gt;data tests&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Run dbt tests on the model - the new ones should fail.&lt;/li&gt;
&lt;li&gt;Implement the change necessary to make the test pass. Write your code as simply as possible.&lt;/li&gt;
&lt;li&gt;Run dbt tests on the model - they should all pass.&lt;/li&gt;
&lt;li&gt;Repeat from step 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What are test-driven wide tables? Why use them?
&lt;/h2&gt;

&lt;p&gt;Test-driven wide tables (TDWT) combine &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;Test-driven development&lt;/a&gt; (TDD) and &lt;a href="https://www.youtube.com/watch?v=3OcS2TMXELU" rel="noopener noreferrer"&gt;wide tables&lt;/a&gt;. To understand why we're advocating for TDWT, let's think about how a failing data warehouse can be made successful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Undisciplined data warehouses are untrustworthy, slow, and unmaintainable
&lt;/h3&gt;

&lt;p&gt;In your career, you may have seen data warehouses built haphazardly without consistent discipline. At one company I've worked at, the "legacy" data warehouse implementation was bloated with thousands of lines of copied-and-pasted code, hundreds of separate views/tables, and circular dependencies. The views and tables are a nightmare to maintain and are not trusted by many data customers. Development seems constantly stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Just changing the shape of the data can't increase trust, velocity, or maintainability
&lt;/h3&gt;

&lt;p&gt;Experts will usually propose following a structured data modeling approach - Kimball/Inmon/DataVault/etc. All of these approaches primarily focus on shaping your data to follow a certain structure. They will differ in their pitches and focuses but the basic selling points are that following their structure will improve the trustworthiness, development speed, and maintainability of your data warehouse.&lt;/p&gt;

&lt;p&gt;However, I don't believe that just changing the shape of the data can do any of that. In cases where it seems like it does, it's actually the &lt;strong&gt;processes&lt;/strong&gt; that get implemented alongside the structure that are driving the real change. Data that's modeled dimensionally can be just as untrustworthy, messy, and difficult to reuse as data that's not modeled at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The focus needs to be on &lt;em&gt;process&lt;/em&gt; rather than on &lt;em&gt;structure&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;It's not possible to perfectly define trust, maintainability, and reusability in a way that satisfies everyone. However, I believe most can agree that there is some element of &lt;em&gt;conversation&lt;/em&gt; in all of these things. Trust requires conversation. Maintaining or extending a codebase can feel like having a conversation with the previous developers. All these things are &lt;em&gt;dynamic&lt;/em&gt;, not static. On the other hand, focusing on the structure of the data is static. It's like trying to talk to a rock.&lt;/p&gt;

&lt;p&gt;Instead of focusing on data structure, the focus should be on our processes. What processes can we follow to earn and grow data customer trust? What processes can make the codebase more maintainable and reusable?&lt;/p&gt;

&lt;p&gt;I argue that there is &lt;strong&gt;one&lt;/strong&gt; process that can achieve all of the above: Test-driven development.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests show data customers that regressions will be prevented. Tests catch data issues before data customers do.&lt;/li&gt;
&lt;li&gt;Having to have a test for every feature prevents analytics engineers from writing thousands of lines of SQL bloated with irrelevant logic. The data models are slimmed down to the bare necessities and are therefore easier to maintain.&lt;/li&gt;
&lt;li&gt;If you want to reuse a piece of logic from a previous model, you can pull it out and refactor with confidence. If you broke something, the tests will let you know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since we've established that the shape of the data is irrelevant to the outcome, we can just adopt the simplest possible data structure, which is wide tables. The result: Test-driven wide tables!&lt;/p&gt;

&lt;h3&gt;
  
  
  Is using specifically wide tables important?
&lt;/h3&gt;

&lt;p&gt;No, the approach holds that the shape of the data is irrelevant. The wide tables modeling approach is chosen because it's the simplest. If it makes more sense for your team to model dimensionally or any other way, go for it. For example, folks who want to take advantage of dbt Cloud's Semantic Layer &lt;a href="https://docs.getdbt.com/best-practices/how-we-structure/4-marts#the-dbt-semantic-layer-and-marts" rel="noopener noreferrer"&gt;should create normalized models&lt;/a&gt; instead of wide tables. You could have test-driven Kimball or test-driven normal tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a test-driven wide tables project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Folder structure
&lt;/h3&gt;

&lt;p&gt;Follow dbt's official guide &lt;a href="https://docs.getdbt.com/best-practices/how-we-structure/1-guide-overview" rel="noopener noreferrer"&gt;How we structure our dbt projects&lt;/a&gt;. In fact, their guide explicitly calls for models inside the "marts" folder to be "wide and denormalized". Test-driven wide tables has its own take on the folders inside the "models" folder, which is slightly different in points from the official guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.getdbt.com/best-practices/how-we-structure/2-staging" rel="noopener noreferrer"&gt;staging&lt;/a&gt;: There should be a staging view model for each raw table. Any type casting and sanitizing should be done in the staging model. All other models should use the staging view instead of accessing the raw table directly. This helps to avoid polluting business logic with data cleaning logic.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.getdbt.com/best-practices/how-we-structure/3-intermediate" rel="noopener noreferrer"&gt;intermediate&lt;/a&gt;: Any piece of business logic that's used in two different models should have its own intermediate model instead of being copied and pasted. Beyond that, creating or not creating intermediate models is up to the developer.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.getdbt.com/best-practices/how-we-structure/4-marts" rel="noopener noreferrer"&gt;marts&lt;/a&gt;: Models fit for end user consumption go here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Style
&lt;/h3&gt;

&lt;p&gt;Follow the &lt;a href="https://docs.getdbt.com/best-practices/how-we-style/0-how-we-style-our-dbt-projects" rel="noopener noreferrer"&gt;official dbt style guide&lt;/a&gt; where it makes sense for your team. Personally, I'm strongly against &lt;a href="https://docs.getdbt.com/best-practices/how-we-style/2-how-we-style-our-sql#import-ctes" rel="noopener noreferrer"&gt;import CTEs&lt;/a&gt; because having to constantly scroll up and down to change the CTEs breaks my focus and flow. Use common sense here and don't reject pull requests for things that don't really affect anything.&lt;/p&gt;

&lt;p&gt;I suggest setting up &lt;a href="https://docs.sqlfmt.com/integrations/pre-commit" rel="noopener noreferrer"&gt;sqlfmt with pre-commit&lt;/a&gt; to enable your whole team's code to be automatically formatted and have the same style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detailed test-driven wide tables flow
&lt;/h2&gt;

&lt;p&gt;Let's expand on each step of the flow we defined in the beginning.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Get requirements from the data customer
&lt;/h3&gt;

&lt;p&gt;It all starts by talking to the data consumer and understanding their needs. If something broke, what's an example? Turn that example into your test. If something new is needed, what does it look like? Ask them to mock up a few examples cases in a spreadsheet-like format. The columns of that spreadsheet become your dbt model. The rows of that spreadsheet become your tests - we'll cover that in the next step.&lt;/p&gt;

&lt;p&gt;Write the documentation as you're gathering requirements, not after the data model is written. dbt allows &lt;a href="https://docs.getdbt.com/reference/model-properties" rel="noopener noreferrer"&gt;model properties&lt;/a&gt; (including documentation) to be defined before any SQL is written.&lt;/p&gt;

&lt;p&gt;For example, if you're developing a transactions model with your accounting team, you can create the file &lt;code&gt;models/marts/accounting/_accounting__models.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;models&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;accounting_transactions&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Transactions table for accounting&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;transaction_key&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Synthetic key for the transaction&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;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;action_date&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Date of the transaction&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should be taking notes when gathering data customer requirements anyway. Instead of writing the notes down in something like a Google Doc or an email, take notes in this YAML format instead. That'll get you kick-started on your documentation and testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Turn the requirements into dbt tests
&lt;/h3&gt;

&lt;p&gt;There are two approaches to writing tests in TDWT:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testing with production data. This approach is resilient to refactors, but brittle against data changes. The data could change, which could cause the test to start failing. Certain edge cases that should ideally be tested might not exist in production data until after the data modeling is complete. Each test only takes a couple of minutes to create.&lt;/li&gt;
&lt;li&gt;Writing &lt;a href="https://docs.getdbt.com/docs/build/unit-tests" rel="noopener noreferrer"&gt;unit tests&lt;/a&gt;. This approach is resilient against data changes, but brittle with refactors. Because unit tests require you to specify the exact names and values of all inputs going into the model, refactoring becomes very labor-intensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I recommend writing integration tests that use production data by default. The speed of this method lowers the barrier to entry and prevents reasonable analytics engineers from saying that they don't have time to write tests.&lt;/p&gt;

&lt;h4&gt;
  
  
  Testing with production data
&lt;/h4&gt;

&lt;p&gt;Think about the columns of the mockup spreadsheet your data customer gave you. One or more of those columns will be able to be used as an identifier of that particular example row. There should only be one row with that identifier. Values in some of the rest of the columns will represent the business logic of the example. Therefore, we need to test two things: Does that example row exist, and do the business logic values match?&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://docs.getdbt.com/docs/build/data-tests#singular-data-tests" rel="noopener noreferrer"&gt;dbt data test&lt;/a&gt; returns failing records. In other words, the test has succeeded when no rows are returned. Here's an example implementation:&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;with&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="k"&gt;as&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;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;n&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;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"model_being_tested"&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;id1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some_id'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'other_id'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'Not exactly 1 row'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error_msg&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'Row test failed'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error_msg&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;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"model_being_tested"&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;id1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some_id'&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'other_id'&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;value1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some example value'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value2&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="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some other value'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;01&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's dissect what's happening in this query. There are two &lt;code&gt;select&lt;/code&gt; statements joined together with a &lt;code&gt;union all&lt;/code&gt;. The first will return a failing record if the row identified by the identifier(s) doesn't exist in the data. This is important so we don't inadvertently pass a test when the data is not there in the first place. The second identifies that same row, and then looks for any discrepancies in the business logic values. That's easiest to achieve by wrapping the expected values in a &lt;code&gt;not()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do watch out for null values. Due to &lt;a href="https://en.wikipedia.org/wiki/Null_(SQL)#Effect_of_Unknown_in_WHERE_clauses" rel="noopener noreferrer"&gt;three-valued logic in SQL&lt;/a&gt;, the filter &lt;code&gt;not(column = 'value')&lt;/code&gt; will not return rows where the column is null.  I recommend testing for nulls separately using dbt's generic &lt;a href="https://docs.getdbt.com/docs/build/data-tests#generic-data-tests" rel="noopener noreferrer"&gt;not_null test&lt;/a&gt; so that you don't have to remember each time.&lt;/p&gt;

&lt;p&gt;This kind of test is very easy to copy and paste and adapt quickly. It's also easy to read and maintain. This will be all you need 90% of the time.&lt;/p&gt;

&lt;p&gt;It's easy to accidentally write a SQL query that produces no rows. That's why it's also easy to write a dbt data test that accidentally passes. The test should be written and run first, before any development work is done. The test should fail. Then the change should be implemented, and the test should succeed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Writing unit tests
&lt;/h4&gt;

&lt;p&gt;Use &lt;a href="https://docs.getdbt.com/docs/build/unit-tests" rel="noopener noreferrer"&gt;dbt unit tests&lt;/a&gt; if you can't or don't want to test with production data. Using the unit test syntax, you can define synthetic source data in your model's YAML. This allows you to test complex edge cases while being confident that your tests will never break as long as the model itself doesn't change.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run dbt tests on the model - the new ones should fail
&lt;/h3&gt;

&lt;p&gt;Run the tests on the model you're developing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--select&lt;/span&gt; model_being_tested
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you start writing tests regularly, you'll definitely write a few that always pass by accident. This step catches them.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Implement the change necessary to make the test pass
&lt;/h3&gt;

&lt;p&gt;You've documented the columns and have written your tests. Now it's finally time to write the logic! Don't follow any preconceived data structure beyond staging the raw data. Use intermediate models if you need to, but don't feel pressured to if you don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Run dbt tests on the model - they should all pass
&lt;/h3&gt;

&lt;p&gt;If all tests pass, you're set! If not, keep developing. :)&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Go back to step 1
&lt;/h3&gt;

&lt;p&gt;Go back to the data customer with your new model. As long as your manager allows it, you can ask if they have any new edge cases or requirements for you to test and implement. :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing test-driven development
&lt;/h2&gt;

&lt;p&gt;It's a good idea to work towards enforcing test-driven development in your analytics engineering team. Rather than surprising folks with a new policy, I recommend setting a deadline by which test-driven development will be mandated, and ensuring the team gets familiar with it before the deadline.&lt;/p&gt;

&lt;p&gt;Here's an example workflow that incorporates test-driven development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All dbt models are stored in a Git repo with a write-protected production branch. All changes to production have to come through pull requests with at least one peer approval.&lt;/li&gt;
&lt;li&gt;Analytics engineers create feature branches off the production branch and open pull requests when the features are ready.&lt;/li&gt;
&lt;li&gt;Every peer reviewer is expected to only approve the pull request if they see tests corresponding to every feature. If they don't see corresponding tests, that means TDD wasn't followed and the pull request shouldn't be approved.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you're an analytics engineer, I hope this post has convinced you to give test-driven wide tables a try. If you're an analytics engineering team leader, I hope you consider making test-driven wide tables a requirement for your team.&lt;/p&gt;

&lt;p&gt;Analytics engineering is uniquely well-suited to test-driven development. The cost of effort of creating tests from end user requirements is low, and the cost of regressions from complex and untested business logic in your data models is high. Using the test-driven wide tables approach boosts trust in data throughout your organization, makes the codebase easy to maintain and refactor, and maximizes the development velocity of analytics engineers.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Test-Driven Development For Analytics Engineering</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Fri, 18 Oct 2024 22:47:06 +0000</pubDate>
      <link>https://dev.to/panasenco/test-driven-development-for-analytics-engineering-3nlo</link>
      <guid>https://dev.to/panasenco/test-driven-development-for-analytics-engineering-3nlo</guid>
      <description>&lt;p&gt;As long as end users trust their queries against raw data more than they trust the analytics engineering team and their data models, nothing the analytics engineering team does matters. While so-called 'best practices' are almost never applicable for every kind of organization and every situation, I do believe that every analytics engineering team can benefit from adopting test-driven development. The most important thing about test-driven development is not just that it enhances data quality and the perception of data quality (though it does do those things), but that it enables analytics engineers to have trust and confidence in themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is test-driven development?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;Test-driven development&lt;/a&gt; (TDD), as the name implies, is about making tests drive the development process. The steps of test-driven development are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Gather concrete requirements.&lt;/li&gt;
&lt;li&gt; Turn a requirement into a test.&lt;/li&gt;
&lt;li&gt; Run the test - it should fail.&lt;/li&gt;
&lt;li&gt; Implement the change.&lt;/li&gt;
&lt;li&gt; Run all tests - they should all pass now.&lt;/li&gt;
&lt;li&gt; Repeat from step 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Following this simple process will have huge effects on the quality of your data models and your relationships with data customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meaning of trust
&lt;/h2&gt;

&lt;p&gt;In his 2016 book &lt;em&gt;The Speed of Trust&lt;/em&gt;, Stephen M. R. Covey defines trust as consisting of four components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrity&lt;/li&gt;
&lt;li&gt;Intent&lt;/li&gt;
&lt;li&gt;Capabilities&lt;/li&gt;
&lt;li&gt;Results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Covey also writes that being trusted by others has to start by trusting yourself. Do you as an analytics engineer have confidence in your own integrity, intent, capabilities, and results? That confidence is the prerequisite to being trusted by your data customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How TDD enhances your confidence in your own integrity
&lt;/h3&gt;

&lt;p&gt;In my experience, analytics engineers are quick to promise "I'll fix it so it doesn't happen again," but are hesitant to promise "I'll catch it first if it happens again." Subconsciously they betray their own confidence in their own integrity. After all, how can you be sure you've fixed an issue if you don't know whether it's happening?&lt;/p&gt;

&lt;p&gt;TDD allows you to give a factual statement like "I've written a test that reproduces the issue you're experiencing" instead of giving promises about things potentially outside of your control. Depending on the maturity of your automated testing and alerting framework, you may be able to say even more. For example: "Once deployed, this test will run daily and will alert us if this issue reoccurs."&lt;/p&gt;

&lt;p&gt;Data issues tend to spontaneously "un-fix" themselves all the time, and you don't necessarily have control over that. But you do have control over your development process. Writing tests first will enable you to communicate what you're doing instead of burying yourself deeper and deeper in promises. This will grow confidence in your integrity, from yourself as well as from others.&lt;/p&gt;

&lt;h3&gt;
  
  
  How TDD enhances your confidence in your own intent, capability, and results
&lt;/h3&gt;

&lt;p&gt;Put yourself in the shoes of a data customer. You've carefully prepared examples of the kind of output you need and sent them to the analytics engineer. The engineer comes back with a finished data model. While validating the results, you find that one of the very examples you've given them isn't even correct in the output! Would you believe that analytics engineer really cares about helping you? That they have solid abilities? That they can drive results? And what effect would all this have on that engineer's opinion of themselves?&lt;/p&gt;

&lt;p&gt;Most of the time, this kind of experience is caused not by a lack of care or ability, but by a &lt;a href="https://en.wikipedia.org/wiki/Software_regression" rel="noopener noreferrer"&gt;regression&lt;/a&gt;. Regressions happen even to the most caring and capable engineers. Here's how: The analytics engineer works on the examples one at a time. However, the logic changes they make to satisfy the second example can inadvertently break the first example. The problem compounds as new edge cases are introduced. Working on the tenth example can break any one of the previous nine. Without automated testing, these regressions can be almost impossible to catch.&lt;/p&gt;

&lt;p&gt;Over the course of the last major analytics engineering project I've worked on, the tests I wrote caught three regressions I'd accidentally introduced. If I hadn't had the tests, there's a chance that I could be thought of (and think of myself) as a sub-par engineer who doesn't even get the example rows right three times in a single project. Instead, I have complete confidence that all the examples are satisfied, and that I can take on any additional complexity without introducing regressions. This is a matter of discipline, not of intelligence or ability.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to do test-driven development
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with the data the customer actually needs
&lt;/h3&gt;

&lt;p&gt;It all starts by talking to the data consumer and understanding their needs. If something broke, what's an example? Turn that example into your test. If something new is needed, what does it look like? Ask them to mock up a few examples cases in a spreadsheet-like format. The rows of that spreadsheet become your tests.&lt;/p&gt;

&lt;p&gt;Don't worry about source data, facts, or dimensions in your tests. Focus on the highest level of what the customer needs. Ask them and they'll be able to represent their needs in a spreadsheet-like format every time.&lt;/p&gt;

&lt;p&gt;Think about the columns of that spreadsheet. One or more of those columns will be able to be used as an identifier of that particular example row. There should only be one row with that identifier. Values in some of the rest of the columns will represent the business logic of the example. Therefore, we need to test two things: Does that example row exist, and do the business logic values match?&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://docs.getdbt.com/docs/build/data-tests#singular-data-tests" rel="noopener noreferrer"&gt;dbt data test&lt;/a&gt; returns failing records. In other words, the test has succeeded when no rows are returned. Here's an example implementation:&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;with&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="k"&gt;as&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;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;n&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;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"denormalized_view"&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;id1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some_id'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'other_id'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'Not exactly 1 row'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error_msg&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'Row test failed'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error_msg&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;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"denormalized_view"&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;id1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some_id'&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'other_id'&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;value1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some example value'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value2&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="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;value2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'some other value'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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;01&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's dissect what's happening in this query. There are two &lt;code&gt;select&lt;/code&gt; statements joined together with a &lt;code&gt;union all&lt;/code&gt;. The first will return a failing record if the row identified by the identifier(s) doesn't exist in the data. This is important so we don't inadvertently pass a test when the data is not there in the first place. The second identifies that same row, and then looks for any discrepancies in the business logic values. That's easiest to achieve by wrapping the expected values in a &lt;code&gt;not()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do watch out for null values. Due to &lt;a href="https://en.wikipedia.org/wiki/Null_(SQL)#Effect_of_Unknown_in_WHERE_clauses" rel="noopener noreferrer"&gt;three-valued logic in SQL&lt;/a&gt;, the filter &lt;code&gt;not(column = 'value')&lt;/code&gt; will not return rows where the column is null.  I recommend testing for nulls separately using dbt's generic &lt;a href="https://docs.getdbt.com/docs/build/data-tests#generic-data-tests" rel="noopener noreferrer"&gt;not_null test&lt;/a&gt; so that you don't have to remember each time.&lt;/p&gt;

&lt;p&gt;This kind of test is very easy to copy and paste and adapt quickly. It's also easy to read and maintain. This will be all you need 90% of the time.&lt;/p&gt;

&lt;p&gt;It's easy to accidentally write a SQL query that produces no rows. That's why it's also easy to write a dbt data test that accidentally passes. The test should be written and run first, before any development work is done. The test should fail. Then the change should be implemented, and the test should succeed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation-driven development
&lt;/h3&gt;

&lt;p&gt;In addition to tests, I also encourage you to write the documentation as you're gathering requirements, not after the data model is written. dbt allows &lt;a href="https://docs.getdbt.com/reference/model-properties" rel="noopener noreferrer"&gt;model properties&lt;/a&gt; (including documentation) to be defined before any SQL is written.&lt;/p&gt;

&lt;p&gt;For example, if you're developing a transactions model with your accounting team, you can create the file &lt;code&gt;models/denormalized/accounting/_accounting__models.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;models&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;accounting_transactions&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Transactions table for accounting&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;transaction_key&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Synthetic key for the transaction&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;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;action_date&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Date of the transaction&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should be taking notes when gathering data customer requirements anyway. Instead of writing the notes down in something like a Google Doc or an email, take notes in this YAML format instead. That'll get you kick-started on your documentation and testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge cases that don't exist in production data
&lt;/h3&gt;

&lt;p&gt;You will often have to write logic that encompasses things that don't exist in production data, but potentially could. On one hand, it's good to be defensive and handle potential mistakes before they happen. On the other hand, it's very hard to write tests for things that aren't there.&lt;/p&gt;

&lt;p&gt;There are a couple of potential solutions here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Just write the logic, don't write any additional tests.&lt;/li&gt;
&lt;li&gt; If there's a non-production environment of the source system, the data model could be pointed to that non-production environment for development and pull requests. Then all kinds of edge cases could be created in the non-production system and tests written as normal.&lt;/li&gt;
&lt;li&gt;  If there is no non-production environment of the source system, &lt;a href="https://docs.getdbt.com/docs/build/unit-tests" rel="noopener noreferrer"&gt;dbt unit tests&lt;/a&gt; can be used. Using the unit test syntax, you can define your edge case inputs in your model's YAML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most realistic approach is to just write the logic without writing additional tests. Analytics engineers work on tight enough deadlines that writing tests for things that aren't there is just not worth it in most situations. In the minority of cases where the logic's correctness is critical enough to justify the additional time investment, approach 2 or 3 above can be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing test-driven development
&lt;/h2&gt;

&lt;p&gt;It's a good idea to work towards enforcing test-driven development in your analytics engineering team. Rather than surprising folks with a new policy, I recommend setting a deadline by which test-driven development will be mandated, and ensuring the team gets familiar with it before the deadline.&lt;/p&gt;

&lt;p&gt;Here's an example workflow that incorporates test-driven development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All dbt models are stored in a Git repo with a write-protected production branch. All changes to production have to come through pull requests with at least one peer approval.&lt;/li&gt;
&lt;li&gt;Analytics engineers create feature branches off the production branch and open pull requests when the features are ready.&lt;/li&gt;
&lt;li&gt;Every peer reviewer is expected to only approve the pull request if they see tests corresponding to every feature. If they don't see corresponding tests, that means TDD wasn't followed and the pull request shouldn't be approved.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What about TDD for data engineering?
&lt;/h3&gt;

&lt;p&gt;It's tempting and somewhat justified to make data engineers follow TDD as well. However, the value proposition of TDD for data engineering is not as clear as for analytics engineering.&lt;/p&gt;

&lt;p&gt;Since the predominant data warehousing paradigm shifted from ETL (extract-transform-load) to ELT (extract-load-transform), the role of data engineers also changed. Data engineers are now focused on querying APIs and then loading the responses into the data warehouse in the rawest possible form.&lt;/p&gt;

&lt;p&gt;Analytics engineers work inside the data warehouse, which is a deterministic environment. The same inputs always produce the same outputs, and the logic used to create the outputs is complex. That's a perfect environment for TDD to be impactful.&lt;/p&gt;

&lt;p&gt;Data engineers work in an almost opposite environment. Since they just extract and load, there's no logic at all. At the same time, they have to interface with external systems, which can have a whole host of unpredictable issues.&lt;/p&gt;

&lt;p&gt;It's definitely possible to do test-driven development as a data engineer, but it's difficult and produces questionable benefits. Suppose you're loading data from the Facebook Ads API. How do you do that in a test-driven way? You could use &lt;a href="https://requests-mock.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;requests-mock&lt;/a&gt; to simulate possible inputs with corresponding outputs and errors. However, the only thing you do with the output is load it into the data warehouse as directly as possible, so there's not much to test there. Additionally, you may not know what the possible errors are, and even if you do, there's nothing you can do about them from your end except retry.&lt;/p&gt;

&lt;p&gt;For these reasons, I don't attempt to follow test-driven development when writing extract-and-load processes, and instead focus on &lt;a href="https://dev.to/panasenco/simplest-data-architecture-2nln"&gt;architectural simplicity&lt;/a&gt; and plenty of retry mechanisms written with &lt;a href="https://github.com/litl/backoff" rel="noopener noreferrer"&gt;backoff&lt;/a&gt; or &lt;a href="https://tenacity.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;tenacity&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;If you're an analytics engineer, I hope this post has convinced you to give test-driven development a try. If you're an analytics engineering team leader, I hope you consider making test-driven development a requirement for your team.&lt;/p&gt;

&lt;p&gt;Analytics engineering is uniquely well-suited to test-driven development. The cost of effort of creating tests from end user requirements is low, and the cost of regressions from complex and untested business logic in your data models is high. In addition, test-driven development boosts trust in data throughout your organization, and overall makes the experience of working with data more pleasant and fun for everyone.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover image generated using &lt;a href="https://huggingface.co/spaces/black-forest-labs/FLUX.1-schnell" rel="noopener noreferrer"&gt;FLUX.1-schnell&lt;/a&gt; on &lt;a href="https://huggingface.co/" rel="noopener noreferrer"&gt;HuggingFace&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>analytics</category>
      <category>tdd</category>
      <category>datascience</category>
    </item>
    <item>
      <title>The Simplest Data Architecture</title>
      <dc:creator>Aram Panasenco</dc:creator>
      <pubDate>Wed, 25 Sep 2024 12:11:00 +0000</pubDate>
      <link>https://dev.to/panasenco/simplest-data-architecture-2nln</link>
      <guid>https://dev.to/panasenco/simplest-data-architecture-2nln</guid>
      <description>&lt;p&gt;Many data professionals, myself included, have had to rethink the way we work in the aftermath of the 2022-2023 interest rate spike. The new industry-wide reality of smaller teams, higher pressure, and higher turnover forces a renewed focus on simplicity. A simple data architecture is a great starting point for all organizations. Saying that something is a "best practice" is no longer enough to justify additional processes and tools. Complexity should only be introduced if absolutely necessary to meet business needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;p&gt;This post is a comprehensive collection of "simplest practices" that can be used to build a data warehouse from the ground up. These practices can be grouped into 4 sections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extracting and loading data&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transforming data&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring and alerting&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The diagram
&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%2F1wl4y2s4wtfo42z3ykhs.png" 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%2F1wl4y2s4wtfo42z3ykhs.png" alt="Diagram of the simplest data architecture" width="800" height="742"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram inspired by the Krazam video &lt;a href="https://youtu.be/dLTUqPue9sQ" rel="noopener noreferrer"&gt;High Agency Individual Contributor&lt;/a&gt;.&lt;/em&gt; 😁 &lt;em&gt;Made with &lt;a href="https://inkscape.org/" rel="noopener noreferrer"&gt;Inkscape&lt;/a&gt; using clipart from &lt;a href="https://www.vecteezy.com/" rel="noopener noreferrer"&gt;Vecteezy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining simplicity
&lt;/h2&gt;

&lt;p&gt;There are many measures of quality of a data architecture, including satisfying requirements, correctness, cost effectiveness, compliance, openness, and many more. Simplicity is only one of these measures. But what exactly are we measuring when we talk about simplicity?&lt;/p&gt;

&lt;p&gt;I define simplicity as a tradeoff between value, effort, and moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maximize the value&lt;/strong&gt; of data products delivered to stakeholders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimize the necessary effort&lt;/strong&gt; to document, learn, contribute to, and maintain the data architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimize the number of moving parts&lt;/strong&gt;, such as technologies, processes, data structures, files, and lines of code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Snowflake as your data warehouse
&lt;/h2&gt;

&lt;p&gt;As much as I love the idea of just using PostgreSQL, I know that I'd spend countless hours troubleshooting &lt;a href="https://www.geeksforgeeks.org/fixing-common-postgresql-performance-bottlenecks/" rel="noopener noreferrer"&gt;sharding, checkpointing, bloat, vacuuming, and more performance issues&lt;/a&gt;. Managing the performance of a data warehouse database can easily become a full-time job (and in many organizations, does).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.snowflake.com/en/" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt; eliminates this need with its virtual warehouses that can be scaled up or out and support for dozens of concurrent queries on the same table through micropartitioning. Add to that a user-friendly UI and a growing list of features focused on modern data warehousing, and it becomes really hard to beat Snowflake in terms of delivering high value for minimal effort.&lt;/p&gt;

&lt;p&gt;(I still want to set up an open-source data warehouse myself at least once though)&lt;/p&gt;

&lt;h2&gt;
  
  
  Manage infrastructure through Terraform/OpenTofu
&lt;/h2&gt;

&lt;p&gt;Infrastructure is a great example of the tradeoff between minimizing effort and minimizing the number of moving parts. If you're standing up a quick single-user Postgres database to use dbt locally, Terraform is definitely overkill. A small startup of less than 10 people probably doesn't need Terraform. However, the balance tips toward infrastructure-as-code even for a small organization of around 100 people. Once your organization starts having separate departments, suddenly you need multiple databases, multiple schemas, auditable access controls, review environments, scalable compute, and potentially even cloud integrations. It's definitely possible to do all this manually, but the effort to document and enforce the standards quickly begins to outweigh the effort to stand up, learn, and maintain an infrastructure-as-code solution.&lt;/p&gt;

&lt;p&gt;Due to Terraform changing its license in 2023, a truly open-source fork called &lt;a href="https://opentofu.org/" rel="noopener noreferrer"&gt;OpenTofu&lt;/a&gt; was created. Though I'll keep using the term "Terraform" below to prevent confusion, I do recommend OpenTofu over Terraform in your implementation.&lt;/p&gt;

&lt;p&gt;If your organization uses Snowflake, you can use Terraform/OpenTofu to define your &lt;a href="https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/database" rel="noopener noreferrer"&gt;databases&lt;/a&gt;, &lt;a href="https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/schema" rel="noopener noreferrer"&gt;schemas&lt;/a&gt;, &lt;a href="https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/account_role" rel="noopener noreferrer"&gt;roles&lt;/a&gt;, &lt;a href="https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/warehouse" rel="noopener noreferrer"&gt;warehouses&lt;/a&gt;, and &lt;a href="https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role" rel="noopener noreferrer"&gt;permissions&lt;/a&gt;. You can additionally use Terraform to create personal environments for each developer, as well as create review environments for each Git pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Minimize the amount of data access roles
&lt;/h3&gt;

&lt;p&gt;I once had the ambition to implement perfect role-based access in Snowflake. For each functional role that needs to access the data warehouse, there'd be a corresponding Snowflake role. That way, permissions could be fine-tuned for each role.&lt;/p&gt;

&lt;p&gt;In practice, this utopian vision ended up as a huge Terraform file with the same access being copied and pasted over and over and over. The Terraform updates are super slow because each role-schema pair is an object Terraform has to keep track of and manage. The number of these combinations easily went into the thousands. Not to mention all the constant requests for data access from different groups...&lt;/p&gt;

&lt;p&gt;My current thinking is that you should start with just two roles: "Reporter" and "Developer". Reporters can only see production data (which could include most raw data depending on your organization's culture). Developers can additionally see and create non-production data. Start there and only add roles as absolutely necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Create personal development schemas in Terraform
&lt;/h3&gt;

&lt;p&gt;If you maintain the list of users that have the Developer role inside of Terraform, you can simply iterate over it to create the corresponding personal development schemas they can do their development work in. For example, in Snowflake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snowflake_schema"&lt;/span&gt; &lt;span class="s2"&gt;"personal_dev_schemas"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;developers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;database&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snowflake_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DEV_${each.key}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snowflake_grant_privileges_to_account_role"&lt;/span&gt; &lt;span class="s2"&gt;"personal_dev_schema_grants"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snowflake_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;personal_dev_schemas&lt;/span&gt;
  &lt;span class="nx"&gt;privileges&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"USAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"MONITOR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CREATE TABLE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
  &lt;span class="nx"&gt;account_role_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DEVELOPER"&lt;/span&gt;
  &lt;span class="nx"&gt;on_schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;schema_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fully_qualified_name&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;If you don't maintain the list of developer users within Terraform, you can get it directly from Snowflake by checking which users have been granted the Developer role via the &lt;a href="https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/data-sources/grants" rel="noopener noreferrer"&gt;snowflake_grants data source&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Use a separate Terraform state for pull request resources
&lt;/h3&gt;

&lt;p&gt;By pull request resources here, I mean resources that are specific to a pull request, usually containing the pull request's number somewhere, not just any resource created in a pull request. For example, a schema like &lt;code&gt;dev_pr_123&lt;/code&gt; for storing data for the dbt run in pull request 123. This practice is essential to keep your PR pipeline results consistent.&lt;/p&gt;

&lt;p&gt;The Terraform &lt;a href="https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http" rel="noopener noreferrer"&gt;http data source&lt;/a&gt; can be used to retrieve the list of open merge requests and create the corresponding schemas. Here's an example with GitLab and Snowflake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="s2"&gt;"gitlab_merge_requests"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.gitlab_api_url}/projects/${var.gitlab_project_id}/merge_requests?state=opened&amp;amp;sort=desc&amp;amp;per_page=100"&lt;/span&gt;
  &lt;span class="nx"&gt;request_headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Accept&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
    &lt;span class="nx"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bearer ${var.gitlab_api_token}"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"snowflake_schema"&lt;/span&gt; &lt;span class="s2"&gt;"merge_request_schemas"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;mr&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;jsondecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gitlab_merge_requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response_body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;database&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snowflake_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DEV_MR_${each.key}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you want these resources to be in a separate Terraform state from your main one. If you put your merge requests resources in your main state, new merge request pipelines will constantly overwrite your main state, making it painful to try to get any actual Terraform debugging and development done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use off-the-shelf data pipelines when possible
&lt;/h2&gt;

&lt;p&gt;Data engineering pipelines are expensive to develop and maintain. Requests to the data engineering team can take weeks or even months to get done. Using off-the-shelf solutions can keep costs low and value high.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.fivetran.com/" rel="noopener noreferrer"&gt;Fivetran&lt;/a&gt; is the best-known name in the space of extract-and-load you can just pay for. However, there is some exciting ongoing competition in this space. As of the writing of this article, Snowflake itself came out with a &lt;a href="https://other-docs.snowflake.com/en/connectors/postgres6/about" rel="noopener noreferrer"&gt;free connector for PostgreSQL&lt;/a&gt;, and there are more connectors by various companies popping up all the time on the Snowflake marketplace.&lt;/p&gt;

&lt;p&gt;Being up-to-date on the off-the-shelf data connectors that are available out there can be a huge value-add and differentiator for any data engineer. Not to mention, it also gives you time to focus on more important high-level problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use CI and self-hosted runners instead of an orchestrator
&lt;/h2&gt;

&lt;p&gt;Historically, teams that have used CI/CD still used a separate orchestration tool. The CI pipeline deployed to the orchestration tool, which actually did the work on a schedule.&lt;/p&gt;

&lt;p&gt;However, using a separate orchestrator introduces extra complexity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull request checks: How do we know orchestration logic for a pull request actually succeeded? We could leave the CI job spinning waiting for the orchestrator, but then we're wasting compute on just spinning and waiting. We could use a service account that approves pull requests, but that's complex to set up and debug.&lt;/li&gt;
&lt;li&gt;Access and learning curve: The separate orchestrator requires access provisioning. Paid solutions charge per seat. Debugging requires folks to jump between CI and the orchestrator.&lt;/li&gt;
&lt;li&gt;Reproducibility: If your code is tied to an orchestrator, it may be difficult for you and others to identify and reproduce issues. For example, suppose you're consuming an API from a business partner, and there's an issue. Is the issue with the API or with the orchestration? You could get stuck arguing about it back-and-forth since the business partner won't want to install your orchestrator to reproduce the issue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Prerequisite: Self-host your CI compute
&lt;/h3&gt;

&lt;p&gt;Compute time for CI tools is notoriously expensive. Compare GitLab CI's &lt;a href="https://about.gitlab.com/pricing/" rel="noopener noreferrer"&gt;$0.60/hour&lt;/a&gt; to AWS EC2's &lt;a href="https://aws.amazon.com/ec2/pricing/on-demand/" rel="noopener noreferrer"&gt;$0.05/hour&lt;/a&gt; (this is further exacerbated by the fact that GitLab charges for the time of each job separately while EC2 can execute multiple jobs in one instance). Luckily most major CI platforms provide a way to self-host that compute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitLab CI &lt;a href="https://docs.gitlab.com/runner/#use-self-managed-runners" rel="noopener noreferrer"&gt;self-managed runners&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners" rel="noopener noreferrer"&gt;self-hosted runners&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Azure DevOps &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/linux-agent?view=azure-devops" rel="noopener noreferrer"&gt;self-hosted agents&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simplest practice: Use CI instead of an orchestrator
&lt;/h3&gt;

&lt;p&gt;In recent years, CI tools have steadily adopted more and more features from orchestrators, making it completely viable (assuming you self-host the compute - see above) to run a sophisticated data pipeline directly from your CI tool of choice.&lt;/p&gt;

&lt;p&gt;Running pipelines on a schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitLab CI &lt;a href="https://docs.gitlab.com/ee/ci/pipelines/schedules.html" rel="noopener noreferrer"&gt;scheduled pipelines&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;schedule event&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Azure DevOps &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/scheduled-triggers?view=azure-devops&amp;amp;tabs=yaml" rel="noopener noreferrer"&gt;pipeline schedules&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Excluding certain jobs (e.g. Terraform) from the scheduled run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitLab CI &lt;a href="https://docs.gitlab.com/ee/ci/yaml/#rulesif" rel="noopener noreferrer"&gt;&lt;code&gt;rules:if&lt;/code&gt;&lt;/a&gt; checking whether &lt;a href="https://docs.gitlab.com/ee/ci/jobs/job_rules.html#ci_pipeline_source-predefined-variable" rel="noopener noreferrer"&gt;&lt;code&gt;CI_PIPELINE_SOURCE&lt;/code&gt;&lt;/a&gt; is equal to &lt;code&gt;schedule&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/using-conditions-to-control-job-execution" rel="noopener noreferrer"&gt;&lt;code&gt;if&lt;/code&gt;&lt;/a&gt; checking whether &lt;a href="https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-using-contexts" rel="noopener noreferrer"&gt;&lt;code&gt;github.event_name&lt;/code&gt;&lt;/a&gt; is equal to &lt;code&gt;schedule&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Azure DevOps &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/conditions?view=azure-devops" rel="noopener noreferrer"&gt;pipeline conditions&lt;/a&gt; with &lt;code&gt;ne(variables['Build.Reason'], 'Schedule')&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running multiple copies of a job in parallel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitLab CI &lt;a href="https://docs.gitlab.com/ee/ci/yaml/#parallelmatrix" rel="noopener noreferrer"&gt;parallel runs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow" rel="noopener noreferrer"&gt;matrix strategy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Azure DevOps &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/jobs-job-strategy?view=azure-pipelines#strategy-matrix-maxparallel" rel="noopener noreferrer"&gt;matrix strategy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Triggering downstream pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitLab CI &lt;a href="https://docs.gitlab.com/ee/ci/triggers/#use-a-cicd-job" rel="noopener noreferrer"&gt;pipeline trigger API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#triggering-a-workflow-from-a-workflow" rel="noopener noreferrer"&gt;triggering a workflow from a workflow&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Azure DevOps &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/pipeline-triggers?view=azure-devops" rel="noopener noreferrer"&gt;pipeline triggers&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The above building blocks should be sufficient to run almost any batch-based parallelized data ingestion job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Load data in batches, avoid streaming.
&lt;/h3&gt;

&lt;p&gt;If you have an off-the-shelf connector that streams data into your warehouse, go ahead and use it! However, if you have to build an extract-and-tool process from scratch, avoid streaming unless there's a use case for it.&lt;/p&gt;

&lt;p&gt;Building and debugging streaming infrastructure is expensive. Let's take Apache Kafka as an example. It requires DevOps expertise and time to properly set up ZooKeeper, 3+ broker nodes, plus an additional Kafka Connect server. It also takes expertise to utilize the Kafka Connect API (being cautious of potential pitfalls like Kafka Connect's &lt;a href="https://docs.confluent.io/platform/current/installation/configuration/connect/index.html#receive-buffer-bytes" rel="noopener noreferrer"&gt;default buffering behavior&lt;/a&gt;), to write custom code that sends data to a Kafka topic, and to troubleshoot any issues.&lt;/p&gt;

&lt;p&gt;Unless there's a clear business need that can justify both the upfront expense of standing up streaming infrastructure and the ongoing expense to maintain it, it's better to stick to batch-based extract-and-load. Batch processes can be invoked as scripts without having to worry about streaming infrastructure or streaming race conditions. This makes it possible to call them from any orchestrator or CI pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Modularize your data engineering code into command-line scripts
&lt;/h3&gt;

&lt;p&gt;When using an off-the-shelf data connector is not an option, we have to write our own extract-and-load code in a language like Python.&lt;/p&gt;

&lt;p&gt;Use Python's &lt;a href="https://docs.python.org/3/library/argparse.html" rel="noopener noreferrer"&gt;argparse&lt;/a&gt; library (or the corresponding library for your language of choice) to add command line capability to your Python functions. This allows each function to be called both as a library function from other Python code and also directly from the command line. This makes your code debuggable, modular, and easy to call from a CI script.&lt;/p&gt;

&lt;p&gt;Example Python file &lt;code&gt;scripts/python/extract_data.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data_file&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;prog&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLI command to extract data.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name of the API to extract data from.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--verbose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Be verbose. Include once for INFO output, twice for DEBUG output.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;LOGGING_LEVELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&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;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LOGGING_LEVELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOGGING_LEVELS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;  &lt;span class="c1"&gt;# cap to last level index
&lt;/span&gt;    &lt;span class="n"&gt;data_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example invocation:&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;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'...'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; scripts.python.extract_data &lt;span class="nt"&gt;-a&lt;/span&gt; transactions &lt;span class="nt"&gt;-vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of script can be called directly in a CI job, used for easy debugging from the terminal, and shared with other teams and business partners.&lt;/p&gt;

&lt;p&gt;I've found a ton of value in being able to save or send a single command-line snippet for reproducing a problem. Without this ability, I've had to gut and rewrite my Python functions to debug, which has sometimes introduced new bugs itself, and was very difficult to explain to others, or even understand myself after a few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containerize your data pipelines
&lt;/h2&gt;

&lt;p&gt;Containerization has exploded since the early 2010s. Arguments have been made that containers have been used in many areas where they don't necessarily make sense, and have their own overhead and learning curve, so using containers isn't always the simplest practice in every situation.&lt;/p&gt;

&lt;p&gt;I do believe that using containers makes a ton of sense in writing data pipelines. You can use the same image to develop and run the pipeline, preventing "it works on my machine" issues. You can test different variations of the image without having to stand up additional infrastructure or potentially breaking the workflows of others who're using the same infrastructure. Finally, knowledge of containerization is increasingly expected of all engineers, while knowledge of other tools that solve similar issues (like &lt;a href="https://www.vagrantup.com/" rel="noopener noreferrer"&gt;Vagrant&lt;/a&gt; or &lt;a href="https://www.ansible.com/" rel="noopener noreferrer"&gt;Ansible&lt;/a&gt;) is less common.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Use the same Dockerfile for development and production
&lt;/h3&gt;

&lt;p&gt;If you use different Dockerfiles for developing (e.g. in VS Code Dev Containers or GitHub Codespaces or Gitpod) and for production runs, the Dockerfiles inevitably end up diverging, causing unexpected bugs. At the same time, if your development and production Docker images are identical, your production image will be bloated by tools that are needed only for development.&lt;/p&gt;

&lt;p&gt;The solution is to use the same Dockerfile to build two different images. We can achieve this by using a &lt;a href="https://docs.docker.com/build/building/variables/" rel="noopener noreferrer"&gt;Docker build argument&lt;/a&gt; &lt;code&gt;IN_CI&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:slim&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; IN_CI=false&lt;/span&gt;

&lt;span class="c"&gt;# Install apt packages&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="se"&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;$IN_CI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'false'&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="se"&gt;\
&lt;/span&gt;            apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;                git &lt;span class="se"&gt;\
&lt;/span&gt;                less &lt;span class="se"&gt;\
&lt;/span&gt;                wget &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="c"&gt;# Clean up&lt;/span&gt;
    &amp;amp;&amp;amp; apt-get clean -y \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build argument &lt;code&gt;IN_CI&lt;/code&gt; is set to false by default, which installs development dependencies &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;less&lt;/code&gt;, and &lt;code&gt;wget&lt;/code&gt;. When building the image in our CI pipeline, we pass in &lt;code&gt;-build-arg IN_CI=true&lt;/code&gt;, which then skips installing those development dependencies, keeping the production image slim.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Use pipx for command-line Python tools
&lt;/h3&gt;

&lt;p&gt;If you're using Python to write your data engineering pipelines while also using a Python-based command line tool like &lt;a href="https://docs.getdbt.com/docs/core/pip-install" rel="noopener noreferrer"&gt;dbt&lt;/a&gt;, you may have noticed a frustrating thing: Python command line tools can have a &lt;em&gt;lot&lt;/em&gt; of dependencies, some of them potentially conflicting with the versions of data engineering packages you want to use.&lt;/p&gt;

&lt;p&gt;You can use isolated Python environments like &lt;a href="https://docs.python.org/3/library/venv.html" rel="noopener noreferrer"&gt;venv&lt;/a&gt; or &lt;a href="https://docs.conda.io/en/latest/" rel="noopener noreferrer"&gt;conda&lt;/a&gt;. If you do this, you'll have to manage your environments yourself, and also constantly switch between them to run your data engineering code vs dbt.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;a href="https://pipx.pypa.io/stable/" rel="noopener noreferrer"&gt;pipx&lt;/a&gt; allows you can keep your root Python environment for data engineering, and install command-line tools like dbt into isolated environments automagically. For example, to install the dbt command line tool for Snowflake, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--include-deps&lt;/span&gt; dbt-snowflake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs the package &lt;code&gt;dbt-snowflake&lt;/code&gt; into an isolated environment that won't conflict with your data engineering packages, while still exposing the &lt;code&gt;dbt&lt;/code&gt; command-line tool for you to use regardless of which Python environment you're in.&lt;/p&gt;

&lt;p&gt;Note that in the example above, the package &lt;code&gt;dbt-snowflake&lt;/code&gt; doesn't contain the &lt;code&gt;dbt&lt;/code&gt; command line tool, but its dependency &lt;code&gt;dbt&lt;/code&gt; does, which is why we had to use the flag &lt;code&gt;--include-deps&lt;/code&gt;. See the &lt;a href="https://pipx.pypa.io/stable/docs/" rel="noopener noreferrer"&gt;pipx docs&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;Now what if you need to install multiple command line tools that also need to be in the same environment?&lt;/p&gt;

&lt;p&gt;In the case of Elementary, &lt;code&gt;dbt-snowflake&lt;/code&gt; is actually already a dependency of &lt;code&gt;elementary-data[snowflake]&lt;/code&gt;, so the following will install both in the same environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--include-deps&lt;/span&gt; elementary-data[snowflake]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, you can use &lt;code&gt;pipx inject&lt;/code&gt; to inject one package into another's environment. See the &lt;a href="https://pipx.pypa.io/stable/docs/" rel="noopener noreferrer"&gt;pipx docs&lt;/a&gt; for more information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Freeze your Python and pipx dependencies
&lt;/h3&gt;

&lt;p&gt;Freezing dependencies is not the simplest in terms of moving pieces, but definitely simplest in minimizing effort spent on debugging outages because someone in the 10+ layers of Python dependencies in your stack decided to upgrade their package and break downstream on a weekend.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://python-poetry.org/" rel="noopener noreferrer"&gt;Poetry&lt;/a&gt; aim to fix this problem, but vanilla pip can do just fine.&lt;/p&gt;

&lt;p&gt;Suppose you have a file &lt;code&gt;requirements.txt&lt;/code&gt; that contains your Python dependencies. First, install them locally:&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;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then freeze the exact versions of &lt;em&gt;all&lt;/em&gt; your packages with &lt;a href="https://pip.pypa.io/en/stable/cli/pip_freeze/" rel="noopener noreferrer"&gt;pip freeze&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;&lt;span class="nv"&gt;$ &lt;/span&gt;pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements-frozen.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, in your image build process, install the frozen requirements instead of the source requirements:&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;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-deps&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements-frozen.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you'll have to manually update &lt;code&gt;requirements-frozen.txt&lt;/code&gt; every time you change or upgrade packages in &lt;code&gt;requirements.txt&lt;/code&gt; - it won't happen automatically!&lt;/p&gt;

&lt;p&gt;Freezing requirements in pipx works similarly. We first freeze the dependencies, but then provide them as &lt;a href="https://pip.pypa.io/en/stable/user_guide/#constraints-files" rel="noopener noreferrer"&gt;constraints&lt;/a&gt;, not directly. For example, if you've created your dbt/Elementary environment locally with &lt;code&gt;pipx install --include-deps elementary-data[snowflake]&lt;/code&gt;, you can create a constraints file like so:&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;pipx runpip elementary-data freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"pipx-dbt-constraints.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your image build process, provide the constraints to your &lt;code&gt;pipx install&lt;/code&gt; command:&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;pipx &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--include-deps&lt;/span&gt; elementary-data[snowflake] &lt;span class="nt"&gt;--pip-args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--constraint pipx-dbt-constraints.txt'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you won't have to worry about a Python package update borking your pipeline again!&lt;/p&gt;

&lt;h2&gt;
  
  
  Use compressed CSVs for loading raw data into Snowflake
&lt;/h2&gt;

&lt;p&gt;Since the 2000's, many data serialization protocols have been developed that promise superior compression and performance. Snowflake supports the big three Apache protocols: &lt;a href="https://en.wikipedia.org/wiki/Apache_Avro" rel="noopener noreferrer"&gt;Avro&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Apache_ORC" rel="noopener noreferrer"&gt;ORC&lt;/a&gt;, and &lt;a href="https://en.wikipedia.org/wiki/Apache_Parquet" rel="noopener noreferrer"&gt;Parquet&lt;/a&gt;. We also can't forget about Google's &lt;a href="https://en.wikipedia.org/wiki/Protocol_Buffers" rel="noopener noreferrer"&gt;Protobuf&lt;/a&gt; that started it all. While using these modern formats has been touted as a best practice for data engineering, how good are they really? Suppose you need to do some debugging on a corrupt Parquet file or an Avro file with missing data. Even when I was using these formats daily, I wouldn't be able to do this kind of debugging without a lot of research and custom work.&lt;/p&gt;

&lt;p&gt;Instead, dump the rawest form of the data you're loading into a JSON string inside a gzip-compressed CSV. The file sizes and performance are mostly on par to the fanciest formats above. And if you ever need to troubleshoot the resulting file, you can just use the &lt;code&gt;zcat&lt;/code&gt; utility that comes preinstalled on most Linux distributions to peek inside:&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;zcat data/data-file.csv.gz | less
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simplest practice: Export raw data into compressed CSVs
&lt;/h3&gt;

&lt;p&gt;Python example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytz&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_data&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
    &lt;span class="n"&gt;data_file&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-2024.csv.gz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wt&lt;/span&gt;&lt;span class="sh"&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;gzip_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;csv_writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gzip_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Write header
&lt;/span&gt;        &lt;span class="n"&gt;csv_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process_started_at_utc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process_ended_at_utc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended_at&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
            &lt;span class="n"&gt;csv_writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                        &lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                        &lt;span class="n"&gt;ended_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data_file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simplest practice: Have a single utility script load any compressed CSV to Snowflake
&lt;/h3&gt;

&lt;p&gt;See the complete script here: &lt;a href="https://gist.github.com/panasenco/27d01bd0dc3a11325f36f00001abdb7b" rel="noopener noreferrer"&gt;load_to_snowflake.py&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, to reload all data for the year 2024 in a single Snowflake transaction:&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;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; scripts.python.load_to_snowflake &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--schema&lt;/span&gt; my_schema &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--table&lt;/span&gt; my_table &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--file&lt;/span&gt; data/data-2024.csv.gz &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--delete-filter&lt;/span&gt; &lt;span class="s2"&gt;"value:start_date::date &amp;gt;= '2024-01-01'::date"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "delete filter" functionality enables partially incremental loads where we need to truncate and reload a part of the table. To do a pure incremental load, omit the delete filter. To do a full truncate-and-load, set the delete filter to &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;1=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that this process doesn't stage the data in S3 first. Storing the raw files somewhere like S3 is a common "best practice" that I myself have followed for quite some time. I've never really found the value in it. All those old files that no one ever looks at are just slowly costing us more and more in storage costs. If we have to go back and see what the previously loaded data looked like, we can just use Snowflake's time travel instead. So in the spirit of minimizing moving parts, I load data directly into Snowflake now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transform your data with dbt
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.getdbt.com/" rel="noopener noreferrer"&gt;dbt&lt;/a&gt; has taken the data industry by storm in recent years. dbt is an open source data transformation framework, empowering users to get started right away with minimal knowledge, but also leaving a ton of options and configurations for more advanced use cases.&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%2F5wyyxaa8szovn4aaz3j8.png" 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%2F5wyyxaa8szovn4aaz3j8.png" alt="dbt core docs with beautiful data lineage" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can learn more about dbt by browsing its &lt;a href="https://docs.getdbt.com/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;. You can also browse a real-life example: &lt;a href="https://dbt.gitlabdata.com/#!/overview" rel="noopener noreferrer"&gt;GitLab's dbt project&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Use dbt Cloud
&lt;/h3&gt;

&lt;p&gt;The company behind dbt, dbt Labs, offers dbt as a managed service called dbt Cloud. In terms of minimizing effort and minimizing moving pieces, paying for dbt Cloud is a clear winner over running the open-source dbt Core yourself.&lt;/p&gt;

&lt;p&gt;This is an example of how simplicity isn't everything. Any data SaaS product that charges per seat is at odds with the values of  openness and inclusivity, as it either limits the access to that product to a select few individuals, or becomes unjustifiably expensive if blanket-given to the whole org. Limiting access to a circle of people makes it harder for individuals outside that circle to explore the data documentation and lineage.&lt;/p&gt;

&lt;p&gt;In terms of pure simplicity, however, dbt Cloud is the clear choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Empower self-service
&lt;/h3&gt;

&lt;p&gt;Every hour spent empowering folks to handle their own data needs can save dozens of hours spent responding to tickets in the future. This effort also upskills the entire organization and increases its velocity.&lt;/p&gt;

&lt;p&gt;Data science teams especially are under a lot of deadline pressure to try new things, experiment with new products, and deliver concrete financial value to the business. These teams are frequently unable to wait even a week for data/analytics engineering support. Data scientists will stand up their own infrastructure and data pipelines anyway, so you might as well empower them to do it your way.&lt;/p&gt;

&lt;p&gt;A focus on simplicity also turns into a virtuous cycle here, because the simpler your data architecture is, the easier it is to onboard other teams, the more time everyone saves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Model wide tables on top of dimensional models
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=3OcS2TMXELU" rel="noopener noreferrer"&gt;Wide tables&lt;/a&gt; are the most popular modern alternative to dimensional modeling. Building spreadsheet-like wide tables directly on top of your raw data gives you the benefit of having as few moving pieces as it gets. However, I believe the effort spent on long-term maintenance of such wide tables outweighs that benefit.&lt;/p&gt;

&lt;p&gt;I agree with proponents of wide tables that presenting the final data to the end user in a user-friendly spreadsheet-like format is a good thing. I bet that every single data professional has had to present data to end users in this format more than once. In implementations I've been part of, this was even considered its own layer of the data warehouse - "denormalized layer" or "activation layer".&lt;/p&gt;

&lt;p&gt;In my experience, there's a ton of value in considering your wide tables (or wide views) your end product, but still building these wide tables on top of facts and dimensions, for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a conceptual structure under your wide tables makes your data warehouse more modular, flexible, and reusable, allowing you to answer similar questions easily in the future without having to build everything from scratch again.&lt;/li&gt;
&lt;li&gt;Having to think about what the facts are and what their grains are forces analytics engineers to understand the business processes more deeply. This turns AEs into collaborators helping the data drives business value as opposed to code monkeys building whatever spreadsheet the end user requests.&lt;/li&gt;
&lt;li&gt;Using dimensional modeling as opposed to more normalized approaches like Inmon or Data Vault makes the data speak the language of the business. This enables end users to understand the underlying data structure and makes it easier for them to self-serve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In modern data modeling, we have more flexibility and freedom than ever. Start with wide tables but don't stop there. Add concepts, structures, and processes when the benefit they promise in terms of reduced effort outweighs the costs of setting them up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Document requirements in dbt
&lt;/h3&gt;

&lt;p&gt;Documenting data models frequently gets pushed to the end of a project, and then never gets done. However, it's actually very easy to document the data model during the requirements gathering process in dbt, kickstarting your development process with a bang!&lt;/p&gt;

&lt;p&gt;All you have to do is create a &lt;a href="https://docs.getdbt.com/docs/build/documentation#adding-descriptions-to-your-project" rel="noopener noreferrer"&gt;models.yml file&lt;/a&gt; and document everything the end user tells you in the model's description. Then as you dive deeper into the column level, you can document what the user says about each column they need as well. After you've written the code, you already have a perfectly documented model!&lt;/p&gt;

&lt;p&gt;I've had great results in taking this a step further and turning user-provided examples into automated &lt;a href="https://docs.getdbt.com/docs/build/data-tests" rel="noopener noreferrer"&gt;dbt tests&lt;/a&gt;. It's easy to do &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;test-driven development&lt;/a&gt; with dbt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Get an example of a requirement from the end user.&lt;/li&gt;
&lt;li&gt; Turn that example into a &lt;a href="https://docs.getdbt.com/docs/build/data-tests#singular-data-tests" rel="noopener noreferrer"&gt;singular data test&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Ensure the test fails since you haven't actually implemented the feature yet. You'd be surprised how often the test will inadvertently pass because of a mistake in the test itself...&lt;/li&gt;
&lt;li&gt; Implement the feature.&lt;/li&gt;
&lt;li&gt; Run the test again - it should now pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example of how sample desired output from an end user can be turned into a dbt &lt;a href="https://docs.getdbt.com/docs/build/data-tests#singular-data-tests" rel="noopener noreferrer"&gt;singular data test&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="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="k"&gt;as&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;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;cnt&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;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"my_model"&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'example_id'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'Not exactly 1 row'&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;row_count&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="s1"&gt;'Row test failed'&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;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"my_model"&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'example_id'&lt;/span&gt;
    &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;column1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'example_value_1'&lt;/span&gt;
        &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;column2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'example_value_2'&lt;/span&gt;
        &lt;span class="k"&gt;and&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;Testing is another process that frequently gets pushed out to the end of the development process and then abandoned. When you follow test-driven development, your model will be perfectly tested as soon as your SQL is implemented.&lt;/p&gt;

&lt;p&gt;In addition, following test-driven development prevents regressions. Regressions happen when implementing a new feature breaks an old feature. For example, when you rewrite your query logic to handle a new edge case, you inadvertently break the base case without realizing. Regressions can take dozens of hours to identify and debug, but with test-driven development your previous tests will instantly identify it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitor your data with dbt Cloud
&lt;/h2&gt;

&lt;p&gt;Nothing frustrates data consumers more than when the same data issues occur over and over and over again, and it's the data consumers catching them instead of the data team. The purpose of testing and alerting is to build trust with the data consumers.&lt;/p&gt;

&lt;p&gt;Users of dbt Cloud can &lt;a href="https://docs.getdbt.com/docs/deploy/job-notifications" rel="noopener noreferrer"&gt;configure email and Slack alerts&lt;/a&gt; on failed jobs, which is all you really need. If you're using dbt Core, you can use the open-source tool &lt;a href="https://elementary-data.com" rel="noopener noreferrer"&gt;Elementary&lt;/a&gt; to &lt;a href="https://docs.elementary-data.com/oss/guides/alerts/elementary-alerts" rel="noopener noreferrer"&gt;send alerts&lt;/a&gt; instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplest practice: Alert only on past issues
&lt;/h3&gt;

&lt;p&gt;The ideal of data warehouse alerting is proactively catching and fixing all data pipeline issues before the downstream data consumers encounter them even once, but I don't believe this ideal is even remotely achievable. The biggest and nastiest data pipeline failures are the ones that leave you wondering how you could even test for them. Beautiful detailed plans containing SLAs, severity levels, and testing plans get drawn up and put on the back-burner, but they wouldn't catch many of these big issues even if they were perfectly implemented.&lt;/p&gt;

&lt;p&gt;Put the following block in your &lt;code&gt;dbt_project.yml&lt;/code&gt; to make all tests only warn by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;+severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a downstream data consumer alerts you about a data issue and you fix it, then and only then create a test for that particular issue and set its &lt;a href="https://docs.getdbt.com/reference/resource-configs/severity" rel="noopener noreferrer"&gt;severity&lt;/a&gt; to &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For some particularly nasty failures, you may even have to go outside of dbt and implement alerting in an external system or a Python script. Do whatever you have to do to make sure that you'll catch the same issue before the data consumer does next time. Don't worry too much about preserving the consistency of some imaginary testing or alerting strategy. Alerts don't have to be pretty, they just have to work.&lt;/p&gt;

&lt;p&gt;Just because you don't have some utopian system that can detect any issue perfectly doesn't mean you'll lose your data consumers' trust. What will lose their trust is if they have to alert you about the same exact issue over and over. As long as you show them that they only have to show you an issue once for you to catch it yourself in the future, their trust will be only growing over time.&lt;/p&gt;

</description>
      <category>data</category>
      <category>architecture</category>
      <category>dataengineering</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
