<?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: Marcus</title>
    <description>The latest articles on DEV Community by Marcus (@marcus1968).</description>
    <link>https://dev.to/marcus1968</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4009616%2F12158dea-c717-41b8-be68-130eacdf65ed.jpg</url>
      <title>DEV Community: Marcus</title>
      <link>https://dev.to/marcus1968</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcus1968"/>
    <language>en</language>
    <item>
      <title>Two Agents, One Working Tree — What Broke When I Ran Parallel Claude Code Sessions in One Git Repository</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 12 Sep 2026 06:46:26 +0000</pubDate>
      <link>https://dev.to/marcus1968/two-agents-one-working-tree-what-broke-when-i-ran-parallel-claude-code-sessions-in-one-git-3kce</link>
      <guid>https://dev.to/marcus1968/two-agents-one-working-tree-what-broke-when-i-ran-parallel-claude-code-sessions-in-one-git-3kce</guid>
      <description>&lt;p&gt;A Claude Code session opens a Git repository with no pending changes, works for half an hour and commits whatever has changed at the end. Run alone, that is correct. As soon as a second session or a human works in the same repository, the very same routine can pull someone else’s work into the commit, and Git does not warn about it.&lt;/p&gt;

&lt;p&gt;This article tells five real cases from a little over a week in which parallel Claude Code sessions shared the same repository, sometimes with each other, sometimes with the maintainer. Three of the five cases were the agent’s mistakes. The agent noticed and reported two of them itself. The third was only discovered by the neighbouring session whose work had vanished. The cases yield countermeasures, and the most obvious one is precisely the one that does not hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The common root:&lt;/strong&gt; A session sees a repository with no pending changes when it starts. If a new or modified file appears during its run because a parallel session or the human has just created or changed it, that looks to the session exactly like a file its own tooling produced. So it treats the foreign file like its own and either commits it or cleans it away. The assumption „everything new comes from me“ is never contradicted, because Git records no author for changes in the working directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The index is shared state:&lt;/strong&gt; Git collects changes for the next commit in a staging area called the index, and &lt;code&gt;git add&lt;/code&gt; puts them there. When several sessions work in the same working directory, they share its index too. Adding only your own file with &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; instead of &lt;code&gt;git add -A&lt;/code&gt; does not protect you, because &lt;code&gt;git commit&lt;/code&gt; without a path commits the entire index, including whatever another session has already put there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive operations are never cleanup in parallel operation:&lt;/strong&gt; Whoever resets a file to its previous version, discards a state or deletes a file hits everything under the given path, not just their own work. The judgement „that was me“ is an assumption that can be checked, not an observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared counters collide:&lt;/strong&gt; If two sessions assign the next sequential number at the same time, say for a new bug entry, both read the same highest number and both assign the same new one. If both then politely step aside to the next number, they collide again. Checking beforehand alone does not prevent this, because the window lies between reading and writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The machine is shared too:&lt;/strong&gt; Parallel sessions share not only the repository but the computer the human is working on at the same time. An agent that starts 220 endless loops for a load test sees only exit codes afterwards, not the desktop that stutters for twelve minutes on the human’s side. These costs have no feedback loop to the agent, and with several sessions running the human cannot even tell which one is causing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What holds:&lt;/strong&gt; Against foreign index entries, &lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt;, a look at the index as a separate step before the commit and a &lt;code&gt;git show --stat&lt;/code&gt; afterwards hold up. Shared counters need a tie-break rule agreed in advance, a right-of-way rule for the tie that decides without negotiation which side steps aside, plus a check after writing that compares the number list against the files on disk instead of just looking for duplicate numbers. None of these commands protects against simultaneous changes to the same file. Where the workflow allows it, each session therefore gets its own Git worktree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule came after the damage:&lt;/strong&gt; The countermeasures have been in a rule file of the blog repository since the second case. They are a consequence of the cases, not a precondition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Basic Git knowledge is assumed, prior Claude Code experience is not. The two Git terms that carry the article, working tree and index, are briefly explained in the first section. The cases come from two repositories, the private app project DI² and the repository of this blog, all between 28 August and 4 September 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Parallel Claude Code Sessions Produce Errors That Don’t Exist Alone
&lt;/h2&gt;

&lt;p&gt;A coding agent session has a clear view of its task and a blind spot for its surroundings. Two Git terms come up again and again in this article and deserve a short introduction. Git calls the files that sit on disk and that it compares against the last commit the working tree. The index is the staging area for the commit: with &lt;code&gt;git add&lt;/code&gt; you put the changes there that the next commit is supposed to contain. The session sees the working tree when it starts, it sees the output of its commands, and at the end it sees which files have changed. What it does not see is whether someone else has used the same working tree in the meantime. Git does not record which process or which session caused a change in the working tree. In normal operation every working tree has its own index, and whoever works in the same working tree shares that index with everyone else there. A second session or a human with an open editor leaves traces in it that cannot be told apart from your own.&lt;/p&gt;

&lt;p&gt;From this follows an assumption that is correct when a session runs alone and wrong when it does not: „The working tree was clean when I started, so everything new comes from me.“ Whether it turns into a problem depends on timing, not on how careful the session is, and none of it feels any different to the session than the normal case.&lt;/p&gt;

&lt;p&gt;All cases in this article go back to the same cause. With every command, a session only thinks about the files it touched itself. Git and the shell do not know that boundary: &lt;code&gt;git commit&lt;/code&gt; takes the whole index, a &lt;code&gt;checkout&lt;/code&gt; or a delete command acts on everything under the given path no matter who changed it, and a load test occupies the whole machine. As long as a session works alone, this goes unnoticed, because everything that changed came from it anyway. As soon as a second session or a human works in the same repository, that is no longer true, and the session does to foreign work exactly what it would do to its own. That is where the errors come from that could not occur at all without a second actor in the same working tree or on the same machine.&lt;/p&gt;

&lt;p&gt;The five cases below occurred within a little over a week in the everyday work of two repositories, without anyone looking for them. Sometimes two sessions worked side by side on different topics, sometimes one session worked while the maintainer touched files in the same repository. Both are perfectly normal modes of operation once you work with more than one agent. The parent article &lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User’s Perspective&lt;/a&gt; places parallel operation within the overall experience. This article deals with it alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: Foreign Work Swept Into a Commit
&lt;/h2&gt;

&lt;p&gt;In the DI² project, the maintainer and an agent session were working on the same repository, at times simultaneously. The session routinely committed with &lt;code&gt;git add -A&lt;/code&gt; and wrote a detailed commit message that stayed close to its topic. Between two of its commits, the maintainer had been working somewhere else entirely, on a test case convention, a skill and a CI guard. The session’s next &lt;code&gt;git add -A&lt;/code&gt; commit swept up those four files. The commit message is about a scroll bugfix. Of the roughly one hundred lines of someone else’s convention work, it says not a word.&lt;/p&gt;

&lt;p&gt;This was noticed not at commit time but hours later, and only by chance. While material was being incorporated into an article collection, it contained quotations from repo files with the note that they were not yet committed and that the follow-up commit should be located during verification. The quoted passages were found, but in a commit whose message is about a completely different topic. Without that cross-check of the evidence, the case would have gone undetected.&lt;/p&gt;

&lt;p&gt;This is not data loss, the work is fully in the repository. The damage lies in traceability. Anyone looking for the origin of the convention change no longer finds it via the commit message, only via a content search with &lt;code&gt;git log -S&lt;/code&gt;. On top of that, the commit silently claims authorship of someone else’s work, complete with the agent’s &lt;code&gt;Co-Authored-By&lt;/code&gt; line. The obvious lesson is to stop staging wholesale and to name your own files individually. The second case shows why that lesson is incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: The Obvious Countermeasure Doesn’t Hold
&lt;/h2&gt;

&lt;p&gt;One day later, in the repository of this blog, two parallel Claude Code sessions were working in the same working tree, this time with no human involved. Session A had staged five renames of code files with &lt;code&gt;git mv&lt;/code&gt; but had not committed them yet. At that moment, session B created its own commit, and did so exactly according to the lesson from case 1, with &lt;code&gt;git add&lt;/code&gt; on a single, explicitly named file. The commit swept up the five foreign renames anyway. Its message is exclusively about the material collection session B had been working on.&lt;/p&gt;

&lt;p&gt;The reason is not carelessness but the way Git works. &lt;code&gt;git add &amp;lt;path&amp;gt;&lt;/code&gt; only controls what additionally goes into the index. &lt;code&gt;git commit&lt;/code&gt; without a path then commits the entire index, including everything someone else has already put there. In a working tree used by several actors at once, the index is thus shared state. Whoever took „name files individually“ as the lesson from case 1 is not protected against this case and feels safe anyway.&lt;/p&gt;

&lt;p&gt;Session B had even built in a check, a &lt;code&gt;git status&lt;/code&gt; in the same command. But it hung behind the commit via &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add material.md &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"…"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git status &lt;span class="nt"&gt;--short&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the output appeared only after the fact had been created. A check that runs after the action is no longer a check, it is a log entry. The reliable order separates the look at the index from the commit and names the paths explicitly on the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-status&lt;/span&gt;     &lt;span class="c"&gt;# separate step before the commit: What is in the index?&lt;/span&gt;
git commit &lt;span class="nt"&gt;--only&lt;/span&gt; material.md &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"…"&lt;/span&gt;
git show &lt;span class="nt"&gt;--stat&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; HEAD      &lt;span class="c"&gt;# afterwards: Does the commit contain exactly the expected files?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters here is not the &lt;code&gt;--only&lt;/code&gt; keyword but the path argument on the commit. As soon as paths appear on the command line, Git, according to its &lt;a href="https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---only" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, commits only their current content and leaves changes already staged for other paths untouched. &lt;code&gt;--only&lt;/code&gt; is the default mode for this and merely makes the intent explicit. The rule file of this blog spells it out anyway, because then the command shows its intent. The behaviour was reproduced for this article in a throwaway repository. With a foreign rename in the index, &lt;code&gt;git add b &amp;amp;&amp;amp; git commit&lt;/code&gt; produces a commit with two files, &lt;code&gt;git commit --only b&lt;/code&gt; produces a commit with a single file, and the foreign rename stays in the index unchanged.&lt;/p&gt;

&lt;p&gt;The path argument does have a limit, though, and it matters for this topic. It protects against foreign entries at other paths, not against foreign changes to the same file. Git commits the current content of the named paths from the working tree, not the state the session put there with &lt;code&gt;git add&lt;/code&gt;. If another session has edited the same file shortly before, its change lands in the commit as well, and no command warns about it. This too was reproduced: the index held one line of the session’s own, the working tree an additional foreign one, and &lt;code&gt;git commit --only&lt;/code&gt; committed both. &lt;code&gt;git show --stat&lt;/code&gt; then showed two insertions instead of one, a hint only noticed by someone who expects the number. So &lt;code&gt;git commit --only&lt;/code&gt; solves the problem of the shared index, not the problem of the shared file.&lt;/p&gt;

&lt;p&gt;In this case too, the damage was traceability, not loss. Not a single line of the renames themselves changed, but the commit message does not describe them. What went well in this case is at the same time the contrast to the next one. Session B noticed the mistake itself, did not repair it on its own authority and informed the other session. A repair would have damaged the history further, because session A’s follow-up commit was already sitting on top of the affected commit. A &lt;code&gt;git reset --soft&lt;/code&gt; to the state before would have taken that commit out of the branch. Its changes would have remained in the working tree, but the next commit would have absorbed them under session B’s message, and the original commit would no longer have been reachable via the branch, typically only via the reflog. This too was reproduced for this article in the throwaway repository. The decision to leave the history as it was rested with the maintainer. Session A did not simply take the report on trust but cross-checked it with &lt;code&gt;git show --stat&lt;/code&gt; and then left a pointer to the actual location in the revision log of the affected article. That pointer closes the gap the commit message left behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: Foreign Work Deleted
&lt;/h2&gt;

&lt;p&gt;Case 3 is the mirror image of case 1. There, a session swept up foreign work, here it deleted foreign work. The cause is the same, the damage is the more expensive of the two.&lt;/p&gt;

&lt;p&gt;In the DI² project, two sessions were working on different topics at the same time. Session A was fixing a bug, session B was drafting a UI concept for another feature in parallel. Session A had seen a working tree with no pending changes at the start. During a longer tool run, two foreign files appeared there, a modified concept file and a new mockup. Session A attributed both to its own run and justified this by claiming its tooling had worked beyond the assignment. Then it discarded both files. It reverted the modified file to its previous version and deleted the new one.&lt;/p&gt;

&lt;p&gt;In the summary to the maintainer, the mistake got worse. Session A not only reported the rollback but also claimed the discarded text had contained a fabricated user approval. That approval had in fact been given, just in the other session. A deletion thus turned into a false accusation against the session’s own tool chain as well.&lt;/p&gt;

&lt;p&gt;It was not session A that found the case. Session B restored its work and sent a message into the other session. It is rendered here in substance, not verbatim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Two files from my working state just disappeared: the modified
concept file and the new mockup. Both are mine, not products of
your tooling. I have restored them.
From now on, please run no restore or cleanup operations
(checkout, restore, delete) on paths you have not touched
yourself in this session. If you notice files you cannot
attribute: report them, do not remove them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message did not undo the deletion, session B had already done that itself. But it formulated the rule that later went into the rule file, and it gave the session that caused the damage a piece of information it could never have read from the working tree: that it was not alone.&lt;/p&gt;

&lt;p&gt;Why the misattribution is understandable was described above. Why it is dangerous nonetheless is shown by the difference between a commit and a change Git does not yet know about at all. A commit can usually be reconstructed or taken back with a &lt;code&gt;git revert&lt;/code&gt;. A discarded change that was neither committed nor put into the index with &lt;code&gt;git add&lt;/code&gt;, by contrast, cannot be recovered by Git. Here it only came back because the second session still had its state in memory. Two lessons follow. Destructive operations such as reverting, resetting and deleting act on everything under the given path, not just on your own work. That is why, in parallel operation, they are never mere cleanup. And your own attribution is an assumption, not an observation. „That was me“ can be checked, via timestamps, content and change history. Whoever does not check it should not claim it either, least of all in a summary to the human.&lt;/p&gt;

&lt;p&gt;Self-control did not work in this case. Without the second session, the work would have vanished silently, and the false claim would have stood as a finding. That makes it the most uncomfortable and therefore the most valuable of the five cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 4: Duplicate Numbers, Because Politeness Is Symmetric
&lt;/h2&gt;

&lt;p&gt;The fourth case has its point not in the conflict itself but in the cooperative reaction to it. Both sides behaved decently, and precisely because of that the conflict arose a second time.&lt;/p&gt;

&lt;p&gt;Two sessions in the DI² project each created a new bug independently of one another. Number assignment follows a documented rule: the highest number assigned in the index file plus one gives the next. Both sessions read the same highest number, and both assigned the same new one. Both noticed the collision, and both stepped aside to the next number. The second collision was identical to the first.&lt;/p&gt;

&lt;p&gt;Checking beforehand would not have helped, because both sides had calculated correctly. The critical window lies between reading and writing. A check run before would have given both sides a green light and lulled them into false security. The first reflex, „just check first“, treats the problem like a slip. In fact it is a race: whoever gets overtaken by the other side between their read and their write has lost despite a correct check. Such collisions on sequential numbers therefore cannot be ruled out in parallel operation, only detected and resolved.&lt;/p&gt;

&lt;p&gt;In the end, two things helped. The first was detection after writing, in both directions. The project has an automatic check for this, called a guard there, which compares the index file against the files on disk. It reports not only „number assigned twice“ but also „file without index row“ and „index row without file“, three different intermediate states of the same process. A check that only knew duplicates would have let the later stages through. The origin of this guard is remarkable. It had been built two days earlier for an entirely different symptom, counters in the header of an index file that had drifted away from the content below. What it then caught was a duplicate assignment by parallel sessions, which nobody had thought of when building it. The reason is transferable. The guard does not check the symptom but the condition behind it that must always hold: index and files on disk must match in both directions. A check that had only looked at whether the counter was right would have stayed silent at the collision.&lt;/p&gt;

&lt;p&gt;The second was an asymmetric resolution rule. The other session proposed sorting alphabetically by identifier, and the smaller identifier gets the smaller number. What matters is not that the rule is particularly clever but that both sides can compute it to the same result without negotiation. On the first collision, stepping aside is the decent thing to do. But if both step aside, the same conflict arises again, just like two people in a corridor each making way for the other. Two cooperative parties cannot get past each other without an asymmetric rule. Whoever runs parallel agents therefore needs not only detection but a tie-break rule that is fixed in advance, a right-of-way rule for the tie. Otherwise the agents negotiate, and negotiating costs rounds.&lt;/p&gt;

&lt;p&gt;Two side findings from the resolution belong here as well. The first concerns the countermeasure from case 2. &lt;code&gt;git commit --only&lt;/code&gt; keeps its promise, but it refuses to work on a new file as long as Git does not know the file yet. The error message then reads &lt;code&gt;pathspec did not match any file(s) known to git&lt;/code&gt;. So the file first has to be added with &lt;code&gt;git add&lt;/code&gt;, and exactly that step puts your own entry next to the foreign entries already sitting in the index. The safeguard held, because the commit took only the one file. But the procedure forces you through precisely the state you wanted to avoid. The second side finding is an ownership criterion for shared files. A file that cannot be split and carries entries from both sides belongs in the commit of the side whose substantive change it carries, not in the commit of the side that happens to finish first. One of the two sessions had initially proposed the wrong direction and was corrected by the other. Its commit would otherwise have contained half a process without the file move that belonged to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 5: The Machine Is Shared Too
&lt;/h2&gt;

&lt;p&gt;The first four cases are about collateral damage in the repository. The fifth hits the human’s machine. Parallel sessions share not only the repository but also the computer the human is working on at the same time. What is shared here is no longer Git state but computing power, and different tools apply, not &lt;code&gt;git&lt;/code&gt; but &lt;code&gt;taskset&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;trap&lt;/code&gt;. The machine’s load is the one resource whose consumption the agent causes and cannot perceive itself.&lt;/p&gt;

&lt;p&gt;The assignment in the DI² project was a test that failed only sporadically. The diagnosis found not a slow test but two time budgets that did not fit together. The test library gives each individual wait 1000 ms, the test runner gives the whole test body 5000 ms, and the densest tests chain seven waits in a row. Without load, the file runs through in just over a second. So the defect can only be observed under load at all. That makes the verification expensive for the machine, and it makes it legitimate at the same time.&lt;/p&gt;

&lt;p&gt;The agent generated the load itself, with a single line. It started 220 CPU endless loops in the background, then three to five full runs of the test suite followed, and the whole thing was repeated three times, over twelve minutes in total. On the maintainer’s 28-core workstation, that produced a load average of 234, roughly eight times the core count, and every core sat at one hundred percent. The agent did not notice. The maintainer noticed his machine no longer responding, opened the process monitor and asked in a third session, with a screenshot: „Are you still doing something? When you run, CPU usage goes up considerably.“&lt;/p&gt;

&lt;p&gt;The actual finding is not a wrong idea but a lost one. An earlier subagent on the same bug track had answered the same question with six endless loops, pinned to two cores via &lt;code&gt;taskset&lt;/code&gt;. That produces the same contention for the test suite, but the rest of the machine stays free. The later subagent went unpinned to 56 loops, then to 200 and finally to 220. The frugal method had already been tried on the same track and was not picked up again when scaling up. Knowledge one session has is not automatically available to the next.&lt;/p&gt;

&lt;p&gt;Cleanup did happen, but in the wrong place. Every command ended in &lt;code&gt;kill $(jobs -p)&lt;/code&gt;, and that worked: no processes were left behind, and the load average dropped back into the single digits. But the cleanup hung on the last line of the command instead of on a &lt;code&gt;trap&lt;/code&gt;. A timeout, a crash or an abort by the human would have left 220 endless loops running until the next reboot, and that precisely at the moment the human steps in because the machine has frozen. Transparency was not lacking either, it just came in the wrong tense. The final report named the method correctly and unprompted, with load average and core count. That disclosure was complete and ineffective at the same time, because it came only once the damage was over. An announcement beforehand would have cost two sentences.&lt;/p&gt;

&lt;p&gt;Why does the agent not notice? It could well measure the load, &lt;code&gt;uptime&lt;/code&gt; or &lt;code&gt;top&lt;/code&gt; would put the load average in its output. What it lacks is the feedback about what that number means for the human. It sees exit codes and no stuttering desktop, and no tool tells it which load the owner of the machine is willing to accept right now. The costs fall entirely on a human it cannot observe. Unlike with tokens or run time, there is no feedback loop here. Resource-intensive verification remains the right thing to do, because load and repetition are often the only way to demonstrate a sporadic problem at all. Three conditions make it tolerable. The load runs pinned instead of on the whole machine, provided the test does not require system-wide contention. The cleanup hangs on a &lt;code&gt;trap&lt;/code&gt; instead of on the last line. And the agent announces beforehand what it is about to do instead of reporting afterwards, because whether a machine may be unusable for twelve minutes is the owner’s decision. The following minimal pattern shows the first two conditions in code. It is not a runbook, and not a cleanup guarantee either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'kill $(jobs -p) 2&amp;gt;/dev/null'&lt;/span&gt; EXIT INT TERM
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 6&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 0,1 &lt;span class="nb"&gt;timeout &lt;/span&gt;900 sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'while :; do :; done'&lt;/span&gt; &amp;amp; &lt;span class="k"&gt;done
&lt;/span&gt;taskset &lt;span class="nt"&gt;-c&lt;/span&gt; 0,1 &amp;lt;&lt;span class="nb"&gt;test &lt;/span&gt;run&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 2 creates the contention on exactly the two cores on which the test also runs in line 3, and gives each loop its own &lt;code&gt;timeout&lt;/code&gt; after which it terminates itself. The rest of the machine stays with the human. Line 1 cleans up the started loops when the shell exits. This &lt;code&gt;trap&lt;/code&gt; has two limits, and both were reproduced for this article. If an abort signal arrives while the test run is still going, Bash executes the &lt;code&gt;trap&lt;/code&gt; only once the test run returns. And against a &lt;code&gt;kill -9&lt;/code&gt; of the shell itself, it does not help at all. That is exactly what the &lt;code&gt;timeout&lt;/code&gt; in line 2 is for. The third condition belongs in the assignment, and there it costs two sentences. The following assignment is a pattern, not a quotation from the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reproduce the sporadic failure under load. Conditions:
load only on two pinned cores (taskset), cleanup via trap,
and tell me beforehand how long the machine will be loaded.
If you need more than two cores, ask first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One side finding concerns attribution. Three sessions were running in parallel, and the maintainer could not tell which of them was causing the load. In the end the evidence came from the session transcripts, where the command stood with a timestamp, and from the decay of the load average, which matched the time of the last logged run. Whoever runs several agents in parallel needs a method to attribute load to a session, and needs it before it becomes necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Holds and What Doesn’t
&lt;/h2&gt;

&lt;p&gt;Countermeasures emerged from the five cases, and they emerged after the fact. After case 2 they went into the Git rule file in the repository of this blog, as a section of its own called „Parallel sessions“, and since case 4 they also carry the side finding about new files. How such a rule file is built and why the countermeasure lives there and not in the maintainer’s head is described in &lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt;. The balance after a little over a week of parallel Claude Code sessions looks like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What holds:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt;:&lt;/strong&gt; The path argument on the commit limits it to the given paths, which can also be directories or patterns, and leaves staged changes outside them alone, &lt;code&gt;--only&lt;/code&gt; makes that explicit. Against foreign index entries this is the simplest and most robust variant, because it does not depend on the session’s attention. Against foreign changes to the same file it does not protect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The look at the index as a separate step before the commit:&lt;/strong&gt; A &lt;code&gt;git diff --cached --name-status&lt;/code&gt; or a &lt;code&gt;git status --short&lt;/code&gt; runs as its own command before the commit and not behind it via &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;. Whatever sits there and does not belong to your own change gets named or excluded. This check is a plausibility check and not a lock, because between the look and the commit another session can change the index again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git show --stat --oneline HEAD&lt;/code&gt; after the commit:&lt;/strong&gt; This look checks whether the commit contains the expected files and only those. It finds the mistake the two steps before let through. Whether the content of the files is right as well is only shown by &lt;code&gt;git show HEAD -- &amp;lt;path&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report instead of repair:&lt;/strong&gt; If foreign work sits in your own commit on a branch someone else is still working on, the session informs the affected side and leaves the decision about the history to the maintainer. If the history stays as it is, the session leaves a pointer to the location where someone would look for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One worktree per session, where the workflow allows it:&lt;/strong&gt; With &lt;code&gt;git worktree add&lt;/code&gt;, every session gets its own working tree with its own index, and cases 1 to 3 disappear in the form described here. Shared remain the repository objects and the branches, among other things &lt;code&gt;HEAD&lt;/code&gt; and the index are specific to each worktree. Everything outside the working tree stays shared as well, the machine and the shared counters included, more on that in the FAQ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the tie-break rule in advance:&lt;/strong&gt; For every shared counter there is an asymmetric rule that both sides can compute to the same result without negotiation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the condition, not the symptom:&lt;/strong&gt; The guard compares index and files on disk in both directions. A check like that also catches errors nobody thought of when it was built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-check instead of believing:&lt;/strong&gt; Both sessions in case 4 re-measured each other’s status reports instead of taking them over. That found an omission in the report that the reporting side could not have noticed itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What doesn’t hold:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;„Just check first“:&lt;/strong&gt; In a race between two writers, the window lies between reading and writing. The check beforehand gives a green light and lulls into security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lesson from case 1 as protection against case 2:&lt;/strong&gt; &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; instead of &lt;code&gt;git add -A&lt;/code&gt; closes one gap and leaves the neighbouring one open. A lesson that closes one gap creates a false sense of security for the next.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair via &lt;code&gt;git reset &amp;lt;commit&amp;gt;&lt;/code&gt;:&lt;/strong&gt; Resetting the branch acts on everything that has been added since that commit. If a commit by another session is already sitting on top, it disappears from the branch. The three modes differ only in what gets reset besides the branch: &lt;code&gt;--soft&lt;/code&gt; leaves index and working tree alone, the default mode resets the index, and &lt;code&gt;--hard&lt;/code&gt; additionally discards unsaved changes to tracked files in the working tree, including another session’s, and overwrites untracked files that stand in the way of a tracked file of the target state. The damage of the repair is then greater than that of the mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup on the last line:&lt;/strong&gt; It does not survive precisely the abort that needs it most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparency after the fact:&lt;/strong&gt; A complete report after the damage is honest, but it no longer changes anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One side finding belongs here because it concerns the figures with which agents demonstrate their diligence. A shared working tree distorts not only files but measurements. A full test run, started at the very moment the other session was renaming a file, reported three failed test files with an error that had nothing to do with them in substance. On the next run, everything was green. Whoever writes such numbers into a report documents a state that never existed. In parallel operation, a single red run is therefore not a finding but a hypothesis, and it only counts once it can be repeated on a quiet tree. How measurements can be separated from assumptions when the agent takes over the diagnosis will be the subject of a separate article on this blog about debugging with a coding agent.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;To a session, its own and foreign changes in the working tree look the same, and Git never contradicts the assumption „everything new comes from me“.&lt;/li&gt;
&lt;li&gt;In a shared working tree, the Git index is shared state. &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; does not protect, because &lt;code&gt;git commit&lt;/code&gt; without a path commits the entire index. Against foreign entries at other paths, &lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt; is the most robust of the variants considered here, supplemented by a look at the index before the commit and a &lt;code&gt;git show --stat&lt;/code&gt; afterwards. Against simultaneous changes to the same file, a separate worktree helps most reliably.&lt;/li&gt;
&lt;li&gt;Destructive operations are never mere cleanup in parallel operation. Your own attribution is an assumption, and whoever cannot check it reports instead of deleting.&lt;/li&gt;
&lt;li&gt;Shared counters can collide at any time in parallel operation. What is needed is a check after writing that verifies the intended state, and an asymmetric tie-break rule fixed in advance.&lt;/li&gt;
&lt;li&gt;Parallel sessions also share the human’s machine, and its load is a resource with no feedback loop to the agent. Resource-intensive verification therefore runs pinned, time-limited, with cleanup via &lt;code&gt;trap&lt;/code&gt; and with an announcement beforehand.&lt;/li&gt;
&lt;li&gt;The rule came after the damage. Three of the five cases were the agent’s mistakes. The agent reported two of them itself, the third was found by the neighbouring session whose work had been lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is it enough to stage files individually instead of using &lt;code&gt;git add -A&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt; only controls what additionally goes into the index. If something from another session or from the human is already sitting there, &lt;code&gt;git commit&lt;/code&gt; without a path takes it along. Protection only comes from &lt;code&gt;git commit --only &amp;lt;paths&amp;gt;&lt;/code&gt;, which ignores the rest of the index, or from a look at the index as a separate step before the commit. A &lt;code&gt;git status&lt;/code&gt; hanging behind the commit via &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; shows the mistake only once it has already happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can &lt;code&gt;git commit --only&lt;/code&gt; prevent foreign changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only one kind of them. The path argument keeps staged changes at other paths out of the commit, that is the shared-index problem from case 2. It does not keep out what another session has changed in the same file, because Git commits the current content of the named paths from the working tree. Whoever wants to be sure what is in the commit checks the content afterwards with &lt;code&gt;git show HEAD -- &amp;lt;path&amp;gt;&lt;/code&gt; or gives every session its own worktree from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when two sessions edit the same file?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a shared working tree the file exists only once, and Git reports nothing. If both change individual spots, both changes end up in the file afterwards, and whoever commits first takes the other’s along. If one session instead rewrites the whole file from an older read, the other session’s change has silently vanished. If it was staged, its state still sits as a blob in the index. After another &lt;code&gt;git add&lt;/code&gt; it is only a dangling object that &lt;code&gt;git fsck --lost-found&lt;/code&gt; may still find, but that is no guarantee. If it was never staged, it is gone. The situation can only be recognised by clues: an &lt;code&gt;MM&lt;/code&gt; in &lt;code&gt;git status&lt;/code&gt; on a file you did not stage yourself, or an editing tool that refuses to write because the file has changed since it was last read. Claude Code’s Edit tool behaved exactly like that in the sessions behind this article, as of September 2026, a &lt;code&gt;sed&lt;/code&gt; or a Python script does not. Git only detects the situation reliably once both sessions work in their own worktrees on their own branches. Then there are two versions, and merging them produces a merge conflict as soon as both have changed the same lines. Different lines Git merges automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do you do when a commit has swept up foreign work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On a branch someone else is still working on, the first reflex, repairing the commit on your own authority, is the wrong one. Whoever works alone on an unpublished branch may reset it. A &lt;code&gt;git reset&lt;/code&gt; to an earlier commit and other interventions in the history act on everything that has been added since that commit. If a commit by another session is already sitting on top, the correction takes it out of the branch, and with &lt;code&gt;--hard&lt;/code&gt; it also discards that session’s unsaved changes to tracked files. Better to inform the affected side and let the maintainer decide whether the history gets touched. If it stays as it is, a pointer to the actual location belongs where someone would look for it, for instance in the revision log of the affected article or in the spec of the affected task. The content is not lost. Only its origin can no longer be found via the commit message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does a session recognise that it is not alone in the repository?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With Git alone, not reliably at all, and that is precisely the core of the problem. There are clues: an index that is not empty at the start, files that newly appear or change during a tool run although your own tooling did not touch them, or a commit in the log that did not come from your own session. None of them is proof. That is why a session treats its attribution as an assumption and checks it via timestamps and content before discarding anything. The most reliable information comes from the human. Whoever starts two sessions tells both that they are not alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are Git worktrees the solution for parallel Claude Code sessions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For cases 1 to 3 they largely solve the problem, because they remove its common cause, the shared working tree. A &lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;worktree&lt;/a&gt; gives every session its own working tree with its own index, only the repository, its objects and its branches stay shared. In practice a worktree also means a branch of its own per session or a detached &lt;code&gt;HEAD&lt;/code&gt;, because a branch cannot be checked out in two worktrees at the same time. So the merge back is part of the workflow, and that is where Git then reports the conflicts on the same file as well. The difference to the shared working tree lies exactly there: own files on disk, own index, own &lt;code&gt;HEAD&lt;/code&gt;, but the same commits and branches underneath. Foreign files in the working tree and foreign entries in the index thereby disappear. The remaining cases persist, though. The machine is still shared, and shared counters then collide when the branches are merged instead of at write time. Besides, in practice the human often works in the main working tree, and a session running there has all five problems again. Worktrees separate the working trees, not the actors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you commission load tests without freezing your own machine?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Best with three conditions in the assignment. The load runs pinned to a few cores, via &lt;code&gt;taskset&lt;/code&gt; for instance, provided the test does not require system-wide contention, and the test runs on the same cores so that the contention arises where it is needed. The cleanup hangs on a &lt;code&gt;trap&lt;/code&gt; so that it takes effect when the script ends, and every load loop gets its own &lt;code&gt;timeout&lt;/code&gt; so that it terminates even if the shell gets killed the hard way. And the agent announces beforehand how long the machine will be loaded, so that the human can decide whether to allow that right now. An agent cannot derive these conditions on its own, because it can measure the load but cannot see its cost to the human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parent article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User’s Perspective — Experience: The Work Doesn’t Disappear, It Shifts&lt;/a&gt; — the overall experience of which parallel operation is one section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hub:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents That Enforce Conventions&lt;/a&gt; — the enforcement system the rule file from this article belongs to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sibling article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click — Debugging Postgres, Docker and a Swapping Server with a Coding Agent&lt;/a&gt; — why a finding only holds once a measurement stands behind it, and why a red test run in a shared working tree is only a hypothesis at first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule files:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt; — how a rule file is built into which a countermeasure like &lt;code&gt;git commit --only&lt;/code&gt; goes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next door:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ssis-vs-transact-sql-source-code-management/" rel="noopener noreferrer"&gt;SSIS vs. SQL: Source Code Management — Why SP Diffs Are Readable and &lt;code&gt;.dtsx&lt;/code&gt; Diffs Are Not&lt;/a&gt; — source code management from the era before agents, with the same core concern: a diff has to stay readable.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>git</category>
      <category>productivity</category>
    </item>
    <item>
      <title>An EU Region Is Not an EU Provider — Vercel, Supabase and the GDPR Third-Country Question</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:25:48 +0000</pubDate>
      <link>https://dev.to/marcus1968/an-eu-region-is-not-an-eu-provider-vercel-supabase-and-the-gdpr-third-country-question-3he4</link>
      <guid>https://dev.to/marcus1968/an-eu-region-is-not-an-eu-provider-vercel-supabase-and-the-gdpr-third-country-question-3he4</guid>
      <description>&lt;p&gt;"We host in Frankfurt, so we're GDPR-compliant." The sentence comes up in almost every conversation about Vercel and Supabase, and it is half the answer to a question that has two halves. The region answers where the data lives. It does not answer whose law applies to the companies that process that data.&lt;/p&gt;

&lt;p&gt;This article takes the second half apart. It explains what an EU region delivers legally and what it does not, why the European Commission has twice declared transfers to the United States permissible by decision and the Court of Justice of the European Union has struck down both decisions while the third is under review right now, and what the CLOUD Act, a US law from 2018, has to do with a data center in Frankfurt. At the end there is a checklist for the third-country question that lets you examine and document your own use of &lt;strong&gt;Vercel or Supabase&lt;/strong&gt;, plus a decision aid for the alternative. The occasion is a concrete one: In the DI² project this question was asked before the first deploy, and the answer shaped the &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;infrastructure&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not compliant by default:&lt;/strong&gt; Vercel and Supabase offer EU regions and a data processing agreement, the contract that lays down what the provider may do with its customers' data. The conditions for a compliant deployment still have to be established and documented by the user. The default setting is not the compliant one, and at Vercel functions run in the United States unless you choose otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The chain counts, not the seat:&lt;/strong&gt; Vercel is a US company. Supabase's contracting party sits in Singapore, the US company Supabase, Inc. appears as a subcontractor for support in its own list, and hosting runs on Amazon Web Services (AWS). Whose law applies is not stated by the provider's legal notice but by its list of subcontractors that touch the data. The GDPR calls them sub-processors, and the providers publish them as a "Subprocessor List".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three attempts at a transfer mechanism:&lt;/strong&gt; The European Commission has three times found by decision that the United States offers an adequate level of protection, each time on the basis of an arrangement with the US government. The first two, Safe Harbor of 2000 and Privacy Shield of 2016, were struck down by the Court of Justice in 2015 and 2020, both times for the same reason. Against the third, the Data Privacy Framework of 2023, the General Court of the European Union dismissed the first action in September 2025, and the appeal is pending before the Court of Justice. As of 9 September 2026 it stands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CLOUD Act does not ask where the data is stored:&lt;/strong&gt; A provider under US jurisdiction must in principle hand over data in its possession or under its control, no matter which country it is stored in. There is no agreement with the EU that would open the law's special objection procedure. Microsoft France confirmed as much before the French Senate in June 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A contract does not cure that:&lt;/strong&gt; The data processing agreement governs the relationship between user and provider. It does not override a US law. Both risks land on the user in the end, because the user is responsible for the processing, even though the provider has obligations of its own. The checklist at the end makes them visible instead of removing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alternative is a provider question:&lt;/strong&gt; Anyone who wants to rule the two risks out rather than carry them has only one route, no provider under US jurisdiction in the chain. That means an EU provider or a server of your own in the EU, and both come at a price that the sibling article on infrastructure puts into numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; The article is aimed at developers and small teams in the EU who run a web application with a login on Vercel, Supabase or comparable services, or plan to. No legal background is required, and terms such as adequacy decision, standard contractual clauses and data processing agreement are explained where they first appear. All provider statements are taken from the providers' own documentation, quotations verbatim, retrieved on 9 September 2026. The article is not legal advice, and it does not replace an assessment of your own case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Answer: Not by Default
&lt;/h2&gt;

&lt;p&gt;Can you use Vercel and Supabase in a GDPR-compliant way? Yes, but not by creating an account and picking Frankfurt as the region. Both providers say so themselves, just not on the landing page. Vercel writes in its compliance documentation that it supports its customers' GDPR compliance and lists five commitments to that end, from the security level to the standard contractual clauses. Supabase writes that it supports GDPR-compliant deployments. In both cases the subject of the sentence is the customer. The provider supplies the tools, and the user has to apply them, document them and answer for them.&lt;/p&gt;

&lt;p&gt;What that involves fits in one paragraph. The user signs the data processing agreement, which the GDPR calls a contract under Article 28 and practice usually calls a DPA. The user chooses the region instead of accepting the default. The user documents the basis on which data may leave the territory of the EU, because with both providers it can do so even with an EU region, at Vercel by the provider's own reservation, at Supabase through support and subcontractors. The user extends the privacy policy to name the recipient, the third country and the basis. And the user consciously decides whether to carry two risks that no contract takes away. Those two risks are the core of this article. The rest is the groundwork needed to understand them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an EU Region Delivers — and Who Is in the Chain
&lt;/h2&gt;

&lt;p&gt;An EU region is a promise about storage location. At Supabase that means: Whoever creates a project in an AWS region gets the database, the auth service and storage hosted in that region, as the &lt;a href="https://supabase.com/security" rel="noopener noreferrer"&gt;security page&lt;/a&gt; states. At Vercel the region is a choice for the part of the application that computes on the server, the so-called serverless functions, and the default is not Europe. The &lt;a href="https://vercel.com/docs/security/compliance" rel="noopener noreferrer"&gt;compliance documentation&lt;/a&gt; says it in one sentence: "The default location for Vercel functions is the U.S." In front of those functions sits Vercel's worldwide delivery network, a CDN (content delivery network), which accepts visitors' requests at the nearest of twenty locations and serves the pages from there. The chosen region applies to the functions, not to that network.&lt;/p&gt;

&lt;p&gt;The question of whether a service can be used in a GDPR-compliant way has two parts. The first is: Where does the data live? The region answers it, and for Supabase and Vercel alike that part is settled. The second is: Which companies process the data, and whose law are those companies subject to? No region answers that. The answer lies in the list of subcontractors the provider brings in to run the service and who touch the data in the process. Both providers publish that list, and at both it reads differently from what the marketing suggests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel:&lt;/strong&gt; Vercel, Inc. is a US company. It is certified under the EU-US Data Privacy Framework, and it additionally relies on standard contractual clauses for transfers. Both statements are in the compliance documentation. Directly below them stands the reservation that puts the region into perspective: "Vercel may transfer data to and in the United States and anywhere else in the world where Vercel or its service providers maintain data processing operations." Vercel may therefore move data to the United States and to any other location where it or its service providers process data. Backups, according to the same page, are replicated globally and are not accessible to customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase:&lt;/strong&gt; Here the research for this article produced a different picture from the expected one. According to the &lt;a href="https://supabase.com/terms" rel="noopener noreferrer"&gt;terms of service&lt;/a&gt; and the &lt;a href="https://supabase.com/legal/dpa" rel="noopener noreferrer"&gt;data processing agreement&lt;/a&gt;, the contracting party is not a US company but Supabase Pte. Ltd., based in Singapore. The terms place the contract under California law, and the data processing agreement relies on standard contractual clauses for transfers, a contract text prescribed by the European Commission in which the recipient commits to European data protection rules, and does not mention the Data Privacy Framework. Supabase's own &lt;a href="https://supabase.com/legal/customer-resources/subprocessor-list" rel="noopener noreferrer"&gt;subprocessor list&lt;/a&gt;, in the version dated 1 June 2026 and retrieved on 9 September 2026, then names the companies that actually touch the data: Amazon Web Services for hosting, Cloudflare, Google and Fly.io for further hosting services, Vercel for hosting, and at the top of the list Supabase, Inc. for support. Supabase's &lt;a href="https://supabase.com/privacy" rel="noopener noreferrer"&gt;privacy policy&lt;/a&gt; names the United States and Singapore as transfer destinations, each on the basis of standard contractual clauses, and describes its own services as "primarily hosted in and provided from the United States". The contracting party sits in Singapore, the data sits with a US host, and the US company has access as a subcontractor.&lt;/p&gt;

&lt;p&gt;For the GDPR the seat in Singapore changes nothing about the classification. An adequacy decision is the European Commission's finding that a country outside the EU protects personal data as well as the EU itself does. Where one exists, data may go there as if it stayed in the EU. For Singapore no such decision existed at the time of writing, and for the United States it applies only to companies with an active certification under the Data Privacy Framework, and only to the data types that certification covers. Every transfer to Singapore is therefore a third-country transfer that needs a mechanism of its own, in Supabase's case the standard contractual clauses. What the case shows is something else: The provider's legal notice is the wrong place to answer the provider question. The right place is the list of subcontractors. As soon as a service provider under US jurisdiction appears there that processes personal data or can access it, and with services on AWS, Google Cloud or Azure one regularly does, two things have to be examined separately for that data. First, whether a third-country transfer takes place and on what basis, which is the next section. Second, what state access risk that link brings with it, which is the section after that. How large the second risk actually is depends on which data the link really has in its possession or under its control. The EU region has settled the storage location. The chain has settled which legal systems have a say.&lt;/p&gt;

&lt;p&gt;Three things come on top with every region, because they do not hang on the storage location. Support access, which the provider needs to operate the service, happens from wherever its staff sit. Metadata, meaning account data, invoices and access logs, runs in the provider's systems and not in the chosen region. And backups sit wherever the provider replicates them, at Vercel explicitly worldwide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transfer Mechanism: Three Attempts
&lt;/h2&gt;

&lt;p&gt;The first of the two risks concerns the basis on which data may leave the EU at all. The GDPR allows transfers to a country outside the EU only under the conditions of its Chapter V. The yardstick behind it is that the data keeps a level of protection there that is essentially equivalent to the European one, and there are three routes to that. The first is an adequacy decision by the European Commission under Article 45, with which the Commission finds across the board that a country offers that level. The second is appropriate safeguards under Article 46, in practice almost always the Commission's standard contractual clauses, with which the recipient contractually commits to European rules, more rarely binding corporate rules within a group of companies. The third is a set of narrowly drawn exceptions under Article 49, such as explicit consent for a single transfer, which are no use for a running service.&lt;/p&gt;

&lt;p&gt;For the United States the Commission has taken the first route three times, and twice the Court of Justice has struck the decision down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe Harbor, the 2000 decision:&lt;/strong&gt; US companies could self-certify that they observed certain principles. After the revelations about US surveillance programs in 2013, Max Schrems, then a law student in Vienna, lodged a complaint against Facebook with the Irish data protection authority because his data was going to the United States. The complaint reached the Court of Justice via the Irish High Court, and the Court declared the decision invalid on &lt;a href="https://curia.europa.eu/jcms/upload/docs/application/pdf/2015-10/cp150117en.pdf" rel="noopener noreferrer"&gt;6 October 2015&lt;/a&gt;. The reasoning was that US authorities had general access to the content of communications and EU citizens had no legal remedy against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy Shield, the 2016 decision:&lt;/strong&gt; The successor came with an ombudsperson in the US State Department and further commitments. Again it was a complaint by Schrems, who by then was running data protection cases full time with the organization &lt;a href="https://noyb.eu/en" rel="noopener noreferrer"&gt;noyb&lt;/a&gt;, and both judgments therefore carry his name, Schrems I and Schrems II. The Court of Justice struck the decision down on &lt;a href="https://curia.europa.eu/jcms/upload/docs/application/pdf/2020-07/cp200091en.pdf" rel="noopener noreferrer"&gt;16 July 2020&lt;/a&gt;, on the same grounds in updated form. The surveillance powers under Section 702 of the Foreign Intelligence Surveillance Act (FISA), the US law on foreign intelligence, and Executive Order 12333, a presidential order issued without Congress, were not limited to what is necessary, EU citizens could not challenge them before any US court, and the ombudsperson was neither independent nor able to bind the intelligence services. In the same judgment the Court made clear that the standard contractual clauses remain valid, but that whoever exports data must assess for the specific transfer whether the law and practice of the receiving country guarantee the level of protection, and must provide additional measures where needed. That assessment is usually called a transfer impact assessment, and it applies then as now. Between 2020 and 2023 it affected almost every transfer to the United States, and since the third attempt only those that do not rely on it, as in the Supabase case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Privacy Framework, the 2023 decision:&lt;/strong&gt; The third attempt rests on an executive order of the US President from October 2022 that lays down proportionality principles for the intelligence services and sets up a review court, the Data Protection Review Court, before which EU citizens can bring complaints against surveillance measures. On that basis the Commission adopted the &lt;a href="https://eur-lex.europa.eu/eli/dec_impl/2023/1795/oj" rel="noopener noreferrer"&gt;adequacy decision&lt;/a&gt; on 10 July 2023. US companies self-certify again, and the US Department of Commerce keeps the list. Vercel says it is on it. Supabase does not mention the framework in its contract, its security page or its privacy policy and relies on standard contractual clauses.&lt;/p&gt;

&lt;p&gt;The decision is under attack. The French member of parliament Philippe Latombe challenged it directly before the General Court of the European Union, and the General Court dismissed the action on &lt;a href="https://curia.europa.eu/jcms/upload/docs/application/pdf/2025-09/cp250106en.pdf" rel="noopener noreferrer"&gt;3 September 2025&lt;/a&gt;. It found that the United States ensured an adequate level of protection at the time of the decision, and it expressly tied that finding to that point in time. On 31 October 2025 Latombe lodged an &lt;a href="https://www.wilmerhale.com/en/insights/blogs/wilmerhale-privacy-and-cybersecurity-law/20251201-european-court-of-justice-to-review-challenge-to-eu-us-data-privacy-framework" rel="noopener noreferrer"&gt;appeal&lt;/a&gt; with the Court of Justice, case number C-703/25 P, on four grounds. On 4 June 2026 the Court admitted Microsoft as an intervener in support of the Commission. As of 9 September 2026 no hearing date is known, and the decision stands.&lt;/p&gt;

&lt;p&gt;Two things are worth keeping in view. First, the framework rests on an executive order, not on a statute, and an executive order can be changed by the next president. Second, the US oversight body that is supposed to monitor compliance with the commitments, the Privacy and Civil Liberties Oversight Board, has been without a quorum since three of its members were dismissed in January 2025. Its own &lt;a href="https://www.pclob.gov/Board" rel="noopener noreferrer"&gt;members page&lt;/a&gt; lists a single serving member on 9 September 2026. The dismissals are themselves the subject of litigation: A federal court in Washington declared them unlawful in May 2025, and the appeals court stayed that ruling in &lt;a href="https://media.cadc.uscourts.gov/orders/docs/2025/07/25-5197LDSN2.pdf" rel="noopener noreferrer"&gt;July 2025&lt;/a&gt; for the duration of the proceedings. Whether and how that enters the judicial review of the framework is open. For the user both mean that the basis of their transfer depends on political decisions in another country.&lt;/p&gt;

&lt;p&gt;If this decision is struck down too, what happened in 2020 happens again. Whoever relied on the framework alone is left without a basis and has to switch to standard contractual clauses, including their own assessment of US law. Whoever documented both from the start, the provider's list entry and the standard contractual clauses in the data processing agreement together with the assessment of US law, has less to do on that day. That is exactly why the checklist below carries them as two separate items.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLOUD Act: Possession or Control, Not Storage Location
&lt;/h2&gt;

&lt;p&gt;The second risk, the CLOUD Act, is independent of the first, and it survives every adequacy decision. Its origin is a dispute over emails. In 2013 US investigators demanded from Microsoft, by warrant, a mailbox that sat in a data center in Dublin. Microsoft refused, arguing that a US warrant did not reach as far as Ireland. An appeals court sided with Microsoft in 2016, the Supreme Court took the case, and before it could decide, Congress passed a law that answered the question. The Clarifying Lawful Overseas Use of Data Act, CLOUD Act for short, was signed on 23 March 2018.&lt;/p&gt;

&lt;p&gt;The core rule is in &lt;a href="https://www.law.cornell.edu/uscode/text/18/2713" rel="noopener noreferrer"&gt;18 U.S.C. § 2713&lt;/a&gt;. A provider must in principle hand over content and subscriber records that are "within such provider's possession, custody, or control", and that "regardless of whether such communication, record, or other information is located within or outside of the United States". In one sentence: What counts is whether the provider possesses or controls the data, not where it is stored. A data center in Frankfurt is no argument against this rule.&lt;/p&gt;

&lt;p&gt;The law provides a special objection procedure for the case that disclosure would violate the law of another state, but only under two conditions at once. The customer concerned must not be a US person, meaning neither a US citizen nor resident in the United States, and the United States must have concluded an agreement under this law with the country where the data is stored. Such agreements exist so far with the United Kingdom, in force since October 2022, and with &lt;a href="https://www.homeaffairs.gov.au/about-us/our-portfolios/national-security/lawful-access-telecommunications/australia-united-states-cloud-act-agreement" rel="noopener noreferrer"&gt;Australia&lt;/a&gt;, in force since January 2024. With the EU or any of its member states there is none. For data in Frankfurt the second condition is missing, and with it this route. What remains is the general possibility of challenging a demand as unlawful, as Microsoft did in the Ireland case. That presupposes a provider that does so.&lt;/p&gt;

&lt;p&gt;On the European side stands Article 48 of the GDPR. It recognizes decisions by authorities of a third country only if they rest on an international agreement, such as a mutual legal assistance treaty in which states promise each other support in investigations. A US provider with EU customers thus stands between two legal systems that demand opposite things. The data processing agreement with the customer does not resolve that conflict, because a contract between two companies does not override a US law. What the contract can deliver is a commitment to inform the customer and to contest demands for disclosure as far as the law allows.&lt;/p&gt;

&lt;p&gt;What that sounds like in practice was shown by Microsoft France before a committee of inquiry of the French Senate in June 2025. Anton Carniaux, in charge of legal and public affairs there, was asked whether he could guarantee that data of French citizens would never be handed to US authorities without France's consent. His answer is in the &lt;a href="https://www.senat.fr/compte-rendu-commissions/20250609/ce_commande_publique.html" rel="noopener noreferrer"&gt;minutes&lt;/a&gt; of the session of 10 June 2025: "Non, je ne peux pas le garantir, mais, encore une fois, cela ne s'est encore jamais produit." No, he could not guarantee it, but it had never happened so far either. And on the question of the obligation: "Lorsque nous sommes obligés de les donner, nous les donnons." When Microsoft is obliged to hand data over, it hands it over. The two sentences belong together. The first names the risk and how often it has materialized so far, the second the rule behind it: disclosure after the provider's own check of whether the demand is lawful, not before. That is not a statement about other providers, but it is one about the limit of what any provider can promise.&lt;/p&gt;

&lt;p&gt;That leads to proportionality. The probability that US authorities take an interest in the user table of an ETL tool with a handful of users is low. The risk is structural, not practical. It consists in the user writing a promise into their privacy policy that they cannot keep, because their provider cannot keep it. Whoever understands that and accepts it consciously acts differently from someone who does not know it. That difference is what the last item of the checklist is about.&lt;/p&gt;

&lt;p&gt;A question that comes up regularly here concerns the European subsidiaries of US groups. If the contracting party is called AWS Europe in Luxembourg or Microsoft Ireland, does that help? The seat of the contracting party alone does not answer the question. The rule turns on possession, custody or control, and whether a company within a group controls the data in that sense is a matter of actual access, not of the org chart. The Microsoft Ireland case was the trigger for the law because it concerned data held by a European subsidiary, but no blanket rule follows from it that every EU subsidiary automatically holds data under the control of its US parent. What does follow is the duty to check which company actually gets to the data. That is exactly what makes the Supabase case above so instructive. There it is the other way around: The contracting party sits outside the United States, and the US company is in the chain regardless, with support access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checklist
&lt;/h2&gt;

&lt;p&gt;What a user has to examine and record for the third-country question at Vercel, Supabase or a comparable service can be worked through in seven items. The list does not create compliance and does not replace an assessment of the processing as a whole. It does not remove the two risks, it makes them visible and documents the decision to carry them. For an operator who gets asked, that is the difference between an answer and a shrug.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the subprocessor list:&lt;/strong&gt; Mark every company subject to US jurisdiction (host, parent company, support), and record for each link which data it processes or can view. Only for that data does the CLOUD Act question arise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign the data processing agreement:&lt;/strong&gt; Conclude the contract under Art. 28 GDPR (DPA) and file it. Check whether it contains standard contractual clauses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the region:&lt;/strong&gt; Do not accept the default. At Vercel set the function region, at Supabase the project region. Record what the region covers and what it does not (CDN, backups, support, metadata).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the transfer mechanism, both separately:&lt;/strong&gt; The provider's active entry under the Data Privacy Framework (look it up yourself on dataprivacyframework.gov, including its scope) and the standard contractual clauses from the contract together with your own assessment of US law (transfer impact assessment). If the framework is struck down, the second basis carries on, but only with that assessment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend the privacy policy:&lt;/strong&gt; Recipient with company name and seat, third country, transfer mechanism, and a note on possible government access under the law of the third country.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscribe to changes of the subprocessor list:&lt;/strong&gt; Both providers announce changes, and the list changes several times a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide the residual risk consciously and record it with a date:&lt;/strong&gt; Which data sits with the provider, how serious would access be, and why is the operator prepared to carry that.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The provider information in this article is current as of 9 September 2026. The lists change, and what counts is always the current version at the provider. Item 1 comes first because it determines the others. Whoever reads the list knows for whom to document item 4 and what to write in item 5. Item 7 is the one most people leave out. It is the only one that demands a decision rather than a document, and it records the actual risk decision that stands behind the other six items.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alternative: EU Provider or Self-Hosting
&lt;/h2&gt;

&lt;p&gt;Anyone who wants to rule the two risks out rather than carry them has only one route, no provider under US jurisdiction in the chain. Anyone who can carry them has the checklist. In practice the one route means either an EU provider that runs its own data center, or a server of your own with such a provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What an EU provider solves:&lt;/strong&gt; With a provider based in the EU, with its own infrastructure and with no third-country subcontractors in the chain, examples for the first two conditions being Hetzner in Germany, IONOS in Germany and OVHcloud in France, without rating the products, the third-country transfer can drop away for the infrastructure layer, provided support access and the remaining data flows also stay in the EU. Then there is no adequacy decision to check, no standard contractual clauses for the host and no CLOUD Act for the data center. The data processing agreement remains mandatory, and so does the look at the subprocessor list, because an EU provider can use US service providers. Reading the list is item 1 here too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What self-hosting costs:&lt;/strong&gt; The price is not primarily money but operations. Whoever runs an application with a login on their own server takes on the reverse proxy, which passes requests from the internet on to the application, the certificates, the identity provider, which manages login and users, the database, the backups and the monthly updates across every layer. In the DI² project that was the decision, and the sibling article &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt; describes what it cost, from the RAM budget through secrets in two places to the monthly bill. On your own server in the EU the third-country question does not arise for the infrastructure. That is the real difference between the two routes, not the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision aid:&lt;/strong&gt; Three questions suffice for most cases. First, which data sits with the provider, only account data or also what users enter into the application? The closer to health, financial or employee data, the heavier item 7 of the checklist weighs. Second, who will ask, a customer with a data protection department of their own, a supervisory authority, nobody? Whoever has business customers in the EU will get the question. Third, who carries operations, and what happens when that person is out for two weeks? A managed service answers the third question better than a server of your own, and that is a legitimate reason for Vercel or Supabase. It should just be taken together with the answers to the first two questions and written down.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;An EU region answers where the data lives. It does not answer whose law applies to the companies that process it. That answer is in the subprocessor list.&lt;/li&gt;
&lt;li&gt;Vercel is a US company with a certification under the Data Privacy Framework, a US default for functions and an explicit reservation to transfer data to the United States and to any other processing location. Supabase's contracting party sits in Singapore, the US company Supabase, Inc. is a subcontractor, and hosting sits with AWS. In both chains there are companies under US jurisdiction.&lt;/li&gt;
&lt;li&gt;The transfer mechanism for the United States, a decision of the European Commission, has been struck down twice by the Court of Justice, and the third is under review right now. As of 9 September 2026 the Data Privacy Framework stands, and appeal C-703/25 P is pending. Whoever documents the list entry and the standard contractual clauses separately has less to do on the day of a judgment.&lt;/li&gt;
&lt;li&gt;The CLOUD Act in principle obliges providers under US jurisdiction to hand over data in their possession or under their control, regardless of storage location. The law's special objection procedure presupposes an agreement that does not exist with the EU. A data processing agreement changes none of that.&lt;/li&gt;
&lt;li&gt;The checklist makes both risks visible and documents the decision to carry them. It does not remove them. Whoever does not want to carry them needs an EU provider or a server of their own in the EU, and both cost operations rather than money.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Vercel or Supabase GDPR-compliant if I pick the Frankfurt region?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not by default, the region only settles the storage location. At Vercel the documented reservation remains, to transfer data to the United States and to any other processing location, and at Supabase the US companies remain in the subprocessor list, above all the host AWS and Supabase, Inc. for support. Both are third-country transfers that need a mechanism, a data processing agreement and a note in the privacy policy. The region is item 3 of the checklist, not the checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the Data Privacy Framework still valid in September 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The European Commission's adequacy decision of 10 July 2023 stands, and the General Court of the European Union dismissed the first action against it on 3 September 2025. Appeal C-703/25 P has been pending before the Court of Justice since 31 October 2025, Microsoft has been admitted as an intervener in support of the Commission since 4 June 2026, and as of 9 September 2026 no hearing date is known. To check the current state, search for the case number on curia.europa.eu.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the Court of Justice strikes down the Data Privacy Framework too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same as in July 2020 with Privacy Shield. Transfers that rest on the framework alone lose their basis from one day to the next. Whoever has additionally agreed standard contractual clauses in the data processing agreement relies on them from that day on and has to carry out the assessment of US law themselves, as every company did between 2020 and 2023. That is why the checklist documents both mechanisms separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the CLOUD Act apply to AWS, Azure or Google Cloud data in Germany?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Storage in Germany changes nothing if the provider is subject to US jurisdiction. The rule turns on possession, custody or control, so the question is which company within the group actually gets to the data. A contracting party based in Luxembourg or Dublin does not answer that by itself, but a US parent in the org chart does not automatically answer it the other way either. Whether and how a group contests a disclosure in a given case is a matter of its contract and its practice, not of the subsidiary's seat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to care about this for a hobby project with five users?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obligations apply regardless of size as soon as personal data is processed, and a login table with email addresses is enough for that. The risk of government access is structural, not practical, with five users. What remains is the duty not to promise anything in the privacy policy that the provider cannot keep. The checklist takes an afternoon for a small project, and item 7 is then an honest line instead of an empty promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sibling article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner — How I Host a Next.js App with Self-Hosted Keycloak&lt;/a&gt; — the route without a US provider in the chain, with the setup, the decisions and the cost side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hub of this branch:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/agentic-coding-experience/" rel="noopener noreferrer"&gt;Agentic Coding from a User's Perspective — Experience: The Work Doesn't Disappear, It Shifts&lt;/a&gt; — the experience level that the infrastructure decisions belong to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cluster hub:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents That Enforce Conventions&lt;/a&gt; — the enforcement system behind the project.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Agentic Coding from a User's Perspective — Experience: The Work Doesn't Disappear, It Shifts</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:46:16 +0000</pubDate>
      <link>https://dev.to/marcus1968/agentic-coding-from-a-users-perspective-experience-the-work-doesnt-disappear-it-shifts-4l09</link>
      <guid>https://dev.to/marcus1968/agentic-coding-from-a-users-perspective-experience-the-work-doesnt-disappear-it-shifts-4l09</guid>
      <description>&lt;p&gt;Since spring 2026, two projects have been taking shape with the same coding agent: an ETL generator on Next.js and PostgreSQL, and this blog. The agent does not complete lines, it works through assignments in the repository. This article collects my agentic coding experience from a user's perspective. I build databases, ETL processes and the framework behind them, and I use the agent as a tool without doing AI research or building AI tools myself.&lt;/p&gt;

&lt;p&gt;Is it worth it? Yes. I spent years building client-server applications in VB.NET and C#, and I am now building a TypeScript application without knowing TypeScript. That is the visible gain. The less visible part is that the work has shifted: from programming to structuring. Rules and skills for the agent, feature specs, bug files, test cases, documentation. In my working life, other people used to deliver that work, a project lead for example, and I consumed it as a developer. I underestimated how much of it there is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is it worth it? Yes:&lt;/strong&gt; A TypeScript application with frontend, tests and CI, built by someone who does not know TypeScript but knows databases, ETL and .NET.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The work is not gone, it has shifted:&lt;/strong&gt; Rules, skills, specs, bug files, test cases, documentation. Project leads and teams used to deliver that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guidance lives in the repo, not in the prompt:&lt;/strong&gt; The prompts are short because rule files and specs carry the work. That includes intermediate states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The infrastructure was the biggest hurdle:&lt;/strong&gt; one server with four environments, self-hosted identity, containers, TLS, and no cookie banner because the architecture does not need one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control is a loop:&lt;/strong&gt; An agent extends the radius of what I can build, not the radius of what I can judge. Whatever I cannot judge therefore goes through recurring checks that the agent runs. A green signal is not proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This article is itself an example:&lt;/strong&gt; The draft comes from the agent. Selection, corrections and publication are my work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; none. Anyone who wants to go deeper will find the way into this blog's technical articles at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Agentic Coding Means Here
&lt;/h2&gt;

&lt;p&gt;A coding agent is a language model that, with tool access, executes several steps in a row on its own inside a repository: reading files, running commands, changing code, running tests, committing. The difference to autocompletion is not the quality of individual lines but the mode of work. I formulate an assignment, the agent works through it and reports a result.&lt;/p&gt;

&lt;p&gt;In both projects the tool is Claude Code, in the terminal and in VS Code. The observations do not depend on the product, though. They depend on an agent having access to repository, shell and database, and on a human deciding what happens with it. The mechanics behind rule files, skills and sub-agents are covered by the hub article &lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt;. This article is about what they changed in my daily work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 1: A TypeScript Application Without TypeScript Skills
&lt;/h2&gt;

&lt;p&gt;The app project is called DI² and is an ETL generator on Next.js and PostgreSQL. The database layer consists of more than 150 DDL files, the repository is private, and I work on it alone. On the ETL side, the app's tools build on a framework of my own that I am publishing on GitHub step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  From .NET to TypeScript
&lt;/h3&gt;

&lt;p&gt;What I bring is data models, SQL, ETL processes and years of client-server applications in VB.NET and C#. I know how an application with database, business logic and user interface is cut, where state lives and where errors originate. What I do not bring is the language. TypeScript, the React ecosystem and the test tooling of the web world were new to me, and to this day I cannot read a file of this application the way I read a stored procedure.&lt;/p&gt;

&lt;p&gt;The frontend, the CI pipeline and the test infrastructure came into being anyway. The clearest example is the 241-line Node script that checks the project's SQL conventions and that I could not have written. My role was a different one: I knew the conventions, I could judge a finding as right or wrong, and I made the framing decisions. Acceptance ran through runs, not through reading the code, as the &lt;a href="https://sql.marcus-belz.de/en/sqlfluff-cant-lint-sql-conventions/" rel="noopener noreferrer"&gt;article about the guard&lt;/a&gt; shows. An agent extends the radius of what I can build. It does not extend the radius of what I can judge. Where the two diverge, acceptance needs a criterion that can be checked without understanding the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conventions in the Repo Instead of in My Head
&lt;/h3&gt;

&lt;p&gt;In a classic solo project, the conventions live in the developer's head. With an agent that starts every session without the history of the previous one, that does not work. In the DI² project this turned into &lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;27 rule files&lt;/a&gt;, 28 by now, one per convention, each with its reasoning. Features and bugs get their own files with a running ID before a single line of code exists. How such a rule set is derived from an existing codebase is described in the &lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;generate-refine-derive loop&lt;/a&gt;. What it costs when rules alone are not enough is shown by &lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;799 hardcoded font sizes&lt;/a&gt; despite an unambiguous design rule.&lt;/p&gt;

&lt;p&gt;The second half of this rule I only noticed when it failed. I accidentally ended a session in the middle of a bug fix and asked a new one to pick up where the old one had left off. The agent found no remains, neither in the working tree nor in the stash. That getting back in still took only minutes was down to the bug file in the repository: error description, database evidence, reproduction steps and three named hypotheses with a checking order. The intermediate state lives in the repo, or it does not live at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Infrastructure Was the Biggest Hurdle
&lt;/h3&gt;

&lt;p&gt;One part of the project that cost me a lot of time is not a line of application code but the infrastructure underneath: a single VPS at Hetzner with four environments, a self-hosted Keycloak as identity provider, an nginx in front of it, a good dozen containers, and a VPS provider that blocks outbound mail ports. I did not write a single line of it. What I decided was that there would be one server instead of four, a self-hosted identity instead of a cloud service, and a location in the EU, and behind these decisions is a design goal I cared about in particular: the application should get by without a cookie banner. What remains are four cookies that are permitted without consent: the login's session token and CSRF token, a language cookie for the public pages, and the state of the navigation. The setup, the decisions behind it and the cost side are described in &lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading Is No Longer a Control
&lt;/h3&gt;

&lt;p&gt;Two things together changed the way I work. The first is the language: Whoever cannot read the language of their code cannot control it by reading. The second is the volume. The agent writes so many lines in so little time that reading and understanding all of it would not be possible even if I knew the language. Reading my own code in full, the way I knew it from my .NET projects, therefore drops out as a control. That is not an admission of failure but an unavoidable change in the way of working. If reading drops out as a control, something else has to take its place.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Control Loop
&lt;/h3&gt;

&lt;p&gt;The control I cannot provide myself has to happen somewhere else: in a loop of checks that the agent runs, but whose yardstick is not its own judgment but rules, tests and audit reports. It does not run once but repeatedly. After every piece of feedback, a rule is sharpened. Recurring checks have skills, that is, versioned prompts in the repository, among them one for security audits and one for update checks. I had unit tests pushed under existing code after the fact. The foundation for that only came in July 2026, and by now there are more than 700 tests. Since the end of August there is a catalog of manual test cases with acceptance criteria. All in all, 14 skills and 28 rule files carry this loop. How that loop runs in detail, what evidence there is for it and where it has failed is described in &lt;a href="https://sql.marcus-belz.de/en/verify-ai-generated-code/" rel="noopener noreferrer"&gt;The Control Loop&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It has found things I would not have found by reading. A rate-limiting rule in the web server had been pointing at an endpoint that no longer existed since the identity provider was switched, and for months it looked like protection. Another case started with a vague sentence from me that the page was barely responding. The agent compared all four environments, found production affected as well although it did not even have the new version, and discovered 2.9 GB in swap. The identity service was sitting on disk, hence 9.6 seconds for the first login and 0.19 seconds for every subsequent one. No application code was involved. Both cases are told in detail in &lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One lesson matters more to me than any number: A success signal is not proof. A test suite was green for four weeks while three fix attempts went right past a scroll bug, because the test had stubbed exactly the value whose wrongness was the bug. An audit report said "passed", and only my follow-up question revealed that the decisive click path had never been executed. Both signals attest that a process ran, not that an effect occurred. Since then, verifying means finding a second, independent route to the same question. How to write test cases that deliver that is covered in the article &lt;a href="https://sql.marcus-belz.de/en/how-to-write-manual-test-cases/" rel="noopener noreferrer"&gt;How to Write Manual Test Cases&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study 2: Writing a Blog with an Agent
&lt;/h2&gt;

&lt;p&gt;This blog has been run editorially with the same tool since May 2026. Back then the articles were moved from WordPress into a repository, one folder per topic with a German and an English version. Today there are 65 topics, 55 of them live in both languages. What the agent does here differs less from the app project than I would have expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spec, Rounds, Revision Log
&lt;/h3&gt;

&lt;p&gt;Before every article comes a spec: role in the topic cluster, target audience, search intent, outline, boundary to neighboring articles. Then the article runs through fixed rounds, from the critique of the existing text through structure and style to the English version. Every round gets an entry in a revision log and its own commit. Nine rule files carry the conventions, among them style heuristics against typical markers of AI-written text: no semicolon in the prose, at most one dash per paragraph, no first-person plural. Every one of these heuristics came out of a piece of feedback from me. The agent writes drafts, I correct, and the correction becomes the rule for the next article.&lt;/p&gt;

&lt;p&gt;The way onto the live site remains manual. I copy the text into the WordPress block editor, set images and SEO fields, and publish. Afterwards the agent verifies remotely via live HTML and REST API. For the article on the &lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;functional aesthetics of SQL&lt;/a&gt; it found four defects across the two language versions that way, defects invisible in the repository because they only arise in the editor: an image that had been pointing at another article's file for months, an SEO title that lost its last letter on paste, a jump link that led nowhere because of an old HTML anchor, and, in the English version, a link to the article on SSMS editor options that pointed at the German instead of the English version. I corrected that link twice in the block editor, and twice it stayed wrong. The agent read the post's modification timestamp via the REST API: It had not moved on either attempt, so my save clicks had not been saves at all. The cause was the editor's link dialog. Whoever types in the new URL and then clicks outside the dialog instead of confirming it with Enter or "Apply" discards the change, even if they press "Update" afterwards. On the third attempt the link held. The finding has been a note in the rule file for live verification ever since.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Review Comes from a Fresh Session
&lt;/h3&gt;

&lt;p&gt;A language model can also evaluate a text, and I have used that from the start. The methodology, however, I had to develop myself. While reviewing an article I noticed that a model anchors on its earlier verdicts within the same chat session: first rating about 8 out of 10, after several revisions in the same session 9.7, in a fresh session clearly more critical again. The high scores were not quality but history. Since then every review runs in a fresh session of a model from a different vendor, with a frozen standard prompt. The score is a diagnosis, not a goal.&lt;/p&gt;

&lt;p&gt;I hand the result back to the agent as a raw paste, and it sorts it into three categories: adopt, matter of taste, already addressed. Experience from more than twenty reviews: 30 to 50 percent of the points are valid. The review is deliberately context-free, and precisely for that reason it needs as its counterpart the agent with repository context, which can look up in the revision log that a suggestion was already rejected months ago. A review is a finder, not proof. When one flagged a statement as "too absolute", the agent's cross-check showed it was not too absolute but wrong. What settled it in the end was a test against running database containers.&lt;/p&gt;

&lt;p&gt;Fresh, though, means more than having no history. A new session of the same model that wrote the text would have no prior context, but it would bring the same blind spots: the patterns I read as markers of AI-written prose are more likely caught by a model from a different vendor than by one from the same family. And a session inside my development environment sees the repository, loads the rules automatically and can look up in the log what the template deliberately leaves out. So the counted review stays with the outside model and the same template for every article, otherwise the scores cannot be compared. A fresh session of my own model has a different place: as a rule check that holds the text against the blog’s style heuristics and conventions. That is a different question from “is the text good”, and it needs exactly the context the reviewer must not have. These review experiences will get an article of their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Generalizes
&lt;/h2&gt;

&lt;p&gt;The agentic coding experience in both projects produced the same patterns. Four of them carry my daily work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The guidance lives in the repo, not in the prompt.&lt;/strong&gt; Rule files, specs, bug files and revision logs are the actual work. The prompts only name the entry point and the assignment. The intermediate state belongs there too: What an agent has worked out is recorded promptly as a commit or a note in the feature or bug file. Language models live on text. What is not written down does not exist for the next session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The structuring work is the price of the gain in radius.&lt;/strong&gt; I get an application I would not have built alone and a blog I would not sustain alone. In return I take on roles that are spread across a team: writing requirements, formulating acceptance criteria, declaring test cases, maintaining documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repo learns, not the agent.&lt;/strong&gt; Every session starts without the history of the previous one. What was learned from a mistake exists for the next session only if it is written down: as a rule or a checkpoint in the repository. That also works backwards: Many defects I correct in old articles today were not violations when they were written, because the rule only came afterwards. The existing body ages relative to the rules that were learned from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning remains mandatory.&lt;/strong&gt; This blog holds the position that &lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/#formatting-is-learning" rel="noopener noreferrer"&gt;formatting is learning&lt;/a&gt;: Whoever indents a statement by hand builds the mental model of the data. For agentic coding that applies with more force. Whoever does nothing but sign off loses, over time, the model they need for signing off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Gets Rough: Limits from a User's Perspective
&lt;/h2&gt;

&lt;p&gt;The limits are the part of my agentic coding experience that has occupied me the most.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Drift Despite Rules
&lt;/h3&gt;

&lt;p&gt;A rule with a stated reason improves the hit rate but does not guarantee compliance, and at high volume every residual rate turns into measurable drift. The frontend case with 799 hardcoded font sizes is the evidence, and the answer was not a better rule but a check that does not get tired. The blog's editorial work knows the same effect: Despite the style heuristics, every first draft from the agent contains semicolons or compressed notation, and I recognize AI-written text by exactly these patterns. What is in the rules still has to be checked, only the checking moves from reading to counting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collateral Damage in the Working Tree and on My Machine
&lt;/h3&gt;

&lt;p&gt;As soon as more than one session works in the same repository, or one session works alongside me, errors arise that do not exist in single operation: someone else's work in my own commit, deleted files of a parallel session, numbers assigned twice, and once 220 infinite loops that froze my computer for twelve minutes. Against the shared index, only &lt;code&gt;git commit --only&lt;/code&gt; with explicit paths turned out to be reliable, and the lesson from the first case would not have prevented the second. The five cases and what holds against them are in &lt;a href="https://sql.marcus-belz.de/en/parallel-claude-code-sessions-same-repo/" rel="noopener noreferrer"&gt;Two Agents, One Working Tree&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  You Can Only Sign Off on What You Can Judge
&lt;/h3&gt;

&lt;p&gt;Review effort is the new bottleneck. The answer is not to read faster but to shift acceptance: to runs instead of reading, to criteria instead of impression, to external reviews instead of my own goodwill. Each of these shifts presupposes that I can formulate the criteria and recognize a result as right or wrong. With the Node guard I could, because I knew the SQL conventions. With a script whose domain logic I could not judge, the same division of labor would be a risk.&lt;/p&gt;

&lt;p&gt;Added to that is a weakness that belongs to the tool's strength. An agent usually tests hypotheses systematically with measurements, and precisely because of that it hardly shows on the occasions when it builds a coherent story from incomplete clues instead. During one troubleshooting session the agent formulated a plausible cause as if it had been measured. It had not been. Since then I ask of every finding which part of it was measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Prompts as Examples
&lt;/h2&gt;

&lt;p&gt;The prompts in both projects are short because the guidance lives in the repository. Three examples show what a prompt still has to do then.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting a revision series.&lt;/strong&gt; Verbatim apart from the translation from German, only the internal identifiers are replaced by their meaning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let's start the triage of the legacy articles. all relevant changes in the
articles and the lessons from revising them go into the article about agentic
coding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines are enough because the spec, the work list and the rule files exist in versioned form. The agent read them, ran the first quality pass and established the material collection for this article as a side duty of the series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Having an article reviewed externally.&lt;/strong&gt; This prompt goes to ChatGPT in a fresh session, that is, to a model from a different vendor. It is versioned and has been frozen since August 2026. The original is German, shown here in translation and shortened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a critical, independent technical editor for a technical blog about
SQL Server, Postgres, data quality and ETL. You know neither the author nor
earlier versions of the text, and you have no reason for leniency.

Rate the article in four dimensions with a score from 1 to 10 each:
technical correctness, structure and readability, SEO fitness, language and style.

Duties: Every point of criticism quotes its location verbatim. For every point
a concrete suggestion for improvement. No courtesy points. At the end, an overall
score and the five most important points, prioritized by impact.

--- ARTICLE ---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important sentence is the second one. Without it, the model rates the history instead of the text. The duty to quote the location verbatim makes the result sortable, because a point without a location can neither be implemented nor rejected with reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A recurring check as a skill.&lt;/strong&gt; A skill is a versioned prompt that I call with a slash command. This is how the description of the skill that checks for updates begins, verbatim apart from the translation and the replaced file paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checks updates for dev dependencies (npm, Docker image declarations, GitHub
Actions) AND for the components that actually run on the VPS (OS packages,
Docker Engine, nginx, running container tags, Certbot). Writes findings to the
update documentation and to a dated security report. Keeps a history of
applied/deferred updates. Modifies nothing on its own.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill defines what is checked, where the result goes and what the agent is not allowed to do. For the server side it produces a checklist of read-only commands that I run myself. The acceptance criterion is the history, which shows me at the next run what was left undone.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Article Came About
&lt;/h2&gt;

&lt;p&gt;This article is about transparency, so it has to deliver it itself. The text is a draft by the agent, produced from a spec and a material file that agent sessions kept over six weeks. Into this file, whichever session was working wrote down, on my instruction, what happened during the work, its own mistakes included. The first version was twice as long and written in the third person. That it became shorter, stands in the first person and mentions the infrastructure at all is the result of a discussion in which I asked the agent to listen first before rewriting.&lt;/p&gt;

&lt;p&gt;My role is the same as in both case studies. I decided that this article exists and what it carries. I read the draft, discard, cut, correct and approve. I have it rated in a fresh session of another model, transfer it into WordPress by hand and check the links. What I do not do is write the text myself. And what I cannot do is in the text: write a Node script, see an effect in swap, retrieve a data point from a lost session. I had no prior experience of what this way of working would bring. What I underestimated was not the tool but the share of rules, specs, test cases and documentation that I had previously received from others.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can you tell that a text was produced with AI assistance?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Often, yes. Such texts give themselves away through recurring patterns. In German, in my experience, these are above all semicolons in the prose, verbless afterthoughts behind a dash, parentheses into which a whole piece of information is packed, and abstract terms that appear as the subject of a sentence and act there like a person. I have recorded these patterns in a rule file, and every draft is read against it before publication. Whether you can tell how a text came about therefore depends less on the model than on the correction pass that a human makes with a concrete list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you build a TypeScript application without TypeScript skills?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, under conditions. You need a model of how an application is cut, where state lives and where errors originate. In my case that came from years with .NET and databases. You need acceptance criteria that can be checked without reading the code: runs, tests, audits. And the infrastructure underneath remains a hurdle of its own that no programming language makes smaller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you control what you cannot judge yourself?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not by reading, but through a loop of checks that work without understanding the code: rules that are sharpened after every piece of feedback, skills for recurring checks, tests that have to be red on the old version, and audits with a report. The loop does not replace understanding, but it replaces reading. Every verdict has to state its open points as a condition, otherwise a "passed" travels further than its caveat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do you start as a user?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a single convention that lives in the repository and one assignment that has an acceptance criterion. Not with a large rule set, because that grows out of feedback. Anyone who wants to set up a database project with Claude Code will find a concrete entry point in the article on the &lt;a href="https://sql.marcus-belz.de/en/claude-code-project-with-database-setup/" rel="noopener noreferrer"&gt;open starter kit&lt;/a&gt;, and the mechanics behind it are explained by the &lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;cluster's hub article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the effort worth it for a solo project?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Going by my agentic coding experience in both projects, yes. The time saved on programming is largely eaten up by the structuring work. What remains is a gain in radius: a TypeScript application including infrastructure that I would not have built alone, and a blog with spec, review protocol and revision log that a single person would not sustain without an agent. The condition is that you take on the roles that project leads and teams otherwise carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hub:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents That Enforce Conventions&lt;/a&gt; — the enforcement system whose user experience this article describes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Spokes of this article:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/gdpr-without-cookie-banner-self-hosted/" rel="noopener noreferrer"&gt;One VPS, Four Environments, No Cookie Banner — How I Host a Next.js App with Self-Hosted Keycloak&lt;/a&gt; — the setup, the decisions behind it and the cost side that this article only touches on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/parallel-claude-code-sessions-same-repo/" rel="noopener noreferrer"&gt;Two Agents, One Working Tree — What Broke When I Ran Parallel Claude Code Sessions in One Git Repository&lt;/a&gt; — five cases from parallel operation and the countermeasures that hold.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/debugging-with-a-coding-agent/" rel="noopener noreferrer"&gt;The Agent Measures Where I Click — Debugging Postgres, Docker and a Swapping Server with a Coding Agent&lt;/a&gt; — four debugging sessions in which measuring replaced guessing, along with the limit of that way of working.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/verify-ai-generated-code/" rel="noopener noreferrer"&gt;The Control Loop — How I Get AI-Generated Code Checked That I Can't Read Myself&lt;/a&gt; — the five stations of the loop, their evidence and their blind spots.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/eu-region-is-not-eu-provider/" rel="noopener noreferrer"&gt;An EU Region Is Not an EU Provider — Vercel, Supabase and the GDPR Third-Country Question&lt;/a&gt; — the second infrastructure spoke: why an EU region settles the storage location but not the provider question, with a checklist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure and deploy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/postgres-database-ci-cd/" rel="noopener noreferrer"&gt;Database CI/CD with PostgreSQL&lt;/a&gt; — the lifecycle from object file to deploy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/github-actions-postgres-deployment/" rel="noopener noreferrer"&gt;GitHub Actions for Postgres Deploys — a Throwaway Database as Quality Gate&lt;/a&gt; — the deploy gate behind the infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool&lt;/a&gt; — the directory convention of the database layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rules, drift, guard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt; — structure and upkeep of the rule files.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes&lt;/a&gt; — the measured drift finding.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/sqlfluff-cant-lint-sql-conventions/" rel="noopener noreferrer"&gt;Why sqlfluff Can't Lint Our SQL Conventions — and a 240-Line Script Can&lt;/a&gt; — the guard I could not have written myself.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code — the Generate-Refine-Derive Loop&lt;/a&gt; — how the rule files come about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Getting started:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-with-database-setup/" rel="noopener noreferrer"&gt;Setting Up a Claude Code Project with a Development Workflow and Database — the Open Starter Kit Explained&lt;/a&gt; — the practical beginning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next door:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;The Functional Aesthetics of SQL — Why Structured Code Is Faster to Edit&lt;/a&gt; — the thesis "formatting is learning".&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/is-claude-male-or-female/" rel="noopener noreferrer"&gt;Is Claude a Woman or a Man? — and Why We Ask in the First Place&lt;/a&gt; — the other non-technical question about the tool.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Design Pattern // Logging an ETL Process with T-SQL — How to Capture Run, Component and Action in Evaluable Log Tables</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 05 Sep 2026 10:10:41 +0000</pubDate>
      <link>https://dev.to/marcus1968/design-pattern-logging-an-etl-process-with-t-sql-how-to-capture-run-component-and-action-in-38ei</link>
      <guid>https://dev.to/marcus1968/design-pattern-logging-an-etl-process-with-t-sql-how-to-capture-run-component-and-action-in-38ei</guid>
      <description>&lt;p&gt;An ETL process finishes without an exception — but was everything really loaded that should have been? The mere fact that a process did not abort says nothing about whether it actually did what was expected of it. A readable, evaluable log is what turns a gut feeling into a defensible statement.&lt;/p&gt;

&lt;p&gt;This design pattern logs an ETL run on three levels and answers the questions that success or failure hinge on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How long does the ETL process take overall?&lt;/li&gt;
&lt;li&gt;How long does a single component take — a stored procedure, an SSIS package or another building block?&lt;/li&gt;
&lt;li&gt;How long does a specific SQL statement take?&lt;/li&gt;
&lt;li&gt;How many rows did a SQL statement actually affect?&lt;/li&gt;
&lt;li&gt;And above all: did the process as a whole, a component or a single statement complete successfully?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — what this article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three-tier logging&lt;/strong&gt; — the tables &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt; capture run, component and action at increasing granularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored-procedure toolkit&lt;/strong&gt; — procedures such as &lt;code&gt;[LL].[spInsertTrace]&lt;/code&gt; and &lt;code&gt;[LL].[spUpdateTrace]&lt;/code&gt; write and update the log records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exception handling with &lt;code&gt;[LL].[Error]&lt;/code&gt;&lt;/strong&gt; — a TRY/CATCH pattern ends the run in an orderly way and records every error in an evaluable form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuation of the ETL architecture&lt;/strong&gt; — the example uses schema &lt;code&gt;T2&lt;/code&gt; from the architecture article and adds the logging layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites.&lt;/strong&gt; SQL Server and a basic understanding of stored procedures and TRY/CATCH. This article is part of the ETL design-pattern cluster. As a lead-in, see &lt;a href="https://sql.marcus-belz.de/en/data-quality-in-an-etl-process/" rel="noopener noreferrer"&gt;Data Quality in an ETL Process&lt;/a&gt; and &lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;The Architecture of an ETL Process&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;At its core the approach is straightforward: at the start of every action a log record is written, and once the action finishes it is updated with the outcome &lt;em&gt;success&lt;/em&gt; or &lt;em&gt;failure&lt;/em&gt; — and, where useful, with further information. In principle, that is all there is to it. In practice, a little more is needed after all.&lt;/p&gt;

&lt;p&gt;Even though creating a log should not be a big deal, there is quite a bit to say about it. This article starts with the result and works backwards from there. The process logging presented here is three-tier and uses the following log tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[LL].[Execution]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[LL].[Component]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[LL].[Trace]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tables log an ETL process and its associated components and steps at increasing granularity — read from top to bottom. The ETL process, the components and the individual steps are each logged with exactly one record.&lt;/p&gt;

&lt;p&gt;Closely tied to logging the process is logging errors. The approach presented here uses the following table for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[LL].[Error]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following figures show the result of three-tier logging for a simple, compact, but complete ETL process.&lt;/p&gt;

&lt;p&gt;In the table &lt;code&gt;[LL].[Trace]&lt;/code&gt; every action is logged with, among other things, the name of the procedure, the target entity the procedure processes, a short description of what was actually done, and further information such as the number of rows affected and the execution time.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrpqn5zsdiu2nhsqmley.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrpqn5zsdiu2nhsqmley.png" alt="Result set of the [LL].[Trace] table: several trace records, one per action, with columns Component, Entity, Action, AffectedRows, State and Success from an example run." width="798" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Table [LL].[Trace]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the table &lt;code&gt;[LL].[Component]&lt;/code&gt; the calls of procedures and SSIS packages — or, more generally, components — are logged. Here, too, a short description of the component’s task, the target entity and the execution time are recorded.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm1aj8xdrvzt4p20seug.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm1aj8xdrvzt4p20seug.png" alt="Result set of the [LL].[Component] table: one record per called component, with Component, Entity, State and Success." width="800" height="73"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Table [LL].[Component]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the top level, every execution of the ETL process is logged with exactly one record in the table &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygp1jx79vvzl7bpzofiu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygp1jx79vvzl7bpzofiu.png" alt="Result set of the [LL].[Execution] table: a single record for the entire ETL run, with Process, Start, End, State and Success." width="797" height="27"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Table [LL].[Execution]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The three process-logging tables — &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt; — contain two columns, &lt;code&gt;[State]&lt;/code&gt; and &lt;code&gt;[Success]&lt;/code&gt;, in which the success or failure of an action, a component or the ETL process is stored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the Derivation…
&lt;/h2&gt;

&lt;p&gt;After this brief preview of the result, the following aspects of the approach need to be clarified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three-Tier Logging&lt;/li&gt;
&lt;li&gt;Table Description, Declaration and Data Model&lt;/li&gt;
&lt;li&gt;ETL Process Execution Status&lt;/li&gt;
&lt;li&gt;Logging Procedures&lt;/li&gt;
&lt;li&gt;Exception Handling&lt;/li&gt;
&lt;li&gt;Example: Logging and Exception Handling in an ETL Process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three-Tier Logging
&lt;/h2&gt;

&lt;p&gt;The approach logs an ETL process in the three tables &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt;. Each table is intended for logging specific artifacts. This section introduces how each table is used, its columns and the code to create it.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Execution]
&lt;/h3&gt;

&lt;p&gt;This table logs the execution of an ETL process with exactly one record. An ETL process needs a clearly identifiable entry point. That can be a stored procedure, an SSIS package, a Talend job or a &lt;em&gt;SQL Server Agent&lt;/em&gt; job. At the start of the entry point’s execution, a log record is inserted into this table. After all tasks have been processed successfully, this record is updated to status &lt;em&gt;success&lt;/em&gt;, or to &lt;em&gt;error&lt;/em&gt; in the failure case. The table thus provides an overview of all executions of the ETL process, along with information such as the status and the duration of the run.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Component]
&lt;/h3&gt;

&lt;p&gt;This table logs the execution of a component with exactly one record. A component can be a stored procedure, an SSIS package or a Talend job. A component is characterized by the fact that it controls and performs one or more data manipulations. As with the &lt;code&gt;[LL].[Execution]&lt;/code&gt; table, a log record is inserted at the start of execution and updated to status &lt;em&gt;success&lt;/em&gt; or &lt;em&gt;error&lt;/em&gt; on completion. The table therefore holds, per ETL run, a list of the executed components, along with information such as the status and the duration of the run.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Trace]
&lt;/h3&gt;

&lt;p&gt;The name of this table already hints that it is intended for detailed logging of the ETL process’s actions — it creates a trace. Which actions are logged is a design decision for the developer. It is advisable, however, to log at least every INSERT, UPDATE and DELETE statement with its own record. In production systems, this granularity should be weighed deliberately against log volume and write load — a single &lt;code&gt;INSERT … SELECT&lt;/code&gt; can move millions of rows and still remains exactly one trace record. Besides the status fields already mentioned, the table also stores the number of rows affected, which lets the developer judge whether the statements did exactly what was expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Error]
&lt;/h3&gt;

&lt;p&gt;Errors detected in an ETL process belong in the log. A distinction has to be made between logging exceptions and logging data errors. The structure of this table is designed for logging data errors and contains columns in which every data error can be recorded completely and in an evaluable form. This article focuses on logging an ETL process rather than logging data errors. Logging the process is closely tied to explicit exception handling, and a separate section is dedicated to exception handling and the logging of exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The LL Schema Name
&lt;/h3&gt;

&lt;p&gt;The schema name &lt;code&gt;LL&lt;/code&gt; stands for &lt;em&gt;Logging Layer&lt;/em&gt;. This schema holds all log tables and the stored procedures used for logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Description, Declaration and Data Model
&lt;/h2&gt;

&lt;p&gt;The following sections describe the tables for logging the process as well as the table for logging errors. Finally, a diagram shows the data model of these tables.&lt;/p&gt;

&lt;p&gt;Three notes up front: The duration of an execution derives, at the execution level, from the columns &lt;code&gt;[Start]&lt;/code&gt; and &lt;code&gt;[End]&lt;/code&gt;, and at the component and trace levels from &lt;code&gt;[CreatedOn]&lt;/code&gt; (written when the action starts) and &lt;code&gt;[ModifiedOn]&lt;/code&gt; (set by the closing update via trigger). The timestamps use &lt;code&gt;datetime&lt;/code&gt; with &lt;code&gt;GETUTCDATE()&lt;/code&gt; for historical reasons — for a new design, &lt;code&gt;datetime2&lt;/code&gt; with &lt;code&gt;SYSUTCDATETIME()&lt;/code&gt; would be the natural choice today, and its higher precision pays off precisely for trace records written in quick succession. And the audit columns &lt;code&gt;[ModifiedOn]&lt;/code&gt;/&lt;code&gt;[ModifiedBy]&lt;/code&gt; are maintained by an AFTER UPDATE trigger per table: that keeps the logging procedures lean, but costs an additional UPDATE on every status change and is a deliberate trade-off at very high log volumes. With the default database setting &lt;code&gt;RECURSIVE_TRIGGERS OFF&lt;/code&gt;, the trigger's self-update does not recurse (measured on SQL Server 2022).&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[Execution]
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;p&gt;The table has the following columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key, sequential run id.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Process]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Name of the ETL process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Start]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Start time of the run (UTC).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[End]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;End time of the run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[DeltaStart]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Start of the delta window (for incremental loads).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[DeltaEnd]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;End of the delta window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[User]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Executing DB login (&lt;code&gt;SUSER_SNAME()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Machine]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Host the run was started from.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Version]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Version number of the ETL process.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Status: &lt;em&gt;processing&lt;/em&gt; / &lt;em&gt;warning&lt;/em&gt; / &lt;em&gt;success&lt;/em&gt; / &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;0 = not (successfully) completed, 1 = successful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Login of the last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;

&lt;p&gt;The table is created with the following statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Execution] - logging table (top level): exactly one record per ETL&lt;/span&gt;
&lt;span class="c1"&gt;-- run. Inserted at the start of the entry procedure with [State] = 'processing'&lt;/span&gt;
&lt;span class="c1"&gt;-- and updated to 'success' or 'error' at the end.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="nb"&gt;datetime&lt;/span&gt;                      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;End&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DeltaStart&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DeltaEnd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;bit&lt;/span&gt;                           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Execution_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Execution_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CK_LL_Execution_StateSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;GO&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TR_LL_Execution_Update&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;SET&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&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="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[Component]
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key of the component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ExecutionId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of source (e.g. &lt;em&gt;SSIS&lt;/em&gt;, &lt;em&gt;T-SQL&lt;/em&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Component]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Name of the component (procedure, package, job).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Version]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Version number of the component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Entity]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Target entity the component processes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Step]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Description of the step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Description]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Additional description.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[FileId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Reference to a processed file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Status: &lt;em&gt;processing&lt;/em&gt; / &lt;em&gt;warning&lt;/em&gt; / &lt;em&gt;success&lt;/em&gt; / &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;0 = not (successfully) completed, 1 = successful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Login of the last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Component] - logging table (middle level): exactly one record per&lt;/span&gt;
&lt;span class="c1"&gt;-- component call (stored procedure, SSIS package, Talend job).&lt;/span&gt;
&lt;span class="c1"&gt;-- Foreign key to [LL].[Execution].&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&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="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;bit&lt;/span&gt;                           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Component_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Component_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Component_ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CK_LL_Component_StateSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;GO&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TR_LL_Component_Update&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;SET&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&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="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[Trace]
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key of the trace record.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ExecutionId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ComponentId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Component]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of source (e.g. &lt;em&gt;SSIS&lt;/em&gt;, &lt;em&gt;T-SQL&lt;/em&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Component]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Name of the calling component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Task]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Task name (e.g. SSIS task).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Entity]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Target entity of the action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Step]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Description of the step.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Description]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Additional description.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[FileId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Reference to a processed file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Action]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Type of action (&lt;em&gt;insert&lt;/em&gt; / &lt;em&gt;update&lt;/em&gt; / &lt;em&gt;delete&lt;/em&gt; / …).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[AffectedRows]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Number of rows affected by the action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Status: &lt;em&gt;processing&lt;/em&gt; / &lt;em&gt;warning&lt;/em&gt; / &lt;em&gt;success&lt;/em&gt; / &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;0 = not (successfully) completed, 1 = successful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ModifiedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Login of the last modification, set by the update trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Trace] - logging table (finest level): exactly one record per single&lt;/span&gt;
&lt;span class="c1"&gt;-- action (INSERT/UPDATE/DELETE, single SQL step, single task). Foreign keys&lt;/span&gt;
&lt;span class="c1"&gt;-- to [LL].[Execution] and [LL].[Component].&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&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="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;AffectedRows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="nb"&gt;bit&lt;/span&gt;                           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Trace_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Trace_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="nb"&gt;datetime&lt;/span&gt;                          &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Trace_ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Trace_ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CK_LL_Trace_StateSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;GO&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TR_LL_Trace_Update&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;SET&lt;/span&gt;
           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModifiedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;
            &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&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="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[Error]
&lt;/h3&gt;

&lt;p&gt;As mentioned above, the structure of this table is designed for logging data errors, which are not the subject of this article. The table is, however, also used for logging exceptions. The following description covers only the columns required for logging an exception.&lt;/p&gt;

&lt;h4&gt;
  
  
  Columns
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Data type&lt;/th&gt;
&lt;th&gt;Null?&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Id]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bigint&lt;/code&gt; (IDENTITY)&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Primary key of the error record.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ExecutionId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ComponentId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Component]&lt;/code&gt; (if known).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TraceId]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bigint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Foreign key to &lt;code&gt;[LL].[Trace]&lt;/code&gt; (if known).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[ErrorType]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;char(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of error (exception vs. data error).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Source]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(5)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Type of source (e.g. &lt;em&gt;SSIS&lt;/em&gt;, &lt;em&gt;T-SQL&lt;/em&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Component]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Component in which the error occurred.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[TaskName]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Task name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Entity]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(128)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Affected target entity.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Step]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Step in which the error occurred.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Description]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error text (&lt;code&gt;ERROR_MESSAGE()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Number]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error number (&lt;code&gt;ERROR_NUMBER()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Line]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error line (&lt;code&gt;ERROR_LINE()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Error state (&lt;code&gt;ERROR_STATE()&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedOn]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;datetime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creation time, default &lt;code&gt;GETUTCDATE()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CreatedBy]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(100)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;Creating login, default &lt;code&gt;SUSER_SNAME()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Declaration
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[Error] - logging table for exceptions and data errors.&lt;/span&gt;
&lt;span class="c1"&gt;-- In the context of this article we only use the exception columns;&lt;/span&gt;
&lt;span class="c1"&gt;-- the data-error columns (ID1Value/ID2Value/ID3Value/ErrorValue,&lt;/span&gt;
&lt;span class="c1"&gt;-- ID*ColumnName, FileName, FileId) are relevant for a follow-up article.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;              &lt;span class="nb"&gt;bigint&lt;/span&gt;        &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;                        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;TraceId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ErrorType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;TaskName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;            &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SchemaName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="nb"&gt;bigint&lt;/span&gt;                            &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID1Value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID1ColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID2Value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID2ColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID3Value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ID3ColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ErrorValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ErrorColumnName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;            &lt;span class="nb"&gt;int&lt;/span&gt;                               &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="nb"&gt;datetime&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Error_CreatedOn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                     &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DF_LL_Error_CreatedBy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;                    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PK_LL_Error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;CLUSTERED&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Error_ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Error_ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FK_LL_Error_TraceId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
       &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;TraceId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
       &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data Model
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkn7zot0z5i9ps5uj0hq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkn7zot0z5i9ps5uj0hq.png" alt="Data model of the LL schema: [LL].[Execution], [LL].[Component] and [LL].[Trace] as a 1:n chain from top to bottom, plus [LL].[Error] with foreign keys to all three tables." width="799" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One deliberate redundancy in &lt;code&gt;[LL].[Trace]&lt;/code&gt; stands out: &lt;code&gt;[ExecutionId]&lt;/code&gt; sits there in addition to &lt;code&gt;[ComponentId]&lt;/code&gt;, so that run-level evaluations can skip the join through &lt;code&gt;[LL].[Component]&lt;/code&gt;. The database does not enforce the consistency of that pair — a trace record could in theory point to a component of a different run. Anyone who wants to enforce it adds a composite foreign key on &lt;code&gt;[Component]([Id], [ExecutionId])&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ETL Process Execution Status
&lt;/h2&gt;

&lt;p&gt;The three process-log tables have two columns, &lt;code&gt;[State]&lt;/code&gt; and &lt;code&gt;[Success]&lt;/code&gt;, which store the current status of the ETL process, of a component’s execution or of a specific action — for example an INSERT, UPDATE or DELETE. The current status is held in the &lt;code&gt;[State]&lt;/code&gt; column with the values &lt;em&gt;processing&lt;/em&gt;, &lt;em&gt;warning&lt;/em&gt;, &lt;em&gt;success&lt;/em&gt; and &lt;em&gt;error&lt;/em&gt;. Success is stored in the &lt;code&gt;[Success]&lt;/code&gt; column as &lt;em&gt;1&lt;/em&gt;. Only the combination of both values reveals the current status reliably. The following combinations are valid:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;[State]&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;[Success]&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;processing&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Action, component or run has started and is still running.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;warning&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Finished, but with a warning — not counted as success.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;warning&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Finished with a warning, yet counted as success.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;success&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Completed successfully.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;error&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Aborted with an error.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Valid combinations of the [State] and [Success] columns&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Other combinations of status values are not allowed. This is enforced twice: the process-logging procedures raise an exception if an invalid combination is passed in, and a CHECK constraint per table additionally rejects invalid combinations at the database level (error 547, measured on SQL Server 2022).&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Procedures
&lt;/h2&gt;

&lt;p&gt;The following procedures, among others, are available for inserting and updating log records in the tables above:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Procedure&lt;/th&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertExecution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens the execution log (top level) at the start of the run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateExecution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closes the execution log with status &lt;em&gt;success&lt;/em&gt; or &lt;em&gt;error&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertComponent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens a component log for a component.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateComponentSuccess&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closes a component log successfully.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateComponentError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closes a component log in the error case.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertTrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes a trace record for a single action.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateTrace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates a trace record (general).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateTraceSuccess&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates a trace record to &lt;em&gt;success&lt;/em&gt; / 1.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spUpdateTraceError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Updates a trace record to &lt;em&gt;error&lt;/em&gt; / 0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spInsertErrorException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes a caught exception to &lt;code&gt;[LL].[Error]&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Procedures for process logging&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In essence these procedures perform an INSERT or UPDATE on the log tables. The values to be logged are passed as parameters. The procedures validate the parameters passed in and raise an exception in case of invalid parameters. For parameter validation and for raising errors consistently, they use the two helpers &lt;code&gt;[dbo].[spRaiseError]&lt;/code&gt; and &lt;code&gt;[dbo].[fnIsNullOrEmpty]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following code examples show the procedures &lt;code&gt;[LL].[spInsertTrace]&lt;/code&gt;, &lt;code&gt;[LL].[spUpdateTrace]&lt;/code&gt; and &lt;code&gt;[LL].[spUpdateTraceSuccess]&lt;/code&gt;. In addition, &lt;code&gt;[LL].[spInsertExecution]&lt;/code&gt; stands in for all the remaining procedures, which follow the same pattern: validate the parameters, run the INSERT or UPDATE, return the generated id.&lt;/p&gt;

&lt;h3&gt;
  
  
  [LL].[spInsertTrace]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spInsertTrace] - inserts a trace record into [LL].[Trace]&lt;/span&gt;
&lt;span class="c1"&gt;-- and returns the generated id via @p_traceId.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Parameters:&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_executionId    bigint        Execution id of the current ETL run&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_componentId    bigint        Component id of the calling component log&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_traceId        bigint OUTPUT Id of the newly inserted trace record&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_source          nvarchar(5)   Source system (SSIS, T-SQL, ...)&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_component       nvarchar(128) Name of the calling component&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_task            nvarchar(128) Task name (e.g. SSIS task), optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_entity          nvarchar(128) Target entity, optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_step            nvarchar(max) Description of the step&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_description     nvarchar(max) Additional description, optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_fileId         bigint        Reference to [LL].[FileList].[Id], optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_action          nvarchar(100) Action label (Insert/Update/Delete/...)&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_affectedRows   bigint        Number of rows affected&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_state           nvarchar(100) processing / warning / success / error&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_success         bit           0 = processing/warning/error, 1 = success&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_componentId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_source&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_component&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_task&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_entity&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_step&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_fileId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="c1"&gt;-- Parameter checks&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_executionId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_componentId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_componentId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_source&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_component&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_step&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_state&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_success&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Validate the State/Success combination (whitelist)&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&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="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Invalid state &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; for p_success = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Write the trace record&lt;/span&gt;
        &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&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="n"&gt;ExecutionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ComponentId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FileId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;AffectedRows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;Inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;
        &lt;span class="k"&gt;VALUES&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_componentId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_source&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_component&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_task&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_entity&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_step&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_fileId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[spUpdateTrace]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spUpdateTrace] - updates an existing trace record&lt;/span&gt;
&lt;span class="c1"&gt;-- with Description, Action, AffectedRows, State and Success.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Parameters:&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_traceId        bigint        Id of the trace record to update&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_description     nvarchar(max) Description of the result&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_action          nvarchar(100) Action label, optional&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_affectedRows   bigint        Number of rows affected&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_state           nvarchar(100) processing / warning / success / error&lt;/span&gt;
&lt;span class="c1"&gt;--    @p_success         bit           0 = processing/warning/error, 1 = success&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tempId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="c1"&gt;-- Parameter checks&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_traceId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_success&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_state&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Validate the State/Success combination (whitelist)&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'warning'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&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="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&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="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Invalid state &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; for p_success = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Check that the trace record exists&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tempId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tempId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'A record with [Id] = &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; could not be found.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Update the trace record&lt;/span&gt;
        &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;SET&lt;/span&gt;
               &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
                                  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;])&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="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;AffectedRows&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_state&lt;/span&gt;
              &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_success&lt;/span&gt;
         &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[spUpdateTraceSuccess]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spUpdateTraceSuccess] - convenience wrapper around spUpdateTrace for&lt;/span&gt;
&lt;span class="c1"&gt;-- the common case "action finished successfully" (State = 'success',&lt;/span&gt;
&lt;span class="c1"&gt;-- Success = 1). Saves setting both fields on every call.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
         &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_traceId&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_description&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_action&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_affectedRows&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;
        &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [LL].[spInsertExecution]
&lt;/h3&gt;

&lt;p&gt;The procedure below was not shown in detail in the original example, but it stands in for all the remaining logging procedures: once you understand how it is built, you can derive &lt;code&gt;spInsertComponent&lt;/code&gt;, &lt;code&gt;spUpdateComponent*&lt;/code&gt;, &lt;code&gt;spUpdateTraceError&lt;/code&gt; and &lt;code&gt;spInsertErrorException&lt;/code&gt; analogously — the only difference is the column assignment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [LL].[spInsertExecution] - reference for the remaining logging procedures.&lt;/span&gt;
&lt;span class="c1"&gt;--&lt;/span&gt;
&lt;span class="c1"&gt;-- Structurally identical to [LL].[spInsertTrace]: parameter checks -&amp;gt; INSERT&lt;/span&gt;
&lt;span class="c1"&gt;-- -&amp;gt; id returned via OUTPUT variable. Once you understand the pattern, you can&lt;/span&gt;
&lt;span class="c1"&gt;-- spInsertComponent, spUpdateComponent*, spUpdateTraceError,&lt;/span&gt;
&lt;span class="c1"&gt;-- derive spInsertErrorException, spUpdateExecution analogously - only the&lt;/span&gt;
&lt;span class="c1"&gt;-- column assignment differs.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_process&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_version&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;fnIsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_process&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is either NULL or an empty string.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;User&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Machine&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;State&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;Inserted&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;
        &lt;span class="k"&gt;VALUES&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_process&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;SUSER_SNAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;HOST_NAME&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_version&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exception Handling
&lt;/h2&gt;

&lt;p&gt;To ensure that an ETL process ends in an &lt;em&gt;orderly&lt;/em&gt; way rather than aborting hard without proper logging, explicit exception handling is required. &lt;em&gt;Orderly&lt;/em&gt; means in this case that the process catches a raised exception, performs an UPDATE on the relevant log record in the tables &lt;code&gt;[LL].[Execution]&lt;/code&gt;, &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt; — wherever applicable — setting &lt;code&gt;[State]&lt;/code&gt; = &lt;em&gt;error&lt;/em&gt; and &lt;code&gt;[Success]&lt;/code&gt; = &lt;em&gt;0&lt;/em&gt;, and logs the exception in the table &lt;code&gt;[LL].[Error]&lt;/code&gt;. The following diagram shows the basic mechanics of exception handling:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4v0utkhqis8f1wergo8e.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4v0utkhqis8f1wergo8e.png" alt="Flowchart of the exception handling: Start, insert log with State processing, workload, branch on exception — the success path updates to success, the error path writes to [LL].[Error], sets State error and optionally re-raises the exception via THROW." width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Exception handling&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Only after logging has completed may the process re-raise the exception to the caller, which would then abort the calling process hard. Re-raising the exception to the caller is not strictly necessary, however, once the error has been logged and the relevant log records have been updated to &lt;code&gt;[State]&lt;/code&gt; = &lt;em&gt;error&lt;/em&gt; and &lt;code&gt;[Success]&lt;/code&gt; = &lt;em&gt;0&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Exception handling is required at least at the top level of an ETL process’s execution, which is logged in the table &lt;code&gt;[LL].[Execution]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One point deserves particular attention here: the interplay of logging and transactions. If the ETL work runs inside an open transaction and the logging procedures write in the same session, a &lt;code&gt;ROLLBACK&lt;/code&gt; in the CATCH block also rolls back the log records already written — of all cases, it is the failure case that loses its log. If an error additionally puts the transaction into the uncommittable state (&lt;code&gt;XACT_STATE()&lt;/code&gt; = -1, the usual case for runtime errors in a TRY block under &lt;code&gt;SET XACT_ABORT ON&lt;/code&gt;), an INSERT into the log tables fails in the CATCH block with error 3930 until the &lt;code&gt;ROLLBACK&lt;/code&gt; has been executed (measured on SQL Server 2022). The order in the CATCH block is therefore: roll back the transaction first, then log. The example in this article works without an explicit transaction and deliberately sidesteps the problem.&lt;/p&gt;

&lt;p&gt;And the error logging itself is not guaranteed to succeed. If one of the logging calls in the CATCH block fails — say, on missing permissions or a full log —, its exception masks the original error. Anyone who wants to guard against that wraps the logging calls in the CATCH block in a TRY/CATCH of their own: the final &lt;code&gt;THROW&lt;/code&gt; then re-raises the original exception in every case.&lt;/p&gt;

&lt;p&gt;The code example below shows exception handling including the logging into the tables &lt;code&gt;[LL].[Execution]&lt;/code&gt; and &lt;code&gt;[LL].[Error]&lt;/code&gt; in line with the diagram above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Exception-handling skeleton: shows the complete pattern of&lt;/span&gt;
&lt;span class="c1"&gt;-- TRY/CATCH + insert error + update execution + THROW. The order in the&lt;/span&gt;
&lt;span class="c1"&gt;-- CATCH block is crucial - log first, then THROW.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Execution logging variables&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;processName&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Error logging variables&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Exception variables (lowercase by convention)&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="c1"&gt;-- Initialize the execution logging variables&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;processName&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Name of ETL-Process'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Initialize the error logging variables&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'sql'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Procedure Name'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Any Entity'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Do something'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;processName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Workload (the actual ETL work happens here)&lt;/span&gt;
    &lt;span class="n"&gt;THROW&lt;/span&gt; &lt;span class="mi"&gt;50001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Any Exception'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_LINE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_STATE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&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;BEGIN&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertErrorException&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example: Logging and Exception Handling in an ETL Process
&lt;/h2&gt;

&lt;p&gt;After these explanations of the basics of exception handling, the following diagram shows the exception handling of a simple, compact, but complete ETL process:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwcrlnuvgpabafgr14vs.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwcrlnuvgpabafgr14vs.png" alt="Diagram of the exception handling in the example ETL process: the entry procedure calls several worker procedures, in the DELETE step of the deepest procedure an exception occurs that is propagated upward and logged at every level." width="800" height="1192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example of exception handling and logging for an example ETL process&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This example shows the logging of an ETL process built entirely from stored procedures, consisting of five stored procedures. The entry procedure &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt; calls the two procedures &lt;code&gt;[T2].[spDoSomething_1]&lt;/code&gt; and &lt;code&gt;[T2].[spDoSomething_2]&lt;/code&gt;. The first one logs an INSERT, an UPDATE and a DELETE step each, while the second procedure executes two further procedures, &lt;code&gt;[T2].[spDoSomething_2_1]&lt;/code&gt; and &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt;. These procedures, too, log an INSERT, an UPDATE and a DELETE step each — the DML statements themselves are deliberately only sketched as placeholder comments in the demo procedures. During the execution of the DELETE statement in the procedure &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt;, an exception is raised that leads to an orderly termination of the ETL process.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dtg136frvu1zkheqyfq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dtg136frvu1zkheqyfq.png" alt="Call hierarchy of the example ETL process: spETLProcess calls spDoSomething_1 and spDoSomething_2, spDoSomething_2 in turn spDoSomething_2_1 and spDoSomething_2_2. Each worker procedure runs INSERT, UPDATE and DELETE, with the failing DELETE step highlighted." width="664" height="1224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Procedure calls of the example ETL process&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since showing all procedures would lead to repetition, only the entry procedure &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt; and the procedure &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt; are shown here. All remaining worker procedures use the same TRY/CATCH pattern as &lt;code&gt;[T2].[spDoSomething_2_2]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two decisions in the example are deliberate. The worker procedures re-raise a caught exception via &lt;code&gt;THROW&lt;/code&gt;, so that every level can close its own log record. The entry procedure &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt;, by contrast, consumes the exception after logging is complete — the run ends in an orderly way with &lt;code&gt;[State]&lt;/code&gt; = &lt;em&gt;error&lt;/em&gt;, without aborting the caller hard. If an orchestrator such as SQL Server Agent is supposed to see the failure, an additional &lt;code&gt;THROW&lt;/code&gt; belongs at the end of its CATCH block, otherwise the job ends as spuriously successful from the scheduler's point of view.&lt;/p&gt;

&lt;p&gt;And when passing on the row count, order matters: &lt;code&gt;@@ROWCOUNT&lt;/code&gt; is captured into the variable &lt;code&gt;@affectedRows&lt;/code&gt; immediately after the respective DML statement, and only that variable is passed to &lt;code&gt;[LL].[spUpdateTraceSuccess]&lt;/code&gt;. After an &lt;code&gt;EXEC&lt;/code&gt;, &lt;code&gt;@@ROWCOUNT&lt;/code&gt; does not carry the business row count of the called component but the value of its last executed statement, and even a simple variable assignment sets the value to 1 (measured on SQL Server 2022). The orchestration steps in &lt;code&gt;[T2].[spETLProcess]&lt;/code&gt; therefore log no row count of their own — the workers deliver it in their trace records. In the demo workers, &lt;code&gt;SET @affectedRows = @@ROWCOUNT;&lt;/code&gt; deliberately remains part of the placeholder comment, and the placeholder steps pass &lt;code&gt;NULL&lt;/code&gt;, because without real DML there would be no business row count to capture. For statements beyond 2 billion rows, &lt;code&gt;ROWCOUNT_BIG()&lt;/code&gt; takes the place of &lt;code&gt;@@ROWCOUNT&lt;/code&gt;. The column &lt;code&gt;[AffectedRows]&lt;/code&gt; is sized as &lt;code&gt;bigint&lt;/code&gt; for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  [T2].[spETLProcess]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [T2].[spETLProcess] - example entry procedure of the ETL process.&lt;/span&gt;
&lt;span class="c1"&gt;-- Calls two worker procedures spDoSomething_1 and spDoSomething_2&lt;/span&gt;
&lt;span class="c1"&gt;-- and logs both calls in [LL].[Trace].&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spETLProcess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Error variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Logging variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&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="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'T-SQL'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[].[]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="c1"&gt;-- Open the execution log&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'ETL process'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Open the component log&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Orchestrate ETL process'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertComponent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- Call [T2].[spDoSomething_1]&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Execute [T2].[spDoSomething_1]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'execute'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spDoSomething_1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Orchestration step without a row count of its own - the DML rows&lt;/span&gt;
        &lt;span class="c1"&gt;-- are logged by the workers in their own trace records&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- Call [T2].[spDoSomething_2]&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Execute [T2].[spDoSomething_2]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'execute'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spDoSomething_2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Close the component log&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Close the execution log&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_LINE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_STATE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&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;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertErrorException&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                 &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&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;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&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;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateExecution&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  [T2].[spDoSomething_2_2]
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- [T2].[spDoSomething_2_2] - deepest worker procedure in the example tree.&lt;/span&gt;
&lt;span class="c1"&gt;-- Called by [T2].[spDoSomething_2] (see diagram 041007.png).&lt;/span&gt;
&lt;span class="c1"&gt;-- Simulates an INSERT, an UPDATE and a DELETE - the DELETE raises an&lt;/span&gt;
&lt;span class="c1"&gt;-- exception to demonstrate the exception-handling pattern.&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;PROCEDURE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spDoSomething_2_2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Error variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;-- Logging variables&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;affectedRows&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="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OBJECT_SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;PROCID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'T-SQL'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[].[]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'The parameter &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;p_executionId&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; is NULL.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spRaiseError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&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="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Open the component log&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Do something'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertComponent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- INSERT (succeeds)&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Insert data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'insert'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- An INSERT statement would go here, immediately followed by:&lt;/span&gt;
        &lt;span class="c1"&gt;-- SET @affectedRows = @@ROWCOUNT;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- UPDATE (succeeds)&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Update data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'update'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- An UPDATE statement would go here, immediately followed by:&lt;/span&gt;
        &lt;span class="c1"&gt;-- SET @affectedRows = @@ROWCOUNT;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="c1"&gt;-- DELETE (deliberately raises an exception)&lt;/span&gt;
        &lt;span class="c1"&gt;-- ----------------------------------------------------------------&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Delete data'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'delete'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&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="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertTrace&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
             &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt; &lt;span class="k"&gt;OUTPUT&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- A DELETE statement would go here - artificially raised exception:&lt;/span&gt;
        &lt;span class="n"&gt;THROW&lt;/span&gt; &lt;span class="mi"&gt;50001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'Exception in [T2].[spDoSomething_2_2]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- Close the component log&lt;/span&gt;
        &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentSuccess&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;TRY&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_LINE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ERROR_STATE&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&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;BEGIN&lt;/span&gt;
            &lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spInsertErrorException&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                 &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p_executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
                &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;error_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&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;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateTraceError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&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;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spUpdateComponentError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;THROW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;CATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following script contains the statement to execute the ETL process. To obtain a compact process log, the log tables are cleared beforehand. This clearing, including &lt;code&gt;DBCC CHECKIDENT&lt;/code&gt;, is a pure demo reset for reproducible ids — in live operation, log tables are not emptied. The closing SELECT statements produce the result shown at the beginning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;-- Example run: execute the ETL process and evaluate the log.&lt;/span&gt;
&lt;span class="c1"&gt;--&lt;/span&gt;
&lt;span class="c1"&gt;-- Block 1 clears the log tables (TRUNCATE / DELETE + RESEED),&lt;/span&gt;
&lt;span class="c1"&gt;-- block 2 starts the example ETL process, block 3 shows the result.&lt;/span&gt;
&lt;span class="c1"&gt;-- The SELECT statements return exactly the three result sets shown at the&lt;/span&gt;
&lt;span class="c1"&gt;-- start of the article (tables 041001, 041002, 041003).&lt;/span&gt;
&lt;span class="c1"&gt;-- ----------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="c1"&gt;-- 01: Clear the log tables&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;DBCC&lt;/span&gt; &lt;span class="n"&gt;CHECKIDENT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[LL].[Component]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RESEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;DBCC&lt;/span&gt; &lt;span class="n"&gt;CHECKIDENT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[LL].[Execution]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RESEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 02: Start the ETL process&lt;/span&gt;
&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T2&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;spETLProcess&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;-- 03: Evaluate the log&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Execution&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LL&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;ETL processes are data-driven processes. If data arrives that was not expected, there is a high probability that either not all data — or even wrong data — ends up in the target system. Something this article mentions only in passing: at the lowest logging level, the number of rows affected can also be logged. Knowing the number of expected versus actually processed rows is a good indicator of the success or failure of an ETL process.&lt;/p&gt;

&lt;p&gt;This approach thus supports the development of robust ETL processes and at the same time makes the data quality of a run assessable.&lt;/p&gt;

&lt;p&gt;What goes without saying in software development is often neglected when building ETL processes: explicit exception handling. The exception handling presented here first ensures that a process ends in an &lt;em&gt;orderly&lt;/em&gt; way in the event of an error. But when is there actually an error? And does an error always have to abort the process? Combined with the knowledge of the expected versus actually processed number of rows, genuine error handling can be implemented.&lt;/p&gt;

&lt;p&gt;The approach presented here is, in a way, an invitation to engage more with the data and the expected result. The log — and in particular the knowledge of the rows processed — helps the developer judge the correctness of the work already during the development of the ETL process.&lt;/p&gt;

&lt;p&gt;The procedures presented here merely provide a toolkit for logging. Used correctly, they produce a readable, evaluable log.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between &lt;code&gt;[LL].[Component]&lt;/code&gt; and &lt;code&gt;[LL].[Trace]&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both log steps of a run, but at different levels of granularity. &lt;code&gt;[LL].[Component]&lt;/code&gt; holds exactly one record per called component (stored procedure, SSIS package, Talend job) — that is, "which building block ran when and with what status?". &lt;code&gt;[LL].[Trace]&lt;/code&gt; goes one level deeper and logs the individual actions within a component (typically every INSERT, UPDATE and DELETE) together with the number of rows affected. A component therefore usually has several trace entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a dedicated &lt;code&gt;[LL]&lt;/code&gt; schema instead of putting the logging tables in the default schema?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;LL&lt;/code&gt; schema ("Logging Layer") cleanly separates the logging infrastructure from the business data. This has practical advantages: permissions can be granted precisely (for example, write access to &lt;code&gt;LL&lt;/code&gt; only for the ETL procedures), retention and backup of the logs can be controlled separately, and in scripts it is immediately clear that an object belongs to logging. It also prevents the logging tables from colliding with business tables of the same name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the pattern be implemented with Postgres instead of SQL Server?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — the concept is database-neutral: the logging tables and the three-tier structure stay the same. The concrete implementation, however, has to be adapted to PL/pgSQL semantics. Instead of &lt;code&gt;THROW&lt;/code&gt;, you re-raise the exception in PL/pgSQL with &lt;code&gt;RAISE&lt;/code&gt;, &lt;code&gt;TRY/CATCH&lt;/code&gt; becomes a &lt;code&gt;BEGIN … EXCEPTION WHEN OTHERS THEN … END&lt;/code&gt; block, and &lt;code&gt;@@ROWCOUNT&lt;/code&gt; corresponds to &lt;code&gt;GET DIAGNOSTICS &amp;lt;var&amp;gt; = ROW_COUNT&lt;/code&gt;. One important difference remains: an &lt;code&gt;EXCEPTION&lt;/code&gt; block forms an implicit subtransaction in PL/pgSQL. If it catches an error, all changes of the block are rolled back — including log records already written (measured on PostgreSQL 16). The error logging therefore belongs in the &lt;code&gt;EXCEPTION&lt;/code&gt; branch — what the handler itself writes is kept. If you want to measure the execution time of individual statements without your own trace table, you can additionally draw on &lt;code&gt;pg_stat_statements&lt;/code&gt;, as an aggregated statement statistic without the run-level correlation via execution, component and trace ids.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this pattern differ from SQL Server Audit or pgAudit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SQL Server Audit&lt;/code&gt; and &lt;code&gt;pgAudit&lt;/code&gt; are database-side auditing mechanisms: they log database and server events — logins, DDL, also object access such as SELECT or INSERT — at the infrastructure level, without the business correlation of an ETL run via execution, component and trace ids. The pattern presented here, by contrast, is &lt;strong&gt;application logging&lt;/strong&gt; — it records the business view of the ETL process: which run, which component, which action, how many rows, success or failure. The two complement each other. For the question "did the ETL run do the right thing functionally?" application logging is the appropriate tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why are log entries missing after an error with a rollback?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the log INSERTs ran in the same transaction as the ETL work. A &lt;code&gt;ROLLBACK&lt;/code&gt; in the CATCH block then also rolls back the log records already written. If the error puts the transaction into the uncommittable state (&lt;code&gt;XACT_STATE()&lt;/code&gt; = -1, the usual case for runtime errors in a TRY block under &lt;code&gt;SET XACT_ABORT ON&lt;/code&gt;), an INSERT in the CATCH block even fails with error 3930 as long as the &lt;code&gt;ROLLBACK&lt;/code&gt; is outstanding. In the CATCH block, the rule is therefore: roll back the transaction first, then log — or keep the logging outside the surrounding transaction in the first place (see the Exception Handling section).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you log the number of rows affected with @@ROWCOUNT?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Capture the value of &lt;code&gt;@@ROWCOUNT&lt;/code&gt; into a variable immediately after the INSERT, UPDATE or DELETE statement and pass that variable to the logging procedure — every further statement in between overwrites the value. In this pattern, the number ends up in the &lt;code&gt;[AffectedRows]&lt;/code&gt; column of the trace record via &lt;code&gt;[LL].[spUpdateTraceSuccess]&lt;/code&gt;. For statements with more than 2 billion rows, &lt;code&gt;ROWCOUNT_BIG()&lt;/code&gt; takes the place of &lt;code&gt;@@ROWCOUNT&lt;/code&gt;. The column &lt;code&gt;[AffectedRows]&lt;/code&gt; is sized as &lt;code&gt;bigint&lt;/code&gt; accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I integrate an SSIS package into the pattern?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An SSIS package is treated like any other component: at the start it calls &lt;code&gt;[LL].[spInsertComponent]&lt;/code&gt; (for example from an Execute SQL Task or a Script Task), performs its tasks and finishes with &lt;code&gt;[LL].[spUpdateComponentSuccess]&lt;/code&gt;, or with &lt;code&gt;[LL].[spUpdateComponentError]&lt;/code&gt; in the error case. The returned &lt;code&gt;ComponentId&lt;/code&gt; is passed along through an SSIS variable so that downstream trace calls can reference it. This way the package appears seamlessly alongside the T-SQL components in the same log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;This article is part of the &lt;strong&gt;ETL design-pattern cluster&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-in-an-etl-process/" rel="noopener noreferrer"&gt;Data Quality in an ETL Process&lt;/a&gt; — the root of the article series.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;Design Pattern // The Architecture of an ETL Process&lt;/a&gt; — provides the schema &lt;code&gt;T2&lt;/code&gt; used in the example of this article.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/etl-vs-elt-explained/" rel="noopener noreferrer"&gt;ETL vs. ELT — How to Tell Which Pattern You Actually Built&lt;/a&gt; — classifies the pattern logged here as ETL vs. ELT.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Checking Data Quality with SQL — a Configurable Framework&lt;/a&gt; — uses the same error-table idea for generic data quality checks.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/safe-type-conversion-t-sql/" rel="noopener noreferrer"&gt;Design Pattern // Safe Type Conversion with T-SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/type-conversion-basics-t-sql/" rel="noopener noreferrer"&gt;Data Quality // Fundamentals of Type Conversion with T-SQL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>etl</category>
      <category>logging</category>
    </item>
    <item>
      <title>Deriving Data Quality Rules from the Schema — What the Metadata Already Knows</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:13:56 +0000</pubDate>
      <link>https://dev.to/marcus1968/deriving-data-quality-rules-from-the-schema-what-the-metadata-already-knows-23p2</link>
      <guid>https://dev.to/marcus1968/deriving-data-quality-rules-from-the-schema-what-the-metadata-already-knows-23p2</guid>
      <description>&lt;p&gt;The rule "&lt;code&gt;country_code&lt;/code&gt; is mandatory" lives in your database twice: once as &lt;code&gt;NOT NULL&lt;/code&gt; in the target table's schema, and once as a hand-typed row in the check configuration. On the next &lt;code&gt;ALTER TABLE&lt;/code&gt;, only one of the two places changes, and the check silently goes wrong. With &lt;strong&gt;derived data quality rules&lt;/strong&gt; you no longer type that repetition: the metadata already knows which columns are mandatory, which keys must be unique and which value ranges the types allow. And by pinning the derived state at deployment time, you detect schema drift instead of suffering it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NOT NULL, key uniqueness and type bounds&lt;/strong&gt; can be projected mechanically from &lt;code&gt;information_schema&lt;/code&gt; — including the message text and the business key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projected at deployment time, not on every run:&lt;/strong&gt; an ETL process is a contract between the source and the target system. The derived rules are generated and persisted at deployment, schema changes are not adopted automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The live projection stays on as a drift detector:&lt;/strong&gt; a diff against the deployed state shows when the contract has to be renegotiated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only is not a convenience:&lt;/strong&gt; derived rules are neither edited nor deactivated, the &lt;code&gt;is_active&lt;/code&gt; flag belongs to the manual rules. Whoever wants a change changes the schema and redeploys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handwritten rules remain for the business logic&lt;/strong&gt; the schema does not contain. Exactly those deserve the review time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres as the example engine and the framework from the sub-hub &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; with its shared error table and the rule configuration &lt;code&gt;dq.check_rule&lt;/code&gt;. The metadata principle needs only &lt;code&gt;information_schema&lt;/code&gt; and thus carries over to any engine that offers it. The code shown uses Postgres syntax and has to be adapted to the dialect of any other engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that already exists
&lt;/h2&gt;

&lt;p&gt;The framework from the sub-hub works off a configuration table: one row is one rule. That is deliberate, because a new check is a data row instead of a code change, and the business side can read along. Such a row is still rolled out through a deployment — why it has to be that way is shown further down in this article. The price of the configuration only shows over time. A large share of the rows you type into such a configuration merely repeats what the target table enforces anyway:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;customer_id&lt;/span&gt;    &lt;span class="nb"&gt;int&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;source_system&lt;/span&gt;  &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;   &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;          &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;            &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;pk_customer&lt;/span&gt;         &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uq_customer_source&lt;/span&gt;  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one table already dictates seven check rules for the staging layer: four mandatory fields (lines 5 through 8), two unique keys (lines 11 and 12), plus the length and value-range bounds of the types. Whoever enters them into &lt;code&gt;dq.check_rule&lt;/code&gt; by hand creates seven copies of a truth that already lives in the schema.&lt;/p&gt;

&lt;p&gt;Copies drift. When a column &lt;code&gt;phone varchar(30) NOT NULL&lt;/code&gt; is added later, someone has to remember to extend the configuration as well. If they forget, the check stays silent and the load fails at the target constraint — exactly the scenario the pre-filter was supposed to prevent. If, the other way around, a &lt;code&gt;NOT NULL&lt;/code&gt; is dropped, the old rule keeps checking against a requirement that no longer exists and produces findings without a basis. Both failures are quiet. Nobody gets a message that configuration and schema have drifted apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which data quality rules can be derived
&lt;/h2&gt;

&lt;p&gt;There is a simple way out of the drift problem: derive these rules instead of typing them. Everything the target schema enforces as a constraint or as a declared type bound translates mechanically into a rule row. The mapping onto the framework's rule types is one to one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metadata source&lt;/th&gt;
&lt;th&gt;Derived check&lt;/th&gt;
&lt;th&gt;Rule type in the framework&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;is_nullable = 'NO'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;mandatory-field check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;constraint&lt;/code&gt; (WHERE clause)&lt;/td&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PRIMARY KEY&lt;/code&gt; / &lt;code&gt;UNIQUE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;uniqueness, cardinality 1 (all key columns &lt;code&gt;NOT NULL&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uniqueness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FOREIGN KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reference check against the master table (see the outlook below)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lookup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consistency / Integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;character_maximum_length&lt;/code&gt;, &lt;code&gt;numeric_precision&lt;/code&gt;/&lt;code&gt;_scale&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;length and value-range bound&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;constraint&lt;/code&gt; (WHERE clause)&lt;/td&gt;
&lt;td&gt;Validity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Which quality dimensions these checks pay into is laid out by the concept article &lt;a href="https://sql.marcus-belz.de/en/data-quality-dimensions-error-classes/" rel="noopener noreferrer"&gt;Data Quality: Dimensions and Error Classes&lt;/a&gt;. The pattern itself comes from a metadata-driven ETL project: there, the projection ran over a dedicated metadata model with business-side extras such as business and alternate keys and a per-column null handling. For this article it has been transferred to &lt;code&gt;information_schema&lt;/code&gt; so that it can be followed without a model of your own. The principle stays the same: the rules live in the metadata, and the projection merely reads them out.&lt;/p&gt;

&lt;h3&gt;
  
  
  NOT NULL from is_nullable
&lt;/h3&gt;

&lt;p&gt;The simplest projection. Every mandatory column of the target table becomes a mandatory-field check on the staging layer — as a finished row in the configuration table's format, including &lt;code&gt;where_clause&lt;/code&gt; and message text:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'constraint'&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%I IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&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;where_clause&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&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;message&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the demo table this yields four rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;where_clause&lt;/th&gt;
&lt;th&gt;severity&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;customer_id is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;source_system is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;source_id is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;country_code IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;country_code is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details are worth a look. The &lt;code&gt;format()&lt;/code&gt; with &lt;code&gt;%I&lt;/code&gt; on line 6 quotes the column name as an identifier — a column name with special characters yields a correct &lt;code&gt;where_clause&lt;/code&gt;, not a broken one. And the projection targets &lt;code&gt;staging&lt;/code&gt; (line 3) although it reads from &lt;code&gt;core&lt;/code&gt;: checking happens at the source, enforcing happens at the target. That is the division of labour of the whole framework. What would fail as a constraint at the target, the pre-filter finds beforehand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uniqueness from PRIMARY KEY and UNIQUE
&lt;/h3&gt;

&lt;p&gt;Keys live in two catalog views: &lt;code&gt;information_schema.table_constraints&lt;/code&gt; knows the constraints, &lt;code&gt;information_schema.key_column_usage&lt;/code&gt; their columns. With multi-column keys, the columns must be sorted &lt;strong&gt;by position&lt;/strong&gt;, that is by &lt;code&gt;ordinal_position&lt;/code&gt; within the key. That field does not count the column's position in the table but its position in the key, so it reproduces exactly the order from the DDL. Without an &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;array_agg&lt;/code&gt; emits the columns in whatever order the join happens to deliver them, and that order is neither guaranteed nor stable over time in Postgres. A different execution plan is enough to turn &lt;code&gt;source_system,source_id&lt;/code&gt; into &lt;code&gt;source_id,source_system&lt;/code&gt; on the next run.&lt;/p&gt;

&lt;p&gt;For the check result this would be harmless, because a &lt;code&gt;GROUP BY&lt;/code&gt; over both columns finds the same duplicates in any order. The damage happens one level up, because &lt;code&gt;check_column&lt;/code&gt; carries the key columns as a comma-separated list, and that list identifies the rule. At runtime the runner splits it back up, builds the multi-column &lt;code&gt;GROUP BY&lt;/code&gt; from it, and writes the value belonging to each listed column into &lt;code&gt;error_value&lt;/code&gt; in the error log. If the order flips, the same rule looks like a different one on the next run, the string comparison against the target constraint fails, and the column-value pairs in the log come out in a different order. The message suffers too, because anyone reading "Key (source_id, source_system) not unique" has to re-sort the columns against the DDL in their head. The projection therefore sorts exactly once, when collecting into the array:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_key_column&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&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;key_column&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UNIQUE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;bool_and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'unique'&lt;/span&gt;                                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;                                 &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;                                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Key (%s) not unique'&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_key_column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: one rule per key, not one per column.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;max_occurrence&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Key (customer_id) not unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_system,source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Key (source_system, source_id) not unique&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The composite key from line 12 of the table DDL lands position-correct as the comma list &lt;code&gt;source_system,source_id&lt;/code&gt; in &lt;code&gt;check_column&lt;/code&gt;, which is exactly the form the runner expects and splits back up.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HAVING&lt;/code&gt; on line 27 additionally draws a semantic line. Only keys whose columns are all &lt;code&gt;NOT NULL&lt;/code&gt; get projected. The additional join on &lt;code&gt;information_schema.columns&lt;/code&gt; takes care of that. The reason lies in the &lt;code&gt;NULL&lt;/code&gt; semantics of &lt;code&gt;UNIQUE&lt;/code&gt;: under the default &lt;code&gt;NULLS DISTINCT&lt;/code&gt;, Postgres allows any number of missing values in a &lt;code&gt;UNIQUE&lt;/code&gt; column, while the check's &lt;code&gt;GROUP BY&lt;/code&gt; collapses several &lt;code&gt;NULL&lt;/code&gt; into one group and would report them as duplicates. A rule projected without this filter onto a nullable key would be stricter than the constraint it claims to mirror — it would flag rows the target accepts without complaint. For a &lt;code&gt;PRIMARY KEY&lt;/code&gt; the condition always holds. A &lt;code&gt;UNIQUE&lt;/code&gt; over nullable columns deliberately stays out and belongs in a user-defined rule instead, with whatever &lt;code&gt;NULL&lt;/code&gt; treatment the business actually means. How the uniqueness check deals with composite keys and the &lt;code&gt;NULL&lt;/code&gt; semantics of &lt;code&gt;UNIQUE&lt;/code&gt; in detail is covered by the spoke &lt;a href="https://sql.marcus-belz.de/en/find-duplicates-with-sql/" rel="noopener noreferrer"&gt;Finding Duplicates with SQL&lt;/a&gt;. The cardinality of derived key rules is always 1: a &lt;code&gt;UNIQUE&lt;/code&gt; constraint knows no "at most three times".&lt;/p&gt;

&lt;p&gt;The target table's foreign keys can be projected into &lt;code&gt;lookup&lt;/code&gt; rules as well. This is deliberately an outlook rather than a recipe, because that projection needs more metadata than the two above. &lt;code&gt;information_schema.referential_constraints&lt;/code&gt; names only the two constraints involved, that is the foreign key and the referenced primary or unique key. The columns themselves live in &lt;code&gt;key_column_usage&lt;/code&gt; again, on both sides: the referencing columns come from a lookup by the foreign key's name in their &lt;code&gt;ordinal_position&lt;/code&gt;, the referenced columns from a second lookup into the same view by the name of the target constraint. The two sides are joined through &lt;code&gt;position_in_unique_constraint&lt;/code&gt;, which states for each foreign key column where its counterpart sits within the referenced key. Only that mapping yields the column pairs the check builds its &lt;code&gt;LEFT JOIN … IS NULL&lt;/code&gt; from. For this, the lookup routine accepts the child and master columns each as a comma-separated list, and both lists have to be position-aligned: the first child field belongs to the first master field, pair by pair. The projection therefore has to produce both lists in exactly the same key order, or the generated check joins the wrong pairs. On &lt;code&gt;NULL&lt;/code&gt; semantics, catalog and routine do agree: under &lt;code&gt;MATCH SIMPLE&lt;/code&gt;, the Postgres default, a composite foreign key accepts every row in which even one of the participating columns is &lt;code&gt;NULL&lt;/code&gt;, and the lookup check skips exactly those rows too, because a missing value is a completeness finding and not an integrity one. Anyone looking that match type up in the catalog finds it there as &lt;code&gt;match_option = 'NONE'&lt;/code&gt;, not as &lt;code&gt;SIMPLE&lt;/code&gt;. A foreign key declared as &lt;code&gt;MATCH FULL&lt;/code&gt; would show up as &lt;code&gt;FULL&lt;/code&gt; and would need a stricter check, because it rejects partially filled keys — one more reason this projection stays an outlook. How the check for &lt;a href="https://sql.marcus-belz.de/en/find-orphaned-records-sql/" rel="noopener noreferrer"&gt;orphaned records&lt;/a&gt; itself works is covered by the corresponding spoke.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type bounds from numeric_precision and character_maximum_length
&lt;/h3&gt;

&lt;p&gt;The data types themselves are check rules too. A &lt;code&gt;varchar(2)&lt;/code&gt; says "at most two characters", a &lt;code&gt;numeric(3,0)&lt;/code&gt; says "absolute value below 1000". In the staging layer, which still holds such values as unchecked text or generous types, this becomes a value-range rule:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'constraint'&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'length(%I) &amp;gt; %s'&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abs(%I) &amp;gt;= %s'&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;trim_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_precision&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_scale&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&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="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s longer than %s characters'&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s outside numeric(%s,%s)'&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_precision&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt;               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character_maximum_length&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="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numeric_precision_radix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;where_clause&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(source_system) &amp;gt; 10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source_system longer than 10 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(source_id) &amp;gt; 20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source_id longer than 20 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;country_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(country_code) &amp;gt; 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;country_code longer than 2 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;length(email) &amp;gt; 100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email longer than 100 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;abs(age) &amp;gt;= 1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;age outside numeric(3,0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The filter on line 32 is this projection's fine print. For &lt;code&gt;int&lt;/code&gt; and &lt;code&gt;bigint&lt;/code&gt; columns, &lt;code&gt;numeric_precision&lt;/code&gt; counts &lt;strong&gt;bits&lt;/strong&gt;, not decimal digits — &lt;code&gt;information_schema.columns&lt;/code&gt; reveals this via &lt;code&gt;numeric_precision_radix = 2&lt;/code&gt;. Without the radix filter, the projection would claim a bound of 10 to the power of 32 for &lt;code&gt;customer_id&lt;/code&gt;, which neither matches the type semantics nor makes for a meaningful check. Only for explicitly declared &lt;code&gt;numeric(p,s)&lt;/code&gt; types (radix 10) does the precision carry a business statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project at deployment time, not on every run
&lt;/h2&gt;

&lt;p&gt;What do you do with the three queries? Two obvious answers each come with a catch. The first: take the results into the configuration by hand via &lt;code&gt;INSERT INTO dq.check_rule&lt;/code&gt;. That only automates the typing effort. The inserted rows would be editable copies, stale again after the next &lt;code&gt;ALTER TABLE&lt;/code&gt;. The second: turn the projection into the runtime rule set directly, as a view, and every rule is fresh at every moment. Exactly that freshness is the second answer's catch.&lt;/p&gt;

&lt;p&gt;An ETL process is a &lt;strong&gt;contract between the source and the target system&lt;/strong&gt;, and that contract is valid at development time. When either side changes, the contract is renegotiated, not adjusted automatically. A live view would do exactly that, in both directions. If the target gets stricter, say through a new &lt;code&gt;NOT NULL&lt;/code&gt; column, the run that was green yesterday fails today without any deployment on the ETL side. If the target gets laxer because a constraint is dropped, the view silently drops the corresponding check, and the pre-filter softens without anyone having decided it. Add the audit angle: which rules were in force for a given run is a question a view can, by principle, not answer.&lt;/p&gt;

&lt;p&gt;For perspective: within a single application that validates its own schema, live derivation is the right model. There is no second contracting party there, the schema is the only truth. The contract logic begins as soon as two systems are involved.&lt;/p&gt;

&lt;p&gt;The conclusion that holds therefore separates two points in time. The projection itself stays a view — the tool that can compute the derived rules fresh from the metadata at any moment:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CTE_pk_column&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- primary key of the target table = business key of the error table&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&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;pk_column&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CTE_key_column&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- every key as an ordered column array; only keys whose columns are&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- all NOT NULL (otherwise the rule would check more strictly than&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="c1"&gt;-- the constraint guarantees - Postgres default NULLS DISTINCT)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&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;key_column&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column_usage&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_schema&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PRIMARY KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UNIQUE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
 &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;bool_and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- NOT NULL from is_nullable&lt;/span&gt;
 &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'constraint'&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;
 &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;
 &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id3_column&lt;/span&gt;
 &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;61&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%I IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&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;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;                                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&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;message&lt;/span&gt;
 &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;66&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;67&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CTE_pk_column&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;69&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'core'&lt;/span&gt;
 &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_nullable&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NO'&lt;/span&gt;
 &lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- uniqueness from PRIMARY KEY / UNIQUE&lt;/span&gt;
 &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'unique'&lt;/span&gt;
 &lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;
 &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;79&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk_column&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;83&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;
 &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Key (%s) not unique'&lt;/span&gt;
 &lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
 &lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;CTE_key_column&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;CTE_pk_column&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;92&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CTE on lines 3 through 22 pulls one more thing out of the metadata that the configuration table would otherwise demand by hand: the business key, through which the error table later maps a finding back to the source record. That, too, lives in the schema — it is the target table's primary key. As an ordered array it spreads across &lt;code&gt;id1_column&lt;/code&gt; through &lt;code&gt;id3_column&lt;/code&gt;, and the &lt;code&gt;HAVING count(*) &amp;lt;= 3&lt;/code&gt; on line 21 draws the line of the error table's contract: it carries no more than three mapping columns.&lt;/p&gt;

&lt;p&gt;The projection becomes the rule set only through the &lt;strong&gt;deployment step&lt;/strong&gt;: it reads the view exactly once and writes the result into a table of its own. That table is the signed contract. It records which derived rules were valid at deployment time, and the runner reads it exclusively:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;check_type&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id1_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id2_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id3_column&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;max_occurrence&lt;/span&gt;  &lt;span class="nb"&gt;int&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;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;        &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;deployed_on&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&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;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- deployment step: sign the contract. Runs at deployment time against&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;-- the target system, not on every run of the runner.&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id1_column&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id2_column&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id3_column&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;schema_name&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id1_column&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id2_column&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;id3_column&lt;/span&gt;
 &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;max_occurrence&lt;/span&gt;
 &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table deliberately carries no &lt;code&gt;active&lt;/code&gt; flag. The configuration table's &lt;code&gt;is_active&lt;/code&gt; belongs exclusively to the manual rules, because a derived rule is neither edited nor deactivated. The &lt;code&gt;deployed_on&lt;/code&gt; field documents, in passing, when the contract was signed.&lt;/p&gt;

&lt;p&gt;The contract idea does not stop at the derived rules. The manual rules in the configuration table change only as part of a deployment as well — inserted, edited or toggled via &lt;code&gt;is_active&lt;/code&gt; in a versioned way, not live. The reason is the same as with the schema: a rule list that can be rearranged by hand between two deployments no longer tells you, at a glance, what was in force during the last run. The execution log does record that, but the list is supposed to predict what runs, not merely document what ran. &lt;code&gt;is_active&lt;/code&gt; is thus a versioned switch, not a live dial.&lt;/p&gt;

&lt;p&gt;The check scope of a table is now — within the limits the projection itself draws — the union of both worlds: the deployed derived rules plus the active handwritten ones from the configuration table. Add a single user-defined rule as an example — an age limit that no constraint contains:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'age &amp;lt; 18'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Customer under 18 - review with business'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'derived'&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'user-defined'&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;where_clause&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;active&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;origin&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;origin&lt;/th&gt;
&lt;th&gt;check_type&lt;/th&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;where_clause&lt;/th&gt;
&lt;th&gt;severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;country_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;country_code IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_id IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system IS NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customer_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;derived&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user-defined&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;age &amp;lt; 18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six out of this table's seven rules come from the schema. Exactly one was typed — the one that actually contains business knowledge. How large that share turns out does depend on the table, though: a target table without mandatory fields and without key constraints gives the projection simply nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The projection as drift detector
&lt;/h2&gt;

&lt;p&gt;What happens when the target schema changes after the deployment? To the running process, at first: nothing. The runner reads the deployed state, and that is exactly the point of the model. The change does not stay invisible, though, because the view keeps computing the live state — and the difference between the two can be queried mechanically:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uq_customer_source&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'added'&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;EXCEPT&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'removed'&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt;
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_rule&lt;/span&gt;
 &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;EXCEPT&lt;/span&gt;
 &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;id3_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_occurrence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
 &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;FROM&lt;/span&gt;
 &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;derived_rule&lt;/span&gt;
 &lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;drift&lt;/span&gt;
 &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;drift&lt;/th&gt;
&lt;th&gt;check_type&lt;/th&gt;
&lt;th&gt;check_column&lt;/th&gt;
&lt;th&gt;message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;added&lt;/td&gt;
&lt;td&gt;constraint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email is mandatory in the target schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;removed&lt;/td&gt;
&lt;td&gt;unique&lt;/td&gt;
&lt;td&gt;&lt;code&gt;source_system,source_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key (source_system, source_id) not unique&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two directions of the diff tell different stories. An &lt;strong&gt;added&lt;/strong&gt; row means: the target has become stricter. Without a redeploy, the pre-filter does not know the new guarantee, and the load would fail at the target constraint instead of at the pre-filter — the detector shows this before it happens. A &lt;strong&gt;removed&lt;/strong&gt; row means: the target has become laxer. The pre-filter checks more strictly than necessary, which endangers no run but is worth a negotiation. In both cases the answer is the same: review the contract and sign it again deliberately, that is, run the deployment step once more. The diff works as a check in the deployment gate or as a warning before each run. It is a signal, never an automatic adoption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why read-only is not a convenience
&lt;/h2&gt;

&lt;p&gt;For the deployed derived rules, a hard convention applies: no &lt;code&gt;UPDATE&lt;/code&gt;, no deactivating, no deleting. That is why the table carries no &lt;code&gt;is_active&lt;/code&gt; either — this flag belongs exclusively to the manual rules in the configuration table. What looks like a limitation is the core of the pattern, not its side effect.&lt;/p&gt;

&lt;p&gt;If the derived rule were editable, it would be a copy with an expiry date. Any change to it would decouple it from its source, and from that moment on there would again be two truths: the one in the schema and the one in the rule. Worse still: a well-meant "let's switch this one check off for a bit" would bypass a guarantee that the target schema keeps enforcing regardless. The load would then fail at a constraint for which there apparently was no active rule. The check would no longer be the target's honest pre-filter but its own drifting opinion of it.&lt;/p&gt;

&lt;p&gt;For the same reason, the criticality of derived rules follows the source semantics rather than an editor's judgement. A &lt;code&gt;NOT NULL&lt;/code&gt; and a unique key are hard constraints at the target. Their derived rules therefore carry &lt;code&gt;E&lt;/code&gt; as in error, because a violation would make the load fail there. That is not an opinion to be debated per rule but a property of the schema.&lt;/p&gt;

&lt;p&gt;Whoever really wants to get rid of a derived rule has exactly one path: change the schema and redeploy. That sounds inconvenient, and it is intended. The discussion "does this column really have to be mandatory?" belongs at the table, not at a configuration row that merely mirrors the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary you don't build
&lt;/h2&gt;

&lt;p&gt;As soon as two rule sources exist, one question suggests itself: what happens when both check the same thing? If someone manually creates a uniqueness rule on &lt;code&gt;customer_id&lt;/code&gt; that already exists as a derived one — do you need conflict detection, a precedence scheme, a merge?&lt;/p&gt;

&lt;p&gt;The better answer is: you never let the situation arise. Instead of detecting and resolving conflicts between derived and user-defined rules, the user-defined territory is cut so that it never enters the derived one. Concretely: the keys the schema already secures are not offered again in the rule editor — no "check primary key" quick pick, no pre-filled uniqueness form for the constraint columns. Those checks already exist, automatically and non-negotiably. The free uniqueness type is there for the cases the schema does &lt;strong&gt;not&lt;/strong&gt; know: a composite business key plus a validity date, or an "at most three times per region", in other words different column combinations and different cardinalities.&lt;/p&gt;

&lt;p&gt;The conflict you don't build does not have to be resolved. There is no merge logic, no precedence table and no special case in the runner — not because the problem was solved elegantly, but because by construction it does not exist. That is a design decision, not an algorithm, and it is cheaper and more robust than any conflict detection you would have to write instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicates among your own rules
&lt;/h2&gt;

&lt;p&gt;Entirely without checks, though, the user-defined territory does not get by. Within the handwritten rules, the same check can accidentally be created twice, and then two identical findings for the same violation would sit in the error table. What gets blocked is therefore the &lt;strong&gt;exact content duplicate&lt;/strong&gt;: same rule type, same columns, same condition.&lt;/p&gt;

&lt;p&gt;What deliberately does &lt;strong&gt;not&lt;/strong&gt; get blocked is worth noting. Several rules of the same type per column are explicitly allowed: two different value-range checks on the same column are two separate, legitimate checks with their own messages. A "one rule per column and type" constraint would forbid exactly the cases a freely configurable rule table is built for.&lt;/p&gt;

&lt;p&gt;The second subtlety: the criticality is &lt;strong&gt;not&lt;/strong&gt; part of a rule's identity. Two rules with an identical condition but different severity count as the same duplicate. Otherwise the same check could exist once as an error and once as a warning, and the same violation would be reported twice — once blocking, once not. Whoever wants to change a rule's severity edits the existing rule instead of placing a second one next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains handwritten
&lt;/h2&gt;

&lt;p&gt;After all the deriving, a remainder is left, and it is the most valuable part of the configuration. The schema only holds what the database can enforce. Everything else is business knowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-field conditions&lt;/strong&gt; — a discount requires an active status, an end date lies after the start date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plausibility and temporal logic&lt;/strong&gt; — an order date does not lie in the future, an age under 18 is possible but worth reviewing (the example rule from above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value lists owned by the business&lt;/strong&gt; — allowed status values or product codes deliberately not cemented as constraints, because the business side maintains them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The derived rules need no review time, because they cannot be wrong — they claim nothing the target schema does not enforce anyway. Every minute a review spends nodding off "&lt;code&gt;customer_id&lt;/code&gt; is mandatory" is missing from the question of whether the age limit is right. The projection shifts the attention to where mistakes are actually possible: into the handwritten rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;The projection is exactly as good as the schema it reads from. That is its strength and its limit at once, and the limit deserves the same clear look as the pattern itself.&lt;/p&gt;

&lt;p&gt;Where mandatory fields exist only in the application, because the column allows &lt;code&gt;NULL&lt;/code&gt; and only the input form enforces a value, &lt;code&gt;information_schema&lt;/code&gt; sees nothing, and the projection yields nothing. The same goes for business keys that were never created as constraints, and for length bounds on &lt;code&gt;text&lt;/code&gt; columns without a declared length. The projection must not silently paper over these gaps: it delivers exactly the guarantees the schema states, and not a single one more. If you want more derived, you have to make the schema more honest — which keys and constraints a target table should carry in the first place is covered by the article on &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres table conventions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This pattern deserves to be called by its name: it is a deficit of application development, and not a rare one. Many applications treat the database as mere storage. Mandatory fields, value ranges and keys are checked by application code, while the schema allows almost anything. As long as only the application itself writes, this goes unnoticed. But anyone who wants to load data into such an application past the input form, say during a migration or through an interface, stands there without any of the guarantees this article projects. The consequence is uncomfortable but clear: exactly the rules that an honest schema would make derivable have to be created and maintained manually as user-defined rules for safe loading — with the full drift risk this article started with.&lt;/p&gt;

&lt;p&gt;On top of that comes a Postgres-specific subtlety: &lt;code&gt;information_schema&lt;/code&gt; is portable but not complete. A &lt;code&gt;CREATE UNIQUE INDEX&lt;/code&gt; without an accompanying constraint does not appear in &lt;code&gt;table_constraints&lt;/code&gt;, and neither do partial unique indexes or the &lt;code&gt;NULLS NOT DISTINCT&lt;/code&gt; behaviour of newer Postgres versions. If you use such constructs, project from the system catalogs &lt;code&gt;pg_constraint&lt;/code&gt; and &lt;code&gt;pg_index&lt;/code&gt; instead — with more detail, but without portability. A partial unique index translated into a full uniqueness rule would simply be wrong, so its index predicate belongs in the rule, or the index deliberately stays out of the projection.&lt;/p&gt;

&lt;p&gt;Deriving does not replace thinking about data quality. It moves the thinking to where it belongs: into the schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which data quality rules can be derived from the schema?&lt;/strong&gt;&lt;br&gt;
Everything the target table declares as a constraint or type: mandatory-field checks from &lt;code&gt;is_nullable&lt;/code&gt;, uniqueness checks from &lt;code&gt;PRIMARY KEY&lt;/code&gt; and &lt;code&gt;UNIQUE&lt;/code&gt; (provided the key columns are &lt;code&gt;NOT NULL&lt;/code&gt;), reference checks from foreign keys, plus length and value-range bounds from &lt;code&gt;character_maximum_length&lt;/code&gt; and &lt;code&gt;numeric_precision&lt;/code&gt;. Not derivable is business logic without a schema trace — cross-field conditions, temporal plausibility, value lists owned by the business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why should derived data quality rules not be editable?&lt;/strong&gt;&lt;br&gt;
Because an editable derived rule would be a copy with an expiry date. After the first change there would be two truths: the one in the schema and the one in the rule. A switched-off check would additionally hide a guarantee the target still enforces. That is why the deployed state deliberately carries no &lt;code&gt;is_active&lt;/code&gt; — the flag belongs to the manual rules. Whoever wants to change a derived rule changes the schema and redeploys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;information_schema&lt;/code&gt; enough, or do I need my own metadata model?&lt;/strong&gt;&lt;br&gt;
For NOT NULL, keys and type bounds, &lt;code&gt;information_schema&lt;/code&gt; is enough, and it is portable — the same views exist in SQL Server. The Postgres system catalogs (&lt;code&gt;pg_constraint&lt;/code&gt;, &lt;code&gt;pg_index&lt;/code&gt;) additionally see unique indexes without constraints and partial indexes. A metadata model of your own pays off only once business metadata should join in that the database does not know — say, business keys without a database constraint or a per-column null-handling semantic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if schema and business requirement contradict each other?&lt;/strong&gt;&lt;br&gt;
Then one of the two sides is wrong, and that belongs resolved at the schema rather than overridden in the configuration. If the business says "email is mandatory" while the column allows &lt;code&gt;NULL&lt;/code&gt;, the schema is too lax: add the constraint, and the derived rule follows automatically. Until then, a user-defined rule with severity warning can make the gap visible without faking a hard guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I keep derived and user-defined rules apart?&lt;/strong&gt;&lt;br&gt;
Through the origin of the data itself: derived rules live only in the deployment table, which only the deployment step fills, user-defined ones only in the configuration table. A combined view with an &lt;code&gt;origin&lt;/code&gt; column (via &lt;code&gt;UNION ALL&lt;/code&gt;) shows the complete check scope. Only one rule of discipline matters: never copy projected rules into the configuration table by hand — there they would be editable copies, and the drift starts all over again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Framework and routines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; — the sub-hub: the configurable framework with error table, rule configuration and runner, whose configuration this article fills automatically.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/validate-data-with-sql/" rel="noopener noreferrer"&gt;Validating Data with SQL&lt;/a&gt; — the routine behind the projected WHERE rules: value ranges, mandatory fields and the NULL trap.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-duplicates-with-sql/" rel="noopener noreferrer"&gt;Finding Duplicates with SQL&lt;/a&gt; — the uniqueness routine: cardinality, composite keys and the NULL semantics of UNIQUE.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-orphaned-records-sql/" rel="noopener noreferrer"&gt;Finding Orphaned Records with SQL&lt;/a&gt; — the lookup routine that derived foreign-key rules build on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-severity-levels/" rel="noopener noreferrer"&gt;Three Severity Levels, Not Pass/Fail&lt;/a&gt; — the severity spoke of the series: why E/W/I can do more than passed/failed, and how severity decides whether a rule becomes a constraint or stays in the pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Theory:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-dimensions-error-classes/" rel="noopener noreferrer"&gt;Data Quality: Dimensions and Error Classes&lt;/a&gt; — the concept frame: which dimensions the derived rules pay into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Target schema:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt; — which keys and constraints a target table should carry so the projection has something to read.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataquality</category>
      <category>sql</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Formatting SQL Statements (Part 2) — Statement Structure: SELECT, WHERE, FROM, JOIN</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:04:54 +0000</pubDate>
      <link>https://dev.to/marcus1968/formatting-sql-statements-part-2-statement-structure-select-where-from-join-4i4i</link>
      <guid>https://dev.to/marcus1968/formatting-sql-statements-part-2-statement-structure-select-where-from-join-4i4i</guid>
      <description>&lt;p&gt;If you can't tell at a glance where the &lt;code&gt;WHERE&lt;/code&gt; clause of a 200-line &lt;code&gt;SELECT&lt;/code&gt; statement starts and ends, you have a structure problem — not a content problem. This article shows how to format SQL statements so that &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt; and &lt;code&gt;JOIN&lt;/code&gt; stay immediately recognizable even in long queries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;→ Part of a series.&lt;/strong&gt; This is part 2 and covers &lt;strong&gt;statement structure&lt;/strong&gt; (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;). The basics for identifiers, delimiters, commas, and aliases are in &lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1 — Identifiers, Delimiters, Commas, Aliases&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR — what this article delivers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Main elements&lt;/strong&gt; (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;) belong on &lt;strong&gt;separate lines&lt;/strong&gt; with consistent indentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WHERE&lt;/code&gt; clause&lt;/strong&gt; — align operands and operators column-style, indent equivalent conditions equally. The parenthesis structure becomes visually readable this way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FROM&lt;/code&gt; clause&lt;/strong&gt; — table directly after the &lt;code&gt;JOIN&lt;/code&gt; operator, &lt;code&gt;ON&lt;/code&gt; keyword on its own line, &lt;code&gt;JOIN&lt;/code&gt; conditions formatted like a mini &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres bridge + auto-formatters&lt;/strong&gt; at the end — &lt;code&gt;[brackets]&lt;/code&gt; vs. &lt;code&gt;"quotes"&lt;/code&gt;, &lt;code&gt;DISTINCT ON&lt;/code&gt;, &lt;code&gt;LATERAL&lt;/code&gt;. sqlfluff and pgFormatter complement manual discipline, they don't replace it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; SSMS or any SQL editor with configurable tab width is enough. A live AdventureWorks database is not required — the examples illustrate patterns, not runnable pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  General Thoughts on Indentation
&lt;/h2&gt;

&lt;p&gt;A useful analogy for SQL structure is the outline of a table of contents. The indented version below is much faster to scan than the flat variant further down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Chapter Level 1
   1.1. Chapter Level 2
      1.1.1. Chapter Level 3
      1.1.2. Chapter Level 3
   1.2. Chapter Level 2
2. Chapter Level 1
   2.1. Chapter Level 2
      2.1.1. Chapter Level 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For comparison, the same TOC flat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Chapter Level 1
1.1. Chapter Level 2
1.1.1. Chapter Level 3
1.1.2. Chapter Level 3
1.2. Chapter Level 2
2. Chapter Level 1
2.1. Chapter Level 2
2.1.1. Chapter Level 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flat TOCs work too, but only with additional formatting options like upper/lower case, bold, or italic to distinguish levels. In a SQL editor, those options are typically not available (SSMS renders plain text with syntax highlighting, no bold for identifiers). So indentation remains the structural tool.&lt;/p&gt;

&lt;p&gt;One thing up front: All layout rules in this article are formatting conventions. SQL enforces neither the line breaks nor the indentation or the position of commas and keywords. The conventions aim to make the structure of a statement visible before you read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Language Elements
&lt;/h2&gt;

&lt;p&gt;This article looks at the six central clauses of a &lt;code&gt;SELECT&lt;/code&gt; statement:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;br&gt;
&lt;code&gt;FROM&lt;/code&gt;&lt;br&gt;
&lt;code&gt;WHERE&lt;/code&gt;&lt;br&gt;
&lt;code&gt;GROUP BY&lt;/code&gt;&lt;br&gt;
&lt;code&gt;HAVING&lt;/code&gt;&lt;br&gt;
&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A complete &lt;code&gt;SELECT&lt;/code&gt; statement can contain further clauses (&lt;code&gt;WITH&lt;/code&gt;, &lt;code&gt;TOP&lt;/code&gt; or &lt;code&gt;LIMIT&lt;/code&gt;, &lt;code&gt;OFFSET&lt;/code&gt;/&lt;code&gt;FETCH&lt;/code&gt;, window definitions) — the same indentation principles apply to them. Treating the six clauses as the first level, their contents sit one indentation level deeper. The resulting basic structure of a SQL statement looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;grouping&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;aggregations&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ground rule of this style guide: The main elements of a SQL statement sit on separate lines.&lt;/p&gt;

&lt;p&gt;As counterexamples, here are two commonly seen formatting styles that ignore this rule. In both, you have to read at least parts of the statement to recognize where a main element starts and ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Left-Aligned Main and Sub-Elements
&lt;/h3&gt;

&lt;p&gt;You occasionally find both top-level elements and the next-level elements left-aligned in the same column. You see this especially often in the &lt;code&gt;FROM&lt;/code&gt; clause: Data sources (tables, views, CTEs) are indented the same as the introducing keyword &lt;code&gt;FROM&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;grouping&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conditions&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;aggregations&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Right-Aligned Keywords
&lt;/h3&gt;

&lt;p&gt;In this example, the main clauses of the &lt;code&gt;SELECT&lt;/code&gt; statement (without regard for the &lt;code&gt;BY&lt;/code&gt; keyword) are right-aligned. This indentation style makes alignment extra work because you have to deal with varying indent widths:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;field1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field3&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;condition1&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition2&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition3&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;grouping&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;condition1&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition2&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;condition3&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simple statements, however, right-aligned keywords can read quite well — &lt;code&gt;CREATE INDEX&lt;/code&gt; is a typical example:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="n"&gt;NONCLUSTERED&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactInternetSalesReason&lt;/span&gt;&lt;span class="p"&gt;]([&lt;/span&gt;&lt;span class="n"&gt;SalesOrderNumber&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For full &lt;code&gt;SELECT&lt;/code&gt; statements with multiple JOINs, group-by columns and complex conditions, this style breaks down.&lt;/p&gt;

&lt;h2&gt;
  
  
  SELECT Field List
&lt;/h2&gt;

&lt;p&gt;The natural reading direction of a SQL statement is left to right and top to bottom. With keyboard and mouse, vertical navigation is easier than horizontal navigation. The scroll wheel and the &lt;code&gt;Page Up&lt;/code&gt;/&lt;code&gt;Page Down&lt;/code&gt; keys make fast vertical navigation possible even within long complex statements — provided that field lists are written vertically. The more important effect is editor-independent: One line per expression makes long field lists scannable and easier to compare, move, or extend.&lt;/p&gt;

&lt;p&gt;Field names should be written as a vertical list with leading commas — the detailed reasoning (comma readability, box-selection pattern) is in &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1, section “The Comma”&lt;/a&gt;. One field per line. Because the field list is logically subordinate to the &lt;code&gt;SELECT&lt;/code&gt; keyword, field names are indented by the agreed indent width:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;field1&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field3&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;field1&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field3&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;field1&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;field3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  WHERE Clause
&lt;/h2&gt;

&lt;p&gt;The order here deliberately departs from SQL syntax: The &lt;code&gt;WHERE&lt;/code&gt; clause introduces the central condition pattern first, which is then reused for the &lt;code&gt;FROM&lt;/code&gt; and &lt;code&gt;HAVING&lt;/code&gt; clauses. A &lt;code&gt;WHERE&lt;/code&gt; clause contains one or more conditions (predicates) connected by logical operators. Two points deserve attention when formatting these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;alignment of operands&lt;/li&gt;
&lt;li&gt;indentation of equivalent conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Alignment of Operands
&lt;/h3&gt;

&lt;p&gt;A simple condition consists of two operands and an operator (&lt;code&gt;=&lt;/code&gt;, &lt;code&gt;!=&lt;/code&gt;, &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;, &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;NOT IN&lt;/code&gt;, etc.). In a compound expression built from multiple single conditions, operands and operators should be aligned column-style. In the following example, field names have different lengths and different operators are applied:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field___1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="s1"&gt;'something'&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field__2&lt;/span&gt;&lt;span class="p"&gt;]&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field_____3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a table-like notation that allows fast visual navigation within the condition components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indentation of Equivalent Conditions
&lt;/h3&gt;

&lt;p&gt;If the &lt;code&gt;WHERE&lt;/code&gt; clause contains more than one condition, they are connected by logical operators like &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;OR&lt;/code&gt;. Parentheses are required when the intended logical grouping differs from operator precedence (&lt;code&gt;AND&lt;/code&gt; binds more tightly than &lt;code&gt;OR&lt;/code&gt;). Beyond that, they can improve readability in complex expressions. Depending on complexity, you quickly end up with deeply nested structures.&lt;/p&gt;

&lt;p&gt;To keep complex nested expressions readable, give particular attention to structure and indentation of the &lt;code&gt;WHERE&lt;/code&gt; clause: equivalent conditions are written underneath each other with the same indent, and a logical connection of equivalent conditions gets an indent that matches the parenthesis hierarchy:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand01&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="n"&gt;operand02&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand03&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="n"&gt;operand05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand05&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="n"&gt;operand06&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand07&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="n"&gt;operand08&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand09&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="n"&gt;operand10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand11&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="n"&gt;operand12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;operand13&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="n"&gt;operand14&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;16&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;The logical connections become visually readable through this indentation. A screenshot of the same &lt;code&gt;WHERE&lt;/code&gt; clause in Notepad++ makes the effect even more obvious, because the editor's vertical guide lines at the tab stops emphasize the parenthesis hierarchy:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr17hf6ws207nrrm3tori.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr17hf6ws207nrrm3tori.png" alt="WHERE clause in Notepad++ with vertical indent guide lines at the tab stops, making the nesting of the parenthesis constructs visible."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FROM Clause
&lt;/h2&gt;

&lt;p&gt;As with the other main elements, the subordinate elements of the &lt;code&gt;FROM&lt;/code&gt; clause are written indented. In most cases these are data sources — tables, views, and Common Table Expressions (CTEs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sub-SELECTs in the &lt;code&gt;FROM&lt;/code&gt; clause can be replaced by CTEs&lt;/strong&gt; when the subquery forms a self-contained logical step, is needed more than once, or makes the statement hard to follow: A CTE structures it top-down. For small subqueries used only once, a derived table is often the more compact choice. Converting is a structuring decision — how the CTE is executed is up to the engine's optimizer. What Postgres does differently (&lt;code&gt;MATERIALIZED&lt;/code&gt; / &lt;code&gt;NOT MATERIALIZED&lt;/code&gt;) is covered in the &lt;strong&gt;FAQ&lt;/strong&gt; at the end of the article.&lt;/p&gt;

&lt;p&gt;For formatting a &lt;code&gt;JOIN&lt;/code&gt; clause there are four building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table (or view / CTE)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JOIN&lt;/code&gt; operator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ON&lt;/code&gt; keyword&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JOIN&lt;/code&gt; conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the following code example, there is no visual anchor to identify the elements of the &lt;code&gt;FROM&lt;/code&gt; clause:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table4&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The elements of the &lt;code&gt;FROM&lt;/code&gt; clause belong on separate lines for readability. Exception: the joined table sits directly after the &lt;code&gt;JOIN&lt;/code&gt; operator. In this style guide, the &lt;code&gt;ON&lt;/code&gt; keyword sits left-aligned with the &lt;code&gt;JOIN&lt;/code&gt; operator on the line below — that keeps &lt;code&gt;JOIN&lt;/code&gt; and join condition visible as two separate structural levels. The same rules as for the &lt;code&gt;WHERE&lt;/code&gt; clause apply to &lt;code&gt;JOIN&lt;/code&gt; conditions:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;table1&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table3&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;field2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;table4&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FK&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
 &lt;span class="mi"&gt;16&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;h2&gt;
  
  
  GROUP BY, HAVING, ORDER BY
&lt;/h2&gt;

&lt;p&gt;The remaining main elements follow the same principles as the &lt;code&gt;SELECT&lt;/code&gt; field list and the &lt;code&gt;WHERE&lt;/code&gt; clause — in short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GROUP BY&lt;/code&gt;&lt;/strong&gt; contains a list of grouping expressions — for simple queries formatted like a scaled-down &lt;code&gt;SELECT&lt;/code&gt;: one expression per line with a leading comma, indented by convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HAVING&lt;/code&gt;&lt;/strong&gt; is a condition list like &lt;code&gt;WHERE&lt;/code&gt; — operands aligned column-style, equivalent conditions indented equally. The difference is semantic (post-&lt;code&gt;GROUP BY&lt;/code&gt; filter), not typographic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORDER BY&lt;/code&gt;&lt;/strong&gt; contains a list of sort expressions — formatted analogous to the &lt;code&gt;GROUP BY&lt;/code&gt; clause, with optional &lt;code&gt;ASC&lt;/code&gt; / &lt;code&gt;DESC&lt;/code&gt; per expression (held in a separate column when both sort directions appear in the same statement).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nb"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Sales&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactSales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nb"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Sales&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;AND&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="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nb"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Auto-Formatters and “Formatting Is Learning”
&lt;/h2&gt;

&lt;p&gt;Tools like &lt;strong&gt;sqlfluff&lt;/strong&gt; (multi-dialect — T-SQL, Postgres, MySQL, BigQuery, …) and &lt;strong&gt;pgFormatter&lt;/strong&gt; (Postgres-focused) generate the layouts shown here automatically and are useful as a pre-commit hook or CI step. They are &lt;strong&gt;not&lt;/strong&gt; a replacement for manual formatting.&lt;/p&gt;

&lt;p&gt;The act of indenting, aligning aliases and placing parentheses &lt;strong&gt;forces&lt;/strong&gt; the writer to read the statement fully and mentally model the table relationships. Auto-formatters produce the layout — they don't produce the mental model that emerges while writing. In the age of Copilot and Cursor, this matters twice: Generated SQL without understanding is a risk, because it produces technically correct queries that still don't answer the business question.&lt;/p&gt;

&lt;p&gt;Pragmatic recommendation: format manually first, then let the formatter run as a final consistency pass (e.g. &lt;code&gt;sqlfluff fix&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres Bridge
&lt;/h2&gt;

&lt;p&gt;The examples in this article use T-SQL notation (&lt;code&gt;[brackets]&lt;/code&gt;, positional &lt;code&gt;T01&lt;/code&gt; aliases). The layout rules themselves are &lt;strong&gt;engine-neutral&lt;/strong&gt; — they apply 1:1 to Postgres as well. There are only a handful of places where Postgres behaves differently, and none of them change the format pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifier quoting:&lt;/strong&gt; The SQL standard uses double quotes for delimited identifiers (&lt;code&gt;"name"&lt;/code&gt;) — Postgres follows it. T-SQL typically uses &lt;code&gt;[brackets]&lt;/code&gt; but also understands double quotes when &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; is active. With case-sensitive identifiers, quoting becomes semantically relevant in Postgres (a separate article on case sensitivity in SQL Server vs. Postgres is planned).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DISTINCT ON&lt;/code&gt;:&lt;/strong&gt; the Postgres idiom for “first row per group”. Which row comes first is determined only by an &lt;code&gt;ORDER BY&lt;/code&gt; that fixes the order within each group unambiguously — its leftmost expressions must be the &lt;code&gt;DISTINCT ON&lt;/code&gt; expressions. In T-SQL, emulate this with &lt;code&gt;ROW_NUMBER() OVER (PARTITION BY … ORDER BY …)&lt;/code&gt; and a filter on &lt;code&gt;rn = 1&lt;/code&gt;. The formatting follows the &lt;code&gt;SELECT&lt;/code&gt; field list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LATERAL&lt;/code&gt;:&lt;/strong&gt; lets a subquery in the &lt;code&gt;FROM&lt;/code&gt; clause reference columns of preceding &lt;code&gt;FROM&lt;/code&gt; items. T-SQL uses &lt;code&gt;CROSS APPLY&lt;/code&gt; / &lt;code&gt;OUTER APPLY&lt;/code&gt; for this. A &lt;code&gt;LATERAL&lt;/code&gt; join is formatted like any other &lt;code&gt;JOIN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RETURNING&lt;/code&gt;:&lt;/strong&gt; Postgres returns the affected rows on &lt;code&gt;INSERT&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt; and, since version 17, on &lt;code&gt;MERGE&lt;/code&gt; as well. SQL Server offers &lt;code&gt;OUTPUT&lt;/code&gt;, a functionally related clause with its own syntax and forms (&lt;code&gt;OUTPUT INTO&lt;/code&gt;). Both are formatted as their own clause line with a field list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTEs:&lt;/strong&gt; &lt;code&gt;WITH …&lt;/code&gt; is standardized and structured essentially the same in both engines, the extensions differ in detail. Postgres has the options &lt;code&gt;MATERIALIZED&lt;/code&gt; / &lt;code&gt;NOT MATERIALIZED&lt;/code&gt; (see FAQ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;JOIN&lt;/code&gt; indentation, &lt;code&gt;WHERE&lt;/code&gt; parenthesis pattern, &lt;code&gt;ORDER BY&lt;/code&gt; lists:&lt;/strong&gt; There is no engine difference for these layout patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the format discipline carries on both engines. A separate follow-up article on the Postgres identifier specifics is planned.&lt;/p&gt;

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

&lt;p&gt;The main elements of a &lt;code&gt;SELECT&lt;/code&gt; statement belong on separate lines, their subordinate elements one indentation level deeper. Every pattern in this article derives from that ground rule: the field list as a vertical list with leading commas, the &lt;code&gt;WHERE&lt;/code&gt; clause with column-aligned operands and equally indented equivalent conditions, the &lt;code&gt;FROM&lt;/code&gt; clause with the table directly after the &lt;code&gt;JOIN&lt;/code&gt; operator and the &lt;code&gt;ON&lt;/code&gt; keyword on its own line.&lt;/p&gt;

&lt;p&gt;Applied consistently, these patterns make the structure of a statement visible before its content is read: The parenthesis hierarchy of a nested &lt;code&gt;WHERE&lt;/code&gt; clause and the relationships between the tables are already in the layout. The pattern is engine-neutral and carries in SQL Server and Postgres alike. The vocabulary level (identifiers, delimiters, commas, aliases) is covered in &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;. More important than any single rule is that the same hierarchy stays consistently visible across the whole project — consistency beats purity.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you format complex SQL queries with multiple JOINs and nested WHERE conditions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Following the rules in this article: each clause on its own line, subordinate elements one level deeper. Each &lt;code&gt;JOIN&lt;/code&gt; gets its table right next to the operator and its &lt;code&gt;ON&lt;/code&gt; on the following line. In the &lt;code&gt;WHERE&lt;/code&gt; clause, equivalent conditions sit underneath each other and the parenthesis hierarchy sets the indent depth. That keeps even a statement with five joins navigable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should a sub-SELECT be replaced by a CTE?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As soon as the subquery makes the statement hard to follow, is needed more than once, or forms a self-contained logical step. CTEs read top-down instead of nested and can be referenced multiple times in the same query. SQL Server, however, may execute the defining query again for each reference, because a CTE is not a materialized intermediate result there. For testing, temporarily replace the outer query with a &lt;code&gt;SELECT * FROM cte_name&lt;/code&gt; — a CTE definition cannot be run standalone. Small subqueries used only once may stay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should the &lt;code&gt;JOIN&lt;/code&gt; operator always be explicit (&lt;code&gt;INNER&lt;/code&gt;, &lt;code&gt;LEFT&lt;/code&gt;, &lt;code&gt;RIGHT&lt;/code&gt;, &lt;code&gt;FULL&lt;/code&gt;)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Syntactically, the &lt;code&gt;INNER&lt;/code&gt; is optional — a bare &lt;code&gt;JOIN&lt;/code&gt; means &lt;code&gt;INNER JOIN&lt;/code&gt;. This style guide still always writes out the &lt;code&gt;JOIN&lt;/code&gt; type (&lt;code&gt;INNER JOIN&lt;/code&gt;, &lt;code&gt;LEFT JOIN&lt;/code&gt;, &lt;code&gt;RIGHT JOIN&lt;/code&gt;, &lt;code&gt;FULL JOIN&lt;/code&gt;): the default is not obvious to every reader, and when skimming, the difference between &lt;code&gt;JOIN&lt;/code&gt; and &lt;code&gt;LEFT JOIN&lt;/code&gt; is easy to miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the format pattern apply to Postgres as well?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — the layout rules transfer unchanged, see the &lt;strong&gt;Postgres Bridge&lt;/strong&gt; section above. The only practical adjustment is identifier quoting (&lt;code&gt;"…"&lt;/code&gt; instead of &lt;code&gt;[…]&lt;/code&gt;). The layout rules for &lt;code&gt;SELECT&lt;/code&gt; field lists, &lt;code&gt;WHERE&lt;/code&gt; clauses, &lt;code&gt;FROM&lt;/code&gt; clauses and &lt;code&gt;JOIN&lt;/code&gt; indentation are engine-neutral.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is an auto-formatter like sqlfluff or pgFormatter enough — or do you still need to format manually?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Auto-formatters deliver layout, but not the learning effect. Anyone who only pipes a 200-line statement through a formatter hasn't read the statement. Anyone who structures it manually builds the mental models of table relationships — and often spots logical errors along the way. Pragmatic workflow: manual first, then the formatter as a finishing pass for consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about &lt;code&gt;MATERIALIZED&lt;/code&gt; / &lt;code&gt;NOT MATERIALIZED&lt;/code&gt; for CTEs in Postgres?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since version 12, Postgres folds a non-recursive, side-effect-free CTE into the parent query by default when it is referenced exactly once — predicate pushdown is then possible. With multiple references, Postgres does not fold such a CTE by default and treats it as a separate computation instead. &lt;code&gt;MATERIALIZED&lt;/code&gt; forces this separate computation, &lt;code&gt;NOT MATERIALIZED&lt;/code&gt; allows folding even with multiple references. For performance-critical queries with expensive CTEs, the &lt;a href="https://www.postgresql.org/docs/current/queries-with.html" rel="noopener noreferrer"&gt;Postgres docs on &lt;code&gt;WITH&lt;/code&gt; queries&lt;/a&gt; are worth a read. SQL Server has no corresponding syntax: a CTE there is not an independently materialized object, it is optimized as part of the overall statement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do I find Part 1 (identifiers, delimiters, commas, aliases)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Part 1 — Formatting SQL Statements&lt;/a&gt;. It covers the smaller building blocks: regular vs. delimited identifiers, leading vs. trailing comma, systematic &lt;code&gt;T01&lt;/code&gt;/&lt;code&gt;T02&lt;/code&gt; aliases, qualified column names. Part 1 + Part 2 together form a complete style guide for &lt;code&gt;SELECT&lt;/code&gt; statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part of the series:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-1/" rel="noopener noreferrer"&gt;Formatting SQL Statements (Part 1) — Identifiers, Delimiters, Commas, Aliases&lt;/a&gt;&lt;/strong&gt; — the vocabulary layer: regular vs. delimited identifiers, comma position, systematic aliases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/structuring-and-formatting-sql-statements/" rel="noopener noreferrer"&gt;Structuring and Formatting SQL Statements — The Cluster Path&lt;/a&gt;&lt;/strong&gt; — the hub article with the reading path through all formatting topics on this blog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Upstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;The Functional Aesthetics of SQL&lt;/a&gt;&lt;/strong&gt; — the “why” of SQL formatting: Structured code is faster to read, review, and change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Downstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/inline-comments-in-complex-sql-statements/" rel="noopener noreferrer"&gt;Inline Comments in Complex SQL Statements&lt;/a&gt;&lt;/strong&gt; — the meta layer: inline and block comments that document the structure shown here without destroying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/editor-options-in-ssms/" rel="noopener noreferrer"&gt;Editor Options in SSMS&lt;/a&gt;&lt;/strong&gt; — the editor basis for this layout: tab width, spaces instead of tabs, block selection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;SQL Conventions // PL/pgSQL Procedures You Can Still Read in Two Years&lt;/a&gt;&lt;/strong&gt; — the continuation of the statement layout for Postgres procedures.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>postgres</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Formatting SQL Statements (Part 1) — Identifiers, Delimiters, Commas, Aliases</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Thu, 27 Aug 2026 12:10:05 +0000</pubDate>
      <link>https://dev.to/marcus1968/formatting-sql-statements-part-1-identifiers-delimiters-commas-aliases-4mi0</link>
      <guid>https://dev.to/marcus1968/formatting-sql-statements-part-1-identifiers-delimiters-commas-aliases-4mi0</guid>
      <description>&lt;p&gt;Anyone who has ever had to debug a badly or completely unformatted SELECT with 30 columns and half a dozen joins knows the feeling: it isn't the SQL that eats up your day — it's hunting down what the statement is actually trying to do. SQL formatting isn't a matter of taste; it's a maintenance tool — and it starts with a naming convention.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;→ Part of a series.&lt;/strong&gt; This is part 1 and covers identifiers, delimiters, commas, and aliases. For the layout of longer statements, continue with &lt;strong&gt;&lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-2/" rel="noopener noreferrer"&gt;Part 2 — Structure and Formatting&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What this article covers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifiers and delimiters&lt;/strong&gt; — when to use square brackets, when quotation marks, and what counts as a regular identifier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comma, semicolon, spacer&lt;/strong&gt; — the small separators with a big impact on readability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table aliases and qualified column names&lt;/strong&gt; — why systematic &lt;code&gt;T01&lt;/code&gt;-style aliases scale better in wide statements than mnemonic abbreviations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Box selection&lt;/strong&gt; as the killer argument for leading commas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SSMS serves as the example editor. The principles apply equally to DataGrip, VS Code, and DBeaver. The SQL examples reference &lt;code&gt;AdventureWorksDW2017&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Naming Convention at All?
&lt;/h2&gt;

&lt;p&gt;Capital letters are an established device in most written languages to emphasise individual words. In programming, that translates into notations such as &lt;strong&gt;CamelCase&lt;/strong&gt; (each compound word starts with a capital), &lt;strong&gt;camelCase&lt;/strong&gt; (the same, except the first word is lower case), or &lt;strong&gt;snake_case&lt;/strong&gt; (everything lower case with underscores between words). Conventions like these are bundled into a &lt;strong&gt;naming convention&lt;/strong&gt; — a deliberate decision about how identifiers, function names, and data types should be written. In practice, however, you'll often find that developers don't even hold to their &lt;em&gt;own&lt;/em&gt; preferred convention.&lt;/p&gt;

&lt;p&gt;Sticking to a naming convention improves readability of any text, and of code in particular. tHE SAME Sentence Written Once Again With DEVIATIONS from the generally KNOWN convention that nouns are CAPITALISED and verbs and adjectives are NOT — and the text Turns Unreadable: "adherence to a NAMING convention IMPROVES The readability of text In General And of code In Particular."&lt;/p&gt;

&lt;p&gt;The following statement comes from the view &lt;code&gt;vTimeSeries&lt;/code&gt; in the database &lt;code&gt;AdventureWorksDW2017&lt;/code&gt;, slightly reworked. It does not follow any naming convention:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModelRegion&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TimeIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calendaryear&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfbuildiso8601date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;CALENDARYEAR&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="mi"&gt;9&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;reportingdate&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vDMPrep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-250'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-750'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the version below, function names are upper case, every field name uses delimiters, and data types are lower case. Every element is laid out consistently:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModelRegion&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TimeIndex&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;]([&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReportingDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;vDMPrep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; 
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-250'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-750'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; 
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;]([&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether you like the formatting or not is a personal call. Either way, the second statement is, at a glance, tidier and easier to grasp.&lt;/p&gt;

&lt;p&gt;The sections that follow walk through the most important parts of a naming convention. The list is not exhaustive; treat it as a starting point. One thing is worth saying up front: some of it is technical fact (how &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; behaves, say, or how Postgres folds case), but most of it is reasoned convention — a choice a team can just as well make differently, as long as it makes that choice consistently. A &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-2/" rel="noopener noreferrer"&gt;second part&lt;/a&gt; picks up with best practices for the structure of SQL statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regular Identifiers
&lt;/h2&gt;

&lt;p&gt;Every database object has a name — its &lt;strong&gt;identifier&lt;/strong&gt;. Each database vendor defines rules for what a valid identifier looks like. In SQL Server, identifiers are typically capped at 128 characters and must not contain spaces. The exact definition of a regular identifier is in the online documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17" rel="noopener noreferrer"&gt;learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For practical purposes, though, that definition is framed too generously. A convention worth having draws tighter boundaries than the database system demands.&lt;/p&gt;

&lt;p&gt;Before settling on a spelling style, take a look at how your database actually treats identifiers. &lt;strong&gt;SQL Server&lt;/strong&gt; with a case-insensitive collation does not distinguish between upper and lower case: &lt;code&gt;FactInternetSales&lt;/code&gt; and &lt;code&gt;factinternetsales&lt;/code&gt; point to the same table. That is the usual situation, since the installation defaults are case-insensitive, but the collation is in fact selectable per instance, per database, and even per column. Case-insensitivity gives you the freedom to choose an emphatic notation such as &lt;strong&gt;CamelCase&lt;/strong&gt; without breaking your statement. &lt;strong&gt;Postgres&lt;/strong&gt;, on the other hand, folds unquoted identifiers to lower case: &lt;code&gt;FactInternetSales&lt;/code&gt; becomes &lt;code&gt;factinternetsales&lt;/code&gt; internally. You can still write CamelCase without quotes, but it loses its capitalisation along the way. The only way to keep it is &lt;code&gt;"double quotes"&lt;/code&gt;, which becomes a burden on every statement you write. That is why &lt;strong&gt;snake_case&lt;/strong&gt; is the common convention in the Postgres world.&lt;/p&gt;

&lt;p&gt;Behind this blog are ten years of SQL Server practice with CamelCase as a settled convention. Since the switch to Postgres three years ago, snake_case has taken over there. The full depth of case-sensitivity differences between the two engines deserves its own article, and one is on the way. The examples here stay with the CamelCase notation that is typical for SQL Server and comes with &lt;code&gt;AdventureWorksDW2017&lt;/code&gt; anyway.&lt;/p&gt;

&lt;p&gt;The underscore &lt;code&gt;_&lt;/code&gt; is a widely used word separator and a legal part of a regular identifier. In the SQL Server world it can nonetheless be avoided for the most part: CamelCase serves the same purpose and keeps identifiers more compact.&lt;/p&gt;

&lt;p&gt;Special characters such as &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, and &lt;code&gt;$&lt;/code&gt; are permitted by the definition, but under this blog's convention they have no place in an identifier. Technically they are allowed even without delimiters inside a regular identifier; they only carry a special meaning in leading position, where &lt;code&gt;@&lt;/code&gt; marks a variable and &lt;code&gt;#&lt;/code&gt; a temporary table. That double role is precisely the problem. Using the characters in column or table identifiers creates visual noise and invites confusion with those special meanings:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;English&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;DayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Spanish&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;DayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FrenchDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;French&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the definition, regular identifiers may even contain Unicode letters from any language. This blog's recommendation nonetheless limits the choice to the letters of the Latin alphabet &lt;code&gt;[a-zA-Z]&lt;/code&gt; plus the digits &lt;code&gt;[0-9]&lt;/code&gt; where digits are unavoidable. That reduces the rules for a good identifier to just two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Letters of the Latin alphabet only&lt;/li&gt;
&lt;li&gt;Digits as a fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A disciplined approach to regular identifiers always goes hand in hand with the development of a naming convention for the objects themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delimiters
&lt;/h2&gt;

&lt;p&gt;Once an identifier contains a space, it stops being a regular identifier. Using non-regular identifiers is bad style. They are nonetheless permitted as long as they are wrapped either in quotation marks or in square brackets.&lt;/p&gt;

&lt;p&gt;Double quotation marks are the form the SQL standard provides for delimiting identifiers. Microsoft additionally allows square brackets, diverging from the standard. This blog prefers the brackets as the proprietary form of delimiter.&lt;/p&gt;

&lt;p&gt;Delimiters set identifiers apart clearly from the other language elements of a SQL statement and so contribute a great deal to readability. This blog's convention therefore applies delimiters throughout, regardless of whether an identifier is regular or not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schemas&lt;/li&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Views&lt;/li&gt;
&lt;li&gt;Column names&lt;/li&gt;
&lt;li&gt;Aliases&lt;/li&gt;
&lt;li&gt;All programmable objects (functions, stored procedures etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a deliberate house convention, not a general best practice. For regular identifiers the delimiters are optional according to the Microsoft documentation, and plenty of teams quote only where they have to. Whoever opts for quoting throughout gains the visual separation, at the price of more characters per identifier.&lt;/p&gt;

&lt;p&gt;The statement below is identical to the second one in the overview, but written without any delimiters:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; 
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ModelRegion&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&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;TimeIndex&lt;/span&gt; 
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quantity&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;Quantity&lt;/span&gt; 
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&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;ReportingDate&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vDMPrep&lt;/span&gt; 
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; 
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-250'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Road-750'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; 
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; 
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Mountain-100'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'M200'&lt;/span&gt; 
 &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-150'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R250'&lt;/span&gt; 
 &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Road-650'&lt;/span&gt;     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'R750'&lt;/span&gt; 
 &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Touring-1000'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'T1000'&lt;/span&gt; 
 &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
 &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Region&lt;/span&gt;
 &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;
 &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Month&lt;/span&gt;
 &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udfBuildISO8601Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CalendarYear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plenty of auto-generated scripts from Microsoft tooling use square brackets as delimiters. That is not a documented recommendation, but it is an observable pattern: SQL Server Management Studio (SSMS) emits brackets in the SELECT and DDL statements generated through the context menu on a table. Microsoft is not entirely consistent here, though: when you create a view through the wizard, or look at the SQL panel of the Edit feature, delimiters are dropped wherever they can be. The two screenshots below — both showing auto-generated SQL — make a fine case study in unmaintainable code:&lt;/p&gt;

&lt;h3&gt;
  
  
  View
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrovljrx9eifisy44nni.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrovljrx9eifisy44nni.png" alt="The SSMS view designer: a relationship diagram of four tables (f, p, d, c) with column lists and join lines at the top, a column grid in the middle with entries such as " width="500" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit Feature
&lt;/h3&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpyq8hbfxgw4ommibwi86.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpyq8hbfxgw4ommibwi86.png" alt="The SSMS Edit feature: at the top an auto-generated SELECT TOP (200) against DimDate without delimiters, below it a data grid with five sample rows (DateKey 20050101–20050105) showing the English, Spanish, and French day names." width="500" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One restriction applies to quotation marks as delimiters: they only work when the SQL Server setting &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; is set to &lt;code&gt;ON&lt;/code&gt; (the default in SSMS and in the common client libraries):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;QUOTED_IDENTIFIER&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the setting &lt;code&gt;OFF&lt;/code&gt;, SQL Server reads double quotes as string literals, and non-regular identifiers can only be written with square brackets. The brackets work regardless of this setting — a practical advantage of the proprietary form. &lt;code&gt;ON&lt;/code&gt; is more than a matter of style, too: several SQL Server features require it, among them indexed views, indexes on computed columns, and filtered indexes.&lt;/p&gt;

&lt;p&gt;Details in the online documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql?view=sql-server-ver17" rel="noopener noreferrer"&gt;learn.microsoft.com/en-us/sql/t-sql/statements/set-quoted-identifier-transact-sql&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spacer
&lt;/h2&gt;

&lt;p&gt;What is meant here is the vertical spacer, that is, the blank line. The natural reading direction of a SQL statement is left to right and top to bottom.&lt;/p&gt;

&lt;p&gt;While general-purpose code consists of many short statements, SQL is designed to do a lot of work in a single statement. An SQL statement can easily span a hundred lines or more. That makes writing a good SQL statement a particular challenge. A key criterion for grasping the structure and intent of a statement quickly isn't only a clear layout and consistent formatting, but also compactness. On a typical monitor at a sensible resolution, SSMS shows around forty lines of SQL when only a query window is open and no result pane. In day-to-day use, twenty-five to thirty lines is the realistic maximum.&lt;/p&gt;

&lt;p&gt;There are colleagues in our trade who insert a blank line after every line&lt;/p&gt;

&lt;p&gt;of code. Excessive blank lines force the reader to lean on the navigation&lt;/p&gt;

&lt;p&gt;keys or the mouse wheel just to get from one end of the statement to the&lt;/p&gt;

&lt;p&gt;other. Worse, the overall context of the statement becomes much harder&lt;/p&gt;

&lt;p&gt;to take in.&lt;/p&gt;

&lt;p&gt;Blank lines can be a useful stylistic device to separate logical blocks. Overusing them, however, makes the statement harder to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semicolon
&lt;/h2&gt;

&lt;p&gt;The SQL standard provides for the semicolon as the terminator of a statement. SQL Server, at least, is forgiving here and doesn't force you to use one. There are only a handful of cases where the semicolon is mandatory.&lt;/p&gt;

&lt;p&gt;One example: when using a &lt;em&gt;Common Table Expression&lt;/em&gt;, the statement preceding the keyword &lt;code&gt;WITH&lt;/code&gt; must end with a semicolon. To sidestep the problem, many developers write the leading &lt;code&gt;WITH&lt;/code&gt; as &lt;code&gt;;WITH&lt;/code&gt;. Formally, the semicolon belongs to the preceding statement — &lt;code&gt;;WITH&lt;/code&gt; is a compatibility idiom, not a syntax of its own.&lt;/p&gt;

&lt;p&gt;Other engines are stricter, or more precisely their tooling and procedural languages are. A single statement handed to &lt;strong&gt;Postgres&lt;/strong&gt; directly by a driver works without a trailing semicolon. In &lt;code&gt;psql&lt;/code&gt; and in multi-statement scripts such as &lt;code&gt;pg_dump&lt;/code&gt; output, the semicolon marks the end of a statement, and inside PL/pgSQL function bodies it is part of the syntax as a statement terminator. &lt;strong&gt;Oracle&lt;/strong&gt; follows the same pattern: in PL/SQL blocks (&lt;code&gt;BEGIN … END;&lt;/code&gt;) the semicolon is required, while in clients such as SQL*Plus its role depends on the execution context.&lt;/p&gt;

&lt;p&gt;Using the semicolon consistently is a sign of care either way, and it helps readability. What it does not do is make a statement portable — SQL dialects differ in entirely different places, from data types through functions to the procedural language. What it does remove is an unnecessary syntactic dependency on the client and the execution context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comma
&lt;/h2&gt;

&lt;p&gt;Field lists in SQL are separated by commas. Some developers put the comma before the field name, others put it after. The pro/con discussion usually centres on how easy it is to add or remove a field. The real reason the comma belongs at the front, though, is readability and the option to format the statement with &lt;strong&gt;box selection&lt;/strong&gt; (called "column editor" or "Spaltenauswahl" in SSMS, "Box Selection" in VS Code and DataGrip). There is more on box selection in the article &lt;a href="https://sql.marcus-belz.de/en/functional-design-aesthetics-of-sql/" rel="noopener noreferrer"&gt;The Functional Aesthetics of SQL&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leading Comma
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&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 want to add another field, say &lt;code&gt;FrenchDayNameOfWeek&lt;/code&gt;, it's easier to add it at the end — you only insert the text &lt;code&gt;,[FrenchDayNameOfWeek]&lt;/code&gt; after &lt;code&gt;[SpanishDayNameOfWeek]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;FrenchDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&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 want the new field at position one, you need two edits: insert the line &lt;code&gt;[FrenchDayNameOfWeek]&lt;/code&gt; before &lt;code&gt;[EnglishDayNameOfWeek]&lt;/code&gt;, and prepend a comma to &lt;code&gt;[EnglishDayNameOfWeek]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FrenchDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;EnglishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;SpanishDayNameOfWeek&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimDate&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trailing Comma
&lt;/h3&gt;

&lt;p&gt;When the comma is written after each field, the situation reverses: it's easier to add a new field at position one than at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Readability
&lt;/h3&gt;

&lt;p&gt;The comma is a separator. It marks the transition from one field to the next. When it sits at the end of each field, it loses its separating character because field names have different lengths.&lt;/p&gt;

&lt;p&gt;In the statement below, it isn't immediately obvious whether the identifier &lt;code&gt;[ProductName]&lt;/code&gt; is a column name or has some other role. The reader has to look at the line above to realise that &lt;code&gt;[ProductName]&lt;/code&gt; is the alias for &lt;code&gt;[EnglishProductName]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the comma sits in front of each field, the ambiguity goes away. It is immediately clear that &lt;code&gt;[ProductName]&lt;/code&gt; is not a separate field — there is no comma in front of it, so it must belong to the line above:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Comma and Box Selection
&lt;/h3&gt;

&lt;p&gt;Adding aliases to every field of the following statement is a little awkward:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every comma has to move at least one position to the right, and the keyword &lt;code&gt;AS&lt;/code&gt; and the alias have to be inserted in front of it. That applies to every row of the field list, one at a time, by hand:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasEnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;ListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasListPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="n"&gt;DealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AliasDealerPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same end result can be achieved with far less work if the SQL statement is laid out so that box selection works on it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx63qjwoclb3c8pone8ib.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx63qjwoclb3c8pone8ib.png" alt="SSMS editor showing the DimProduct SELECT in box-selection-ready form: to the right of each column are the aligned aliases AS [AliasEnglishProductName], AS [AliasSize], AS [AliasColor] and others. The prefix Alias is highlighted as a blue box-selection band spanning all five rows — a single keystroke applies to every row simultaneously." width="415" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No commas need to move. With box selection, &lt;code&gt;AS&lt;/code&gt; is typed once, the field names are used as the basis for the alias, copied as a block after &lt;code&gt;AS&lt;/code&gt;, and the prefix &lt;code&gt;Alias&lt;/code&gt; is prepended — again with box selection. The best part: the effort is essentially independent of the number of rows you have to process.&lt;/p&gt;

&lt;p&gt;Box selection becomes truly effective only with leading commas. That is the killer argument for putting the comma at the front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on the SSMS bias.&lt;/strong&gt; This argument is largely about SSMS and its classic box selection. &lt;strong&gt;DataGrip&lt;/strong&gt; and other modern editors offer more powerful refactoring tools — multi-cursor at arbitrary positions, automatic alias generation via refactor commands, semantic search-and-replace across the whole codebase. In that world you save the same effort even without leading commas. The box-selection argument carries less weight there. The &lt;strong&gt;readability arguments&lt;/strong&gt; (see the "Readability" sub-section) hold regardless of the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function Names
&lt;/h2&gt;

&lt;p&gt;SSMS highlights function names in pink, which makes them easy enough to spot. Since not every editor does syntax highlighting, function names should also be visually distinguishable by consistent upper- or lower-case spelling.&lt;/p&gt;

&lt;p&gt;In other words: function names should be either fully upper case or fully lower case.&lt;/p&gt;

&lt;p&gt;Microsoft itself, like most database vendors, writes function names in upper case in the online documentation. That documentation spelling is not a binding standard, but it is a convenient orientation — this blog's convention follows the upper-case form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Aliases
&lt;/h2&gt;

&lt;p&gt;A widely used practice is to derive a table alias as a "speaking" abbreviation from the table name. For the table &lt;code&gt;FactInternetSales&lt;/code&gt; one might use the alias &lt;code&gt;IS&lt;/code&gt;, taking the starting letters of the compound words in the table name (ignoring the &lt;code&gt;Fact&lt;/code&gt; prefix). Since &lt;code&gt;IS&lt;/code&gt; is a reserved word, that alias must be wrapped in delimiters. The example also shows a weakness of mnemonic abbreviations: they can collide with reserved words and then force additional quoting. For other tables, you might end up with aliases like these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DimCustomer&lt;/td&gt;
&lt;td&gt;CUST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DimProduct&lt;/td&gt;
&lt;td&gt;P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DimProductCategory&lt;/td&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DimProductSubcategory&lt;/td&gt;
&lt;td&gt;PSC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A SELECT statement built on those tables might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;CUST&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;CUST&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;PSC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductSubCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactInternetSales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimCustomer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CUST&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CUST&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductSubcategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;PSC&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PSC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductCategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;PC&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;PC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PSC&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uneven indentation of the field names is a side-effect of the different lengths of the aliases — somewhere between one and four characters. The field list looks "restless", and as soon as more language elements join the party (functions, CASE expressions etc.) it can become hard to read quickly.&lt;/p&gt;

&lt;p&gt;Now imagine a SELECT over twenty tables or more. At some point it becomes hard to come up with a meaningful alias for every table. From roughly the fifth alias onward — that is a rule of thumb, not a hard boundary — the derivation from table names rarely improves readability any further, because the aliases are simply too cryptic.&lt;/p&gt;

&lt;p&gt;Wouldn't systematic — possibly even indexed — aliases be easier to identify in a complex statement?&lt;/p&gt;

&lt;p&gt;A systematic alias scheme could be defined like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Aliases must be a fixed number of characters&lt;/li&gt;
&lt;li&gt;Aliases are indexed (with one or more leading letters)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If, for instance, you use the letter &lt;code&gt;T&lt;/code&gt; for &lt;em&gt;Table&lt;/em&gt; (or &lt;code&gt;F&lt;/code&gt; for &lt;em&gt;Fact table&lt;/em&gt;, &lt;code&gt;D&lt;/code&gt; for &lt;em&gt;Dimension&lt;/em&gt;, etc.) followed by a two-digit 1-based index, you end up with aliases such as &lt;code&gt;T01&lt;/code&gt;, &lt;code&gt;T02&lt;/code&gt;, &lt;code&gt;D01&lt;/code&gt;, &lt;code&gt;F01&lt;/code&gt;, ….&lt;/p&gt;

&lt;p&gt;Using those aliases, the statement above reads more cleanly:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T05&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;EnglishProductSubCategoryName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;FactInternetSales&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T01&lt;/span&gt;
 &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimCustomer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;
 &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T02&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;CustomerKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProduct&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;
 &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductSubcategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;
 &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductSubcategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;DimProductCategory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;T05&lt;/span&gt;
 &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;ON&lt;/span&gt;
 &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;T05&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;T04&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;ProductCategoryKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field names in the SELECT list line up cleanly.&lt;/p&gt;

&lt;p&gt;The real killer argument is the same here, too: aliases of equal length are what makes box selection useful in the first place.&lt;/p&gt;

&lt;p&gt;There's another important reason to use aliases at all, and it applies to mnemonic and systematic ones alike: &lt;strong&gt;Intellisense&lt;/strong&gt; (in SSMS) only really works once aliases are in place. When the developer types an alias followed by a dot, a context menu opens listing only the columns of the corresponding table.&lt;/p&gt;

&lt;p&gt;When the cursor sits after the dot, the context menu can also be invoked manually with the shortcut &lt;code&gt;Ctrl+Space&lt;/code&gt;. It works without a preceding alias as well. In that case, however, SSMS effectively offers the entire T-SQL vocabulary at once.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxirpk0l903uhhdhrxox.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxirpk0l903uhhdhrxox.png" alt="The SSMS Intellisense context menu appears after typing T01. — it lists only the columns of the table bound to the alias, FactInternetSales (CarrierTrackingNumber highlighted, CurrencyKey, CustomerKey, CustomerPONumber, DiscountAmount, DueDate, DueDateKey, ExtendedAmount, Freight, …). A tooltip on the right shows the data type of the highlighted column: " width="480" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Aliases make SQL code easier to read and therefore more maintainable. In statements spanning several tables they are practically indispensable. This blog's convention applies them to simple single-table statements as well, for the sake of uniformity and Intellisense comfort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qualified Column Names
&lt;/h2&gt;

&lt;p&gt;As soon as a statement references multiple tables, sooner or later the same column name appears in more than one of them and stops being unambiguous. In the example below we want to return name and phone number for both the employee and the reseller of a &lt;code&gt;FactResellerSales&lt;/code&gt; fact:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr25r7oln125j57oh3m2.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmr25r7oln125j57oh3m2.png" alt="SSMS editor showing the FactResellerSales SELECT: the field list contains [Phone] AS [Employee_Phone] and [Phone] AS [Reseller_Phone] twice without any table alias prefix. The FROM clause uses the T01/T02/T03 aliases but the SELECT never references them — the statement is ambiguous and not executable." width="357" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The statement is not executable because the column &lt;code&gt;Phone&lt;/code&gt; exists in both dimensions &lt;code&gt;DimEmployee&lt;/code&gt; and &lt;code&gt;DimReseller&lt;/code&gt;. SQL Server aborts with two errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  1: Msg 209, Level 16, State 1, Line 4
  2: Ambiguous column name 'Phone'.
  3: Msg 209, Level 16, State 1, Line 6
  4: Ambiguous column name 'Phone'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specifying a table alias in front of the column name is mandatory here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;T01&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Employee_Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or — if no table aliases are used — the table name itself has to precede the column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DimEmployee&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Employee_Phone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latter, however, does nothing for readability.&lt;/p&gt;

&lt;p&gt;A fully qualified object name in SQL Server consists of up to four parts: server, database, schema, and object. A column reference adds the column as a fifth element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="k"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example with fully qualified table names that include the database name:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favzm2ya138b2wouf2n7s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Favzm2ya138b2wouf2n7s.png" alt="SSMS editor showing the same FactResellerSales SELECT as before, this time with fully qualified table names [AdventureWorksDW2017].[dbo].[FactResellerSales] T01, [AdventureWorksDW2017].[dbo].[DimEmployee] T02, and [AdventureWorksDW2017].[dbo].[DimReseller] T03 in the FROM/JOIN clause — the database name is baked into the table reference and binds the statement to this specific database." width="462" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any qualification beyond the schema prevents the application from being deployable in a database that doesn't share the name &lt;code&gt;AdventureWorksDW2017&lt;/code&gt;. Cases like that do occur in practice: ETL pipelines whose tables were fully qualified down to the database name (&lt;code&gt;[Database].[Schema].[Table].[Column]&lt;/code&gt;) could not be deployed to production once they were finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engine note: Postgres.&lt;/strong&gt; The five-part scheme &lt;code&gt;Server.Database.Schema.Table.Column&lt;/code&gt; only applies in SQL Server. &lt;strong&gt;Postgres&lt;/strong&gt; by default does &lt;strong&gt;not&lt;/strong&gt; allow cross-database queries: a cluster does hold several databases, but a connection is bound to exactly one of them, so the &lt;code&gt;Database.&lt;/code&gt; element doesn't exist in practice. If you really need to read or write across database boundaries, you'll need the extensions &lt;code&gt;postgres_fdw&lt;/code&gt; (Foreign Data Wrapper) or &lt;code&gt;dblink&lt;/code&gt;. Both are extra setup, not a default. In practical terms this means that within a Postgres database, &lt;code&gt;Schema.Table.Column&lt;/code&gt; (three parts) is the relevant qualification, while in SQL Server it is one option among several. The recommendation stays the same in both worlds: &lt;strong&gt;never qualify beyond the schema level&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;A naming convention is the entry point to maintainable SQL code: regular identifiers built from letters and, where needed, digits; delimiters applied consistently; function names in upper case. The small separators have the largest effect of all. The leading comma keeps field lists readable and ready for box selection, the semicolon makes statements unambiguous towards the client and the execution context, and sparing blank lines keep the whole picture on a single screen.&lt;/p&gt;

&lt;p&gt;Systematic aliases such as &lt;code&gt;T01&lt;/code&gt; play to their strength in wide statements and line the field list up cleanly. Qualified column names belong up to the schema level and no further. Which individual rule you pick matters less than it seems: SQL becomes maintainable through a consistent convention that makes structure visible and changes easy. &lt;a href="https://sql.marcus-belz.de/en/formatting-sql-statements-part-2/" rel="noopener noreferrer"&gt;Part 2 — Structure and Formatting&lt;/a&gt; builds on this and covers the layout of longer statements.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Leading comma — isn't that unusual?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When reading, the comma loses its separating role if it sits at the end of a line (see the section "Readability"). When writing, the leading comma saves the most effort the moment box selection comes into play. Modern auto-formatters make the comma position configurable. &lt;code&gt;sqlfluff&lt;/code&gt; (T-SQL and Postgres) places commas at the end of the line by default and only switches to leading commas through its layout configuration (&lt;code&gt;line_position = leading&lt;/code&gt;). &lt;code&gt;pgFormatter&lt;/code&gt; (Postgres) likewise defaults to the trailing comma and offers &lt;code&gt;--comma-start&lt;/code&gt; for leading ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need square brackets in Postgres too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Postgres does &lt;strong&gt;not&lt;/strong&gt; support &lt;code&gt;[brackets]&lt;/code&gt;. Its delimiters are double quotes: &lt;code&gt;"EnglishProductName"&lt;/code&gt;. There is no &lt;code&gt;QUOTED_IDENTIFIER&lt;/code&gt; setting either. Identifier quoting in Postgres is always on. Postgres folds identifiers without quotes to lower case (&lt;code&gt;MyColumn&lt;/code&gt; becomes &lt;code&gt;mycolumn&lt;/code&gt;). Identifiers in quotes keep their spelling and have to be quoted exactly that way on every reference. If your codebase is multi-engine in the long run, &lt;code&gt;"double quotes"&lt;/code&gt; are ANSI-SQL-compliant and work in both worlds — the &lt;code&gt;[brackets]&lt;/code&gt; are SQL Server-specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Systematic &lt;code&gt;T01&lt;/code&gt;-style aliases vs. speaking aliases — which is better?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a trade-off. Speaking aliases (&lt;code&gt;CUST&lt;/code&gt;, &lt;code&gt;P&lt;/code&gt;, &lt;code&gt;PC&lt;/code&gt;) are mnemonic and work fine as long as the statement only has three or four tables. As soon as five or more tables come into play — typical for ETL pipelines or wide reporting queries — the mnemonic edge fades and the alignment suffers from the varying lengths. Systematic &lt;code&gt;T01&lt;/code&gt;-style aliases scale, stay the same length, and make box selection effortless. The price is semantic: &lt;code&gt;T05.[CustomerKey]&lt;/code&gt; doesn't tell you the table. Anyone who wants to know jumps to the FROM clause. Systematic aliases optimise for visual structure and mechanical editing, then, while speaking aliases optimise for orientation in the content. As a rule of thumb, this blog uses speaking aliases up to four tables and systematic ones from five onward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if my team uses a different convention?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consistency beats purity. If the team has settled on trailing commas or speaking aliases, applying that convention consistently is more important than picking the "better" variant.&lt;/p&gt;

&lt;p&gt;In practice this is one of the hardest points overall. In every larger team, each developer has a very personal sense of what "looks good" and what doesn't. That individual matter of taste is exactly what stands in the way of team-wide uniformity, and experience across many projects bears this out again and again. All the more reason for the convention that the team has agreed on to be applied consistently across every team member. Only then do the SQL statements stay readable and maintainable for everyone involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-cursor — does it replace box selection?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern editors like VS Code, DataGrip, and DBeaver provide true multi-cursor support (&lt;code&gt;Ctrl+Alt+Down Arrow&lt;/code&gt; or &lt;code&gt;Alt+Click&lt;/code&gt;) that goes beyond the rectangular box selection: cursors can sit at arbitrary positions rather than only along a rectangular column. SSMS only has classic box selection (&lt;code&gt;Alt+Shift+Arrow&lt;/code&gt;). Azure Data Studio, once part of this line-up, was retired by Microsoft on 28 February 2026; the recommended successor is VS Code with the mssql extension. Conceptually, multi-cursor covers the same need and goes beyond it. Box-selection-friendly formatting remains the prerequisite either way. If you work across several editors, format once cleanly rather than once per tool.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>database</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Three Severity Levels, Not Pass/Fail — and Why Severity Decides Where the Rule Lives</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:39:01 +0000</pubDate>
      <link>https://dev.to/marcus1968/three-severity-levels-not-passfail-and-why-severity-decides-where-the-rule-lives-ecn</link>
      <guid>https://dev.to/marcus1968/three-severity-levels-not-passfail-and-why-severity-decides-where-the-rule-lives-ecn</guid>
      <description>&lt;p&gt;Most data quality systems don't die of bad checks. They die of a missing field in the rule model: the severity. A missing country code in three out of 80,000 rows blocks the nightly load, someone switches the check off "temporarily", and from that moment everything runs unchecked. Steering data quality with &lt;strong&gt;severity levels&lt;/strong&gt; instead of binary pass/fail escapes that trap. And severity can do more: it decides whether a rule belongs in the target schema as a constraint or in the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binary pass/fail ends in one of two dead ends:&lt;/strong&gt; either a triviality blocks the whole load, or everything passes and nobody reads the findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three levels are enough:&lt;/strong&gt; an error blocks, a warning is visible without blocking, an information merely documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity is a routing decision:&lt;/strong&gt; only an error rule is a candidate for a &lt;code&gt;CHECK&lt;/code&gt; or &lt;code&gt;UNIQUE&lt;/code&gt; constraint in the target schema — warnings and information stay in the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promoting has a price:&lt;/strong&gt; when a warning becomes an error, the existing data suddenly has to be clean. &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; make the transition controllable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The limits can be named precisely:&lt;/strong&gt; cross-row, cross-table and time-based rules are beyond what a constraint can do. Those belong in the pipeline, permanently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres as the example engine and the framework from the sub-hub &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; with the shared error table, the rule configuration &lt;code&gt;dq.check_rule&lt;/code&gt; and the quality gate. The principle itself is engine-neutral and applies just as well to SQL Server or any other system with constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why binary fails
&lt;/h2&gt;

&lt;p&gt;A binary check system knows exactly one question per record: pass or fail. That sounds clean and leads, in practice, into one of two dead ends.&lt;/p&gt;

&lt;p&gt;Dead end one: &lt;strong&gt;everything that fails blocks.&lt;/strong&gt; Then a missing email address stops the same load as a duplicate customer number. The process stands still, the business day waits, and the person on call makes a decision at three in the morning that actually belongs in a rule review: they switch the check off. Not out of carelessness, but because the system offers them no other gradation.&lt;/p&gt;

&lt;p&gt;Dead end two: &lt;strong&gt;nothing blocks, everything is merely logged.&lt;/strong&gt; Then a findings table fills up that somebody still reads in the beginning. A few weeks later thousands of entries sit there, important ones next to trivial ones, and because the system itself doesn't say which of them demand action, at some point nobody looks anymore. The check keeps running and is dead anyway.&lt;/p&gt;

&lt;p&gt;Both reflexes are understandable. The problem is not that people handle the system badly. The problem is that a binary model withholds the one piece of information that should steer the handling: how bad is this finding?&lt;/p&gt;

&lt;h2&gt;
  
  
  The three levels
&lt;/h2&gt;

&lt;p&gt;Three severity levels answer exactly that question, and you rarely need more than three. The pattern is not new: syslog levels and linters have known it for decades. What makes it interesting is what each level concretely triggers in the ETL process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error (&lt;code&gt;E&lt;/code&gt;)&lt;/strong&gt; blocks. A record with an error finding does not pass the quality gate and does not reach the target layer. Error is the right level for everything the load would fail on at the target anyway: missing mandatory values, duplicate keys, unknown references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning (&lt;code&gt;W&lt;/code&gt;)&lt;/strong&gt; is visible but does not block. The record flows on, the finding stays logged. That covers the largest share of real-world data problems: things that are odd, unclean or worth a look, but must not hold up the process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Information (&lt;code&gt;I&lt;/code&gt;)&lt;/strong&gt; merely documents. No pressure to act, no blocking, pure awareness, for instance a missing phone number whose frequency you want to observe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the framework's rule model, the severity (some rule models call the field "criticality") is a column of the configuration table, with a strict default and a &lt;code&gt;CHECK&lt;/code&gt; on itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;   &lt;span class="nb"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'E'&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_check_rule_severity&lt;/span&gt;  &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'E'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'I'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default &lt;code&gt;E&lt;/code&gt; is a deliberate decision: whoever writes a rule means it seriously at first. Downgrading to a warning is an active, documented step. The opposite default would silently turn new rules into mere observers. Five rules show the three levels in the configuration table's format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;check_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;schema_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;check_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;where_clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country_code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country_code IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'E'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country code is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'age IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'E'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age is mandatory in the target schema'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'age'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'age &amp;lt; 18'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer under 18 - needs business review'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'email IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'W'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email missing - needs business review'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="s1"&gt;'constraint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'phone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'phone IS NULL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'I'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'phone number not provided - for information only'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The information rule deliberately targets a field that is optional from a business point of view: a missing phone number is not a defect, but a value whose rate you want to know. On &lt;code&gt;age&lt;/code&gt;, by contrast, two rules with different levels sit side by side: if the age is missing entirely, that is an error, because the target treats the column as mandatory. If it is merely conspicuously low, that is a warning. The choice of level is a business statement, not a technical one.&lt;/p&gt;

&lt;p&gt;What the levels mean in practice is shown by a run against seven demo rows (one clean, one per severity, and three combinations). The framework's staging table is extended by the optional column &lt;code&gt;phone&lt;/code&gt; for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+49 30 1234567'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- clean&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'b@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+49 40 2345678'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- E: country code missing&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'c@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+43 1 3456789'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;-- W: under 18&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'d@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;-- I: phone number missing&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'e@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'+41 44 5678901'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- E+E: country code and age missing&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'f@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'+49 89 6789012'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- E+W: country code missing, under 18&lt;/span&gt;
   &lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;-- E+E+W+I: everything at once&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_run_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_findings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner writes the severity counters back into the source table, one per record and level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;country_code&lt;/th&gt;
&lt;th&gt;email&lt;/th&gt;
&lt;th&gt;age&lt;/th&gt;
&lt;th&gt;phone&lt;/th&gt;
&lt;th&gt;sys_error&lt;/th&gt;
&lt;th&gt;sys_warning&lt;/th&gt;
&lt;th&gt;sys_info&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;DE&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:a@example.com"&gt;a@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;+49 30 1234567&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:b@example.com"&gt;b@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;+49 40 2345678&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:c@example.com"&gt;c@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;+43 1 3456789&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;CH&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:d@example.com"&gt;d@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:e@example.com"&gt;e@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;+41 44 5678901&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:f@example.com"&gt;f@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;+49 89 6789012&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality gate queries only one of the three columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
   &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
   &lt;span class="n"&gt;sys_error&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of the seven rows pass the gate. The warning row and the information row flow along, the four rows with at least one error stay behind — logged, with a plain-text message, findable for follow-up. The combination rows show in passing that &lt;code&gt;sys_error&lt;/code&gt; is a counter, not a flag, and that the three counters run independently of each other: row 5 carries two errors, row 6 one error and one warning, row 7 fills all three counters at once. The gate still asks only for &lt;code&gt;sys_error = 0&lt;/code&gt;. No on-call engineer has to decide at night whether a missing phone number matters more than the daily close. That decision was made long ago, in the rule review, when someone wrote &lt;code&gt;W&lt;/code&gt; into the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Severity is a routing decision
&lt;/h2&gt;

&lt;p&gt;Up to here, severity looks like a label on the finding. It can do more: it answers the question of &lt;strong&gt;where a rule may be enforced&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reasoning behind it is simple. A constraint in the target schema is the hardest form a rule can take: on its own, it knows no exception, no log line and no "flows on anyway". Only the calling application can soften that, the constraint itself knows no gradation. Only a rule whose violation must truly never reach the target deserves that hardness. That is exactly the definition of the error level. A warning, on the other hand, is &lt;em&gt;supposed&lt;/em&gt; to be allowed through, otherwise it would be an error. A constraint that rejects warning violations would contradict the rule's own definition. The routing follows from that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;E&lt;/code&gt; is a candidate for a constraint:&lt;/strong&gt; &lt;code&gt;NOT NULL&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;, &lt;code&gt;UNIQUE&lt;/code&gt;, foreign keys. The rule is enforced twice: up front as a check in the source, hard as a guarantee at the target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;W&lt;/code&gt; and &lt;code&gt;I&lt;/code&gt; stay in the pipeline:&lt;/strong&gt; they exist only as check rules, produce findings and never block. A target constraint for them is not merely unnecessary, it is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Severity is therefore not a report field but the switch that decides about constraint derivation. How the error rules of the configuration can conversely be &lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;derived from the target schema&lt;/a&gt; is shown by this series' sibling article. The coupling works in both directions, and derived rules always carry &lt;code&gt;E&lt;/code&gt; there, because their source is a hard constraint.&lt;/p&gt;

&lt;p&gt;Concretely, the routing looks like this. The target layer materializes the two error rules as &lt;code&gt;NOT NULL&lt;/code&gt; on &lt;code&gt;country_code&lt;/code&gt; and &lt;code&gt;age&lt;/code&gt;, while the warning rules and the information rule deliberately have no counterpart there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;           &lt;span class="nb"&gt;int&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;pk_customer&lt;/span&gt;  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;
   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
   &lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
   &lt;span class="n"&gt;sys_error&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="c1"&gt;-- INSERT 0 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 17-year-old row and the row without a phone number arrive as well. That is exactly the intent: the warning has served its purpose (the finding sits in the log), and the target accepts the record. What would happen if someone materialized the warning rule as a constraint after all is shown by the direct attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
   &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_customer_adult&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: check constraint ck_customer_adult of relation customer&lt;/span&gt;
&lt;span class="c1"&gt;--        is violated by some row&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ALTER TABLE&lt;/code&gt; fails on its own existing data, on precisely the row the warning was explicitly supposed to let pass. The error is not an accident but the database pointing out the contradiction: this rule was classified as &lt;code&gt;W&lt;/code&gt;, and a &lt;code&gt;CHECK&lt;/code&gt; is the enforcement form of &lt;code&gt;E&lt;/code&gt;. The same holds for the information rule with the opposite sign: its hard counterpart would be a &lt;code&gt;NOT NULL&lt;/code&gt; on &lt;code&gt;phone&lt;/code&gt; — technically perfectly expressible, but wrong, because an information must never block. The target allows the missing phone number deliberately, the pipeline merely keeps count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four places, one decision
&lt;/h2&gt;

&lt;p&gt;Zooming out of the framework, there are four places where a data quality rule can live. Each can do something the others cannot, and each has a price:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Place&lt;/th&gt;
&lt;th&gt;What it can do&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Source&lt;/strong&gt; (pre-filter before the load)&lt;/td&gt;
&lt;td&gt;finds &lt;em&gt;all&lt;/em&gt; bad records up front, classifies by severity, the load carries on with the good ones&lt;/td&gt;
&lt;td&gt;its own infrastructure: error table, rule configuration, runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Pipeline&lt;/strong&gt; (check steps in the process)&lt;/td&gt;
&lt;td&gt;the most expressive option — every rule type, every severity, trends and reports&lt;/td&gt;
&lt;td&gt;the rule lives next to the data, and every path around the pipeline bypasses it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Target schema&lt;/strong&gt; (constraints)&lt;/td&gt;
&lt;td&gt;the only guarantee that &lt;em&gt;every&lt;/em&gt; write path respects, including the manual hotfix&lt;/td&gt;
&lt;td&gt;knows only two outcomes, can only express error semantics, blocks the whole run of a set-based load when in doubt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Reporting&lt;/strong&gt; (dashboards, analyses)&lt;/td&gt;
&lt;td&gt;makes warnings and information visible over time, shows trends&lt;/td&gt;
&lt;td&gt;enforces nothing, depends on the findings of the other places&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Today's industry default clearly sits on the second place: tools like Great Expectations, Soda or dbt tests formulate checks in the pipeline, and most of them know severity gradations as well. dbt, for instance, distinguishes &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;warn&lt;/code&gt; per test. That is a workable model, not a mistake. You should just be able to name what you are buying into: a rule that lives exclusively in the pipeline is valid only there. The colleague with the direct &lt;code&gt;INSERT&lt;/code&gt;, the second load script, the weekend migration — none of them run through the pipeline, and none of them hit its checks. A constraint in the target schema has no such gap, because it sits in the data itself.&lt;/p&gt;

&lt;p&gt;The four places are therefore not competitors you pick one of. The decision is made per rule, and severity is its first criterion: error rules deserve the double floor of pre-filter plus constraint, warnings and information belong in pipeline and reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the source still checks first
&lt;/h2&gt;

&lt;p&gt;If only error rules become constraints, why check in the source at all? You could simply let the &lt;code&gt;CHECK&lt;/code&gt; do what it is there for.&lt;/p&gt;

&lt;p&gt;The answer is already in the &lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;framework article&lt;/a&gt;, and it remains fully valid under the routing view: a constraint knows only two outcomes. The row fits, or the whole load breaks. When loading thousands of rows, "breaks" is the worst of all options, because a single bad row stops the complete process, and the error message names at best that one row, not the other nine that were still waiting behind it.&lt;/p&gt;

&lt;p&gt;One qualification belongs here: this all-or-nothing applies to the set-based load, that is, a single &lt;code&gt;INSERT … SELECT&lt;/code&gt; moving all rows in one transaction. That is exactly how an ETL process developed in SQL works, and dbt belongs on this side as well, because its models compile to set-based SQL statements. Row-based ETL tools like SSIS or Talend process the rows individually instead: a failing row can be routed out through an error output there, the remaining rows carry on, and the constraint stops only that one row rather than the whole load. You pay for that convenience with the row-by-row processing itself, which is considerably slower than a set-based load, and with error handling that lives inside the tool instead of in a queryable table. The pre-filter is the set-based answer to the same need: it does what a row-based tool's error output does — just up front, in set logic and with severity levels.&lt;/p&gt;

&lt;p&gt;The error output also has a second, less obvious weakness: the checks in the data flow run sequentially. The first hit routes the row out, and the remaining checks never see it — unless you explicitly wire the error path through all further check steps, which quickly clutters the data flow. In practice that means: you fix the first error found, run the load again, find the second one, fix it, find the third. The pre-filter knows no such iterating, because every rule runs set-based across all rows. That is why row 7 of the demo sits in the log completely after a single run, with two errors, one warning and one information.&lt;/p&gt;

&lt;p&gt;The pre-filter in the source and the constraint at the target are therefore not alternatives but two halves of the same error rule. The check in the source finds &lt;em&gt;all&lt;/em&gt; records that would fail at the target, classifies them and lets the load carry on with the clean ones. The constraint at the target guarantees the rule even for everything that goes past the pre-filter: the manual hotfix, the forgotten second script. If the pre-filter fails, the load breaks loudly instead of silently accepting bad data. If the constraint is dropped, the pre-filter still checks. Only together do the two produce the property neither half has alone: complete findings &lt;em&gt;and&lt;/em&gt; a hard guarantee.&lt;/p&gt;

&lt;p&gt;So the question is not &lt;em&gt;whether&lt;/em&gt; schema or pipeline. The question is which rule deserves both — and that is answered by the severity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you promote a rule
&lt;/h2&gt;

&lt;p&gt;Rules are not static. The business decides that customers under 18 must no longer be created: the warning from above is to become an error. In the configuration that is one statement:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_rule&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
   &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'E'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
       &lt;span class="k"&gt;schema_name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;where_clause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'age &amp;lt; 18'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_run_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'staging'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_findings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the next run on, the 17-year-old row blocks at the gate (&lt;code&gt;sys_error = 1&lt;/code&gt;). That is the easy part. The demanding part follows from the routing: an error rule is a constraint candidate, so the target schema should guarantee the new hardness as well. And right here waits the price of promotion — &lt;strong&gt;the existing data suddenly has to be clean&lt;/strong&gt;. The 17-year-old row arrived at the target long ago, perfectly legitimately, under the old rule. The direct &lt;code&gt;ADD CONSTRAINT&lt;/code&gt; fails on it, as seen above.&lt;/p&gt;

&lt;p&gt;Postgres offers a controlled path for this transition. &lt;code&gt;NOT VALID&lt;/code&gt; accepts the constraint immediately without checking the existing data. For new rows it applies from the first second anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
   &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_customer_adult&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'x@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: new row violates check constraint ck_customer_adult&lt;/span&gt;

&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ck_customer_adult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order is the point. First &lt;code&gt;NOT VALID&lt;/code&gt; seals the future, then the existing data is cleaned up calmly (in the demo via &lt;code&gt;DELETE&lt;/code&gt;, in practice more likely: routed out for business review), and only the final &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; re-checks the existing rows and completes the guarantee. Between the two steps the constraint sits in a documented intermediate state: it applies to new rows while the existing data is still unchecked. Postgres shows this state in the catalog as &lt;code&gt;convalidated = false&lt;/code&gt;. The &lt;code&gt;VALIDATE&lt;/code&gt; does not lock the table against writes, it runs with a weak lock alongside normal operation. One subtlety in passing: a &lt;code&gt;CHECK&lt;/code&gt; does not fire on &lt;code&gt;NULL&lt;/code&gt;, so a missing age would pass &lt;code&gt;ck_customer_adult&lt;/code&gt; without complaint. Here that case is already caught by the &lt;code&gt;NOT NULL&lt;/code&gt; that the error rule &lt;code&gt;age IS NULL&lt;/code&gt; materializes at the target anyway. The same pattern carries a retrofitted &lt;code&gt;NOT NULL&lt;/code&gt; on an existing table. How to roll that out without downtime is shown in &lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;a dedicated article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For SQL Server the principle holds with different mechanics: &lt;code&gt;WITH NOCHECK&lt;/code&gt; likewise adds a constraint without checking the existing data. The difference sits in the aftermath. The constraint stays permanently marked as not trusted until a &lt;code&gt;WITH CHECK CHECK CONSTRAINT&lt;/code&gt; re-validates the existing rows, and an untrusted constraint is ignored by the optimizer in plan decisions. Whoever does only the first step has the guarantee but gives away performance.&lt;/p&gt;

&lt;p&gt;Demoting, the reverse path, is just as much a routing decision, by the way: &lt;code&gt;E&lt;/code&gt; becomes &lt;code&gt;W&lt;/code&gt;, so the corresponding constraint at the target has to go, otherwise it keeps blocking a rule that is only supposed to observe. Promoting pulls constraints in, demoting clears them away — configuration and schema move together.&lt;/p&gt;

&lt;p&gt;Taken together, that is the real payoff of the three levels: a rule has a controlled lifecycle. It starts as an observation, proves itself, gets promoted and moves into the schema as a guarantee via &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; — and, when needed, walks the same path back. Severity is the control knob of that lifecycle, not just a label on the finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;The routing "&lt;code&gt;E&lt;/code&gt; becomes a constraint" has one restriction that hides in the word &lt;em&gt;candidate&lt;/em&gt;: not every error rule &lt;em&gt;can&lt;/em&gt; become a constraint. A &lt;code&gt;CHECK&lt;/code&gt; sees exactly one row. Within that row it may compare several columns, a &lt;code&gt;CHECK (end_date &amp;gt; start_date)&lt;/code&gt; is perfectly legitimate. But three rule families lie beyond its reach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-row rules.&lt;/strong&gt; "A key may occur at most three times" is one row in the configuration as a check rule (&lt;code&gt;max_occurrence = 3&lt;/code&gt;), but not expressible with the declarative constraints (&lt;code&gt;UNIQUE&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;, foreign keys), because &lt;code&gt;UNIQUE&lt;/code&gt; knows only cardinality 1. Sum, share and distribution rules belong here as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-table rules.&lt;/strong&gt; A &lt;code&gt;CHECK&lt;/code&gt; may not contain a subquery, Postgres rejects that with a clear error message. The only cross-table guarantee the schema knows is the foreign key. Everything beyond it stays pipeline work, say "the discount code must match the customer group".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-based rules.&lt;/strong&gt; Postgres silently assumes that a &lt;code&gt;CHECK&lt;/code&gt; expression always yields the same result, but does not enforce that immutability: a &lt;code&gt;CHECK (order_date &amp;lt;= current_date)&lt;/code&gt; is accepted, and that is precisely what makes it a trap. What is valid today is no longer valid tomorrow, and at the latest when restoring a dump or running a &lt;code&gt;VALIDATE&lt;/code&gt;, existing rows fail that were correct when they were created. Rules with a time reference belong in the pipeline, where every run checks against the current reference date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The direction of view matters: these rules do not land in the pipeline &lt;em&gt;because sadly nothing better exists&lt;/em&gt;. The pipeline is the right place for them: it checks set-based, knows the run context and can still rate a violation as an error and block it at the gate. A cross-row error rule is an error without a constraint counterpart, and that is not a contradiction to the routing but its honest limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;p&gt;The per-rule decision, condensed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule type&lt;/th&gt;
&lt;th&gt;Typical severity&lt;/th&gt;
&lt;th&gt;Place of enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mandatory field the target enforces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter in the source &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;NOT NULL&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard value range (type bound, country-code format)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;CHECK&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key uniqueness&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;PRIMARY KEY&lt;/code&gt;/&lt;code&gt;UNIQUE&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference to a master table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-filter &lt;strong&gt;+&lt;/strong&gt; &lt;code&gt;FOREIGN KEY&lt;/code&gt; at the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business anomaly (worth a look, not blocking)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pipeline, finding in the log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pure observation (frequencies, missing optional values)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pipeline &lt;strong&gt;+&lt;/strong&gt; reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-row (cardinality &amp;gt; 1, sums, shares)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;E&lt;/code&gt; or &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pipeline only — &lt;code&gt;UNIQUE&lt;/code&gt; cannot do cardinality &amp;gt; 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-table beyond the foreign key&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;E&lt;/code&gt; or &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pipeline only — &lt;code&gt;CHECK&lt;/code&gt; cannot hold a subquery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-based (date against a reference day)&lt;/td&gt;
&lt;td&gt;mostly &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pipeline only — a constraint ages badly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two readings sit in this table. Top to bottom: the harder the guarantee, the higher the rule stands, and only the &lt;code&gt;E&lt;/code&gt; rows reach the schema. And across: the place is never "either source or target". Every constraint row carries both, because the pre-filter delivers the findings and the constraint the guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why is binary pass/fail not enough?&lt;/strong&gt;&lt;br&gt;
Because it withholds the urgency of a finding. Either every triviality then blocks the load, or nothing blocks and the findings list goes stale. The three severity levels error, warning and information separate "must not proceed" from "worth a look" and "for information only", and they make the reaction configurable per rule instead of negotiable per night shift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which check rules belong in a CHECK constraint?&lt;/strong&gt;&lt;br&gt;
Only error rules whose violation must never reach the target, and of those only the ones a constraint can express: checks on a single row without a time reference and without looking into other tables. Warnings never belong in a constraint, because a constraint that rejects non-blocking findings contradicts the rule's own definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when I promote a warning to an error?&lt;/strong&gt;&lt;br&gt;
Two things. From the next run on, affected records block at the quality gate. And the target schema should follow, because an error rule deserves a constraint counterpart. For that, the existing data has to be clean: in Postgres, &lt;code&gt;ADD CONSTRAINT … NOT VALID&lt;/code&gt; takes the guarantee on immediately for new rows, then the existing data is cleaned up and re-checked via &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a warning block the load?&lt;/strong&gt;&lt;br&gt;
No, and that is its purpose. A record with warning findings passes the quality gate, and the finding stays in the log with a plain-text message. Blocking is reserved for the severity error alone. A warning that would need to hold up the process is misclassified and should be promoted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does data quality belong in dbt and Great Expectations or in the database?&lt;/strong&gt;&lt;br&gt;
Both have their place. Pipeline tools like dbt tests, Great Expectations or Soda are expressive, well maintained and know severity gradations themselves. Their checks, however, only apply to paths through the pipeline. A direct &lt;code&gt;INSERT&lt;/code&gt; past it hits none of them. Hard error rules therefore deserve an additional constraint in the target schema that every write path respects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Framework and routines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-checks-with-sql/" rel="noopener noreferrer"&gt;Data Quality Checks with SQL&lt;/a&gt; — the series' sub-hub: error table, rule configuration, runner and the quality gate whose severity mechanics this article deepens.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/validate-data-with-sql/" rel="noopener noreferrer"&gt;Validate Data with SQL&lt;/a&gt; — the WHERE routine: value ranges, mandatory fields and the NULL trap of three-valued logic.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-duplicates-with-sql/" rel="noopener noreferrer"&gt;Find Duplicates with SQL&lt;/a&gt; — the uniqueness routine: cardinality, composite keys and the NULL semantics of UNIQUE.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/find-orphaned-records-sql/" rel="noopener noreferrer"&gt;Find Orphaned Records with SQL&lt;/a&gt; — the lookup routine: checking referential integrity without foreign keys.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/derive-data-quality-rules-from-schema/" rel="noopener noreferrer"&gt;Derive Data Quality Rules from the Schema&lt;/a&gt; — the routing's opposite direction: target constraints become error rules of the configuration, projected mechanically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Theory:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/data-quality-dimensions-error-classes/" rel="noopener noreferrer"&gt;Data Quality: Dimensions and Error Classes&lt;/a&gt; — the conceptual frame: which dimensions the checks cover and which they don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Target schema and deployment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt; — which keys and constraints a target table should carry so hard counterparts exist at all.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;Adding a NOT NULL Column to an Existing Table&lt;/a&gt; — the same promotion pattern for mandatory fields: expand/contract without downtime.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/design-pattern-the-architecture-of-an-etl-process-how-to-isolate-bad-data-cleanly/" rel="noopener noreferrer"&gt;Design Pattern // The Architecture of an ETL Process&lt;/a&gt; — the staging architecture in which the pre-filter and the gate have their place.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataquality</category>
      <category>sql</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Database CI/CD with PostgreSQL — the Complete Lifecycle from Object File to Automated Deploy</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:30:30 +0000</pubDate>
      <link>https://dev.to/marcus1968/database-cicd-with-postgresql-the-complete-lifecycle-from-object-file-to-automated-deploy-5gi0</link>
      <guid>https://dev.to/marcus1968/database-cicd-with-postgresql-the-complete-lifecycle-from-object-file-to-automated-deploy-5gi0</guid>
      <description>&lt;p&gt;In many projects the database schema lives in the database instead of the repository — grown out of years of hand-run &lt;code&gt;ALTER&lt;/code&gt;s, fully documented nowhere. It only becomes visible when a second environment is needed or a deploy breaks. &lt;strong&gt;Postgres database CI/CD&lt;/strong&gt; flips that relationship: the repository describes the desired state, and every environment — from the throwaway database in CI to production — is built from the same versioned scripts. This article walks the complete lifecycle in six stations, deliberately without a migration framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key points up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lifecycle in six stations: bootstrap, object deploy, tests, evolution with data, CI gate, teardown.&lt;/li&gt;
&lt;li&gt;Each station solves a concrete problem — and has its own deep-dive article in this cluster.&lt;/li&gt;
&lt;li&gt;The tools: Bash, &lt;code&gt;psql&lt;/code&gt;, and GitHub Actions — deliberately without Flyway or Liquibase.&lt;/li&gt;
&lt;li&gt;The honest limit: when a migration tool is the better choice — and which stations it still will not take off your hands.&lt;/li&gt;
&lt;li&gt;Everything runs: the command sequence comes from the open starter kit and is verified end-to-end against Postgres 17.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisite:&lt;/strong&gt; Postgres, a bash, and a GitHub repository; Docker if you want to replay the lifecycle locally. All building blocks come from the open &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;DI² Starter Kit on GitHub&lt;/a&gt; — the scripts, the example schema tree, and the CI pipeline live there in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lifecycle in Six Stations
&lt;/h2&gt;

&lt;p&gt;"Introducing database CI/CD" often gets translated as "installing a migration tool". That falls short — a tool answers only part of the question. The complete lifecycle of a database environment has six stations, and each of them needs an answer, framework or not:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph13z0o7mka4xv46gk1u.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph13z0o7mka4xv46gk1u.png" alt="Pipeline diagram of the database deployment lifecycle: six stations from Bootstrap (create.sh) through Deploy (deploy.sh), Test (db/tests), Evolve (transitions), and CI Gate (ci.yml, highlighted in blue) to Teardown (drop.sh); a dashed arrow leads from Teardown back to Bootstrap." width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a command sequence, the whole cycle is unspectacular — and that is precisely the ambition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash db/scripts/create.sh &lt;span class="nb"&gt;local&lt;/span&gt;        &lt;span class="c"&gt;# Station 1: bootstrap — database, roles, schema&lt;/span&gt;
bash db/scripts/deploy.sh all &lt;span class="nb"&gt;local&lt;/span&gt;    &lt;span class="c"&gt;# Station 2: object deploy — all schema objects&lt;/span&gt;
bash db/tests/run.sh                   &lt;span class="c"&gt;# Station 3: object tests against a throwaway DB&lt;/span&gt;
bash db/scripts/deploy.sh all &lt;span class="nb"&gt;local&lt;/span&gt;    &lt;span class="c"&gt;# second run: idempotency check + run-once skip&lt;/span&gt;
bash db/scripts/drop.sh &lt;span class="nb"&gt;local&lt;/span&gt;          &lt;span class="c"&gt;# Station 6: teardown — database and roles gone for good&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These five lines are verified end-to-end against Postgres 17 in a throwaway Docker container: bootstrap, full deploy, and all object tests run green, the second deploy acknowledges the run-once scripts with &lt;code&gt;skipped (already applied)&lt;/code&gt;, and after the teardown, database and roles are gone without a trace — &lt;code&gt;pg_database&lt;/code&gt; no longer contains an entry for the created database, &lt;code&gt;pg_roles&lt;/code&gt; none of the created roles. Station 4 (evolution with data) and station 5 (CI gate) have no commands of their own: the evolution lives in the deployed files themselves, and the gate runs the same sequence automatically on every pull request.&lt;/p&gt;

&lt;p&gt;The following sections walk through the stations — each with the problem the station solves and a pointer to the article that provides the depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 1: Bootstrap — Creating Database, Roles, and Schema Reproducibly
&lt;/h2&gt;

&lt;p&gt;Before any schema object can be deployed, the environment has to exist: the database itself, the extensions, the application schema, and the roles. The bootstrap creates all of that with one script call — &lt;code&gt;create.sh &amp;lt;env&amp;gt;&lt;/code&gt; — and it does so for every environment with the same scripts. &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;dev&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt; differ only in a small configuration file with connection coordinates, not in the procedure.&lt;/p&gt;

&lt;p&gt;Two decisions shape this station. First, the &lt;strong&gt;role separation&lt;/strong&gt;: four roles emerge, each with a clear job — the database owner, the schema owner (under which every later deploy runs), a read-write group role, and the service account the application connects with. Passwords never end up in files: the script only needs the existing superuser password; for the three new roles you choose your own passwords and, ideally, put them straight into the password manager.&lt;/p&gt;

&lt;p&gt;Second, the &lt;strong&gt;drop-and-recreate semantics&lt;/strong&gt;: the bootstrap is deliberately not idempotent. If the database or roles already exist, a preflight aborts and points to the teardown — a half-overwritten setup would be worse than an aborted one. Reproducibility here means: an environment is built entirely from the versioned scripts — not adjusted by hand after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 2: Object Deploy — the Directory Is the Migration Plan
&lt;/h2&gt;

&lt;p&gt;The object deploy is the centerpiece of the model. Every schema object — table, function, procedure, trigger, view — lives as its own file in the repository and describes its &lt;strong&gt;desired state&lt;/strong&gt;, phrased idempotently: &lt;code&gt;CREATE TABLE IF NOT EXISTS&lt;/code&gt; and &lt;code&gt;CREATE OR REPLACE&lt;/code&gt; where the engine offers them; where it does not — for triggers or constraints — &lt;code&gt;DROP … IF EXISTS&lt;/code&gt; plus re-creation does the same job, at the price that the object briefly disappears and is recreated during the deploy. The deploy runner loads these files in a fixed section order straight from the directory structure — tables before functions, functions before triggers — and within each section by file-name number. There is no separate deploy plan to maintain (and forget): &lt;strong&gt;the directory is the migration plan.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The essential advantage over a classic migration chain: the DDL core of every schema object sits in one place, readable as one file — not spread across forty change scripts building on each other. And every deploy is the same call, whether against an empty or a current database: &lt;code&gt;deploy.sh all &amp;lt;env&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why this model works without a migration chain, what the directory convention looks like in detail, and where its limits are, is laid out in &lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool&lt;/a&gt; — the foundation all further stations build on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 3: Apply-Smoke and Object Tests
&lt;/h2&gt;

&lt;p&gt;A merged DDL change that does not run on an empty database is broken by definition — it just does not fail where it was written, but on the &lt;strong&gt;next&lt;/strong&gt; environment that gets built from zero. What helps against this error class is a discipline, not a tool: before every merge, the complete deploy runs once against an empty throwaway database — the apply-smoke.&lt;/p&gt;

&lt;p&gt;On top of that come the &lt;strong&gt;object tests&lt;/strong&gt;: one SQL file per schema object with &lt;code&gt;DO $$ … ASSERT&lt;/code&gt; blocks checking the happy path, expected error guards, and constraints. Both together run locally in a self-cleaning Docker container (&lt;code&gt;db/tests/run.sh&lt;/code&gt;) — and identically once more in the CI gate of station 5. Whoever forgets the smoke locally is reminded in the pull request at the latest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 4: Evolution with Data
&lt;/h2&gt;

&lt;p&gt;Before go-live, schema evolution is trivial: teardown, bootstrap, deploy — the database is disposable. As soon as environments hold data nobody may lose, this station becomes the most demanding part of the lifecycle. The framework-free model answers it with two building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Convergent object files:&lt;/strong&gt; an existing table is never dropped and recreated. New columns join the object file as an idempotent &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;&lt;code&gt;ALTER TABLE … ADD COLUMN IF NOT EXISTS&lt;/code&gt;&lt;/a&gt; — the file keeps describing the desired state, and every environment converges to it on the next deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run-once transitions:&lt;/strong&gt; everything that cannot be phrased idempotently — backfills, conversions, data rescues — becomes a timestamped change script that runs exactly once per database. A tracker inside the database records what has already run, by filename and checksum; editing an applied script aborts the deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harder cases — renames, type changes, dropped columns — follow the same two-building-block pattern, but almost always need the transition part plus an intermediate step in which the old and the new form exist side by side.&lt;/p&gt;

&lt;p&gt;The two deep dives: &lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;Adding a NOT NULL Column to a Populated Table&lt;/a&gt; plays through the most common single case — the expand/contract pattern that makes structural change and backfill work together on a populated table. And &lt;a href="https://sql.marcus-belz.de/en/track-database-schema-changes/" rel="noopener noreferrer"&gt;Tracking Schema Changes Without a Framework&lt;/a&gt; builds the run-once tracker from scratch — including the realization that Flyway and Liquibase make the same four design decisions in their tracking core, for all the tools bring along beyond it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 5: CI as the Gate — GitHub Actions
&lt;/h2&gt;

&lt;p&gt;All previous stations run on demand — station 5 makes them binding. On every pull request, GitHub Actions spins up a Postgres &lt;a href="https://docs.github.com/en/actions/using-containerized-services/creating-postgresql-service-containers" rel="noopener noreferrer"&gt;service container&lt;/a&gt; as a throwaway database and runs half the lifecycle on it automatically: bootstrap, full deploy, object tests — and then a &lt;strong&gt;second deploy in the same run&lt;/strong&gt;, which checks the idempotency of all object files and the skip path of the run-once scripts. Wired up as a required status check, a red run blocks the merge.&lt;/p&gt;

&lt;p&gt;The gate catches the error classes that can be checked mechanically — syntax and structural consistency, load order, idempotency — and needs not a single secret to do so. What the workflow looks like in detail, what exactly the double-deploy trick checks, and how it continues from the gate to deploys on real environments: &lt;a href="https://sql.marcus-belz.de/en/github-actions-postgres-deployment/" rel="noopener noreferrer"&gt;GitHub Actions for Postgres Deploys&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Station 6: Teardown
&lt;/h2&gt;

&lt;p&gt;The teardown is the underrated station of the lifecycle. &lt;code&gt;drop.sh &amp;lt;env&amp;gt;&lt;/code&gt; terminates active connections, drops the database, and removes all four roles — each with &lt;code&gt;IF EXISTS&lt;/code&gt;, so the script can safely run again after a partial failure. In the verified run, nothing was left behind: no entry for the database left in &lt;code&gt;pg_database&lt;/code&gt;, none of the four roles left in &lt;code&gt;pg_roles&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why a station of its own for throwing things away? Because teardown is the &lt;strong&gt;litmus test for stations 1 and 2&lt;/strong&gt;: an environment may only disappear without hesitation if bootstrap and object deploy can rebuild it completely. If the thought of &lt;code&gt;drop.sh&lt;/code&gt; makes you nervous because knowledge lives in the database that exists nowhere in the repository, the lifecycle is not closed yet. Before go-live, drop-and-rebuild is the normal reset path; after that, the hard rule applies: never on an environment with data worth protecting without a verified backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does a Migration Tool Make Sense?
&lt;/h2&gt;

&lt;p&gt;The framework-free lifecycle carries well as long as three things come together: &lt;strong&gt;one&lt;/strong&gt; database engine, a manageable team, and a deploy that is shell-based anyway. Then Bash and &lt;code&gt;psql&lt;/code&gt; deliver the core — desired-state files, run-once tracking, CI gate — without a new dependency in the stack. A tacit prerequisite here: the schema can be described fully declaratively in the repository. Where externally managed objects, replication setups, or extensive partitioning are involved, the claim of a "complete lifecycle" only holds with reservations.&lt;/p&gt;

&lt;p&gt;A migration tool like &lt;a href="https://flywaydb.org/" rel="noopener noreferrer"&gt;Flyway&lt;/a&gt; or &lt;a href="https://www.liquibase.com/" rel="noopener noreferrer"&gt;Liquibase&lt;/a&gt; becomes legitimate as soon as its extras are genuinely needed: support for multiple database engines, rollback or undo workflows, a lock table against concurrently running deploys, dry-run and report tooling for audit requirements, programmatic migrations. Add to that what cannot be pinned to individual stations: an established ecosystem with documentation, IDE integration, and support — and a standard several teams can agree on without having to learn a home-grown build first. From multi-team or multi-database operation onward, these are no longer comfort features but necessities — then the framework is the right choice, without anything being wrong with the model shown here. Beyond the scope of this article remain the operational topics outside the deploy model: zero-downtime strategies such as blue/green or rolling deployments, and the locking questions of very large tables — the latter is shown on a concrete case in the &lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;expand/contract article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The honest assessment cuts the other way too, though: &lt;strong&gt;at its core, a migration tool replaces two of the six stations&lt;/strong&gt; — the object deploy and the evolution mechanics. Bootstrap, object tests, the CI gate, and the teardown remain your own work, framework or not. Having built the lifecycle once yourself, you make the tooling decision from the inside: you know which stations the tool takes over, which ones it leaves open — and you read error messages like "Migration checksum mismatch" for what they are: the same design decisions, just behind a different surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do you need Flyway or Liquibase for database CI/CD with Postgres?&lt;/strong&gt;&lt;br&gt;
No, not necessarily. The core — versioned desired-state files, run-once tracking for data changes, and a CI gate against a throwaway database — fits into Bash and &lt;code&gt;psql&lt;/code&gt;. A framework pays off when its extras are needed: multi-engine support, rollback workflows, a lock against concurrent deploys, or audit reporting. It takes two of the six lifecycle stations off your hands; four remain your own work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you deploy a Postgres schema automatically?&lt;/strong&gt;&lt;br&gt;
With a script runner that applies the object files from the repository in a fixed section and number order, and a CI workflow that calls exactly this runner — on every pull request against a throwaway database, on release against the real environment. Since every file describes its desired state idempotently, the deploy is always the same call, whether the database is empty or current.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between an object deploy and migrations?&lt;/strong&gt;&lt;br&gt;
A migration chain describes the path: numbered change scripts that build on each other and run in sequence. An object deploy describes the destination: one file per schema object with its desired state, applicable any number of times. The object approach keeps the current state readable in one place; for data-dependent steps it additionally needs run-once scripts — exactly the point where the two models meet again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you test DDL changes before the merge?&lt;/strong&gt;&lt;br&gt;
With a full dress rehearsal against an empty throwaway database: complete deploy, then object tests as &lt;code&gt;DO $$ … ASSERT&lt;/code&gt; blocks, then a second deploy as the idempotency check. Locally a Docker container takes care of it; in the pull request, the same procedure runs as a GitHub Actions gate. What this test bench does not see — effects on existing data, locks on large tables — remains the job of the evolution discipline from station 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What roles does a Postgres environment need at minimum?&lt;/strong&gt;&lt;br&gt;
Four have proven themselves, with a clear division of labor: a database owner, a schema owner under which all deploys run (deployed objects automatically belong to it), a read-write group role for data access, and a service account for the application that is a member of that group role. The &lt;code&gt;postgres&lt;/code&gt; superuser stays reserved for bootstrap and teardown and appears in no application connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool — Directory Convention Instead of Flyway or Liquibase&lt;/a&gt; — station 2 in depth: the apply model the whole lifecycle builds on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/add-not-null-column-existing-table-postgres/" rel="noopener noreferrer"&gt;Adding a NOT NULL Column to a Populated Table — the Expand/Contract Pattern&lt;/a&gt; — station 4 played through on its most common single case.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/track-database-schema-changes/" rel="noopener noreferrer"&gt;Tracking Schema Changes Without a Framework — Run-Once Scripts, Checksums and Immutability&lt;/a&gt; — station 4: the tracker built from scratch, with the Flyway comparison.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/github-actions-postgres-deployment/" rel="noopener noreferrer"&gt;GitHub Actions for Postgres Deploys — a Throwaway Database as Quality Gate&lt;/a&gt; — station 5 in depth: service container, double deploy, environments.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/verify-migration-data-quality/" rel="noopener noreferrer"&gt;Verifying the Migration — Data Quality and Row Reconciliation After the Move&lt;/a&gt; — if the reason for the rebuild was a database migration: the acceptance check afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² Starter Kit on GitHub&lt;/a&gt; — all six stations runnable in context.
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Setting Up a Claude Code Project with a Development Workflow and Database — the Open Starter Kit Explained</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 29 Jul 2026 14:05:30 +0000</pubDate>
      <link>https://dev.to/marcus1968/setting-up-a-claude-code-project-with-a-development-workflow-and-database-the-open-starter-kit-19k4</link>
      <guid>https://dev.to/marcus1968/setting-up-a-claude-code-project-with-a-development-workflow-and-database-the-open-starter-kit-19k4</guid>
      <description>&lt;p&gt;An empty repo and Claude Code, Anthropic's coding agent in the terminal — that's all it takes to get going. And that is exactly the problem: the model writes code immediately, but by default nothing ensures that a specification exists first, that a review happens afterwards, or that the database schema deploys reproducibly. A Claude Code project setup is therefore less a tooling decision than a process decision — the question is not whether the AI can code, but what sequence it works through.&lt;/p&gt;

&lt;p&gt;This article shows a complete setup for that, in overview: the open DI² starter kit — a fixed, numbered development workflow from requirements to deploy, a deployable database part, and a template that prunes itself during setup. It was not designed on a drawing board; it is a distillate of a running project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The essentials up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The scaffold:&lt;/strong&gt; conventions live as rules, tools as skills in &lt;code&gt;.claude/&lt;/code&gt; — and the difference between the two decides what every session costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The workflow:&lt;/strong&gt; eleven numbered phases from &lt;code&gt;/init&lt;/code&gt; to &lt;code&gt;/deploy&lt;/code&gt;, with the bug loop running across them; each feature spec is the contract the phases work against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database part:&lt;/strong&gt; plain SQL with a directory convention, idempotently deployable, tested in CI against a throwaway database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The design decision:&lt;/strong&gt; start maximal and prune via the &lt;code&gt;/init&lt;/code&gt; interview, instead of starting empty and building up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deliberate omissions:&lt;/strong&gt; no hooks, no subagents — simplicity is part of the design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open on GitHub:&lt;/strong&gt; the kit is forkable as a starter kit (MIT license).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Claude Code and an editor; for the database part, optionally a PostgreSQL (local or in a container). The examples come from the open &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;DI² starter kit on GitHub&lt;/a&gt;. Mechanics as of July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scaffold: What Lives in .claude/
&lt;/h2&gt;

&lt;p&gt;Setting up a Claude Code project starts at the top level with four directories — &lt;code&gt;.claude/&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;features/&lt;/code&gt; and &lt;code&gt;db/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/
├── rules/      loads automatically: conventions, security rules, the SQL rule tree — and stack.md
└── skills/     loads on demand: one folder per workflow phase, invoked as /requirements, /qa, …
docs/           PRD, bug tracking, knowledge base
features/       INDEX.md + one spec per feature — the project's working state
db/             the database part: schemas, deploy scripts, tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split under &lt;code&gt;.claude/&lt;/code&gt; is the scaffold's most important mechanism. &lt;strong&gt;Rules&lt;/strong&gt; — the &lt;code&gt;CLAUDE.md&lt;/code&gt; and every file under &lt;code&gt;.claude/rules/&lt;/code&gt; — are fully in context at every session start, in the model's working memory: the constitution of the project, applying without being asked. &lt;strong&gt;Skills&lt;/strong&gt; are tools on demand: initially only their description line sits in context; the body loads on invocation. One rule holds a special role: &lt;code&gt;stack.md&lt;/code&gt; is the authoritative source for the tech stack — runtime, UI, database, deploy, CI including the build and test commands; the &lt;code&gt;CLAUDE.md&lt;/code&gt; carries only a short summary of it. That keeps the skills stack-neutral: they read the stack at runtime instead of hard-coding it in their own text.&lt;/p&gt;

&lt;p&gt;How the two loading paths differ in detail — and what an overstuffed rules folder costs every single session in context — is taken apart in &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow: From Requirements to Deploy
&lt;/h2&gt;

&lt;p&gt;The heart of the setup is a numbered sequence: eleven phases, 0 through 10, each one its own skill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 /init → 1 /requirements → 2 /architecture → [3 /ux] → [4 /backend] → [5 /frontend]
        → 6 /qa → 7 /review → 8 /check-updates → 9 /security → 10 /deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8cwhj55h1j6pekhr38g.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr8cwhj55h1j6pekhr38g.png" alt="The starter kit's numbered workflow: top row /init (0), /requirements (1), /architecture (2), /ux (3), /backend (4), /frontend (5); bottom row /qa (6), /review (7), /check-updates (8), /security (9), /deploy (10). /ux, /backend and /frontend are dashed as optional, the arrow between /security and /deploy is labeled " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Specs drive the sequence: &lt;code&gt;/requirements&lt;/code&gt; writes one specification per feature, with user stories and acceptance criteria, and &lt;code&gt;features/INDEX.md&lt;/code&gt; tracks the status of all features — the single overview every later phase orients itself by. The rest of the chain works against that spec: &lt;code&gt;/architecture&lt;/code&gt; drafts the technical design, &lt;code&gt;/qa&lt;/code&gt; tests against the acceptance criteria, &lt;code&gt;/review&lt;/code&gt; checks the diff against spec and conventions. Played through on a small feature, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/requirements   spec feat-0001-csv-import.md — user stories + acceptance criteria
/architecture   design: staging table plus COPY import; no UI → /ux is skipped
/backend        implementation against the spec
/qa             tests against the spec's acceptance criteria
/review         diff check against spec and conventions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The square brackets in the chain mark phases not every project needs: an API service without a user interface knows no &lt;code&gt;/ux&lt;/code&gt; and no &lt;code&gt;/frontend&lt;/code&gt;; the pruning at setup time removes them entirely. And phase 0 falls out of line: &lt;code&gt;/init&lt;/code&gt; runs exactly once, at setup — more on that in a moment.&lt;/p&gt;

&lt;p&gt;Two properties keep the sequence in check. First, no phase jumps to the next on its own: every transition is triggered by a human; the kit calls this human-in-the-loop. Second, &lt;code&gt;/security&lt;/code&gt; is set as a gate: the project-wide security audit comes before a production deploy, not after it. Running across all phases are &lt;code&gt;/bug&lt;/code&gt; (records and closes bugs under &lt;code&gt;docs/bugs/&lt;/code&gt;, at any time), &lt;code&gt;/auth&lt;/code&gt; (for auth projects) and &lt;code&gt;/help&lt;/code&gt; — which answers the question "where am I right now, and what comes next".&lt;/p&gt;

&lt;h2&gt;
  
  
  The Database Part: Plain SQL, Deployable and Tested
&lt;/h2&gt;

&lt;p&gt;What sets this setup apart from typical Claude Code templates: it has a real database part. Under &lt;code&gt;db/&lt;/code&gt; sits a complete, runnable PostgreSQL example — tables with audit columns, procedures, triggers, a view, seed data, plus deploy scripts and tests for the database objects. None of this is carried by a migration tool but by a directory convention: object files in fixed numbering, written idempotently, an apply runner plays them in in a defined order; one-off, data-dependent steps run alongside as run-once transitions. In CI, every schema state is brought up against a throwaway database and tested before it reaches a real environment. Doing without a migration tool is a deliberate decision of this setup, not a universal recommendation: where a team already has an established migration tool in place, there is no reason to switch.&lt;/p&gt;

&lt;p&gt;The conventions these SQL objects are written to — naming, alignment, object skeletons — live as a rule tree under &lt;code&gt;.claude/rules/sql/&lt;/code&gt; and are the subject of the sister overview &lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt;, with its deep dives on &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;tables&lt;/a&gt;, &lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;procedures&lt;/a&gt; and &lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How the deploy works without Flyway or Liquibase in detail — idempotency, run-once transitions, the &lt;code&gt;schema_apply_log&lt;/code&gt; table, the runner and the CI safety net — is described in &lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Decision: Start Maximal, Then Prune
&lt;/h2&gt;

&lt;p&gt;The template ships all fourteen skills and every rule tree — including the parts a given project will never need. Pruning happens at setup: &lt;code&gt;/init&lt;/code&gt; is a one-time bootstrap interview. It first captures the product vision and the stack's parameters — runtime, UI, backend, database, auth, deploy, CI — and writes the vision into the &lt;code&gt;CLAUDE.md&lt;/code&gt;, the stack profile into &lt;code&gt;stack.md&lt;/code&gt;. It then removes what the answers rule out: no UI means &lt;code&gt;/ux&lt;/code&gt; and &lt;code&gt;/frontend&lt;/code&gt; go, UI rule tree included; no database means the entire &lt;code&gt;db/&lt;/code&gt; tree disappears. At the end, &lt;code&gt;/init&lt;/code&gt; deletes itself — a bootstrap runs only once.&lt;/p&gt;

&lt;p&gt;Why delete instead of build up? Because deleting is the easier operation: whoever deletes decides against the complete picture: every file is a yes/no question. Whoever builds up has to remember what is missing, and forgotten things don't speak up. On top of that comes the context math: rules that don't apply would otherwise load into every single session.&lt;/p&gt;

&lt;p&gt;The interview, the role of &lt;code&gt;stack.md&lt;/code&gt; as single source of truth, and the full pruning matrix — which answer removes which files — are described in &lt;a href="https://sql.marcus-belz.de/en/claude-code-project-template-self-pruning/" rel="noopener noreferrer"&gt;Maximal Template Over Empty Repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Kit Deliberately Leaves Out
&lt;/h2&gt;

&lt;p&gt;An overview is only complete with what's missing — and the starter kit leaves out a few things on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No hooks.&lt;/strong&gt; Claude Code can bind shell commands to events, to be executed automatically — before a tool call, after a change, at session start. The kit passes on that: what happens, happens visibly in the dialog and is written in the rules, where the human reads it and the model follows it. That costs a degree of automation but saves the hard-to-trace layer between intent and execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No subagents.&lt;/strong&gt; Claude Code can hand subtasks to separate agents with their own context. Here too the kit keeps the work in the main context, where the human sees and can steer it. It is the same decision that keeps the phase transitions user-initiated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No app skeleton.&lt;/strong&gt; There is no &lt;code&gt;src/&lt;/code&gt;: the kit is not a Next.js, Python or whatever template. The application scaffold emerges through the workflow, against the stack the project chose in the interview.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exactly one option per matter of taste.&lt;/strong&gt; One UI ruleset (React/Tailwind/shadcn), two SQL dialects (PostgreSQL and SQL Server) — no further variants sit side by side. If a project needs a different one, the rule is: port, don't stack — copy the shipped rule tree, adapt it, remove the original.&lt;/p&gt;

&lt;p&gt;These omissions are part of the picture: the kit is not a framework claiming completeness but an opinionated setup from a real project. Where an opinion doesn't fit, it is replaceable in Markdown files — that is exactly why everything lies open.&lt;/p&gt;

&lt;p&gt;Honest framing also includes the counter-calculation: the workflow costs real extra effort per feature: spec, QA and review are steps of their own, not side effects. For a throwaway script or a quick prototype the setup is oversized; there, the empty repo remains the better choice. And the structure only carries with discipline: skip phases regularly and you end up with the folders, but not the process. What that effort buys, the comparison shows:&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;Empty repo&lt;/th&gt;
&lt;th&gt;Starter kit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirements&lt;/td&gt;
&lt;td&gt;in the chat history&lt;/td&gt;
&lt;td&gt;one spec per feature, versioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order&lt;/td&gt;
&lt;td&gt;ad hoc&lt;/td&gt;
&lt;td&gt;eleven numbered phases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conventions&lt;/td&gt;
&lt;td&gt;re-explained every session&lt;/td&gt;
&lt;td&gt;rules, loaded automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review and QA&lt;/td&gt;
&lt;td&gt;on request&lt;/td&gt;
&lt;td&gt;phases of their own, security as a gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;manual work&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;db/&lt;/code&gt; tree, idempotent and CI-tested&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Setting Up a New Claude Code Project with the Starter Kit
&lt;/h2&gt;

&lt;p&gt;The kit is public on &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT-licensed. For a new project there are two equivalent paths: use the repository as a GitHub template ("Use this template") or run the bundled copy script (&lt;code&gt;new-project.ps1&lt;/code&gt; or &lt;code&gt;new-project.sh&lt;/code&gt;). Both paths produce a repository with a fresh Git history. A direct &lt;code&gt;git clone&lt;/code&gt; of the template is off-limits as a project start: it drags along the template's history and upstream linkage, neither of which belongs in the new project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;my-project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;marcusbelz/di2-starter-kit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--clone&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the new project in Claude Code, invoke &lt;code&gt;/init&lt;/code&gt;, answer the interview — and specify the first feature with &lt;code&gt;/requirements&lt;/code&gt;. From there, the numbered workflow carries.&lt;/p&gt;

&lt;p&gt;Two transparency notes belong to the provenance. First, the lineage: the kit is the generalized, product-free &lt;code&gt;.claude/&lt;/code&gt; toolbox of the DI² project. That project in turn descends from the AI Coding Starter Kit by AlexPEClub (MIT); the attribution is documented in the repository. Second, the making: the texts in the kit are written by Claude, under human direction — every file commissioned, checked and signed off; the README states this working mode openly. And an honest maturity note: the conventions grew out of daily use in the DI² project and are proven there; the packaging as a self-pruning template is young. Whoever adopts the kit takes on a well-thought-out setup, not a framework matured over many years.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I set up a Claude Code project in a structured way?&lt;/strong&gt;&lt;br&gt;
With a template that brings process and conventions along: create a new repository from the template (GitHub "Use this template" or the copy script — no direct &lt;code&gt;git clone&lt;/code&gt;), invoke &lt;code&gt;/init&lt;/code&gt; in Claude Code and answer the interview about vision and tech stack. After that the project prunes itself, and &lt;code&gt;/requirements&lt;/code&gt; specifies the first feature — from there the numbered workflow leads all the way to deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a Claude Code project need a fixed folder structure?&lt;/strong&gt;&lt;br&gt;
Only &lt;code&gt;.claude/&lt;/code&gt; with rules and skills is a convention of Claude Code itself. Everything else — &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;features/&lt;/code&gt;, &lt;code&gt;db/&lt;/code&gt; — is a decision of the setup. But that is exactly where the difference between "the model writes code" and "the project has a process" lives: specs, bugs and schema states need a fixed place, or they exist only in the chat history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between rules and skills?&lt;/strong&gt;&lt;br&gt;
Rules load fully into context at every session start and apply without being asked; skills load on demand and run on command. The decision rule: what must always apply becomes a rule — what executes as a procedure becomes a skill. The loading mechanics in detail are in &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I integrate a database into the Claude Code workflow?&lt;/strong&gt;&lt;br&gt;
As a dedicated &lt;code&gt;db/&lt;/code&gt; tree in the repository: object files in a directory convention, written idempotently, a runner plays them in, and CI tests every state against a throwaway database. No migration tool is needed for that — how it works is shown in &lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the starter kit tied to a tech stack?&lt;/strong&gt;&lt;br&gt;
No. The workflow skills name no language and no framework — they read the stack at runtime from &lt;code&gt;stack.md&lt;/code&gt;, which &lt;code&gt;/init&lt;/code&gt; fills at setup. Stack-specific are only the rule trees for UI and SQL dialect; the pruning keeps exactly the variant the project chose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the setup work with Cursor or GitHub Copilot — why Claude Code?&lt;/strong&gt;&lt;br&gt;
The building blocks are tied to Claude Code mechanics: rules and skills under &lt;code&gt;.claude/&lt;/code&gt; with their distinct loading paths exist natively there, as do the slash invocations of the phases. The ideas behind them — specs as a contract, numbered phases, security before deploy — are tool-neutral: anyone working with Cursor or Copilot can adopt the process but would have to port the template themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-template-self-pruning/" rel="noopener noreferrer"&gt;Maximal Template Over Empty Repo — a Claude Code Setup That Prunes Itself via /init&lt;/a&gt; — the design decision "start maximal, then prune" in detail.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt; — the loading mechanics behind the scaffold.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deploy-sql-schema-without-migration-tool/" rel="noopener noreferrer"&gt;Deploying a SQL Schema Without a Migration Tool&lt;/a&gt; — the database part in depth.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt; — the sister overview: the SQL conventions that live in the kit as a rule tree.&lt;/li&gt;
&lt;li&gt;SQL depth behind the rule tree: &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt;, &lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Procedure Conventions&lt;/a&gt;, &lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Function Conventions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-rules-claude-md/" rel="noopener noreferrer"&gt;I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md&lt;/a&gt; — the experience report on this setup's rule structure: anatomy, don'ts, inventories.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² starter kit on GitHub&lt;/a&gt; — template, workflow skills and database part, ready to fork.

&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Gave Claude Code 27 Rule Files Instead of One CLAUDE.md</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 25 Jul 2026 16:49:51 +0000</pubDate>
      <link>https://dev.to/marcus1968/i-gave-claude-code-27-rule-files-instead-of-one-claudemd-4nl0</link>
      <guid>https://dev.to/marcus1968/i-gave-claude-code-27-rule-files-instead-of-one-claudemd-4nl0</guid>
      <description>&lt;p&gt;There is a moment when a CLAUDE.md tips over. That moment does not sit on any calendar, it shows in behavior: the file keeps getting longer, and the rules inside it keep getting followed less. Every new convention you write in dilutes the ones already there. That is exactly where the project behind this article stood — and the answer was not a better CLAUDE.md, but its dissolution into individual rule files. Today there are 27 of them.&lt;/p&gt;

&lt;p&gt;This article is the experience report: how Claude Code rules need to be structured so they hold up over months, what a single rule file must carry, and why the most important parts are not the rules themselves. They are the don'ts and the inventories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key points up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One file per convention&lt;/strong&gt; — 27 rule files instead of one CLAUDE.md, each with a single topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anatomy of a rule that holds:&lt;/strong&gt; rule, reasoning, don'ts, inventory, cross-references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don'ts beat prescriptions&lt;/strong&gt; — at least in the DI² project: a negative example is concrete and recognizable, a prescription competes against the training prior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventories are the drift radar:&lt;/strong&gt; the part of a rule that claims an actual state of the code — and therefore the first to stand out when code and documentation diverge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;paths:&lt;/code&gt; scoping&lt;/strong&gt; lets large rules load only where they apply — 3 of the 27 files use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The honest downside:&lt;/strong&gt; maintenance effort, rule conflicts, and a state in which the documentation deliberately runs ahead of the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites.&lt;/strong&gt; A project with Claude Code and a &lt;code&gt;.claude/rules/&lt;/code&gt; directory. The pattern transfers to any coding agent that loads convention files into its context — Cursor rules or comparable mechanisms in other tools work on the same principle. What loads into context when, and what that costs, is covered by the sibling article &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt; — this article starts one level earlier: at the question of what the rule files themselves have to look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point: One File That Kept Growing
&lt;/h2&gt;

&lt;p&gt;The project behind the numbers is DI², an ETL generator built on Next.js and PostgreSQL, its code written almost entirely AI-assisted with Claude Code. In the beginning, all conventions lived where every Claude Code project collects them first: in the CLAUDE.md. That works as long as the file is short. It stops working once the file becomes a container in which database conventions, color tokens and commit rules all sit side by side.&lt;/p&gt;

&lt;p&gt;This effect is hard to measure, but it is easy to feel. It also matches what research shows about long contexts: language models make measurably worse use of information sitting in the middle of long inputs (&lt;a href="https://arxiv.org/abs/2307.03172" rel="noopener noreferrer"&gt;"Lost in the Middle", Liu et al. 2023&lt;/a&gt;). In my experience, a rule sitting in line 40 of a long file gets followed less than the same rule in its own, topically named file. A second effect has less to do with the model than with the humans: in a 400-line file, even the author cannot find a rule again when they want to check whether it still holds. Why a single file does not scale structurally, and which loading mechanics sit behind that, belongs to the &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;skills-vs-rules article&lt;/a&gt; — this one is about what comes after.&lt;/p&gt;

&lt;p&gt;The consequence in the project: a &lt;code&gt;.claude/rules/&lt;/code&gt; directory holding 27 rule files, one per convention (as of July 22, 2026). The same count holds for the public edition of this rule structure, the &lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;di2-starter-kit on GitHub&lt;/a&gt; — you can verify it there, not counting the subfolder READMEs. There is a file for the table conventions, one for dialogs, one for loading states, one for the security model. The directory keeps growing with the project. In the very week this article was written, three new files arrived, for views, triggers and database policies. A rule system is finished when the project is finished, which is to say never.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a Rule That Holds
&lt;/h2&gt;

&lt;p&gt;After several months with this system, a fixed structure has emerged. A rule file that holds consists of five parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# &amp;lt;Convention&amp;gt; (&amp;lt;project&amp;gt;)&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; One-sentence summary: What does this rule enforce, and where does it apply?&lt;/span&gt;

&lt;span class="gu"&gt;## The Rule&lt;/span&gt;

Every &lt;span class="nt"&gt;&amp;lt;structure&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;procedure&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;does&lt;/span&gt; &lt;span class="na"&gt;exactly&lt;/span&gt; &lt;span class="na"&gt;one&lt;/span&gt; &lt;span class="na"&gt;thing&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stated&lt;/span&gt; &lt;span class="na"&gt;imperatively&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;.
One convention per file — do not mix topics.

&lt;span class="gu"&gt;## Reasoning&lt;/span&gt;

Why this rule exists: the concrete incident, bug or review finding
that triggered it. A rule without a reason gets weighed away in
trade-offs — the reason is part of the rule, not decoration.

&lt;span class="gu"&gt;## Don'ts&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`&amp;lt;concrete negative example from your own code&amp;gt;`&lt;/span&gt; — why it drifts.
&lt;span class="p"&gt;-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`&amp;lt;second negative example&amp;gt;`&lt;/span&gt; — what applies instead (with the target spelling).

&lt;span class="gu"&gt;## Inventory&lt;/span&gt;

| Usage site | File | Status |
|---|---|---|
| &lt;span class="nt"&gt;&amp;lt;Site&lt;/span&gt; &lt;span class="na"&gt;A&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; | &lt;span class="sb"&gt;`components/site-a.tsx`&lt;/span&gt; | ✅ compliant |
| &lt;span class="nt"&gt;&amp;lt;Site&lt;/span&gt; &lt;span class="na"&gt;B&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; | &lt;span class="sb"&gt;`components/site-b.tsx`&lt;/span&gt; | ⏳ retrofit open |

&lt;span class="gu"&gt;## Cross-References&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;neighbor-rule.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;neighbor-rule.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; — boundary: what is governed there, not here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule itself (the first two or three sentences under &lt;em&gt;The Rule&lt;/em&gt;) is the smallest part, and that is no accident. It states imperatively what applies. Everything beyond that belongs to the other four parts.&lt;/p&gt;

&lt;p&gt;The reasoning is not a courtesy to the reader. An agent weighs trade-offs, and as observed in the DI² project, a rule without a reason loses that weighing more easily against a plausible counterargument from the concrete case. A rule with a reason anchors what the concrete case has to argue against. The difference shows in exactly the moments that matter: namely, when the model considers an exception justified. How a convention and its reasoning come into being in the first place is described in the methodology article &lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The remaining three parts are the actual substance of this article. The don'ts and the inventory each get their own section below, and the cross-references almost explain themselves: they draw the boundary to the neighboring rule so that two files do not creep into governing the same topic. Every boundary violation that surfaces later gets recorded there as an explicit reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Don'ts Beat Prescriptions
&lt;/h2&gt;

&lt;p&gt;This is the central observation from several months of rule maintenance in the DI² project. Whether it transfers to other projects has not been examined — it is project experience, not a study. A prescription says what should be. A negative example shows what must not be — with a concrete, recognizable spelling. The difference looks small at first glance. In practice it is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Before: a prescription without an anchor --&amp;gt;&lt;/span&gt;

Use only the project tokens for font sizes.

&lt;span class="c"&gt;&amp;lt;!-- After: negative examples with recognition value --&amp;gt;&lt;/span&gt;

&lt;span class="gs"&gt;**Don'ts:**&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`text-[12px]`&lt;/span&gt; — raw pixel value, drifts; snap target is &lt;span class="sb"&gt;`text-token-meta`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; ❌ &lt;span class="sb"&gt;`text-sm`&lt;/span&gt; — framework default instead of the project scale; in the app
  scope the linter guard flags it as an error.
&lt;span class="p"&gt;-&lt;/span&gt; ❌ Inline &lt;span class="sb"&gt;`line-height`&lt;/span&gt; override via &lt;span class="sb"&gt;`[line-height:Xpx]`&lt;/span&gt; — the scale ships
  its own line height; a deliberate override needs a code comment with
  a justification.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prescription "use only the project tokens" is factually correct, and yet it accomplishes little. At every generation it competes against the model's training prior, in which &lt;code&gt;text-sm&lt;/code&gt; is the statistically most common way to write small text. "Only project tokens" first has to be translated onto the concrete case, and the rule gets lost in that translation.&lt;/p&gt;

&lt;p&gt;The negative example skips the translation. &lt;code&gt;text-[12px]&lt;/code&gt; is precisely the string the model is about to write — it sits verbatim in the rule, marked with a ❌ and the reason. A don't leaves no room for interpretation. It additionally names the snap target, the spelling that applies instead. Whoever reads the don't, whether human or model, afterwards knows both things: what is wrong and what belongs in its place. There is no don't-specific magic behind this, but a familiar effect from prompt research: concrete examples are easier for language models to act on than abstract prescriptions. Negative examples are simply the form in which a rule file can harness that effect.&lt;/p&gt;

&lt;p&gt;The best don'ts do not come from the rule author's imagination but from real finds. Every time a review or a bug surfaces a new bypass variant, exactly that variant goes into the rule file as a don't. The rule system thereby learns the same mistakes the code has already made once. There was no shortage of material: the sibling article &lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes&lt;/a&gt; documents the drift finding from which the font-size don'ts emerged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventories as a Drift Radar
&lt;/h2&gt;

&lt;p&gt;A rule states a target state, and target states have an inconvenient property: they cannot become wrong. "Every dialog carries a leading icon" stays correct as a sentence even when six dialogs without icons have long been sitting in the code. The rule notices none of it.&lt;/p&gt;

&lt;p&gt;An inventory changes that. It lists the concrete usage sites of the convention together with their actual state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Inventory + Retrofit Backlog&lt;/span&gt;

As of now, this rule is the documented truth. Existing sites without
&lt;span class="nt"&gt;&amp;lt;convention&amp;gt;&lt;/span&gt; are brought in line as a tracked follow-up step (retrofit) —
until then the code deliberately lags behind the documentation.

| Dialog | File | Status |
|---|---|---|
| Editor (domain object A) | &lt;span class="sb"&gt;`components/object-a-editor-dialog.tsx`&lt;/span&gt; | ✅ icon leads left |
| Inspector (domain object B) | &lt;span class="sb"&gt;`components/object-b-inspector.tsx`&lt;/span&gt; | ✅ icon leads left |
| Invite user | &lt;span class="sb"&gt;`components/user-invite-dialog.tsx`&lt;/span&gt; | ⏳ retrofit open |
| Bulk delete | &lt;span class="sb"&gt;`components/bulk-delete-dialogs.tsx`&lt;/span&gt; | ⏳ retrofit open (destructive tone) |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table is the part of a rule file that can fail against the actual state of the code. A reasoning section or a cross-reference can go stale too, but only the inventory makes a checkable claim about what the code looks like right now. If a new dialog arrives and is missing from the table, the inventory is incomplete. If a listed dialog gets rebuilt and its status is not updated, the inventory is stale. Exactly this vulnerability is what makes it valuable: a rule without an inventory can exist past the code unnoticed for years, while the lag of a rule with an inventory becomes visible at the next reconciliation, at the latest. The inventory is the rule's drift radar.&lt;/p&gt;

&lt;p&gt;In the project, 9 of the 27 rule files carry such sections. They come in two flavors. The &lt;strong&gt;caller inventory&lt;/strong&gt; lists who uses a component or convention — it answers the question "if I change this, what is affected?" before anyone has to search. The &lt;strong&gt;retrofit backlog&lt;/strong&gt; lists which existing sites do not yet satisfy the convention. Both forms share the mechanics but differ in the direction of view: one looks at the rule's users, the other at its open debts.&lt;/p&gt;

&lt;p&gt;The inventory is not maintained in a separate documentation session but in the same commit as the code change. Whoever adds a dialog adds it to the table. Whoever completes a retrofit sets the status to ✅. That sounds like a high demand on discipline. In an agent workflow, though, it is the cheapest possible moment, because the agent usually has the rule file in context anyway when it works inside the rule's scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules That Load Only Where They Apply
&lt;/h2&gt;

&lt;p&gt;As the file count grows, the context-cost question returns. 27 files that all load all the time would just be a partitioned CLAUDE.md at the same cost. The lever against that is a &lt;code&gt;paths:&lt;/code&gt; front matter that binds a rule to its path scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/app/api/**"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/lib/db*"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Backend conventions&lt;/span&gt;

This rule loads only when the task touches files in its path scope —
API routes and the database access layer. A frontend task does not
pay its context costs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the project, 3 of the 27 files carry this front matter: the backend rules, the frontend rules and the security model. The selection follows a simple criterion. These three files are large, and their scope is a clearly bounded subtree of the project. A backend rule inside a pure frontend task is dead context. The remaining 24 files load unscoped because they are either small or apply across the whole project, like the commit conventions.&lt;/p&gt;

&lt;p&gt;This article deliberately goes no deeper here. The mechanics behind it — what Claude Code loads into context when, what separates rules from skills, and how the costs add up — are the subject of &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;. For the structural question of this article, the finding suffices: &lt;code&gt;paths:&lt;/code&gt; scoping is the main reason 27 files do not mean 27-fold costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Downside
&lt;/h2&gt;

&lt;p&gt;A rule system of this size is not free, and an experience report that hides that would be advertising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance is real work.&lt;/strong&gt; Every rule file wants attention at every convention change, inventories want updating in the same commit, and the cross-references between files go stale when a rule moves. The effort is not a one-time investment but a running cost. In the project it is the kind of work that pays off, but it does not disappear just because you approve of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules end up in conflict.&lt;/strong&gt; With 27 files it happens that two rules govern the same case from different angles — the dialog rule wants an icon, the confirmation-dialog rule forbids one for its special case. The resolution is the same every time: the conflict gets written into both files as an explicit carve-out, with references to each other. Undecided conflicts are the worst thing that can happen to a rule system, because then the prioritization stays ambiguous — which rule prevails depends on the particular context and the task at hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The documentation runs ahead of the code — deliberately.&lt;/strong&gt; When a new convention is decided, it applies to new code immediately. The existing sites are not rebuilt within the hour but tracked as a retrofit backlog in the inventory and brought in line step by step. That state is not a failure as long as it is documented. The rule file says honestly: this is the truth, and the code deliberately lags behind at these listed sites. Untracked, the same state would be a lie, because then the documentation claims an actual state that does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Rule Needs a Linter
&lt;/h2&gt;

&lt;p&gt;Prose has a limit, and the sibling article &lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes&lt;/a&gt; has measured it out in detail: a documented rule raises the generation hit rate but does not guarantee it. At high volume, any residual rate turns into visible drift.&lt;/p&gt;

&lt;p&gt;From that follows a division of labor that has proven itself in the project. Whatever is machine-checkable gets a guard in addition to the rule — in the font-size case a custom ESLint rule at error level that flags exactly the spellings from the don'ts. The rule file remains the source all the same: it explains the why, defines the mapping and lists the deliberate exceptions, while the linter enforces only the checkable subset.&lt;/p&gt;

&lt;p&gt;Whatever is not machine-checkable stays a pure prose rule and needs the inventory as its substitute radar. No linter can decide whether a dialog icon is the right one for the domain. Whether all dialogs have one is written in the inventory table. The rule of thumb from the project: a rule whose violation can be expressed as a search pattern is a linter candidate. A rule whose violation a human has to recognize needs an inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently Today
&lt;/h2&gt;

&lt;p&gt;Looking back, I would approach three things differently, and all three can be named concretely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too many files too early.&lt;/strong&gt; The first weeks produced rule files for topics that did not even have a second usage site yet. A convention for a single case is not a convention but a note. Today a new rule file comes into being only when the same decision comes up for the second time. And cleanup is part of the deal: a rule whose scope has disappeared, or whose content a neighboring rule meanwhile carries, gets deleted or merged into that neighbor. A rule set that only ever grows becomes the very unwieldy file it was meant to replace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules without inventories that went stale silently.&lt;/strong&gt; The early files consisted of rule and reasoning, without a state-of-the-code part. After two months, some of them described a state the code had long left behind, and nobody had noticed. Only the inventory sections made the aging visible. In hindsight, every rule with usage sites should have carried one from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovered &lt;code&gt;paths:&lt;/code&gt; too late.&lt;/strong&gt; The scoping arrived only when the context costs were already noticeable, and converting existing files to clean path scopes was more tedious than an early cut would have been. Anyone starting today should ask one short question at every new rule file: does this apply everywhere, or in one subtree? Answering that question costs ten seconds at creation time. Not asking it costs a refactor later.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should I split my CLAUDE.md?&lt;/strong&gt;&lt;br&gt;
At the latest when, while writing a new convention into it, the question comes up where it actually belongs. That is the signal that the file carries multiple topics. A second signal is the repeated ignoring of a rule that verifiably sits in the file — the rule is drowning in its surroundings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many rule files are too many?&lt;/strong&gt;&lt;br&gt;
The number itself is not the limit, the context costs are. 27 files work because the large ones load path-scoped and the unscoped ones are small. How those costs add up and where the line runs is covered by &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do negative examples work better than prescriptions?&lt;/strong&gt;&lt;br&gt;
Because they close the translation gap. A prescription first has to be applied by the model onto the concrete case, while a don't already contains the wrong spelling verbatim — precisely the string the training prior would suggest, marked with the reason and the target spelling. Recognizing is more reliable than deriving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do I do when two rules contradict each other?&lt;/strong&gt;&lt;br&gt;
Write the conflict into both files, as an explicit carve-out with references to each other. Which rule wins in the overlap case must be stated in the files, not in the author's head. An undecided conflict otherwise gets decided by chance, depending on which rule happens to sit more prominently in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the code always have to match the documentation?&lt;/strong&gt;&lt;br&gt;
No — and that may be the most counterintuitive lesson. A new convention applies to new code immediately, while the existing code is brought in line step by step as a tracked retrofit backlog. The tracking is what matters: a documented lag is a deliberate decision, an undocumented one is documentation that lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Going deeper:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code — What Auto-Loads, What Loads on Demand&lt;/a&gt; — the loading mechanics and context costs behind this article.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-template-self-pruning/" rel="noopener noreferrer"&gt;Maximal Template Over Empty Repo — a Claude Code Setup That Prunes Itself via /init&lt;/a&gt; — how a rule inventory gets tailored at project start.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-project-with-database-setup/" rel="noopener noreferrer"&gt;Setting Up a Claude Code Project with a Development Workflow and Database&lt;/a&gt; — the sub-pillar: the setup this rule system lives in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Upstream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-code-drift-799-font-sizes/" rel="noopener noreferrer"&gt;AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes&lt;/a&gt; — the drift finding that makes rules and guards necessary in the first place.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code — the Generate-Refine-Derive Loop&lt;/a&gt; — how a convention comes into being before you pour it into a rule file.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents&lt;/a&gt; — the pillar: the enforcement system as a whole.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Starter kit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² starter kit on GitHub&lt;/a&gt; — a project template built on this article's one-file-per-convention principle, ready to tailor.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI-Assisted Coding Gave Me 799 Hardcoded Font Sizes</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Tue, 21 Jul 2026 23:42:38 +0000</pubDate>
      <link>https://dev.to/marcus1968/ai-assisted-coding-gave-me-799-hardcoded-font-sizes-27fn</link>
      <guid>https://dev.to/marcus1968/ai-assisted-coding-gave-me-799-hardcoded-font-sizes-27fn</guid>
      <description>&lt;p&gt;It didn't start with an audit. It started with a nagging feeling: the interface looked restless. You don't notice it at first glance, but on the second and third look it is there — a timestamp slightly larger here than there, a dialog title a touch smaller than in the neighboring dialog. Everyone knows the discipline from letters and résumés: same typeface, same font size, same alignment. The same holds for an application interface, except the violation doesn't show up in any one spot. It shows up as a diffuse restlessness across many screens.&lt;/p&gt;

&lt;p&gt;Only that feeling led to a counting command across the frontend, and the count delivered the explanation: 799 hits for raw pixel font sizes like &lt;code&gt;text-[13px]&lt;/code&gt;, spread across 25 distinct pixel values in 74 files. And that in a project which had a documented font-size scale with six tokens all along. The scale was used 263 times and bypassed roughly 1,180 times. This is not a sloppiness finding from some legacy codebase grown over a decade — it is the state of a codebase built AI-assisted with Claude Code from day one. This is what &lt;strong&gt;AI code drift&lt;/strong&gt; looks like: every single suggestion is locally plausible, and what adds up is the restlessness you can see before you can measure it.&lt;/p&gt;

&lt;p&gt;This article is the experience report — with the real numbers, the rule that ended the problem, and the honest admission that a convention living only in prose loses against a language model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key points up front:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The finding:&lt;/strong&gt; 799 raw pixel font sizes against 263 token usages, even though the scale was documented. The ratio is the message, not the single number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The biggest source of drift lies between the tokens:&lt;/strong&gt; values like &lt;code&gt;11px&lt;/code&gt; and &lt;code&gt;12px&lt;/code&gt;, for which no token existed at all, form the majority at roughly 400 occurrences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No code review catches this&lt;/strong&gt;, because every diff is harmless on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better prompts raise the hit rate but don't eliminate the drift&lt;/strong&gt; — at high generation volume, any residual rate becomes visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What holds:&lt;/strong&gt; a 6-step scale, a fixed element-to-token mapping, an unambiguous snap rule for all in-between values, and a linter at &lt;code&gt;error&lt;/code&gt; level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; The example uses Tailwind CSS and ESLint in a Next.js project. The pattern applies to any codebase with design tokens, regardless of framework — and, as the end of the article shows, just as much to SQL conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Finding: 799 Against 263
&lt;/h2&gt;

&lt;p&gt;The project behind the numbers is DI², an ETL generator built on Next.js and PostgreSQL whose code was written almost entirely AI-assisted. For font sizes, a clear convention existed: six named tokens from &lt;code&gt;text-di-h1&lt;/code&gt; (18px) down to &lt;code&gt;text-di-label&lt;/code&gt; (10px), defined in the Tailwind configuration and described in a brand rule file that the agent loads for every frontend task. Everything that follows is therefore the measurement of a single project — one data point, not proof. What makes the case interesting beyond the project is the mechanism behind it, and that mechanism, as we will see, is not project-specific.&lt;/p&gt;

&lt;p&gt;The inventory on June 25, 2026 across all &lt;code&gt;src/**/*.tsx&lt;/code&gt; files produced three categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Declaration style&lt;/th&gt;
&lt;th&gt;Occurrences&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Assessment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw pixels &lt;code&gt;text-[Xpx]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;799&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;drift, 25 distinct pixel values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind defaults &lt;code&gt;text-xs&lt;/code&gt;/&lt;code&gt;text-sm&lt;/code&gt;/&lt;code&gt;text-base&lt;/code&gt; …&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;383&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;drift within the app scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical tokens &lt;code&gt;text-di-*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;263&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;the target pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three rows distinguish three ways of declaring the same font size. &lt;code&gt;text-[13px]&lt;/code&gt; is Tailwind's arbitrary-value syntax: the pixel value sits literally inside square brackets and acts like an inline &lt;code&gt;font-size&lt;/code&gt; — any value is possible, and that is how 25 different ones come into being. &lt;code&gt;text-xs&lt;/code&gt;, &lt;code&gt;text-sm&lt;/code&gt; and &lt;code&gt;text-base&lt;/code&gt;, on the other hand, are named size steps, but they belong to the framework's bundled default scale (12, 14 and 16 pixels). That looks disciplined, because it follows a scale. It is just the wrong one: Tailwind's generic scale instead of the project's own, whose six tokens don't even contain those three values. The third row, finally, is the project's own scale — the target pattern. Read top to bottom, the table is a ladder: freehand value, foreign scale, own scale.&lt;/p&gt;

&lt;p&gt;That two scales coexist is not an accident, by the way — it is the framework's default. Tailwind ships its complete size ladder with every project, and the project's own tokens were added via &lt;code&gt;theme.extend&lt;/code&gt; — and &lt;code&gt;extend&lt;/code&gt; means exactly that: extend, not replace. From that point on, every &lt;code&gt;text-sm&lt;/code&gt; compiles just as happily as every &lt;code&gt;text-di-body&lt;/code&gt;. There is no moment at which the project consents to the foreign scale, and none at which it announces itself. It is simply there, from day one, as the statistically closest choice for anyone who needs small text — human or model.&lt;/p&gt;

&lt;p&gt;A second term needs an explanation, because it recurs throughout the article. The &lt;strong&gt;app scope&lt;/strong&gt; is the application behind the login: the dashboard and administration pages plus the components they render. The token scale applies only there. Outside it lie two zones with rights of their own — the public pages (landing, legal, login), which deliberately carry larger formats, and the bundled UI base components, which internally work with Tailwind defaults. A &lt;code&gt;text-sm&lt;/code&gt; is therefore not wrong per se; on a marketing page it is legitimate. That is why the second row says drift &lt;strong&gt;within the app scope&lt;/strong&gt;: of the 383 occurrences, only the share inside the application counts as drift.&lt;/p&gt;

&lt;p&gt;The convention, then, was ignored at a ratio of roughly 1:4.5. To rerun the inventory in your own project, all you need is a search tool like &lt;code&gt;ripgrep&lt;/code&gt; (the examples are written in PowerShell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Raw pixel font sizes: total number of occurrences&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'text-\[[0-9.]+px\]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Line&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Distribution: which pixel value occurs how often?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'text-\[[0-9.]+px\]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Group-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# For comparison: the canonical tokens&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--no-filename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'text-di-(h1|h2|body|meta|micro|label)'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Line&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeline delivers a punchline of its own. Between the first count and the recount three weeks later, shortly before the cleanup migration started, the total grew from 799 to 813 occurrences and from 25 to 26 distinct values. The drift kept growing while its removal was already being planned. A convention that is not enforced doesn't lose once — it loses a little more every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nobody Caught It
&lt;/h2&gt;

&lt;p&gt;The first reflex at a number like this: how did that slip through? The answer is uncomfortable because it describes no negligence, but a structural gap.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;text-[12px]&lt;/code&gt; is not wrong in any single diff. It renders correctly, it looks fine in the preview, it breaks no test. A reviewer reading the diff of a new dialog checks the logic, the states, the accessibility. They do not compare whether the font size in this file is consistent with the one in 73 other files. The drift doesn't live in any one file. It lives &lt;strong&gt;between&lt;/strong&gt; the files.&lt;/p&gt;

&lt;p&gt;It was visible all the same — just not as a defect, but as the nagging feeling from the beginning. A restless interface shows the symptom, not the location: which of the 74 files do you point at when no single line is wrong on its own? So the impression stayed without consequence for a long time. It couldn't be pinned to any diff, and what can't be pinned to a diff ends up in no review comment and no ticket. Visual regression tests usually don't catch this form of inconsistency either, because they compare each view against its own baseline. Two views that are both brand new share no baseline against which the difference could stand out. Only the inventory turned the feeling into a finding with numbers — and thereby into something fixable.&lt;/p&gt;

&lt;p&gt;Then there is the speed. A person who builds one dialog a day makes a handful of font-size decisions per week, and muscle memory keeps them reasonably stable. An agent that creates twenty components in the same week makes the same decision a hundred times over — and each one is optimized locally, not for consistency with all the previous ones. The inconsistency doesn't come from carelessness; it comes from the sheer volume of independent single decisions. The densest single file in the inventory accounted for 70 raw pixel sizes on its own.&lt;/p&gt;

&lt;p&gt;Humans produce the same drift — honesty demands saying so. The difference is not the kind of mistake but the pace and the volume. What a team accumulates in two years of wild growth, AI-assisted development manages in a quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Drift Lives Between the Tokens
&lt;/h2&gt;

&lt;p&gt;The most revealing part of the inventory is the distribution of the 25 pixel values. It splits into three classes, and the middle one is the interesting one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact token matches:&lt;/strong&gt; values that correspond to a token, just written raw — &lt;code&gt;13px&lt;/code&gt; instead of &lt;code&gt;text-di-body&lt;/code&gt; (166 occurrences), &lt;code&gt;11.5px&lt;/code&gt; instead of &lt;code&gt;text-di-meta&lt;/code&gt; (46), plus the remaining token values. This class is mechanically repairable and visually a no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off-token values:&lt;/strong&gt; roughly 400 occurrences on values for which &lt;strong&gt;no token exists at all&lt;/strong&gt; — led by &lt;code&gt;11px&lt;/code&gt; with 196 and &lt;code&gt;12px&lt;/code&gt; with 143 occurrences, plus &lt;code&gt;12.5px&lt;/code&gt; (42) and &lt;code&gt;14px&lt;/code&gt; (19).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliberate exceptions:&lt;/strong&gt; marketing and legal pages with large formats of their own, such as &lt;code&gt;22px&lt;/code&gt; or &lt;code&gt;44px&lt;/code&gt;, which intentionally sit outside the app scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second class deserves the second look. &lt;code&gt;11px&lt;/code&gt; and &lt;code&gt;12px&lt;/code&gt; both sit next to the same token, &lt;code&gt;text-di-meta&lt;/code&gt; (11.5px), and both were later migrated to it. Together that is 339 places where two different pixel values played the same semantic role — meta text, timestamps, helper lines. Nobody ever decided that both values should exist. There is no commit with the message "we are introducing 11px as an alternative to 12px." Both values simply came into being, suggestion by suggestion, because each looked reasonable on its own.&lt;/p&gt;

&lt;p&gt;That is what separates this drift from a copying error. Someone who mistypes a documented value produces a findable defect. Someone who puts plausible values into a gap of the scale a hundred times over produces a creeping second scale that was never decided anywhere and therefore never stands out anywhere. That is AI code drift in its purest form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Better Prompts" Fallacy
&lt;/h2&gt;

&lt;p&gt;The obvious reaction to the finding would have been to state the convention more forcefully. Add a chapter to the brand rule, repeat the tokens in the prompt, instruct the agent more insistently.&lt;/p&gt;

&lt;p&gt;That doesn't carry far, and the reason lies in how the model works. A language model is trained on vast amounts of public code, among it countless Tailwind projects, and there &lt;code&gt;text-sm&lt;/code&gt; or &lt;code&gt;text-[12px]&lt;/code&gt; is the overwhelmingly most common way to write small text. A project-specific convention like &lt;code&gt;text-di-meta&lt;/code&gt;, by contrast, is exactly one file in a rules directory. At every single generation the rule competes against that weight, and it wins often — but not always. Across a thousand decisions, a hit rate of 90 percent still leaves a hundred drift spots, and even a considerably higher rate ends up in the dozens at sufficient volume.&lt;/p&gt;

&lt;p&gt;Rules in prose improve the rate, and rules with reasons improve it further. How a ruleset comes into being that an agent actually follows is described in the methodology article &lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code&lt;/a&gt;. But any rate below 100 percent means drift at high volume. For a convention that is meant to hold without exception, the prompt is the wrong tool. It needs a check that doesn't get tired.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Works: Scale, Category Mapping, Snap Rule
&lt;/h2&gt;

&lt;p&gt;The cleanup consisted of three building blocks that only work in combination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the scale itself.&lt;/strong&gt; Six steps, defined in exactly one place in the Tailwind configuration, each with its line height built in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.ts — the scale as the single source of truth&lt;/span&gt;
&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-h1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;18px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23.4px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;21px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;18.85px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;11.5px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-micro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10.5px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13.65px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di-label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;letterSpacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.05em&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An important decision was to keep the scale at six steps. The tempting alternative would have been to create new tokens for &lt;code&gt;11px&lt;/code&gt; and &lt;code&gt;12px&lt;/code&gt; and thereby legalize the status quo. That would have turned the drift into an official eight-step scale, and the next in-between value would have found a gap again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, the element-category mapping.&lt;/strong&gt; A table in the rule file defines which kind of element carries which token. Section headings get &lt;code&gt;di-h1&lt;/code&gt;, dialog titles &lt;code&gt;di-h2&lt;/code&gt;, table cells and buttons &lt;code&gt;di-body&lt;/code&gt;, timestamps and helper texts &lt;code&gt;di-meta&lt;/code&gt;, counter pills &lt;code&gt;di-micro&lt;/code&gt;, uppercase labels &lt;code&gt;di-label&lt;/code&gt;. With that, "what size does this element need?" is no longer a matter of taste but a lookup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, the snap rule.&lt;/strong&gt; The name says it: roughly 400 occurrences sat on in-between values like &lt;code&gt;11px&lt;/code&gt; or &lt;code&gt;12px&lt;/code&gt;, and during the migration each of them had to snap onto one of the six tokens — like an object being pulled onto the grid in a graphics editor. Which token it becomes is decided in two stages. First comes the element's role from the category mapping: a timestamp gets &lt;code&gt;di-meta&lt;/code&gt; because it is meta text, no matter which pixel value used to be there. Only when an occurrence cannot be assigned to any category does numeric proximity to the nearest token decide. Even the single tie is settled explicitly: &lt;code&gt;14px&lt;/code&gt; sits exactly between &lt;code&gt;di-body&lt;/code&gt; (13px) and &lt;code&gt;di-h2&lt;/code&gt; (15px), and &lt;code&gt;di-body&lt;/code&gt; wins as the default. Role before number is not a formality, either. A &lt;code&gt;12px&lt;/code&gt; in a sticky table header belongs to &lt;code&gt;di-micro&lt;/code&gt; (10.5px) by category, although &lt;code&gt;di-meta&lt;/code&gt; (11.5px) would be numerically closer. And because the rule leaves no room for judgment, two people — or two agent runs — resolve the same raw value identically, guaranteed.&lt;/p&gt;

&lt;p&gt;With those three building blocks the migration itself was unspectacular: 80 files in the app scope, converted category by category, with shifts of less than a pixel. Exactly one deliberate exception remained — a large page title outside the app scale, marked with an inline comment and a justification.&lt;/p&gt;

&lt;p&gt;One side finding from the same cleanup deserves a mention for completeness: the tokens had to be explicitly registered as font-size classes in the &lt;code&gt;tailwind-merge&lt;/code&gt; configuration, because the library otherwise classifies unknown &lt;code&gt;text-*&lt;/code&gt; classes as text color and silently discards one of them on conflict. That, however, is an ordinary trap when introducing custom tokens and has nothing to do with AI. Know it, pin it with a small test, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Even the Fresh Rule Drifted
&lt;/h2&gt;

&lt;p&gt;The most instructive finding of the whole story comes from the migration's quality assurance, and it cuts against the author.&lt;/p&gt;

&lt;p&gt;The category rule — "the element category wins over pixel proximity" — was written into the rule file in the same commit that carried the migration. And in that very commit it was undercut: the data cells of the densest table registers, previously &lt;code&gt;12px&lt;/code&gt;, belonged to &lt;code&gt;di-body&lt;/code&gt; (13px) according to the category table. They were migrated numerically to &lt;code&gt;di-meta&lt;/code&gt; (11.5px) instead, because that matched the density intent of those views. The freshly written rule and its first application contradicted each other, and it surfaced only in the downstream QA pass, which filed it as a documentation inconsistency.&lt;/p&gt;

&lt;p&gt;You can consider the finding small — half a pixel in dense tables. Its value lies elsewhere: it shows that even a carefully worded, freshly printed rule drifts at the moment of its application when only humans and prose carry it. Not out of ignorance, but because a second legitimate consideration intervened in the concrete case and nobody checked against the wording. This rule-based check is one of the tasks a machine performs more reliably than any participant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcement: A Linter at error
&lt;/h2&gt;

&lt;p&gt;The fourth building block therefore makes the convention machine-checkable. The tool class is secondary: a compiler check, a Tailwind plugin or a CI script can play the same role. For this project, a custom ESLint rule was the most practical form — it reports every raw pixel font size and every Tailwind size default in the app scope as an &lt;strong&gt;error&lt;/strong&gt;, not a warning. The core of the rule fits on a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint-rules/no-raw-font-size.mjs — the core of the rule&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PX_RE&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/text-&lt;/span&gt;&lt;span class="se"&gt;\[(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:\.\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?)&lt;/span&gt;&lt;span class="sr"&gt;px&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;!&lt;/span&gt;&lt;span class="se"&gt;[\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;text-&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;xs|sm|base&lt;/span&gt;&lt;span class="se"&gt;)(?![\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;

&lt;span class="c1"&gt;// Snap table: raw value -&amp;gt; canonical token (from the rule file)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PX_SNAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;11&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12.5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;13&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;14&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-di-h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;problem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rawPx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Raw pixel font size `{{match}}`. Use {{target}}.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PX_RE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rawPx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PX_SNAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a token from the scale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// DEFAULT_RE analogous&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&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="p"&gt;},&lt;/span&gt;
      &lt;span class="nc"&gt;TemplateElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&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;cooked&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions have proven themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The rule inspects all string literals and template parts&lt;/strong&gt;, not just JSX attributes (the &lt;code&gt;Literal&lt;/code&gt; and &lt;code&gt;TemplateElement&lt;/code&gt; visitors at the end of the rule). In practice, class strings also arise in &lt;code&gt;cn()&lt;/code&gt; arguments, in &lt;code&gt;.join(" ")&lt;/code&gt; helpers and in exported constants — a rule that only sees &lt;code&gt;className="…"&lt;/code&gt; would have blind spots there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The error message names the snap target&lt;/strong&gt; (the &lt;code&gt;target&lt;/code&gt; field in the report data). Whoever sees the error sees the fix and doesn't have to go find the rule file first. That holds for human readers just as much as for the agent reacting to the linter error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;error&lt;/code&gt;, not &lt;code&gt;warn&lt;/code&gt;.&lt;/strong&gt; A warning is a number in a summary; an error breaks the build. Only the second one is enforcement. The halfway path — warnings plus occasional cleanup — ends up reproducing the very state that led to the drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One obvious alternative deserves a mention because it looks simpler than it is: remove Tailwind's default scale from the configuration altogether by defining &lt;code&gt;fontSize&lt;/code&gt; without &lt;code&gt;extend&lt;/code&gt;. Then &lt;code&gt;text-sm&lt;/code&gt; simply would not exist anymore. That fails on two counts. An unknown &lt;code&gt;text-sm&lt;/code&gt; produces no error in Tailwind — it produces no CSS at all, so the text would silently fall back to the browser's default size, and a silent failure like that is harder to find than the drift it is meant to prevent. And the bundled UI base components, like the marketing pages, build on exactly those default classes internally. The radical fix would break the very zones that legitimately live off the standard scale. A linter with a scope can express that; a global configuration cannot.&lt;/p&gt;

&lt;p&gt;The deliberate exceptions therefore live not in the rule but in the ESLint configuration: a path list exempts marketing, legal and login pages, whose large formats intentionally sit outside the app scale. That records the convention's scope machine-readably in one place, congruent with the scope in the rule file. The direction of the definition is worth noting: the app scope itself is never listed anywhere — it is simply everything that was not exempted. A new public page that nobody adds to the exemption list is treated as app scope, and the guard flags its large formats as errors. That is the right direction to fail: a forgotten list entry shows up as a loud false alarm in the build instead of slipping through as silent drift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// eslint.config.mjs — carve-out as a path list (abridged)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FONT_SIZE_CARVE_OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/page.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// landing: deliberately larger formats&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/impressum/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/login/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.test.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// tests reference classes as test data&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/**/*.{ts,tsx}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di2/no-raw-font-size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FONT_SIZE_CARVE_OUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;di2/no-raw-font-size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One sequencing detail to close: the guard was activated as the &lt;strong&gt;last&lt;/strong&gt; step, after the app scope had been migrated clean. The other way around, every unfinished file would have needed a temporary exemption list, and temporary exemption lists have a tendency to become permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Linter Cannot Do
&lt;/h2&gt;

&lt;p&gt;For the experience report to stay honest, the limits belong in it.&lt;/p&gt;

&lt;p&gt;The guard checks the &lt;strong&gt;spelling&lt;/strong&gt;, not the &lt;strong&gt;assignment&lt;/strong&gt;. That an element carries a token at all — that it enforces reliably. Whether it is the right token for the element's category it cannot know — in that gap lived the rule contradiction from the QA finding, and there it can arise again. The category assignment remains a rule in prose, with all the weaknesses described, just on a much smaller attack surface.&lt;/p&gt;

&lt;p&gt;Two limits are chosen deliberately. Larger Tailwind defaults from &lt;code&gt;text-lg&lt;/code&gt; upward the rule does not flag, because a strikingly large text in an app file is a case for design review, not a candidate for mechanical snapping. And dynamically assembled class strings, such as values concatenated from variables, are only partially covered by a static rule. Both gaps are documented rather than concealed, because a check whose omissions nobody knows creates false confidence.&lt;/p&gt;

&lt;p&gt;And finally: the linter preserves the scale, it does not justify it. Whether six steps are the right six and which category deserves which token remains a design decision that precedes the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Pattern in SQL
&lt;/h2&gt;

&lt;p&gt;The case is a frontend case on purpose, because that is where the drift was measurable. The pattern behind it is bound to no framework. Transferring it to SQL is therefore a reasoned analogy, not a second measurement — but one that follows from the same mechanism.&lt;/p&gt;

&lt;p&gt;A SQL convention — singular table names, snake_case parameters, a fixed procedure skeleton — is the same class of rule as a font-size scale: a project-specific commitment competing against the statistically more common spelling from other codebases. An agent generating PL/pgSQL drifts there for the same reason as with the pixel values, and the answer has the same structure. The convention lives versioned in a rule file, with reasons and negative examples, the way the &lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions&lt;/a&gt;, the &lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Procedure Conventions&lt;/a&gt; and the &lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Function Conventions&lt;/a&gt; demonstrate. And what is machine-checkable gets checked by a tool: for SQL layout, say, &lt;code&gt;sqlfluff&lt;/code&gt; with a project-specific configuration; for structural conventions, a script in the CI gate.&lt;/p&gt;

&lt;p&gt;The division of labor is the same in both worlds. The prose rule explains the why and raises the hit rate. The tool closes the gap between a high hit rate and zero exceptions. Because the underlying pattern stays the same, whether in the frontend or in the database: local plausibility creates global inconsistency. How the overall interplay of rules, skills and agents is set up is described in the pillar article &lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I find consistency drift in my own project?&lt;/strong&gt;&lt;br&gt;
It often announces itself as an impression first: the interface feels restless without any single spot to point at. It becomes tangible through a counting inventory: one search pattern for the canonical spelling, one for the bypasses, and the two numbers set in relation. For design tokens those are expressions like &lt;code&gt;text-\[[0-9.]+px\]&lt;/code&gt; against the token names. More revealing than the total is the distribution of values — clusters on values without a token show where the scale has a gap or where an unofficial second convention has formed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't it enough to put the rule into CLAUDE.md?&lt;/strong&gt;&lt;br&gt;
A documented rule improves the hit rate but doesn't guarantee it — at high generation volume, any residual rate turns into measurable drift. Rules in prose and machine checks are therefore not alternatives but two halves: the rule explains the why, the linter enforces the what. Where rules live in Claude Code and what they cost in context is covered in &lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a custom ESLint rule instead of an off-the-shelf one?&lt;/strong&gt;&lt;br&gt;
Because the error message should name the project's own snap target, and the check has to cover class strings outside JSX attributes as well. Generic approaches like &lt;code&gt;no-restricted-syntax&lt;/code&gt; can flag the pattern but cannot suggest a contextual fix — and it is exactly that fix which makes the error immediately actionable for humans and agents alike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this only apply to CSS and design tokens?&lt;/strong&gt;&lt;br&gt;
No. AI code drift affects any project-specific convention that competes against a more widespread standard spelling — SQL naming, file structures, error-handling patterns all drift by the same mechanism. The countermeasure is the same everywhere: document with reasons, and pour the machine-checkable part into a tool at error level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't a human team produce the same drift?&lt;/strong&gt;&lt;br&gt;
It does, and that is the honest core. The difference is pace: an agent makes as many individual decisions in a week as a team makes in months, and it makes each one optimized locally rather than for consistency with the previous ones. Conventions that held up passably at human speed break visibly under that volume — which also means AI drift merely makes an old problem visible faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Going deeper:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/deriving-sql-conventions-with-claude-code/" rel="noopener noreferrer"&gt;Deriving SQL Conventions with Claude Code — the Generate-Refine-Derive Loop&lt;/a&gt; — how a convention comes into being before you can enforce it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/ai-assisted-sql-development-with-claude-code/" rel="noopener noreferrer"&gt;AI-Assisted SQL Development with Claude Code — Rules, Skills and Agents&lt;/a&gt; — the pillar: the enforcement system as a whole.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sql.marcus-belz.de/en/claude-code-skills-vs-rules/" rel="noopener noreferrer"&gt;Skills vs. Rules in Claude Code&lt;/a&gt; — where conventions live and what they cost in context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Convention spokes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/postgres-table-conventions/" rel="noopener noreferrer"&gt;Postgres Table Conventions — Naming, Keys and Audit Columns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/plpgsql-procedure-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Procedure Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sql.marcus-belz.de/en/plpgsql-function-conventions/" rel="noopener noreferrer"&gt;PL/pgSQL Function Conventions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Starter kit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/marcusbelz/di2-starter-kit" rel="noopener noreferrer"&gt;The open DI² starter kit on GitHub&lt;/a&gt; — a project template with the rules structure described here; the font-size guard from this article ships with it as a reusable template.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>eslint</category>
      <category>tailwindcss</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
