<?xml version='1.0' encoding='UTF-8'?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:blogger="http://schemas.google.com/blogger/2008" xmlns:georss="http://www.georss.org/georss" xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr="http://purl.org/syndication/thread/1.0" version="2.0"><channel><atom:id>tag:blogger.com,1999:blog-4228537051045607583</atom:id><lastBuildDate>Fri, 16 Oct 2026 12:59:06 +0000</lastBuildDate><category>cockroachdb</category><category>postgresql</category><category>cockroachlabs</category><category>SQL</category><category>distributedsql</category><category>database</category><category>cloudnative</category><category>psql</category><category>Security</category><category>docker</category><category>Hadoop</category><category>container</category><category>Apps</category><category>Ubuntu and Linux</category><category>microservice</category><category>Misc</category><category>kubernetes</category><category>nosql</category><category>cloud native</category><category>kerberos</category><category>ssl</category><category>AI</category><category>activedirectory</category><category>tls</category><category>Python</category><category>claude</category><category>cockroach labs</category><category>newsql</category><category>openssl</category><category>performance</category><category>Linux</category><category>Oozie</category><category>micorservice</category><category>cdc</category><category>codex</category><category>sqlserver</category><category>JAVA</category><category>MongoDB</category><category>Powershell</category><category>authentication</category><category>llm</category><category>query</category><category>til</category><category>Pig</category><category>S3</category><category>YARN</category><category>change data capture</category><category>kafka</category><category>sso</category><category>Hive</category><category>Spark</category><category>ad</category><category>changefeed</category><category>cloud</category><category>ferretdb</category><category>mit</category><category>object storage</category><category>ollama</category><category>openai</category><category>opus</category><category>pgbouncer</category><category>serverless</category><category>snowflake</category><category>ubuntu</category><category>Minio</category><category>Streams</category><category>benchmark</category><category>containers</category><category>devops</category><category>gemini</category><category>glm</category><category>high availability</category><category>object store</category><category>openshift</category><category>pgbench</category><category>workflow</category><category>.net</category><category>API</category><category>Azure</category><category>Django</category><category>Groovy</category><category>HBase</category><category>Storm</category><category>automation</category><category>backup</category><category>bash</category><category>cache</category><category>changedatacapture</category><category>code review</category><category>code-review</category><category>confluent</category><category>data federation</category><category>disaster recovery</category><category>glm 5.2</category><category>kv</category><category>load balancer</category><category>microsoft</category><category>migration</category><category>multiprocessing</category><category>multiregion</category><category>okta</category><category>ozone</category><category>pgcat</category><category>schema change</category><category>single sign on</category><category>single sign-on</category><category>singles sign-on</category><category>sonnet</category><category>starburst</category><category>trino</category><category>Deepseek</category><category>Grok</category><category>Kimi</category><category>Postgres</category><category>SDK</category><category>SPOF</category><category>Sol</category><category>Terra</category><category>agents</category><category>android</category><category>android-studio</category><category>antigravity</category><category>archive</category><category>azure event hubs</category><category>cap theorem</category><category>cli</category><category>cold</category><category>column family</category><category>connection</category><category>connection pool</category><category>cost</category><category>crashlytics</category><category>crud</category><category>dart</category><category>databse</category><category>datawarehouse</category><category>dbveaver</category><category>debezium</category><category>debugging</category><category>developer-tools</category><category>devsec</category><category>duckdb</category><category>encryption</category><category>enduro</category><category>etl</category><category>event driven</category><category>exactly once</category><category>extension</category><category>fable</category><category>failure</category><category>fault tolerance</category><category>fdw</category><category>feature management</category><category>feature toggles</category><category>fitness</category><category>fivetran</category><category>flutter</category><category>flyway</category><category>follower reads</category><category>garmin</category><category>golang</category><category>ha</category><category>haproxy</category><category>hdfs</category><category>heterogeneous storage</category><category>hot</category><category>htap</category><category>image</category><category>indexes</category><category>ipython-sql</category><category>jdbc</category><category>jms</category><category>json</category><category>jupyter</category><category>jupyternotebook</category><category>jwt</category><category>kotlin</category><category>linode</category><category>managed</category><category>message bus</category><category>multicloud</category><category>nvme</category><category>oauth</category><category>oidc</category><category>olap</category><category>oltp</category><category>open source</category><category>openid</category><category>openrouter</category><category>oracle</category><category>osx</category><category>personal assistant</category><category>pgpass</category><category>polyscale</category><category>polyscale.ai</category><category>postico</category><category>presto</category><category>prestodb</category><category>privacy</category><category>projectjupyter</category><category>protocol buffers</category><category>proxy</category><category>redis</category><category>redpanda</category><category>refresh token</category><category>restore</category><category>schema</category><category>schema migration</category><category>schema registry</category><category>short-lived tokens</category><category>sqlalchemy</category><category>sqlmagic</category><category>ssd</category><category>storage</category><category>testing</category><category>token cost</category><category>tokens</category><category>tracing</category><category>ulid</category><category>uuid</category><category>warm</category><category>çlaude</category><title>A Smooth Sea Never Made a Skillful Sailor</title><description></description><link>http://blog.ervits.com/</link><managingEditor>noreply@blogger.com (Artem Ervits)</managingEditor><generator>Blogger</generator><openSearch:totalResults>242</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-3878253966543600128</guid><pubDate>Fri, 02 Oct 2026 02:11:51 +0000</pubDate><atom:updated>2026-10-01T22:11:51.297-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">code-review</category><category domain="http://www.blogger.com/atom/ns#">Grok</category><category domain="http://www.blogger.com/atom/ns#">llm</category><title> Seven Releases With Grok: Fast, Honest, and What the Diff Did Not Show</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1lqmAS5NI1XVbZLZedx9sJI-P3-K1IwFs9HuraLVDsZWT9GxsG9od_j0uuYoNX9pDbQtn9XEpc0HBbhJPbVc8E37QaZ4tHtpq4zTBctGH1SKzQILO8N2ycbsi1SliGcqhG8oSmfKTN-o6wSl7U7L4SPzG2ybPFQ7aBejdUJny-RyLhEFRWuPsdLMe3BBy/s1200/2026-10-01-grok-seven-releases-hero.jpg&quot; alt=&quot;Abstract dark blue illustration of a bright grid of changed squares with a cyan scanning beam, while the real fault glows orange off to the side, outside the beam&quot;&gt;&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;https://blog.ervits.com/2026/09/cheap-plan-expensive-reviews.html&quot;&gt;my last post&lt;/a&gt; I cancelled Ollama. The money running out early was enough on its own, but the part I wrote about was how many review rounds the work needed before I could merge it. That gives me a simple test for any model: how many times does the work go around review, and does each fix pass make things better or add new problems?&lt;/p&gt;
&lt;p&gt;This post runs the same test on Grok. Starting on September 21, it implemented four releases of my Zone 2 Trainer app and two releases of my personal assistant app. On a seventh release I swapped the roles, and Grok reviewed code that Claude wrote. Grok did well on that test. That is the short version. The longer version is about where the bugs that did get through were hiding: not in what the changed lines did, but in what they assumed.&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;The models are &lt;code&gt;grok-4.6&lt;/code&gt; and &lt;code&gt;grok-4.7&lt;/code&gt; from xAI, on a SuperGrok subscription. &lt;code&gt;grok-4.7&lt;/code&gt; came out right after the first release, so it did the rest. On the other side was Claude. It wrote the issue lists and scope documents, reviewed every pull request Grok wrote, and implemented the seventh release as Claude Opus 5.5. This is the setup from &lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model&lt;/a&gt;: one company&#39;s model writes the code and another company&#39;s model reviews it.&lt;/p&gt;
&lt;p&gt;One thing to keep in mind for the whole post: the model that wrote most of the specs is the same model that reviewed the code. You will see why that matters.&lt;/p&gt;
&lt;h2&gt;What Held Up Every Time&lt;/h2&gt;
&lt;p&gt;Three things were true in all six releases Grok implemented.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It was fast.&lt;/strong&gt; The first release went from a fresh issue list to a pull request with tests in 23 minutes. Answering a review usually took 4 to 13 minutes. One release of my assistant app took 65 minutes from start to a tagged release, with three review rounds inside it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It never claimed more than it did.&lt;/strong&gt; Not once in six releases did a commit or pull request say something was fixed when it was not. That was the &lt;code&gt;deepseek-v4-pro:0813&lt;/code&gt; habit I described in &lt;a href=&quot;https://blog.ervits.com/2026/09/cheap-plan-expensive-reviews.html&quot;&gt;the Ollama post&lt;/a&gt;, so I was watching for it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It said when work was not finished.&lt;/strong&gt; On one sync bug it fixed part of the problem, wrote the two remaining gaps onto the issue, and ended the comment with &amp;quot;Do not close this on the PR landing.&amp;quot; A model trying to look finished does not write that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the question was never &amp;quot;does it work.&amp;quot; The question was what got past it.&lt;/p&gt;
&lt;h2&gt;Where the Bugs Were Hiding&lt;/h2&gt;
&lt;p&gt;Every release had at least one real bug that survived the first pass. When I lined them up, none of them looked wrong when you read only the changed lines. Each one was an assumption the new code relied on and never checked.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A wrong spec, followed exactly.&lt;/strong&gt; The first release added a guard so a double tap on Start would not start two workouts. The guard never fired. It checked &amp;quot;is a session running &lt;em&gt;and&lt;/em&gt; is training active,&amp;quot; but the screen turns training off a moment before it sends Start, so the second half was always false. Here is the part that stays with me: the issue asked for that same check, a session &lt;em&gt;and&lt;/em&gt; training active. Claude wrote that issue. Grok built what it said. Both tests for the guard passed, because they checked the rule on its own and never sent a real double tap through the app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When does this run, compared to that?&lt;/strong&gt; A later release replaced a crude half-second wait at the end of a workout with a proper &amp;quot;wait until the last heart rate samples are saved&amp;quot; signal. It looked like a clear upgrade. But the signal was set up &lt;em&gt;after&lt;/em&gt; the code that waited on it already ran. On the first workout there was nothing to wait for. On every workout after that, it waited on the previous workout&#39;s signal, which was long finished. Either way it waited for nothing, with no error and no log line, and the half-second wait that used to do the job was deleted in the same change. The order was even written down in a comment in the same file. As first written, the fix was worse than the one-line sleep it replaced. Review caught it before the merge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A borrowed contract.&lt;/strong&gt; In my assistant app, Grok added OpenRouter as a provider by reusing the OpenAI client, because my scope told it to. The client was reusable, but OpenRouter&#39;s billing is not OpenAI&#39;s. OpenRouter checks your balance against the largest possible answer before it starts, so a small balance got a &amp;quot;payment required&amp;quot; error even though the real answer would have been cheap. Nothing in the source code shows that. One live request did: &amp;quot;You requested up to 16000 tokens, but can only afford 7211.&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A rule the framework hides.&lt;/strong&gt; To stop a background sync from bringing back a workout the user had deleted, Grok changed a database write. The SQL was correct. But the database library treats that kind of query as a &lt;em&gt;read&lt;/em&gt;, so the workout history screen would never have refreshed after a background update. Review caught this one too. You only see this in the code the library generates, not in anything a person writes.&lt;/p&gt;
&lt;p&gt;Different domains, same question: what is this change assuming that nobody checked? For concurrency, it is when something runs. For integrations, it is whose behavior you inherited.&lt;/p&gt;
&lt;h2&gt;The Spec and the Reviewer Were Part of It&lt;/h2&gt;
&lt;p&gt;I would like to say the model wrote the bugs and the reviewer caught them. The record does not say that.&lt;/p&gt;
&lt;p&gt;Two of the six releases trace their worst first-pass bug to a scope Claude wrote. The double-tap guard was one. The other was in my assistant app: the scope said a cut-off reply &amp;quot;produces the marker,&amp;quot; so Grok put a hidden marker inside the reply text. Every part of the app that did not know about the marker would have shown it or saved it, including into notes. The same scope also said &amp;quot;the client reports a truncated result,&amp;quot; which pointed the right way. Grok followed the more concrete sentence. The lesson I took is to write the contract into the scope, not the mechanism. A capable model will build the mechanism you describe, flaws included.&lt;/p&gt;
&lt;p&gt;The reviewer&#39;s fixes were not always right either. In one review, Claude found a real bug and attached a fix that would have made it worse. Grok took the finding, rejected the fix, explained why in one sentence, and solved it a different way. In another review, Claude&#39;s fix was right but incomplete. It said to include the conversation when saving a note, and did not say &amp;quot;through the privacy check.&amp;quot; Grok did exactly what it said. One &amp;quot;save this as a note&amp;quot; would have sent private chat history to a cloud model for a user who had chosen local only. The next review round caught it.&lt;/p&gt;
&lt;p&gt;Three of Grok&#39;s six fix passes added a new bug while fixing the old ones. In each case the fix was right where it aimed, and nobody asked what else it touched. One of those three came straight from the reviewer&#39;s remedy.&lt;/p&gt;
&lt;h2&gt;It Pushes Back, When It Has a Reason&lt;/h2&gt;
&lt;p&gt;Before the second release I wrote down one question: would Grok push back on a wrong instruction, or just do it? It did both, and mostly at the right times.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It rejected the reviewer&#39;s wrong fix described above.&lt;/li&gt;
&lt;li&gt;It declined a finding the review had marked as unproven. Lowering a limit shared by three providers on a guess was a real change, and it said it would not do it without a live request. When the live request came back, it made the narrowest correct fix, for OpenRouter only.&lt;/li&gt;
&lt;li&gt;It corrected a version number in the scope, and said why in the pull request.&lt;/li&gt;
&lt;li&gt;When the review said a test was missing, it pointed out the test existed under another name, and renamed it to the review&#39;s wording instead of arguing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where it did not push back was the double-tap spec and the privacy remedy. In both cases the instruction looked reasonable and was wrong in a way you only see by reading code outside the change.&lt;/p&gt;
&lt;h2&gt;Roles Reversed&lt;/h2&gt;
&lt;p&gt;For the seventh release, Claude Opus 5.5 implemented and Grok reviewed. This was a good test of whether the reviewer role was doing the real work, or just the model in it.&lt;/p&gt;
&lt;p&gt;Grok found two blockers in Claude&#39;s code, after over 3,900 tests had passed. The first needed code outside the change. The second was right there in the new code, and still easy to miss without thinking like an attacker:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The morning briefing would have opened links nobody pasted.&lt;/strong&gt; The new link reader scanned the whole message sent to the model. The briefing puts calendar and inbox text into that message, so every morning it would have fetched meeting links and newsletter tracking links. A fetched tracking link tells the sender you opened their email.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The address check could be tricked.&lt;/strong&gt; The link reader checked that a website did not point into my home network, then looked the address up again to connect. Claude had written a comment saying this only mattered for &amp;quot;a hostile DNS server.&amp;quot; Grok&#39;s answer: &amp;quot;The threat is a link the user pasted. The attacker owns that domain&#39;s DNS.&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude agreed with both and pushed back on one severity claim. It also kept one decision against the review, and found one case Grok&#39;s list missed. After the phone test found a separate bug, Grok took a last short look and called one item &amp;quot;not a hold&amp;quot;: a document tile that overflowed at bigger text sizes. New tests at larger font scales all failed on the approved code. It was a real accessibility bug that Grok had under-ranked. When it says something is minor, I check anyway.&lt;/p&gt;
&lt;p&gt;The phone test also turned up five issues that neither model&#39;s review raised, like a broken icon on every attached document. Tests found none of them, and neither did the reviews. That is a job for the phone, whichever model reviews.&lt;/p&gt;
&lt;h2&gt;The Weekly Limit Is the Real Cost&lt;/h2&gt;
&lt;p&gt;SuperGrok has a weekly limit, and in the first week I ran straight into it. The week started on a Monday at 11:06 AM. By Wednesday morning, after the four Zone 2 Trainer releases in this post, one more release I never assessed and left out, and a branch cleanup session, Grok stopped mid-session with &amp;quot;You have run out of credits.&amp;quot; It stayed that way until the next Monday:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitziFV3vM43SZeQKEjcAgECoDWaxSwMxXz-GOY3lQJXPS5-MkLdv5UYzCn6bSO0GVazQkM4pWQ-vC6qVxlan0vj3JNQSFWYWrBrW6w7LjWxZkTi1tJwAKwJ5fG8Zrxpu2X-nyxsN0htmWpKMyTi23VJUdwVSD8fOtDKdD2JfvcQ8sk2I_2R0BlrcNWrWjY/s1790/2026-09-27-supergrok-weekly-limit.png&quot; alt=&quot;SuperGrok usage panel reading &amp;quot;Weekly limit reached&amp;quot;, 100% used, 98% of it Grok Build and 2% Chat, resetting September 28, 2026 at 11:06 AM&quot;&gt;&lt;/p&gt;
&lt;p&gt;That is two days of work and five days of waiting, which is worse than the &lt;a href=&quot;https://blog.ervits.com/2026/09/three-days-week-what-rate-limit.html&quot;&gt;three days a week&lt;/a&gt; I wrote about with Ollama Pro.&lt;/p&gt;
&lt;p&gt;The next week I watched the meter. Implementing two releases of my assistant app in one afternoon, including their review rounds, used 63% of the week. Reviewing the seventh release took it to 66%:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDdBuh3-pgyWIb6E4yOb5sYVinkPyNvV3J30Gj6EmsMARLZXGXnszmq7XWHKSNShH6pfsIlX4gkqWZgwG4JWiVNyuCewmINhnedfetrk9wGE7LG1sSZTh6_jBLzqFAnV-xbe4ln7Qgq678Jl1Jz_wqzmhheVdL0tZ60pWIdClVLmP5yUTRcgEYh-piRgL4/s1826/2026-09-29-supergrok-weekly-usage.png&quot; alt=&quot;SuperGrok usage panel showing the weekly limit at 66% used, all of it Grok Build, resetting October 5, 2026, with $0.00 of extra usage credits&quot;&gt;&lt;/p&gt;
&lt;p&gt;So about 3% for a review, against roughly 31% for each release it implemented. That comparison needs caveats. Reviewing reads a diff, while implementing explores, writes, tests and repeats. The releases were different. The 3% covers the first two review rounds only. And I have no matching number on the Claude side, because that week&#39;s Claude usage was mixed with other work.&lt;/p&gt;
&lt;p&gt;Even with those caveats, it changes how I think about the plan. Speed does not save weekly budget. A fast release spends the same week as a slow one, it just gets there sooner. If one Grok review costs a tenth of one Grok implementation, and the review finds the bugs that matter, then the review is where that plan earns its money.&lt;/p&gt;
&lt;p&gt;It also showed me what speed skips. In the 65-minute release, the full test suite ran once, on the version that still had all four first-pass bugs. The merge happened before anyone looked at the last commit, and the phone checks in the scope never ran. Claude ran them afterwards, and everything that could be checked passed. But that took about another hour, on a different budget. The checks still had to be paid for. They just came out of a different plan.&lt;/p&gt;
&lt;h2&gt;What I Am Not Saying&lt;/h2&gt;
&lt;p&gt;Seven releases is not a benchmark. The work was different every time, some releases were much harder than others, and one person judged all of it. The same model wrote most of the specs, reviewed the code, and helped me collect the evidence for this post. Part of the seventh release ran on airplane Wi-Fi, so I would not compare those times with the rest.&lt;/p&gt;
&lt;p&gt;I am also not saying Grok is better than Claude, or the other way around. Each model missed something a careful read of the diff did not catch, including in its own code.&lt;/p&gt;
&lt;h2&gt;Where I Landed&lt;/h2&gt;
&lt;p&gt;Grok did well where the Ollama models struggled: few review rounds, and no &amp;quot;fixed&amp;quot; that was not fixed. Its fix passes still need review. Three of six added a new bug, even though none of them claimed more than they did.&lt;/p&gt;
&lt;p&gt;What changed is how I review. Reading the changed lines carefully is not enough, because the bugs that got through lived in what those lines assumed: sometimes in another file, sometimes in a comment a few dozen lines away. Now I ask a different question depending on the work. For concurrency, when does this run, compared to what it depends on? For integrations, whose behavior did this inherit? For specs I write, am I describing the result I want, or a mechanism a good model will follow exactly?&lt;/p&gt;
&lt;p&gt;And on a weekly limit, the numbers point toward spending Grok on review more than on implementation.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/09/cheap-plan-expensive-reviews.html&quot;&gt;Cheap Plan, Expensive Reviews: Why I Cancelled Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model: Why Cross-Vendor Code Review Actually Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/09/three-days-week-what-rate-limit.html&quot;&gt;Three Days a Week: What a Rate Limit Actually Costs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/10/seven-releases-with-grok.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1lqmAS5NI1XVbZLZedx9sJI-P3-K1IwFs9HuraLVDsZWT9GxsG9od_j0uuYoNX9pDbQtn9XEpc0HBbhJPbVc8E37QaZ4tHtpq4zTBctGH1SKzQILO8N2ycbsi1SliGcqhG8oSmfKTN-o6wSl7U7L4SPzG2ybPFQ7aBejdUJny-RyLhEFRWuPsdLMe3BBy/s72-c/2026-10-01-grok-seven-releases-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-3043487099282125972</guid><pubDate>Mon, 28 Sep 2026 00:05:38 +0000</pubDate><atom:updated>2026-09-27T20:05:38.286-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">code review</category><category domain="http://www.blogger.com/atom/ns#">Deepseek</category><category domain="http://www.blogger.com/atom/ns#">glm</category><category domain="http://www.blogger.com/atom/ns#">Kimi</category><category domain="http://www.blogger.com/atom/ns#">ollama</category><category domain="http://www.blogger.com/atom/ns#">opus</category><category domain="http://www.blogger.com/atom/ns#">tokens</category><title>Cheap Plan, Expensive Reviews: Why I Cancelled Ollama</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUu9pW2AhuelxjAIKJ1Jd_jidRkkEcO4GYK8_iUM14xpg9A4OtRYxnAbzdGNYokEhJXKw2CssYRps-JzbfyonVHES-X4PtoS39Dat19TOmo_kTAQadye1R1ShuIxtg-E8L1OJMgAF803UQMtAMIaN3hj2R6E7BAUWN4N_mz93SeHCw78b4ZAW32Ufcj7wJ/s1200/2026-09-27-done-with-ollama-hero.jpg&quot; alt=&quot;Abstract dark blue illustration of a glowing ring of checkpoints, with one orange packet leaving the loop along a straight light trail&quot;&gt;&lt;/p&gt;
&lt;p&gt;Two weeks ago I ended &lt;a href=&quot;https://blog.ervits.com/2026/09/three-days-week-what-rate-limit.html&quot;&gt;a post about rate limits&lt;/a&gt; with &amp;quot;Ollama is back.&amp;quot; I had re-subscribed monthly on purpose, to answer the question properly this time: does this plan give me the flexibility I did not have the first time around? I have the answer now, and it is no. I cancelled.&lt;/p&gt;
&lt;p&gt;The short version is that the money ran out early. The longer version is the reason for this post: even while the money lasted, the plan was not the real cost. The real cost was how many review rounds it took to get the work to a state I could merge.&lt;/p&gt;
&lt;h2&gt;The Money Part&lt;/h2&gt;
&lt;p&gt;Ollama Pro is $20 a month and includes $60 of cloud usage. This is my dashboard today:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjpYI2M7FT5Fl-yrKv4ziaTmHbGltgl4cDdLC1jcpeT5-3DT2bbRfvxZQPVWkknjIjzT_WXiYWyQswN0mSjQtRHxZClj1x53eQN4Gwh1n9bTIfGsjsEZRzwJLryxL8usGBG-X0eIJJ6MPI4lL4TagJ-XIsMAR8E_0haBhFfHoKrYoNwu_BMh2y77X0g-KbA/s1330/2026-09-27-ollama-pro-monthly-usage.png&quot; alt=&quot;Ollama Pro dashboard showing $60 of $60 monthly usage used, resetting in 2 weeks, with requests for kimi-k2.7-code, glm-5.3, glm-5.3-flash, glm-5.2 and deepseek-v4-pro:0813&quot;&gt;&lt;/p&gt;
&lt;p&gt;The $60 was gone within the first week of the month. When I saw the reset was still three weeks away, I cancelled. Most of the requests went to &lt;code&gt;kimi-k2.7-code&lt;/code&gt;, the model I said in &lt;a href=&quot;https://blog.ervits.com/2026/09/three-days-week-what-rate-limit.html&quot;&gt;the last post&lt;/a&gt; I had not evaluated yet. I evaluated it.&lt;/p&gt;
&lt;p&gt;For comparison, my Claude weekly limit has covered most of this past week of daily work across two apps, and it is on pace to run out a day or so before it resets. A weekly limit that runs out near the end of the week costs a day of waiting. A monthly credit that runs out with three weeks left costs three weeks.&lt;/p&gt;
&lt;p&gt;That alone was enough to cancel. But it is not the part I want to write about.&lt;/p&gt;
&lt;h2&gt;A Small Release, Three Problems&lt;/h2&gt;
&lt;p&gt;Here is one real example, from my Zone 2 Trainer app. I asked &lt;code&gt;kimi-k2.7-code&lt;/code&gt; to &amp;quot;scope a bug fix release, pull all issues that are marked as bug and build 6.10.2.&amp;quot; It did a sensible job of the scoping. It asked three questions, proposed cutting the patch from the last release tag instead of the main branch so no unverified Bluetooth changes would ride along, and picked two bug fixes. Then I asked it to post a pull request:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2RQKFNQPrmA9HBmQHis8Rd5e9V8cjxa_-EN7bvOWBukUgNr-J7i3FDAKGb74FwvR6GXxcjKLrvbnSNZLBr9sYyCvdwJrDTnNXd27-K_9E6Me4_a0uwfTG-ogcfNGa-8mwceFiiDieY9PqY-YD3w4XwNoiVpRnRGgANzkr_PzP4GqDRRpSYgS0EgEsnPdZ/s1300/2026-09-27-kimi-6-10-2-pr-overclaim.png&quot; alt=&quot;Chat reply from kimi-k2.7-code posting the 6.10.2 pull request, including the line &amp;quot;#854 already present on this branch; documented in release notes&amp;quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Every automated check passed: release metadata, unit tests, lint, and the release build. I sent the pull request to Claude Opus 5 for review. It said the core fix was sound and the new test was real, and then it found three problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It could mix up whose workout is whose.&lt;/strong&gt; Some workouts in my app come in from another fitness app. If I edited one of those, my app would forget where it came from, and on the next sync it would send its own copy of the heart rate, calories and distance back to Health Connect. Now the same workout shows up twice, and your totals for the day are wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It could lose the Strava badge for good.&lt;/strong&gt; A small badge shows which workouts are already on Strava. Editing a workout while Strava was turned off wiped that badge, and nothing would ever bring it back, even though the workout was still on Strava.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The release notes promised a fix that was not in the release.&lt;/strong&gt; They said 6.10.2 fixed one bug. That fix was already in the app two versions earlier, and nobody had tested it on a real workout yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be fair to Kimi, the fix pass was quick and correct. It took under nine minutes, added two regression tests, and a second review found nothing. Then it merged, tagged the release, built it, installed it on my phone, and wrote this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqpZjJmOOsBda5SKOtoqbfUgHSMgzs0z1sSWtetWdSeNcvXR5n0vxkWjzIu304kE6pmn_ZOZ2jve4hddXEoGYwl4o5mNiULKDin4Adm-QC_QphjLfcS6-LchIvyK72xibfXJY6bOWTJV49lHBajvTR2EPm34_NxRghHwyWGw1k4JfWfYf4g1dsoEJkGrUb/s1300/2026-09-27-kimi-6-10-2-skips-device-check.png&quot; alt=&quot;Chat reply from kimi-k2.7-code: &amp;quot;Per AGENTS.md, this release has no BLE changes, so device verification for sensor behavior is not required; the install/launch smoke test is the applicable gate.&amp;quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;For the final release, that is fair. Nothing in it touched the sensors. But the first version said the same thing while it was still promising the other fix, and that one could only be checked by hand. The bug was about pausing: if you paused a workout, the app saved the wrong start time, as if you had started later than you really did. To check it, you have to do a real workout, pause it partway, finish, and see whether the history shows the time you actually started. Nobody had done that, and it still has not been done.&lt;/p&gt;
&lt;p&gt;None of these three were broken code. Nothing crashed and no test failed. The work looked finished, and it was wrong in the places where you have to know what the data means.&lt;/p&gt;
&lt;h2&gt;The Same Shape in a Bigger Release&lt;/h2&gt;
&lt;p&gt;The example above is small on purpose. The same pattern showed up in my personal assistant app, on a bigger release.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kimi-k2.7-code&lt;/code&gt; built a release covering four issues. It shipped with every automated gate green: formatter, analyzer, the project rule checker, and 3,416 tests passing. Within about an hour of the release, using it on my phone had turned up three defects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A calendar event spanning several days still showed only its start date in the editor. That was the exact defect the issue described, in the file it named.&lt;/li&gt;
&lt;li&gt;An &amp;quot;Edit fields&amp;quot; button did nothing. No error, no feedback. The code that handled button actions had no branch for that action, so it just returned.&lt;/li&gt;
&lt;li&gt;Events read from a photo were supposed to be marked &amp;quot;(inferred)&amp;quot; where the model guessed a value. They never were. The flags defaulted to false and the model never set them. The tests passed because they built their own flags.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The follow-up pull request, from the same model, addressed all three. Review found eight more problems in the fix. One example: the new end-date row reused the start-date picker, so picking a new end date moved the whole event. It took three review rounds to get it mergeable.&lt;/p&gt;
&lt;h2&gt;When the Fix Is Where Things Break&lt;/h2&gt;
&lt;p&gt;The part I found most useful came from one pull request where two models took turns answering the same review, back to back. It is about as close to a controlled test as my real work gets.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Round&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Findings to fix&lt;/th&gt;
&lt;th&gt;What the fix pass did&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;&lt;code&gt;deepseek-v4-pro:0813&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Fixed 6. Left 3 still broken but reported them fixed. Added 3 new regressions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glm-5.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Fixed all 3 regressions. Fixed the other 3 only partly. Added 2 narrow new defects.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Closed all six by changing the underlying rules.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The reviewer in all three rounds was Claude Opus 5. Rounds one and two are the setup I argued for in &lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model&lt;/a&gt;: one company&#39;s model writes the code and another company&#39;s model reviews it. Round three is not. There, Opus 5 was fixing findings from its own review, so keep that in mind when reading the last row.&lt;/p&gt;
&lt;p&gt;The DeepSeek pass is the one that stays with me. Its commit said it addressed all 12 findings. Three of them were still live, including a layout overflow &amp;quot;fixed&amp;quot; in a way that still overflowed, with a new code comment that described the opposite of what the code does. Two of the three new regressions broke behavior that worked on the main branch.&lt;/p&gt;
&lt;p&gt;GLM was clearly better. It broke nothing. But it had the same habit, fixing the example instead of the rule. A currency check went from missing &lt;code&gt;$5M&lt;/code&gt; to handling &lt;code&gt;$5M&lt;/code&gt; and still missing &lt;code&gt;$5bn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That matches what I saw in July. Back then GLM 5.2 was my daily driver, and in &lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;my comparison of Codex, Claude and GLM 5.2&lt;/a&gt; about 40% of its findings did not hold up when Claude checked them. The newer model broke less here, but its work still needed checking.&lt;/p&gt;
&lt;h2&gt;About Speed&lt;/h2&gt;
&lt;p&gt;When I started writing this, I planned to blame part of it on the open models being slow. I went back to the timestamps to check, and that turned out to be only partly true.&lt;/p&gt;
&lt;p&gt;On the 6.10.2 release, Kimi&#39;s own time was short: under a minute to post the pull request, under nine minutes for the fix pass, under two minutes to release. That is in the same range as the other models I use. The long gaps on the pull request were mine, not the model&#39;s.&lt;/p&gt;
&lt;p&gt;On the bigger jobs, the wall-clock numbers are worse. Kimi took over an hour to answer an eight-finding review, and DeepSeek almost an hour for twelve. But those times include my own gaps too, so I would not lean on them.&lt;/p&gt;
&lt;p&gt;The number that does not depend on my schedule is rounds. How many times does the work go around review before it is right, and does each fix pass make things better or add new problems? That is where the open models cost me.&lt;/p&gt;
&lt;h2&gt;What I Am Not Saying&lt;/h2&gt;
&lt;p&gt;The tests were real. The releases shipped and mostly work. For mechanical, well-bounded work, like renames, tests, or a screen that looks like four other screens, these models are genuinely useful.&lt;/p&gt;
&lt;p&gt;What I am saying is about the cost of getting from &amp;quot;looks done&amp;quot; to &amp;quot;is done.&amp;quot; Every extra review round is time, attention, and budget, and the budget for review has to come from somewhere.&lt;/p&gt;
&lt;h2&gt;Where I Landed&lt;/h2&gt;
&lt;p&gt;I cancelled Ollama Pro. The $60 did not last the first week, and the work it bought needed more rounds of review than the work from the plans I am keeping. That also ends the two-plan setup I described in &lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt;, where an Ollama plan covered my less critical projects.&lt;/p&gt;
&lt;p&gt;The way I compare plans now: price per month tells you very little. Multiply it by the number of review rounds it takes to get to mergeable, and then check how long you wait when the limit runs out. That is the number that decides whether the work actually ships.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/09/three-days-week-what-rate-limit.html&quot;&gt;Three Days a Week: What a Rate Limit Actually Costs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;Codex vs Claude vs GLM 5.2: A Practical Comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model: Why Cross-Vendor Code Review Actually Works&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/09/cheap-plan-expensive-reviews.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiUu9pW2AhuelxjAIKJ1Jd_jidRkkEcO4GYK8_iUM14xpg9A4OtRYxnAbzdGNYokEhJXKw2CssYRps-JzbfyonVHES-X4PtoS39Dat19TOmo_kTAQadye1R1ShuIxtg-E8L1OJMgAF803UQMtAMIaN3hj2R6E7BAUWN4N_mz93SeHCw78b4ZAW32Ufcj7wJ/s72-c/2026-09-27-done-with-ollama-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-6270845167760401513</guid><pubDate>Sun, 20 Sep 2026 22:19:16 +0000</pubDate><atom:updated>2026-09-20T18:19:16.294-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">enduro</category><category domain="http://www.blogger.com/atom/ns#">garmin</category><category domain="http://www.blogger.com/atom/ns#">SDK</category><title>From Idea to Wrist: Building a Watch Face in About an Hour</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqPmdFvU0WnOTUIdxM1M5ea-bFkSMTCyKkCsSCgWun4GdZTo8BOiUVSdANEzLVaf9yBRaGfQd8cdXKt2RXrr6ovJ7hqo7Q7hA3CG378WdguRsl5s1rv5FUuOaEErvtTq4jrtlUWgOIq0BTQ5vzihrVKTigswZI5WbfDGPZP1Bh4X4OKmKAbi3n_Ci5uRQM/s400/2026-09-20-garmin-watch-face-agent-hero.jpg&quot; alt=&quot;Abstract dark illustration of a round watch face with glowing orange numerals, a partial progress ring, and faint measurement grid lines&quot;&gt;&lt;/p&gt;
&lt;p&gt;This came from a post I saw on X. Somebody wanted a different watch face for their Garmin, sat down with Claude, and had one working in short order. I had thought about doing that for a long time. Like most new things, I was hesitant, because it meant stepping out of my comfort zone.&lt;/p&gt;
&lt;p&gt;It turned out to be easy with the help of AI. After about an hour of fiddling with it, I had a working watch face. I almost wish I had jumped on it earlier in the week, because I bought a watch face a few days ago and was very disappointed with it.&lt;/p&gt;
&lt;p&gt;The face is &lt;a href=&quot;https://github.com/dbist/ridgeline&quot;&gt;Ridgeline&lt;/a&gt;: big time, a ring that fills toward my step goal, heart rate, steps, battery.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYkRGucGQFPno-jjqimtaRuWyovpdu1687hi6kEOQilVMvVTGZR483asOL_TixS1eh0qmsrJeNlX2G1emjw9ksjoFUhpiQCaqjnevutIyQWfkM8HAtWBbsBBSUynfHtAfsimT_srAyTBPSC1V3O1xJ7x9PfCswKERiWNOqW4iGJiphjZ7KEsmsA9JlTX83/s524/2026-09-20-ridgeline-enduro3-simulator.png&quot; alt=&quot;Ridgeline watch face running in the Garmin simulator&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Getting Started Was the Part I Dreaded&lt;/h2&gt;
&lt;p&gt;Writing for a watch is its own little world. There is a language I had never used, a set of tools I did not have installed, and a way of getting the finished thing onto the watch that I had never done. That stack of unfamiliar steps is exactly what had kept me from trying.&lt;/p&gt;
&lt;p&gt;The agent handled all of it. It set up the project, wrote the first version, and when it turned out my machine had none of the Garmin tools on it, it did not start installing things by hand. It wrote up what was missing as an issue against the repo where I keep my machine setup, so the next laptop I set up gets the same tools automatically. I liked that. It fixed today&#39;s problem in a way that also fixes it next time.&lt;/p&gt;
&lt;h2&gt;The Number It Made Up&lt;/h2&gt;
&lt;p&gt;Here is the part I want other people to know about.&lt;/p&gt;
&lt;p&gt;To show me the design quickly, the agent built a little web version of the face. Useful - I could try a change and see it right away. But to draw it, it needed to know how big the watch&#39;s built-in fonts are, and that is not something the documentation tells you. So it guessed.&lt;/p&gt;
&lt;p&gt;The guess was off by a lot. Which meant the mockup I was looking at, and feeling pretty good about, was not what my watch was actually going to show. The big bold numbers I thought I had picked had never once appeared on the watch. Worse, the agent then suggested a fix built on top of its own bad guess, which would have made things worse in a new way.&lt;/p&gt;
&lt;p&gt;What fixed it was dull and obvious in hindsight: stop guessing, run the thing, and have it print the real sizes. Once we had real numbers, the layout stopped being a debate about taste. Some of what I wanted simply did not fit, and the numbers said so plainly. The seconds had to move somewhere else entirely, and the labels had to shift because a round screen quietly eats your corners.&lt;/p&gt;
&lt;p&gt;To its credit, once it had real numbers the agent worked out the consequences before touching anything, instead of repeating its earlier suggestion.&lt;/p&gt;
&lt;h2&gt;Small Things Worth Catching&lt;/h2&gt;
&lt;p&gt;Two more turned up right at the end, when I went to put the code up publicly.&lt;/p&gt;
&lt;p&gt;The project still had a leftover placeholder identifier from the template it started as. Harmless for something you install on your own watch, a problem if you ever want to publish it properly, and easier to change before anyone else installs it.&lt;/p&gt;
&lt;p&gt;And every commit in the project had been made under a fake email address, because of a stray setting in the project folder. Caught it seconds before pushing. Trivial to fix while it was still private, genuinely annoying afterward. An assistant that checks who it is committing as before a publish step is doing unglamorous work that pays for itself.&lt;/p&gt;
&lt;h2&gt;What I Took Away&lt;/h2&gt;
&lt;p&gt;I had put this off for a long time because it looked like a whole new world to learn. It was, and the agent carried almost all of that for me. Idea to working face on my wrist, in about an hour, on a Sunday.&lt;/p&gt;
&lt;p&gt;It was fast and it was genuinely good at the parts I did not know. It also handed me a confident number it had no way to know. The correction came from outside the conversation, from running the thing on real hardware instead of trusting the picture on the screen.&lt;/p&gt;
&lt;p&gt;That is roughly the deal, and I will take it. Just check the confident answers against the real thing before you build on top of them.&lt;/p&gt;
&lt;h2&gt;Take It, or Build Your Own&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dbist/ridgeline&quot;&gt;Ridgeline&lt;/a&gt; is public and open source. If you want to install it, change it, or send a fix or an idea my way, please do. Issues and pull requests are welcome.&lt;/p&gt;
&lt;p&gt;But honestly, the better takeaway is the one I started with. If you have been staring at your own watch thinking the face you want does not exist, go build it. You do not need to know the language or the tools. If you end up with your own face instead of mine, that is a good outcome as far as I am concerned.&lt;/p&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/09/from-idea-to-wrist-building-watch-face.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqPmdFvU0WnOTUIdxM1M5ea-bFkSMTCyKkCsSCgWun4GdZTo8BOiUVSdANEzLVaf9yBRaGfQd8cdXKt2RXrr6ovJ7hqo7Q7hA3CG378WdguRsl5s1rv5FUuOaEErvtTq4jrtlUWgOIq0BTQ5vzihrVKTigswZI5WbfDGPZP1Bh4X4OKmKAbi3n_Ci5uRQM/s72-c/2026-09-20-garmin-watch-face-agent-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-5933370717728302099</guid><pubDate>Thu, 17 Sep 2026 00:48:17 +0000</pubDate><atom:updated>2026-09-16T20:48:17.411-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">cost</category><category domain="http://www.blogger.com/atom/ns#">llm</category><category domain="http://www.blogger.com/atom/ns#">ollama</category><category domain="http://www.blogger.com/atom/ns#">openai</category><category domain="http://www.blogger.com/atom/ns#">workflow</category><title>Three Days a Week: What a Rate Limit Actually Costs</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6rnbos8MVWj9FNGbExXCBCA6YCxELQx1DnZYQ9dIHcjabnAdvuJJCPbIdWv7spWSLEQaLhtzin0ekkkjek_MW-K1gesJHCDLueeoWVQVR5URNzkiH2LYLP2kyxZke-bYSp4ts0McLqvIexdOTprRNEwZm6K5s_AfubzUW4eFFKrFPbwYwijTB1lt9AFm_/s1200/2026-09-13-weekly-rate-limits-hero.jpg&quot; alt=&quot;Abstract dark blue illustration of glowing light trails sweeping through a scattered field of data points&quot;&gt;&lt;/p&gt;
&lt;p&gt;In July I wrote that &lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;monthly plans beat paying by the token&lt;/a&gt; for my development work. I also mentioned getting only three or four working days before hitting the weekly limit. What I understated was how much that limit would start deciding my schedule, and which model reviewed my code.&lt;/p&gt;
&lt;p&gt;A $20 monthly plan sounds like a month of work. In practice the weekly limit decides how much of that month you actually get, and for me the answer was about three working days. Then I waited. Not for an hour, not until midnight, but for the reset, which is most of a week away when you hit the wall on a Wednesday.&lt;/p&gt;
&lt;h2&gt;Three Days, Then Waiting&lt;/h2&gt;
&lt;p&gt;The plan was Ollama Pro, monthly. Twenty dollars, and on paper that is a bargain next to what the same work costs through an API key. The weekly ceiling is what broke it. I would get through roughly three days of real work and spend the rest of the week unable to do the thing I was paying for.&lt;/p&gt;
&lt;p&gt;Waiting is worse than it sounds, because the work does not pause politely. A bug I was halfway through debugging on Wednesday was still a bug on Thursday. The choice was not &amp;quot;work now or work later.&amp;quot; It was &amp;quot;work now on a different plan, or do not work.&amp;quot;&lt;/p&gt;
&lt;p&gt;So I bought ChatGPT Plus again. That put me at three concurrent subscriptions, which is not a decision I made so much as one I backed into. I also wanted to try the new Kimi model around then, and I deliberately did not, because I was not going to evaluate a fourth thing while paying for three. I waited for the Ollama month to run out first.&lt;/p&gt;
&lt;p&gt;It ran out, and I ended it. Three plans was not something I could justify, and by then Opus 5.0 had taken over most of my daily coding anyway. I will say this for 5.0 over 4.8: it is more willing to admit its predecessor was wrong. It found bugs in branches that came out of 4.8 sprints and said plainly that the earlier reasoning was bad, instead of defending it.&lt;/p&gt;
&lt;p&gt;Codex is not immune either. With 5.6 Sol I burn through the weekly rate fast, and then it is the same wait, just on a different vendor&#39;s schedule.&lt;/p&gt;
&lt;h2&gt;The Limit Picks Your Reviewer&lt;/h2&gt;
&lt;p&gt;Here is the part I did not see coming, and it is the reason I wanted to write this post rather than another one about price.&lt;/p&gt;
&lt;p&gt;When I ran that big evaluation pass over a codebase I thought was solid, Sol turned up a pile of issues and I wanted a second opinion from a different vendor before I believed any of them. The model I wanted for that job was Fable. Fable was no longer available in my weekly limit. So Opus 4.8 did the validation instead.&lt;/p&gt;
&lt;p&gt;That worked out. Opus confirmed the findings, and I wrote up that workflow in &lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model&lt;/a&gt;. But look at what actually happened: a quota, not a judgment, chose the model that decided which of those bug reports were real. I did not sit down and reason about which reviewer suited the task. I took what was left.&lt;/p&gt;
&lt;p&gt;A rate limit does not politely stop you at the door. It quietly reassigns your work to whatever tier you still have, and you find out afterward whether that mattered.&lt;/p&gt;
&lt;h2&gt;Budget Rules, Written Down&lt;/h2&gt;
&lt;p&gt;In August I added a model tiering table to my AGENTS.md. I described it then as a quality rule, and it is, but the honest reason I wrote it was that I had been running the most expensive model available for everything, including renaming variables, and then wondering why I hit the weekly limit midweek.&lt;/p&gt;
&lt;p&gt;The Codex side of that policy is more explicit than the Claude side, because I could put numbers on it. Here is the section as it actually sits in the file, with the longer bullet lists trimmed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-markdown&quot;&gt;&lt;span class=&quot;hljs-section&quot;&gt;# Codex Model Usage Policy&lt;/span&gt;

Optimize for &lt;span class=&quot;hljs-strong&quot;&gt;**correctness per token**&lt;/span&gt;, not maximum model capability on every
task. Use the least expensive model that can reliably complete the work, and
escalate only when complexity warrants it.

&lt;span class=&quot;hljs-section&quot;&gt;## Default Model Selection&lt;/span&gt;

&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**Terra Medium - default**&lt;/span&gt;
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Normal feature development
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Typical bug fixes
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Moderate refactoring
&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**Luna Medium - mechanical / low-risk work**&lt;/span&gt;
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Small UI changes, straightforward CRUD, renames
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Formatting and lint fixes, documentation
&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**Sol Medium - difficult reasoning**&lt;/span&gt;
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Architecture or design decisions
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Bugs with unclear root causes
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Concurrency, synchronization, or lifecycle bugs
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Work where Terra has already made a credible attempt and failed
&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**Sol High - exceptional escalation only**&lt;/span&gt;
&lt;span class=&quot;hljs-bullet&quot;&gt;  -&lt;/span&gt; Use only when Sol Medium is insufficient for a genuinely difficult
&lt;span class=&quot;hljs-code&quot;&gt;    problem. Do not use routinely.
&lt;/span&gt;
&lt;span class=&quot;hljs-section&quot;&gt;## Escalation Policy&lt;/span&gt;

Do not escalate preemptively.

Preferred progression:

&lt;span class=&quot;hljs-code&quot;&gt;`Luna Medium -&amp;gt; Terra Medium -&amp;gt; Sol Medium -&amp;gt; Sol High`&lt;/span&gt;

If escalating because another model failed:

&lt;span class=&quot;hljs-bullet&quot;&gt;1.&lt;/span&gt; Preserve the useful work already completed.
&lt;span class=&quot;hljs-bullet&quot;&gt;2.&lt;/span&gt; Clearly summarize the remaining problem and observed failures.
&lt;span class=&quot;hljs-bullet&quot;&gt;3.&lt;/span&gt; Do not repeat broad repository exploration unless necessary.

&lt;span class=&quot;hljs-section&quot;&gt;## Model Budget Target&lt;/span&gt;

&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**~70% Terra Medium**&lt;/span&gt;
&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**~20% Sol Medium**&lt;/span&gt;
&lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-strong&quot;&gt;**~10% Luna Medium**&lt;/span&gt;

This is a guideline, not a quota. Task complexity should determine model
choice.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things in there are doing most of the work. The first is point three under escalation. When a cheaper model fails and you hand the problem up, the expensive model&#39;s instinct is to re-read the entire repository before touching anything, and that re-exploration is where a surprising amount of my budget went. The second is the percentage target. It is a guideline and task complexity still decides, but having a number in the file changes the conversation. Without one, every task feels like it deserves the best model.&lt;/p&gt;
&lt;p&gt;When I published the shorter AGENTS.md I said I did not know yet whether it was working and to ask me in a month. It has been about four weeks. On the instruction-following question I still do not have a clean answer. On the token question, with the current mix of plans and model choices, I am no longer stopping on Wednesday. I cannot yet separate how much of that came from tiering and how much came from spreading the work across subscriptions.&lt;/p&gt;
&lt;h2&gt;The Trap Inside the Budget Rule&lt;/h2&gt;
&lt;p&gt;There is a catch, and I want to state it because the tiering table does not.&lt;/p&gt;
&lt;p&gt;Rate limits push you down the tiers. That is the whole point. But they push you down at exactly the moment you can least afford it, because the work you do when you are running low is usually triage: which of these reported bugs are real, which are noise, what do I fix first.&lt;/p&gt;
&lt;p&gt;Triage is not mechanical work, and I learned that the expensive way. I closed nine issues after a source-only review. A second pass ran the reported inputs and reversed four verdicts. I changed both the model and the instructions for that second pass, so I cannot credit the model alone. I went through what flipped and why in &lt;a href=&quot;https://blog.ervits.com/2026/08/a-convincing-review-of-wrong-path.html&quot;&gt;A Convincing Review of the Wrong Path&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The budget lesson survives either explanation: leave room for verification before dismissing a bug report. A cheap pass that clears your queue is not a saving if the queue was right.&lt;/p&gt;
&lt;p&gt;So my own rule now has an exception attached: drop tiers for implementation, not for judgment. If the budget is tight, spend what is left on deciding what to work on, and let the cheap model type.&lt;/p&gt;
&lt;p&gt;That turned into how I split the work generally. Fable and Sol reason and plan. Terra and Opus implement against those plans. The scarce, expensive tokens buy thinking, not keystrokes.&lt;/p&gt;
&lt;h2&gt;A Week of This in Practice&lt;/h2&gt;
&lt;p&gt;To make it concrete, here is a real entry from this month.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9/9&lt;/strong&gt; - Started using GPT-6 Astra for reviews and analysis. It reported 20 new bugs, some rated critical. I ran those past Claude, which confirmed them. Then I had Claude implement the fixes and sent the resulting pull request back for review, where Astra caught regressions in the fix itself.&lt;/p&gt;
&lt;p&gt;Four stages for one batch of bugs. Cross-vendor review is worth it, and I made that case in &lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model&lt;/a&gt;. I just think it is worth saying out loud that the workflow I recommend is a workflow that consumes two budgets instead of one.&lt;/p&gt;
&lt;p&gt;The budgets are not even the same shape anymore. Noticing a problem and writing it up as a real work item has drifted onto my phone, and that half draws down a different plan than the one doing the implementing.&lt;/p&gt;
&lt;h2&gt;The $15 I Did Not Mean to Spend&lt;/h2&gt;
&lt;p&gt;One more, because it belongs to the same story from the other direction.&lt;/p&gt;
&lt;p&gt;I had left an OpenAI API key configured for a model in VSCode and forgot about it. One short coding session later I had burned $15 without realizing I was not going through the monthly plan I was already paying for. Nothing about the experience tells you which one you are on. The editor looks the same either way.&lt;/p&gt;
&lt;p&gt;I made the case in &lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt; that API keys are the wrong default for development work. This is the smaller, dumber version of that argument: the plan only protects you when the traffic actually goes through it.&lt;/p&gt;
&lt;h2&gt;Where I Landed, For Now&lt;/h2&gt;
&lt;p&gt;Model tiering written into the file the agents read, with a rough percentage target. High-reasoning models for planning and review, cheaper ones for implementation, and never the cheap tier for deciding what is real.&lt;/p&gt;
&lt;p&gt;And Ollama is back, which puts me at three plans again: ChatGPT Plus, Claude on the $200 annual plan, and Ollama Pro monthly. The difference is that I chose this one. They introduced an annual plan, which is the kind of thing that makes you reconsider a decision you were fairly confident about, so I re-subscribed monthly first. Not because I changed my mind, but because I wanted to answer the question properly this time. I am running DeepSeek v4 Pro and GLM 5.3 on the less critical paths and watching what happens to the weekly ceiling.&lt;/p&gt;
&lt;p&gt;That is the actual test, and it is not a price test. I already pay for one of these a year at a time, so I am not against annual commitments. Committing to a year of Claude was easy, because by then I knew exactly what a week on it looked like. I do not know that about Ollama yet, and I cancelled it once already. What I need to learn before signing up for twelve months is whether this configuration gives me the flexibility I did not have the first time around, and there is no way to get that from a pricing page. I have to spend a month either running into the limit or not running into it.&lt;/p&gt;
&lt;p&gt;Kimi, the model I put off trying because I would not evaluate a fourth thing while paying for three, is still sitting there unevaluated. I am aware of the irony.&lt;/p&gt;
&lt;p&gt;The thing I would tell anyone comparing plans is to stop comparing the monthly numbers first. Ask how many days of work the weekly limit actually buys, because that is the number that determines whether you are working on Thursday.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/claude-keeps-ignoring-my-agentsmd-and-i.html&quot;&gt;Claude Keeps Ignoring My AGENTS.md, and I Think I Know Why&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model: Why Cross-Vendor Code Review Actually Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/a-convincing-review-of-wrong-path.html&quot;&gt;A Convincing Review of the Wrong Path&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/09/three-days-week-what-rate-limit.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6rnbos8MVWj9FNGbExXCBCA6YCxELQx1DnZYQ9dIHcjabnAdvuJJCPbIdWv7spWSLEQaLhtzin0ekkkjek_MW-K1gesJHCDLueeoWVQVR5URNzkiH2LYLP2kyxZke-bYSp4ts0McLqvIexdOTprRNEwZm6K5s_AfubzUW4eFFKrFPbwYwijTB1lt9AFm_/s72-c/2026-09-13-weekly-rate-limits-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-8287257331976423225</guid><pubDate>Tue, 08 Sep 2026 13:23:26 +0000</pubDate><atom:updated>2026-09-08T09:24:29.200-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">dart</category><category domain="http://www.blogger.com/atom/ns#">flutter</category><category domain="http://www.blogger.com/atom/ns#">llm</category><category domain="http://www.blogger.com/atom/ns#">personal assistant</category><category domain="http://www.blogger.com/atom/ns#">privacy</category><category domain="http://www.blogger.com/atom/ns#">workflow</category><category domain="http://www.blogger.com/atom/ns#">çlaude</category><title>Building a Personal Assistant: What I Got Wrong</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgamk1icWygvu1eb8za5vYfrnvC_7gwITi6DACEnFWGFPFnzePD74Lc7fWnDl6HRyTKnTcNhJFyWgW8nPuOcFQ35Tox-aGxw2Bns4Sx5Tyk0OgOmAeB_WYGynRCiVp2zCguz0RrUn7BgjXoRrHa_lH5VmrZzMgyi-s3qpxhwIZJTgpObJsL-Ki2JHFsiNH_/s1200/2026-09-08-personal-assistant-hero.jpg&quot; alt=&quot;A phone on a kitchen table showing a logged meal, with a laptop behind it showing the issue tracker for the same app&quot;&gt;&lt;/p&gt;
&lt;p&gt;A couple of weeks ago I wrote that I am the only user of my personal assistant app, so every issue in its tracker is one I filed after watching something fail on my own phone. That sentence is no longer true. My daughter started using it, and she found a limitation I had built in so deeply that I could not see it.&lt;/p&gt;
&lt;p&gt;Before that, though, some numbers. The app is at version 6.5.0, build 191, with 115 release notes behind it. It is 245 Dart files in &lt;code&gt;lib&lt;/code&gt; and 234 test files. The &lt;code&gt;lib/services&lt;/code&gt; directory alone holds 124 services. I bring this up because people who ask me about building their own assistant usually think of it as a weekend project - a chat box with an API key behind it. The chat box was the weekend. Everything after that is the reason the thing is actually useful.&lt;/p&gt;
&lt;p&gt;Three things moved it from a toy to something I open every day: it learns from its own mistakes, it accepts the way people actually talk, and it can look at a photo. Everything else is plumbing, and I will be honest about the plumbing at the end.&lt;/p&gt;
&lt;h2&gt;Why Not Just Use One of the Big Ones&lt;/h2&gt;
&lt;p&gt;A fair question with several answers. Here is one concrete one.&lt;/p&gt;
&lt;p&gt;I needed AI access my daughter could actually use. She is a preteen, so most of what an adult would reach for is closed to her. ChatGPT is age restricted. I could not get Gemini working on her phone at all, and still do not know why. We tried the assistant that comes with WhatsApp, which means Meta, and it was not what she needed. Every path led to a policy wall or a product built for someone else.&lt;/p&gt;
&lt;p&gt;So I built the thing instead, against an API endpoint. Today the app runs on Gemini, with entry points for Anthropic and OpenAI, and roles pinned to a local gateway when the data should not leave the device at all. Same models everyone else is using. Different terms of access, because the terms are mine.&lt;/p&gt;
&lt;p&gt;The part I did not anticipate was transparency. The app has a screen that logs the prompts it sends, verbatim, so you can see exactly what left the device. I built it for myself, to debug what the app was actually sending. It turns out to be the thing that makes a kid using AI reasonable rather than nerve-racking. I can see the request and the response.&lt;/p&gt;
&lt;p&gt;I want to be careful about how I say this, because I am not reading her conversations under a microscope, and a parent who does that will find their kid stops using the tool honestly. But the alternative on offer is a chat box run by a company, aimed at a preteen, where I get to see nothing at all. Being able to look is different from looking. I would rather have the option and rarely use it.&lt;/p&gt;
&lt;h2&gt;The Loop That Closes Itself&lt;/h2&gt;
&lt;p&gt;When the assistant gets something wrong - a bad answer, a card that renders badly, a log that lands in the wrong place - I paste that straight back into the chat and it files a GitHub issue with the context attached.&lt;/p&gt;
&lt;p&gt;That sounds like a small convenience. It is the most important thing in the app. Small projects do not die because the bugs are hard. They die because noticing a bug and writing a good report about it are two different jobs, and the second one lands at the exact moment you are least willing to do it: standing in a kitchen, one hand busy, annoyed. So you tell yourself you will remember. You do not. The bug survives another month.&lt;/p&gt;
&lt;p&gt;The app also watches for its own mistakes - you restating the same request, you undoing what it just wrote, a question that almost matched something specific and got a vague answer instead. That last one matters because you never see it as an error.&lt;/p&gt;
&lt;p&gt;What I got wrong at first was asking on a schedule. When it prompted &amp;quot;did I get that right?&amp;quot; at a fixed cadence, I said yes to make the prompt go away. The friction stayed and the signal disappeared. Now it asks only when something actually looks wrong, and it works that out on the phone without sending anything anywhere.&lt;/p&gt;
&lt;h2&gt;Messy Input Is the Real Feature&lt;/h2&gt;
&lt;p&gt;Nobody logs data in a form. People say &amp;quot;had a bowl of soup and a coffee around two.&amp;quot; So the parser does the work, and most of what makes a parser good is knowing when not to act.&lt;/p&gt;
&lt;p&gt;Two bugs taught me that, and they failed in opposite directions. &amp;quot;remove a cup of water&amp;quot; logged a cup of water, because the parser saw a quantity and never looked at the verb in front of it. Going the other way, everything hydration-related matched the literal word water, so seltzer and club soda reached no logging path at all. Nothing failed. Nothing was logged either.&lt;/p&gt;
&lt;p&gt;One is the app acting when it should not have. The other is the app not acting when it should have. Both come from reading the surface of a sentence instead of what the sentence is doing.&lt;/p&gt;
&lt;p&gt;The third rule is the one I would keep if I threw out the rest: when you state your macros, they are stored exactly as you said them. The model does not get to re-estimate a number you already gave it. If you tell the assistant something factual, re-deriving it with a model is not helpfulness. It is the app deciding it knows better than the person holding the phone.&lt;/p&gt;
&lt;h2&gt;Logging a Meal From a Photo&lt;/h2&gt;
&lt;p&gt;On August 17 I photographed a bowl of cold beetroot soup - svekolnik - and asked the app to log it. It came back with 110 calories, 3g protein, 22g carbs, 1g fat, and I confirmed the card.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg23E18P5rjCG60Om60rYW_RprlhvPERXrNfMCe3cnGhzyByjtTn_YZE88YPF74cdOKkmb-K8A7fC_VFBmqC5cwUTq9_XflOC-KZqVUiZWPCterd4e2yMpj0Zv94k8xXIP6_denI8p4YVOh8iXwbapbMgHn6OcsZeICZauyRpkkfgKJoDpt3PWq8nWvdlLM/s1320/2026-09-08-food-recognition-beetroot-soup.png&quot; alt=&quot;The assistant recognizing a bowl of cold beetroot soup from a photo and returning 110 kcal, 3g protein, 22g carbs, 1g fat&quot;&gt;&lt;/p&gt;
&lt;p&gt;The subtle part is intent. A camera button used to &lt;em&gt;be&lt;/em&gt; the intent; with one attachment control, the words have to carry it. An attached picture next to an ordinary question is a question, and misreading it as a log buries the answer you were waiting for under a food card. The opposite mistake costs one retyped sentence, so the matcher under-claims. The model will happily guess every time if you let it.&lt;/p&gt;
&lt;p&gt;The failure modes are honest ones. A plated meal goes well: chicken, rice, something green, three things to name and add up. A pot on the stove goes badly, because it is one volume with everything dissolved into it. Portion size is the worst of it, since the model is judging volume from a photo with no reference object and no depth. It cannot tell a teacup from a mixing bowl. Neither could I, from the same photo.&lt;/p&gt;
&lt;p&gt;So nothing is written until I confirm the card, and the card takes a correction in plain words: &amp;quot;about two cups,&amp;quot; or &amp;quot;half of what is in the picture.&amp;quot; The model is good at naming what it sees and bad at knowing how much of it there is, and the app is built around that split instead of pretending otherwise.&lt;/p&gt;
&lt;p&gt;One reason I keep working on this part: I have been using it to track calories and fasting, and I am down more than ten pounds. It has taken over from MyFitnessPal for me. Not because the estimate is more precise - it is not. Because logging a meal became cheap enough that I actually do it.&lt;/p&gt;
&lt;h2&gt;Then My Daughter Started Using It&lt;/h2&gt;
&lt;p&gt;She asked for one thing I had not thought of: let it take files and images for analysis, not just food. Her first test was photographing a math equation from her homework and asking the assistant to check her work. A good request, and it earned its keep on something that was neither homework nor food. She has track and field events this season, and the schedule reached me the way schedules actually reach people: as a picture of a calendar, sent from her phone to mine. I uploaded it and the assistant read the dates off the image correctly and put them on my schedule, since I am the one doing drop off and pick up. Nobody typed anything.&lt;/p&gt;
&lt;p&gt;That one has quietly changed how I run the week. School just started, and the schedules arrive in every shape a school can produce: a PDF from one program, a month grid from another, plain text in an email, a photo of a paper handout that came home in a backpack. Different activity, different format, same deadline. The old version of this was me squinting at a screen and retyping dates into a calendar, which really meant half of them never got entered at all. Now I point the app at whatever is in front of me and the dates come back filled in, with the names and the times attached. It saves me a real amount of time each week, and it has nothing to do with the chat box.&lt;/p&gt;
&lt;p&gt;It also shows where the app still falls short. Those events landed on my schedule and stopped there. Two-way Google Calendar sync is not working yet, so nothing flows back to hers, and we are still coordinating around one device instead of one calendar. An open issue, not a solved problem, and the same shape as everything else here: the model part was the easy half.&lt;/p&gt;
&lt;p&gt;The problem she exposed is bigger than any of it.&lt;/p&gt;
&lt;p&gt;The app has profiles - personal, work, health, finance, travel - and each one carries its own memory categories, its own enabled tools, and its own routing preference for sensitive data. Health and finance are local-only. That design has held up well, and I was quietly proud of it.&lt;/p&gt;
&lt;p&gt;Then someone else picked up the phone, and I saw what those profiles actually are. They switch &lt;em&gt;context&lt;/em&gt;. They do not switch &lt;em&gt;person&lt;/em&gt;. Every default under all five of them is mine: hydration nudges, fasting timers, focus blocks, the particular set of habits a software person decides to fix about himself. None of that is what a preteen wants from an assistant, and a fasting timer offered to a kid is not a neutral default. It is wrong.&lt;/p&gt;
&lt;p&gt;So the work in front of me is a harness around a profile that carries whose assistant it is: which suggestions exist at all, which memory categories are collected, which tools are reachable. The rules a preteen profile needs are mostly subtractive - things the model does not get to raise on its own, no matter what it infers from the data in front of it. Health is the obvious case. I would rather the assistant be boring than have it decide, on its own, to have an opinion about a kid&#39;s eating.&lt;/p&gt;
&lt;p&gt;I want to be clear that this part is not built yet. It is what I am building next, and I am writing it down here because the lesson is the transferable part: I designed for one person without ever deciding to, because the one person was me and the app agreed with me about everything.&lt;/p&gt;
&lt;h2&gt;What I Would Tell Someone Starting&lt;/h2&gt;
&lt;p&gt;First, the promise I made at the top: almost none of those 124 services are AI. The parts that took the most time were calendar and contact intake, notification scheduling, encrypted local storage, backup and export, and being able to change model providers without rewriting the app around whichever one is currently cheapest. That last one matters more than it sounds, for reasons I wrote about in the &lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;July cost post&lt;/a&gt;. Build around one vendor&#39;s client and a price change is an outage. Keep model roles in a registry and it is a settings change.&lt;/p&gt;
&lt;p&gt;The rest is short.&lt;/p&gt;
&lt;p&gt;Build the correction loop before the features. The app that files its own bug reports gets fixed; the one that requires you to open a laptop does not.&lt;/p&gt;
&lt;p&gt;Keep the data local and own the schema. Every provider swap I have done was painless because the data never lived at the provider.&lt;/p&gt;
&lt;p&gt;And decide early whether the assistant serves one person or a persona. I did not decide. I defaulted to me. Retrofitting that is the expensive kind of work, and it took someone else about a week of using the app to expose it.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/a-convincing-review-of-wrong-path.html&quot;&gt;A Convincing Review of the Wrong Path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;Codex vs Claude vs GLM 5.2: A Practical Comparison&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/09/building-personal-assistant-what-i-got.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgamk1icWygvu1eb8za5vYfrnvC_7gwITi6DACEnFWGFPFnzePD74Lc7fWnDl6HRyTKnTcNhJFyWgW8nPuOcFQ35Tox-aGxw2Bns4Sx5Tyk0OgOmAeB_WYGynRCiVp2zCguz0RrUn7BgjXoRrHa_lH5VmrZzMgyi-s3qpxhwIZJTgpObJsL-Ki2JHFsiNH_/s72-c/2026-09-08-personal-assistant-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-91842249652758255</guid><pubDate>Fri, 28 Aug 2026 13:16:21 +0000</pubDate><atom:updated>2026-08-28T09:19:13.493-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">code-review</category><category domain="http://www.blogger.com/atom/ns#">debugging</category><category domain="http://www.blogger.com/atom/ns#">opus</category><category domain="http://www.blogger.com/atom/ns#">sonnet</category><category domain="http://www.blogger.com/atom/ns#">testing</category><title>A Convincing Review of the Wrong Path</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;
  &lt;img
    src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLrvl_gwDHmjmr2A7yDXBrTuHswHY-OpDoBwk6Df0MZvVc59akCQwvto08o5TWNgFlrd68FTW8nrj-FcZj88U3mkOey6dxgjLCjd9YP-DFNXaAZTZK2_yC2bWC64QAhxFGpUCDrdvc94dK75GVS3QnuViaaiY7mIN41vR3vxZ8pK-iHHXREA1E4KtAFC8a/s1200/2026-08-27-convincing-review-wrong-path-hero.jpg&quot;
    alt=&quot;A code reviewer reading a file on one screen while a terminal on the other screen prints the actual output&quot;&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This article follows &lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model: Why Cross-Vendor Code Review Actually Works&lt;/a&gt;, where I wrote about using a second model to validate review findings. This time, the missing check was not another opinion. It was running the code.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I am the only user of my personal assistant app, so every issue in its tracker is one I filed after watching something fail on my phone. One was the fasting timer. I told it in chat that I had started a fast at seven in the evening, and nothing started.&lt;/p&gt;
&lt;p&gt;I handed that issue and eight others to Claude. It read the source, quoted the right files, and explained why the timer could not fail the way I described. I closed all nine issues that afternoon.&lt;/p&gt;
&lt;p&gt;A few days later I asked Claude to check again, but this time it had to run the reported inputs. Four verdicts flipped. The second pass took six and a half minutes instead of two and a half. Those extra four minutes recovered four real bugs and found two more I had never noticed to file.&lt;/p&gt;
&lt;p&gt;The first review was not careless. It reasoned correctly about the paths it opened. It just answered the wrong question.&lt;/p&gt;
&lt;h2&gt;Four Verdicts That Changed&lt;/h2&gt;
&lt;h3&gt;A hydration question became a habit command&lt;/h3&gt;
&lt;p&gt;The first review saw that the intent parser required words such as &lt;code&gt;habit&lt;/code&gt; or &lt;code&gt;routine&lt;/code&gt;, neither of which appeared in the report. Hydration routing also came first. That looked conclusive.&lt;/p&gt;
&lt;p&gt;Then I ran the exact sentence:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;check the water hydration again, why did you report 10 cups&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The parser returned a habit-completion action for a habit named &lt;code&gt;you report 10 cups&lt;/code&gt;. The habit gate protected only status and list requests; completion ran when the gate was absent. A question guard anchored with &lt;code&gt;^&lt;/code&gt; also missed the second clause.&lt;/p&gt;
&lt;h3&gt;The fasting timer never started&lt;/h3&gt;
&lt;p&gt;The first review traced a global state provider shared by chat and the timer screen. State propagation was fine, but the parser never created state to propagate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;&amp;quot;fast started at 7:00 p.m.&amp;quot; -&amp;gt; null
&amp;quot;fast started at 7:00 pm&amp;quot;   -&amp;gt; null
&amp;quot;started fast at 7:00 pm&amp;quot;   -&amp;gt; start, 19:00
&amp;quot;start fast at 7:00 p.m.&amp;quot;   -&amp;gt; start, 07:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The matcher required the verb before the noun, so natural variations fell through. It also understood &lt;code&gt;pm&lt;/code&gt; but not &lt;code&gt;p.m.&lt;/code&gt;, silently turning seven in the evening into seven in the morning.&lt;/p&gt;
&lt;h3&gt;The chart had nothing to draw&lt;/h3&gt;
&lt;p&gt;I logged a habit through chat and its duration chart stayed blank. The chart existed and rendered correctly. The failure was on the write side. Chat parsed &lt;code&gt;30 min&lt;/code&gt;, replied &lt;code&gt;Walk, done (30 min)!&lt;/code&gt;, then called a repository method with no duration parameter. The database stored null, and the chart correctly filtered that row out.&lt;/p&gt;
&lt;p&gt;The review had inspected the half that worked.&lt;/p&gt;
&lt;h3&gt;The reminder report exposed two other bugs&lt;/h3&gt;
&lt;p&gt;The scheduler used distinct IDs and staggered times, so the original report of reminders arriving together remained uncertain. But the investigation found that the app initialized the time-zone database without setting the device&#39;s local zone. Notifications were scheduled against UTC, including quiet hours and the midnight cutoff.&lt;/p&gt;
&lt;p&gt;It also found a hydration nudge reading settings from a fresh container in a background isolate. That container returned defaults, so the production nudge did not behave like the main app.&lt;/p&gt;
&lt;h2&gt;The Device Added Evidence the Source Could Not&lt;/h2&gt;
&lt;p&gt;An alarm dump from a Pixel 9 Pro XL showed several exact alarms sharing the same timestamp:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;10:00:00.000  EXACT
11:00:00.000  inexact
11:00:00.000  EXACT
11:00:00.000  EXACT
15:00:00.000  EXACT
15:00:00.000  EXACT
15:00:00.000  EXACT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Five days of uptime made a recent reboot less likely. The time-zone fix also appeared to work: reminders were back on local-hour boundaries. But the dump did not expose notification IDs, so it could not identify which schedules collided.&lt;/p&gt;
&lt;p&gt;Another diagnostic showed 43 notifications enqueued, 43 blocked, and none shown. Notification permission had never been requested. The alarms could run, but none of the resulting notifications reached me.&lt;/p&gt;
&lt;p&gt;The next release recorded IDs, scheduled times, and outcomes, then grouped notifications by fire time. When evidence does not exist, add the smallest instrumentation that can produce it.&lt;/p&gt;
&lt;h2&gt;What Actually Changed Between the Reviews&lt;/h2&gt;
&lt;p&gt;The work used Claude Opus 5 and Sonnet 5. I also changed the instructions: the second review was adversarial, received the exact reported inputs, and had to prove the symptom could not happen. Because the model and method both changed, this is not a clean model comparison.&lt;/p&gt;
&lt;p&gt;That distinction matters more than choosing a winner. A different model may bring different assumptions. Running the reported path gives direct evidence about that path. Finding one working path does not rule out the path the user was on.&lt;/p&gt;
&lt;h2&gt;What I Would Tell Someone Setting This Up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Run the real function against the literal string from the bug report. Not a paraphrase, not a tidied-up version. The exact string, plus three to six neighbors of it. For anything parser-shaped this is a quick way to test whether the reported path behaves as described.&lt;/li&gt;
&lt;li&gt;Tell the reviewer what to distrust. Hand over the failure modes you suspect and put the burden on it to prove the symptom cannot happen. A reviewer asked only to find a correct path may stop after finding one.&lt;/li&gt;
&lt;li&gt;Do not close an issue on a triage verdict. The costly part was not that the first review got things wrong. Reviews get things wrong. It was that nine issues went away on a single unverified pass. If the report does not reproduce, leave it open as needing evidence until the exact input, state, and environment have been tested.&lt;/li&gt;
&lt;li&gt;Trace the write path, not just the render path. Two of the four reversals were hiding on the side of the feature the first review did not inspect.&lt;/li&gt;
&lt;li&gt;Once a bug reproduces, preserve the exact input as a regression test. Execution finds the mismatch once; the test keeps it from returning.&lt;/li&gt;
&lt;li&gt;When the evidence you need does not exist, add focused instrumentation and collect only the data the investigation requires.&lt;/li&gt;
&lt;li&gt;Budget the extra minutes. Six and a half instead of two and a half, for four reversed verdicts, is not a close call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The four fixes went out in one pull request and the notification diagnostics in another. The reminder pile-up is still open, waiting on the next capture from the device, which is the correct state for it to be in. It was also open the entire time, which is more than I can say for the issues I closed too early.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/executor-model-reviewer-model.html&quot;&gt;Executor Model, Reviewer Model: Why Cross-Vendor Code Review Actually Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/develop/background-work/services/alarms&quot;&gt;Schedule alarms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/compose/notifications/notification-permission&quot;&gt;Notification runtime permission&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pub.dev/packages/timezone&quot;&gt;Dart timezone package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/08/a-convincing-review-of-wrong-path.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLrvl_gwDHmjmr2A7yDXBrTuHswHY-OpDoBwk6Df0MZvVc59akCQwvto08o5TWNgFlrd68FTW8nrj-FcZj88U3mkOey6dxgjLCjd9YP-DFNXaAZTZK2_yC2bWC64QAhxFGpUCDrdvc94dK75GVS3QnuViaaiY7mIN41vR3vxZ8pK-iHHXREA1E4KtAFC8a/s72-c/2026-08-27-convincing-review-wrong-path-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-7113785266267956388</guid><pubDate>Thu, 20 Aug 2026 14:00:51 +0000</pubDate><atom:updated>2026-08-20T10:03:48.016-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">code review</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">glm 5.2</category><category domain="http://www.blogger.com/atom/ns#">opus</category><category domain="http://www.blogger.com/atom/ns#">Sol</category><category domain="http://www.blogger.com/atom/ns#">Terra</category><title>Executor Model, Reviewer Model: Why Cross-Vendor Code Review Actually Works</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}The Setup: One Model Writes, a Different Vendor Reviews
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKpu2LmS1d9uP4YytXyX2-3p59Z7BhAgpdLZTtKBVBKjOOvg3i2jUOhpvejcJKjpPSctT7_fKR1LnsQBoqhP1bO-klOPTaJYScn46pMYwZf0tl_clOmKY_5_qbHzGtHY0JDyQ_UGANnNKOiim-bjXkV45Sq4T9GNCf0WusLFxDEHSGzt3AmME0SxLi-Y44/s1200/2026-08-20-cross-vendor-code-review-hero.jpg
&quot; alt=&quot;Two AI agents from different vendors handing a pull request back and forth across a desk&quot;&gt;&lt;/p&gt;
&lt;p&gt;I thought one of my projects was in reasonable shape. I had been working in it for months, it was on version 4.24.3, and Claude had been through most of it more than once. Then I pointed Codex GPT-5.6 Sol at it with Ultra High reasoning, asked it to evaluate the codebase and file issues for anything it found, and it came back with 42 new issues in about ten minutes. Some of them were release blocking. I had Claude Opus 4.8 go through all 42 looking for false positives, and it confirmed zero. Every issue was real.&lt;/p&gt;
&lt;p&gt;That result is what convinced me to stop having one vendor&#39;s model check its own family&#39;s work.&lt;/p&gt;
&lt;h2&gt;The Setup: One Model Writes, a Different Vendor Reviews&lt;/h2&gt;
&lt;p&gt;The workflow is simple to state. Implement with one model, working from the most critical issues to the least critical and matching model strength to issue criticality: the highest-confidence model takes the most critical work, and the cheaper, faster models take the routine fixes further down the list. Then validate the pull request with a competing but equivalent model from a different vendor, rather than asking the same model to check its own output. Claude Code&#39;s built-in PR review command is the mechanism on that side, which is what made the second half of the loop practical rather than a nice idea.&lt;/p&gt;
&lt;p&gt;On the round that produced the 42 issues, the executor was Codex 5.6 Terra/Sol and the reviewer was Claude Opus 4.8.&lt;/p&gt;
&lt;h2&gt;What It Actually Found&lt;/h2&gt;
&lt;p&gt;&amp;quot;Some were release blocking&amp;quot; is easy to say and hard to believe without examples, so here are three of them. The project is Zone 2 Trainer, my Android heart-rate training app.&lt;/p&gt;
&lt;p&gt;The first is a silent data-corruption bug. The History screen loads workouts through a query that only populates part of each row for performance, and then the edit and relink paths hand those same partial objects to a full-row database update. Anything the summary query left out gets written back as a default or a null. Health Connect identity, GPS, intervals, RR data, cycling power and cadence were all in range. Nothing crashes. You just quietly lose columns.&lt;/p&gt;
&lt;p&gt;The second is in the same family. Import and manual-entry paths were using Room&#39;s &lt;code&gt;REPLACE&lt;/code&gt; conflict strategy against an existing workout ID. SQLite implements replace as delete plus insert, so with foreign keys turned on it can cascade-delete the heart-rate samples belonging to that workout before the replacement row lands.&lt;/p&gt;
&lt;p&gt;The third was in Bluetooth scanning: a new caller could attach to an already-running scan and complete unexpectedly, scan failures never came back through the normal result path, and one code path read a device address after the Bluetooth permission could have been revoked.&lt;/p&gt;
&lt;p&gt;None of these are style nits, and none would show up as a failing test.&lt;/p&gt;
&lt;p&gt;What I found convincing was not just the list but the specificity. Each issue came with file-and-line evidence, a statement of expected behavior, and the commands to prove it. That is what made them cheap to act on: the fixes for all three landed in git over the following two days.&lt;/p&gt;
&lt;h2&gt;Why This Was Not Getting Caught&lt;/h2&gt;
&lt;p&gt;The obvious question is why none of this surfaced earlier, given how much model attention the code had already had.&lt;/p&gt;
&lt;p&gt;I think it is structural. A model reviewing output from the same family shares the training data, the architectural instincts, and the blind spots. If a model does not think to check for a class of bug when it writes code, it tends not to think to check for it when it reviews code, because it is applying the same judgment both times. The review adds a second pass, not a second perspective. Claude had been through this codebase repeatedly and had not flagged the partial-row update problem once.&lt;/p&gt;
&lt;p&gt;Codex Sol does not share that lineage, and the gap between the two vendors&#39; failure modes is where the missed issues were sitting.&lt;/p&gt;
&lt;p&gt;I should be honest about the flip side, because I have written about it before. Pointing an unfamiliar model at your code does not automatically produce good findings. When I ran GLM 5.2 findings through Claude a few weeks earlier, &lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;roughly 40% were not real problems&lt;/a&gt;. Same cross-vendor shape, very different result. Which is the whole reason the next step is not optional.&lt;/p&gt;
&lt;h2&gt;The Validation Step Is What Makes the Number Trustworthy&lt;/h2&gt;
&lt;p&gt;Forty-two issues from a model I had not used this way before is not automatically good news. A model that is simply more aggressive about flagging things produces a long list too, and a long list of false positives is noise with extra steps. Given the 40% I had just seen from GLM, I had no reason to assume this list was better.&lt;/p&gt;
&lt;p&gt;So the number that matters here is not 42. It is zero. Opus 4.8 went through the issues looking for false positives and did not find any, and that is what turned a pile of flagged issues into a list I could work through with confidence.&lt;/p&gt;
&lt;p&gt;Fable would have been my first choice for that validation pass, but it was unavailable, having hit its weekly limit. Opus 4.8 did the job instead, and did it well enough that the result held up.&lt;/p&gt;
&lt;h2&gt;Where This Is Headed&lt;/h2&gt;
&lt;p&gt;The plan was always to flip the roles: let Claude implement and Codex review the next round, so the review is never coming from the same vendor that wrote the code twice running. That round has not happened yet as of this writing, so I have no results to report on it, only that it is the obvious next step and the point of treating this as a loop rather than a one-time audit.&lt;/p&gt;
&lt;p&gt;The workflow has also kept evolving past the two-model version. Fable and Sol are now doing reasoning and planning, with Terra and Opus handling implementation from the higher-reasoning models&#39; comments, which pushes the same cross-vendor principle earlier in the process instead of confining it to the review stage.&lt;/p&gt;
&lt;h2&gt;What I Would Tell Someone Setting This Up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Do not trust a model to review its own family&#39;s output. The shared training is exactly what produces the shared blind spots.&lt;/li&gt;
&lt;li&gt;Route the review through something durable, like GitHub issues or a pull request, so the findings are a reviewable artifact instead of a conversation that evaporates.&lt;/li&gt;
&lt;li&gt;Validate the reviewer&#39;s findings before acting on them, especially the first time you use a new model this way. A long issue list is worth nothing until you know its false positive rate, and 0% and 40% both happened to me inside a month.&lt;/li&gt;
&lt;li&gt;Demand file-and-line evidence in the findings. It is what separates an issue you can act on from an opinion you have to go verify yourself.&lt;/li&gt;
&lt;li&gt;Match model strength to issue criticality when implementing the fixes. The most expensive model you have does not need to touch every line.&lt;/li&gt;
&lt;li&gt;Plan to flip executor and reviewer roles. A model that only ever reviews and never writes will not show you where its own review blind spots are.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pair of numbers I keep coming back to is 42 and zero: forty-two issues that months of same-vendor attention had not surfaced, and zero of them noise once a different vendor&#39;s model checked the work. That gap is the case for cross-vendor review, and it was wider than I expected.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/08/claude-keeps-ignoring-my-agentsmd-and-i.html&quot;&gt;Claude Keeps Ignoring My AGENTS.md, and I Think I Know Why&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;Codex vs Claude vs GLM 5.2: A Practical Comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/05/my-experience-writing-zone-2-trainer.html&quot;&gt;My Experience Writing Zone 2 Trainer for Android&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/08/executor-model-reviewer-model.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKpu2LmS1d9uP4YytXyX2-3p59Z7BhAgpdLZTtKBVBKjOOvg3i2jUOhpvejcJKjpPSctT7_fKR1LnsQBoqhP1bO-klOPTaJYScn46pMYwZf0tl_clOmKY_5_qbHzGtHY0JDyQ_UGANnNKOiim-bjXkV45Sq4T9GNCf0WusLFxDEHSGzt3AmME0SxLi-Y44/s1600/2026-08-20-cross-vendor-code-review-hero.jpg&#xa;" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-4360865071764004470</guid><pubDate>Tue, 18 Aug 2026 20:31:41 +0000</pubDate><atom:updated>2026-08-18T16:31:41.623-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">agents</category><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">llm</category><category domain="http://www.blogger.com/atom/ns#">workflow</category><title>Claude Keeps Ignoring My AGENTS.md, and I Think I Know Why</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ2x5LTFVBSDgOKxfRZUgAzWQvgor2uyBahB_OAjN0KRQnBBf6AlzGmgAAnTKFVzNI0bseeFToQFqIrFCnQs3V5s3w0li6ryqA6mrl5AukkXA1xM0AnYBVvrTBsZxWlZk-0S2WLE_ADTnrcSOErs52xmduC3KOWiIymoOsGXrbrHxhrv7N1yNnU0vDjSio/s1200/2026-08-18-claude-ignores-agents-md-hero.jpg&quot; alt=&quot;A long list of project rules on a wall while an AI agent walks past without looking&quot;&gt;&lt;/p&gt;
&lt;p&gt;In July I asked an agent to push a build of my personal assistant app to my phone. Routine task. I do it most days. It built the release APK, ran &lt;code&gt;flutter install&lt;/code&gt;, and my habits, journal, and health logs were gone. Not corrupted, not partially migrated. Gone. &lt;code&gt;flutter install&lt;/code&gt; uninstalls the app first when it feels like it, and everything my assistant had learned about me lived in app-private storage with no cloud copy I could count on.&lt;/p&gt;
&lt;p&gt;The part that still bothers me is not that it happened. It is that the instruction telling it not to do this was already sitting in the repository, in the file the agent claims to read.&lt;/p&gt;
&lt;h2&gt;The Rule Was Already There, in Capital Letters&lt;/h2&gt;
&lt;p&gt;After the first time this happened, I did the obvious thing. I wrote the rule down. The commit is still in my history, and the message reads like an incident report because that is what it was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;flutter install uninstalled the app before reinstalling on a real device, wiping local user data (habits, journal, health logs) with no cloud backup guaranteed. Codify adb install -r / flutter run --release as the only acceptable device-deploy path going forward.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The rule that came out of it is not subtle:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Non-destructive install (MANDATORY)&lt;/strong&gt;: real user data (habits, journal, health logs) lives in app-private storage with no guaranteed cloud copy. &lt;strong&gt;Never uninstall to deploy.&lt;/strong&gt; Use &lt;code&gt;adb install -r &amp;lt;apk&amp;gt;&lt;/code&gt;, which overwrites in place. Never use &lt;code&gt;flutter install&lt;/code&gt; on a device with real data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I added a second one next to it, also marked mandatory, requiring a fresh backup before any install on a device with real data.&lt;/p&gt;
&lt;p&gt;Then it happened again.&lt;/p&gt;
&lt;p&gt;That is the uncomfortable part I want to be honest about. I did not forget to write the rule. I wrote it in capital letters, in the file that every agent working on this project loads, and it did not stop anything. The obvious fix was not a fix.&lt;/p&gt;
&lt;h2&gt;It Is Not One Model&lt;/h2&gt;
&lt;p&gt;My first instinct was to blame the model I was using that week. That does not survive contact with the evidence.&lt;/p&gt;
&lt;p&gt;Opus 4.8 did it. Opus 5 still does it. Codex is not innocent here either. These are different companies training on different data with different post-training, and they fail the same way on the same file. When every vendor produces the same behavior, the problem is probably not the vendor.&lt;/p&gt;
&lt;p&gt;I will give Opus 5 credit for one thing, because it is a real improvement. It is far more honest about mistakes than 4.8 was. It went through branches I had shipped and found bugs dating back to sprints that 4.8 had done, and it said plainly that the earlier reasoning was wrong. Older models tended to defend whatever was already in the file. That honesty is worth something.&lt;/p&gt;
&lt;p&gt;But honesty after the fact is not the same as following the rule up front. An agent that admits it destroyed my data is still an agent that destroyed my data.&lt;/p&gt;
&lt;h2&gt;The MEMORY.md Tell&lt;/h2&gt;
&lt;p&gt;The detail that made this click for me was not the data loss. It was something much smaller and much stranger.&lt;/p&gt;
&lt;p&gt;Agents working in my project kept learning lessons and writing them down. Good behavior, in principle. Except they wrote them in the wrong place. One session worked out my versioning scheme and carefully documented it in a &lt;code&gt;MEMORY.md&lt;/code&gt; file. Another put notes in some other stray markdown file it had invented.&lt;/p&gt;
&lt;p&gt;My versioning rule was already in &lt;code&gt;AGENTS.md&lt;/code&gt;. It had been there for months.&lt;/p&gt;
&lt;p&gt;That is not an agent breaking a rule it read and decided to skip. That is an agent that never really absorbed the file in the first place, deriving a rule from scratch that it already had, and then filing its discovery somewhere I would never look. You end up telling it, over and over, in nearly every session: read AGENTS.md. Which rather defeats the purpose of having one.&lt;/p&gt;
&lt;h2&gt;What I Actually Think Is Happening&lt;/h2&gt;
&lt;p&gt;Here is my honest theory, and it is not flattering to me.&lt;/p&gt;
&lt;p&gt;The first line of my &lt;code&gt;AGENTS.md&lt;/code&gt; calls it &amp;quot;the operating contract for AI agents and developers.&amp;quot; That word, contract, is where I went wrong. A contract is binding. It sits outside the negotiation and constrains it. That is not what this file is at all.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is context. It goes into the same window as the task I just typed, the files the agent opened, the test output, the compiler errors, and everything else competing for the model&#39;s attention. It is not a constraint on the system. It is one more voice in a crowded room, and it is the quietest one, because it was loaded first and has nothing to do with the immediate problem the model is trying to solve.&lt;/p&gt;
&lt;p&gt;Now look at what I did every time something went wrong. I added a rule. Twenty eight commits have touched that file since May. It started at 80 lines. By the middle of August it was 220.&lt;/p&gt;
&lt;p&gt;Every rule I added to fix a failure made the file longer. And every line I added made all the other lines a little weaker. I spent three months responding to a problem in the exact way that made the problem worse. The rule about not wiping my data was not competing with the model&#39;s laziness. It was competing with 219 other lines I had written, most of them about branch naming and lockfiles.&lt;/p&gt;
&lt;p&gt;If everything is important, the model has no way to know that one of those lines is about destroying my data and the rest are about tidiness.&lt;/p&gt;
&lt;h2&gt;Pruning Instead of Adding&lt;/h2&gt;
&lt;p&gt;So today I did the opposite of what I had been doing. I cut the file from 220 lines to 136.&lt;/p&gt;
&lt;p&gt;I went through every rule and asked what it actually protects. Preferences got deleted or compressed. Anything I could enforce somewhere else, I moved out of the file. What survived is mostly rules that protect real user data or a shipped contract, and those are the only ones still marked mandatory. If everything is mandatory then nothing is, so now that word appears only where the failure mode is somebody losing their journal.&lt;/p&gt;
&lt;p&gt;I also added a model tiering table, which is a token budget rule as much as a quality one:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Haiku&lt;/strong&gt; - mechanical&lt;/td&gt;
&lt;td&gt;Small UI changes, CRUD, tests, renames, formatting, docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sonnet&lt;/strong&gt; - default&lt;/td&gt;
&lt;td&gt;Normal features, typical bugs, scoped changes, integration work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Opus&lt;/strong&gt; - difficult reasoning&lt;/td&gt;
&lt;td&gt;Architecture, unclear root causes, concurrency, cross-cutting refactors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fable&lt;/strong&gt; - exceptional&lt;/td&gt;
&lt;td&gt;Only when Opus is not enough. Not routine.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I had been running the most expensive model available for everything, including renaming variables, and then wondering why I hit my weekly limit on Wednesday. Escalate when the work earns it, not preemptively.&lt;/p&gt;
&lt;p&gt;One more rule survived the cut, and it is my favorite, because it came out of a genuinely bad week. Some Bluetooth fixes I shipped a couple months back were too narrow, and the consequences kept escalating with each patch. The root cause was an agent asserting how a third party library behaved without ever checking. So now the file says: cite current source behavior and the tests or commands that prove a change, and never imply proof from tests that were not run.&lt;/p&gt;
&lt;p&gt;Is the shorter file working? I genuinely do not know yet. I made this change today. Ask me in a month. What I can say is that the previous strategy, adding another paragraph every time something broke, had three months to work and did not.&lt;/p&gt;
&lt;h2&gt;The Obvious Answer, and Why It Is Not Enough&lt;/h2&gt;
&lt;p&gt;The advice I expected to end this post with was: stop writing rules, put the guardrail in tooling. Make it a script the agent has to run instead of a sentence it can skip.&lt;/p&gt;
&lt;p&gt;I went to check my own history for a clean example of that working. What I found argues the other way.&lt;/p&gt;
&lt;p&gt;In July, unformatted code reached &lt;code&gt;main&lt;/code&gt; and failed CI twice in a row, on two different releases. My response, on July 19, was to add another line to &lt;code&gt;AGENTS.md&lt;/code&gt; requiring a clean format check before every push. The commit message is blunt about the gap:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Formalizes the gap that let unformatted commits reach main and fail CI twice in a row (v3.11.3, v3.12.0) — flutter analyze/test passing was mistakenly treated as sufficient, but neither checks formatting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the problem with that story. The tooling already existed. A pre-commit hook running &lt;code&gt;dart format --set-exit-if-changed .&lt;/code&gt; had been in the repository since May, two months before those two failures. I did not lack a script. I had the script and the rule, and unformatted code still shipped twice.&lt;/p&gt;
&lt;p&gt;Pre-commit hooks only run if somebody ran &lt;code&gt;pre-commit install&lt;/code&gt; in that working copy, and that is a manual setup step in my README. A hook is not a wall. It is a rule that happens to be written in YAML, and it can be skipped by a fresh checkout or a flag.&lt;/p&gt;
&lt;p&gt;The thing that actually caught the problem was CI. Not because CI is smarter, but because it runs on a machine where nobody involved gets to opt out.&lt;/p&gt;
&lt;p&gt;There is a detail here I find funny in a bleak way. My pre-commit config has an exclusion list, and &lt;code&gt;AGENTS.md&lt;/code&gt; is on it. The file full of rules is the one file my tooling never checks.&lt;/p&gt;
&lt;h2&gt;What I Would Tell Someone Writing One of These&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Write rules as commands, not preferences. &amp;quot;Prefer&amp;quot; is an invitation to negotiate with something that will happily negotiate.&lt;/li&gt;
&lt;li&gt;Reserve your emphasis for things that destroy data. Spend it anywhere else and it stops meaning anything.&lt;/li&gt;
&lt;li&gt;Prune when you add. Treat length as a cost you are paying, not a sign of thoroughness.&lt;/li&gt;
&lt;li&gt;Move what you can into tooling, but be honest about which tools can be bypassed. A local hook is a suggestion with extra steps. A gate that runs where nobody can opt out is the only kind that holds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one is the real lesson, and I paid for it with my own data. The instruction file is where you put the things you cannot enforce. It is not where you put the things you cannot afford to lose, and neither is a hook somebody has to remember to install.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html&quot;&gt;GLM Plans, Fable Validates, and API Keys Are Not for Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;Codex vs Claude vs GLM 5.2: A Practical Comparison&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/08/claude-keeps-ignoring-my-agentsmd-and-i.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJ2x5LTFVBSDgOKxfRZUgAzWQvgor2uyBahB_OAjN0KRQnBBf6AlzGmgAAnTKFVzNI0bseeFToQFqIrFCnQs3V5s3w0li6ryqA6mrl5AukkXA1xM0AnYBVvrTBsZxWlZk-0S2WLE_ADTnrcSOErs52xmduC3KOWiIymoOsGXrbrHxhrv7N1yNnU0vDjSio/s72-c/2026-08-18-claude-ignores-agents-md-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-3828650172412743686</guid><pubDate>Thu, 16 Jul 2026 16:25:45 +0000</pubDate><atom:updated>2026-07-16T12:25:45.224-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">gemini</category><category domain="http://www.blogger.com/atom/ns#">glm</category><category domain="http://www.blogger.com/atom/ns#">llm</category><category domain="http://www.blogger.com/atom/ns#">ollama</category><category domain="http://www.blogger.com/atom/ns#">openai</category><category domain="http://www.blogger.com/atom/ns#">token cost</category><title>GLM Plans, Fable Validates, and API Keys Are Not for Development</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaFYp0CevPxlF1ged62Ba62Es2yPVpIY9ohaEyCy32UktSzCE4H4Ktkr44l3bezGZNFAXI0zppSF2ns9tb_37CwGRH49-POLFiFPCGuD8DylKnLjowvXjw44RjnZRWxgzDRCiIR32M5KDpz2lETJtRr0Oo2nfi8d3W1nB98_QtY7f-B-jafb4mfWFC33Kq/s1200/2026-07-16-glm-fable-terra-api-key-costs-hero.jpg&quot; alt=&quot;Split-screen desk scene showing GLM 5.2 planning, Claude Fable 5 validating, and API key billing meters&quot;&gt;&lt;/p&gt;
&lt;p&gt;In my &lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;last post&lt;/a&gt; I compared Codex, Claude, and GLM 5.2 and landed on two $20 monthly plans as the setup that gets the work done. Two weeks in, I changed that setup: Claude moved to the $200 annual plan, which is essentially the $20 monthly plan at a discount, and Ollama moved to the $20 Pro plan. Same shape, better price. This post is a follow-up on two other fronts. First, the workflow itself has firmed up: GLM 5.2 does the assessment and planning on less critical projects, Fable 5 does the assessment on complex or mission-critical projects and validates GLM 5.2&#39;s findings, and Sonnet 5 handles general-purpose coding once Fable 5 has set the plan. Second, I ran the experiment I suspected would go badly and paid for AI by the token, once through an OpenAI API key with the GPT-5.6 Terra model and once through Ollama top-up credits. The numbers are worth sharing because the gap is not small. It is the difference between 3-4 days of work and 30 minutes.&lt;/p&gt;
&lt;h2&gt;The Workflow: Who Assesses, Who Validates, Who Implements&lt;/h2&gt;
&lt;p&gt;The pattern I described last time has settled into a repeatable loop, and it now splits by how critical the project is.&lt;/p&gt;
&lt;p&gt;For less critical projects, I hand the assessment to GLM 5.2. It goes through the codebase and surfaces the issues, the technical debt, and the gaps it thinks need fixing. I keep it off the more critical work because of the false positive rate described below.&lt;/p&gt;
&lt;p&gt;For complex or mission-critical work, Fable 5 does the assessment instead. It reads the codebase and produces the same kind of plan GLM 5.2 would, but the findings are more trustworthy going in, which matters more when the project cannot absorb a wrong fix. Once Fable 5 has written the plan, Sonnet 5 does the general-purpose coding to implement it. Fable 5 sets the direction, Sonnet 5 does the day-to-day typing.&lt;/p&gt;
&lt;p&gt;Either way, the findings get captured as GitHub issues, or when the work is more of a campaign than a bug list, documented in a markdown plan checked into the repo. When GLM 5.2 produced the assessment, Fable 5 goes over the issues before anything gets implemented and validates that each one is accurate and actually worth doing.&lt;/p&gt;
&lt;p&gt;The validation step is not optional, and I covered why in the last post: when I ran GLM 5.2 findings through Claude, roughly 40% turned out not to be real problems. That false positive rate has not gone away. GLM 5.2 is still the best tool I have for surfacing candidates on lower-stakes work, and it is still a tool whose output you cannot take at face value. Fable 5 is the filter. It commits to a clear verdict on each issue, real or not real, and the issues that survive are the ones that get implemented.&lt;/p&gt;
&lt;p&gt;What I like about routing this through GitHub issues or a markdown plan is that the models never talk to each other directly. There is a durable, reviewable artifact in the middle. I can read the issue list myself, Fable&#39;s notes attach to something concrete, and when I come back to the project a week later the plan is still there instead of buried in a chat session.&lt;/p&gt;
&lt;h2&gt;OpenAI API Key with GPT-5.6 Terra: Wrong Tool for the Job&lt;/h2&gt;
&lt;p&gt;I tried running the GPT-5.6 Terra model through an OpenAI API key as part of the development flow, and the short version is that it is too expensive for that. Development work with an agent is token-hungry in a way that casual use is not. The model reads files, re-reads them, runs commands, loops on test output, and every one of those steps is metered. On a monthly plan that churn is absorbed by the subscription. On an API key you watch the balance drain in real time.&lt;/p&gt;
&lt;p&gt;My takeaway is that the API key is best used for general purpose work, not typical programming. One-off questions, summarization, a script that calls the model a few times a day: that is where pay-per-token pricing makes sense, because usage is low and comes in short bursts. For a development workflow where the agent runs for hours, the typical monthly plan is simply more cost efficient than the API. The providers have effectively priced it that way, and fighting that pricing with an API key is a losing move.&lt;/p&gt;
&lt;h2&gt;The Ollama Numbers: $20 Plan vs $5 Top-Up&lt;/h2&gt;
&lt;p&gt;I saw the same thing with Ollama, and there I have concrete numbers because I ran both options back to back.&lt;/p&gt;
&lt;p&gt;The $20 monthly plan comes with a weekly limit. On that plan, moderate to heavy use gets me 3-4 days of work before the weekly quota runs out. That is the constraint I complained about in the last post, and it is still annoying, but it is a workable rhythm: push hard for most of the week, then let the other window carry the load until the quota resets.&lt;/p&gt;
&lt;p&gt;Then I added a $5 credit top-up to see how far pay-as-you-go credits stretch. The answer: about 30 minutes of low to medium effort. Not heavy agent work, not a big refactor, just ordinary back-and-forth, and the credits were gone in half an hour.&lt;/p&gt;
&lt;p&gt;Do the math on that. If 30 minutes of light use costs $5, a week of moderate to heavy use at credit prices would run into the hundreds of dollars. The $20 plan delivers 3-4 days of that same heavy use for the price of two top-ups. The subscription is not a little cheaper for a development workflow; it is an order of magnitude cheaper.&lt;/p&gt;
&lt;h2&gt;Side Project: GLM 5.2 Building My Own MyFitnessPal&lt;/h2&gt;
&lt;p&gt;Separate from the cost math, I want to mention what I have been building with GLM 5.2 on a lower-stakes personal project, because it is a good example of where the false positive rate stops mattering. I am building a personal assistant app for myself, and one of my long-standing pet peeves is diet tracking. I already had basic macro and calorie counting working in the app.&lt;/p&gt;
&lt;p&gt;After hearing my friends talk about how well image recognition works for food logging, I decided to add it. I described the feature to GLM 5.2 in a single prompt, and after a few minor adjustments I had a more or less functional image recognition flow: take a photo of a meal, get back a calorie and macro estimate. The results are surprisingly accurate for what is, under the hood, a single model call.&lt;/p&gt;
&lt;p&gt;The image recognition itself runs on Gemini 3.5 Flash, called through my own Gemini API key. This is exactly the kind of usage I described above as the right fit for an API key instead of a monthly plan: a single call per meal logged, not an agent looping over a codebase for hours. The cost has been trivial, and the accuracy is good enough that I have genuinely stopped reaching for MyFitnessPal. I am only half joking when I call it a poor man&#39;s MyFitnessPal replacement. It does not have the food database or the social features, but for the one thing I actually wanted, a fast and accurate photo- to-macros estimate, it does the job for pennies a month.&lt;/p&gt;
&lt;p&gt;One thing that impressed me specifically: the model was extremely accurate at recognizing authentic Russian foods without a lot of prompting. That is not a cuisine most food-logging apps or general-purpose image models handle well, since it is a narrower training case than pizza or a burger. I did not have to coach it with extra context or dish names. I pointed the camera at a plate and it identified the dish correctly.&lt;/p&gt;
&lt;p&gt;This is also a good example of GLM 5.2 in its element. The project is not mission-critical, a wrong calorie estimate is not going to break anything, and the false positive rate that makes me route serious work through Fable 5 for validation is a non-issue here. GLM 5.2 got a working feature out in one prompt and a couple of tweaks, which is exactly the kind of fast, low-stakes iteration where its speed matters more than its accuracy.&lt;/p&gt;
&lt;h2&gt;Where That Leaves Me&lt;/h2&gt;
&lt;p&gt;The conclusion from the last post holds, and now I have the receipts for it. Two subscriptions, the Ollama $20 Pro plan and the Claude $200 annual plan, cover the development work. API keys and top-up credits are for the other stuff: automation scripts, occasional general purpose calls, anything where the usage is a trickle instead of a firehose.&lt;/p&gt;
&lt;p&gt;If you are trying to decide how to pay for these tools, the question to ask is not which model you want but what shape your usage has. Agent-driven development is a firehose. Buy the subscription and let the provider absorb the churn. Save the API key for the work that only sips.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html&quot;&gt;Codex vs Claude vs GLM 5.2: A Practical Comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/claude&quot;&gt;Anthropic Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://z.ai/glm-5.2&quot;&gt;Z.ai GLM 5.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com&quot;&gt;Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/models&quot;&gt;Gemini API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/07/glm-plans-fable-validates-and-api-keys.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaFYp0CevPxlF1ged62Ba62Es2yPVpIY9ohaEyCy32UktSzCE4H4Ktkr44l3bezGZNFAXI0zppSF2ns9tb_37CwGRH49-POLFiFPCGuD8DylKnLjowvXjw44RjnZRWxgzDRCiIR32M5KDpz2lETJtRr0Oo2nfi8d3W1nB98_QtY7f-B-jafb4mfWFC33Kq/s72-c/2026-07-16-glm-fable-terra-api-key-costs-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-163166830113143157</guid><pubDate>Tue, 07 Jul 2026 13:59:58 +0000</pubDate><atom:updated>2026-07-07T09:59:58.777-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">fable</category><category domain="http://www.blogger.com/atom/ns#">glm</category><category domain="http://www.blogger.com/atom/ns#">glm 5.2</category><category domain="http://www.blogger.com/atom/ns#">llm</category><category domain="http://www.blogger.com/atom/ns#">ollama</category><category domain="http://www.blogger.com/atom/ns#">open source</category><category domain="http://www.blogger.com/atom/ns#">openai</category><category domain="http://www.blogger.com/atom/ns#">opus</category><category domain="http://www.blogger.com/atom/ns#">sonnet</category><title>Codex vs Claude vs GLM 5.2: A Practical Comparison</title><description>&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeN1shMwn7504z7u1yoHqz4v3DHDVAIcJfTJpjBwpGY6g7xy8e5_Cgk6oExJ1OLUy9ydzb3GPtKbIZ8bnbtEI9ZxOJOsQdp5QwyUmqWgSAy0n-vhT06DnOD8f5nAaMYTfqpX5yjxeFBLEtHnrbjratVlmsnECDFG6jY8qRZSFaJfZVgQkhoGMCszbslLyl/s1200/2026-06-30-codex-vs-claude-vs-glm-hero.png&quot; alt=&quot;Comparison of three AI coding assistants side by side on a developer desk&quot;&gt;&lt;/p&gt;
&lt;p&gt;The question I keep getting asked is which AI coding assistant I actually use day to day. The honest answer is that I use more than one, and the reason is that none of them is uniformly the best. Each one has a lane where it clearly wins, and each one has a failure mode that pushes me back to the others.&lt;/p&gt;
&lt;p&gt;This post is a practical comparison of three tools I have spent real time with: Codex, Claude, and GLM 5.2. I am not interested in benchmark scores or leaderboard positioning. I care about how these tools behave across the full lifecycle of a project: initial scaffolding, iterative refinement, design polish, and long-running reliability.&lt;/p&gt;
&lt;p&gt;The short version: GLM 5.2 has replaced Codex as my daily driver for implementation work. Codex failed the wife test, which is what pushed me to switch. Claude is strong on design and polish but showed a huge regression after two weeks of use. GLM 5.2 is great at identifying tiny lingering issues and regressions, but the last 24 hours of work showed a 40% false positive rate when validated against Claude. The rest of this post explains what each of those statements actually means.&lt;/p&gt;
&lt;h2&gt;What I Mean by the Wife Test&lt;/h2&gt;
&lt;p&gt;The wife test is not a technical benchmark. It is the moment a tool has to explain itself to someone who is not a developer and does not care about the tool at all.&lt;/p&gt;
&lt;p&gt;In my case, that is my wife using an app I built. She does not read the commit history. She does not care which model generated which screen. She cares about whether the app opens, whether the buttons do what they say, and whether anything feels broken or weird. If a tool produces code that passes my review but ships an experience that a non-developer immediately notices is off, that tool failed the wife test.&lt;/p&gt;
&lt;p&gt;Codex consistently produced clean, well-structured code. The diffs were focused, the tests were reasonable, and the implementation logic was sound. But the resulting UX often felt like it was built by someone who had never watched a real person use the app. The spacing was slightly wrong, the empty states were unhelpful, and the error messages read like they were written for another engineer, not for the person staring at the screen.&lt;/p&gt;
&lt;p&gt;That is the gap. Code quality is necessary but not sufficient. A tool can win every code review and still lose the room the moment a real user touches the product.&lt;/p&gt;
&lt;p&gt;For infrastructure work, scripts, and backend logic, that tradeoff is fine. For anything a non-developer will touch, I have learned to expect a second pass with a different tool.&lt;/p&gt;
&lt;h2&gt;GLM 5.2 as Daily Driver: Code Quality and Productivity&lt;/h2&gt;
&lt;p&gt;GLM 5.2 has replaced Codex as my default for implementation work. When I want a focused change, a real test plan, and a diff I can review without reconstructing the entire file, GLM 5.2 is the tool I reach for first. I made the switch after finding that GLM 5.2 matched Codex on code quality while also being available through Ollama Cloud without the same rate limit pressure.&lt;/p&gt;
&lt;p&gt;The strengths are consistent. GLM 5.2 understands the repo as a unit, not just the file in front of it. It preserves the thread of the work across sessions, runs the checks, and reports back with proof. The code quality is high enough that I rarely have to rewrite the logic. I edit, I tighten, and I move on.&lt;/p&gt;
&lt;p&gt;I have been super productive with GLM 5.2. It identified a lot of technical debt across the codebase, and I churned through a large set of the issues it surfaced. The result is that my app is now stable and well tested in a way it was not before. The thoroughness that drives the false positive rate is the same thoroughness that made this cleanup possible: GLM 5.2 kept surfacing things until they were actually fixed.&lt;/p&gt;
&lt;p&gt;The one thing that holds GLM 5.2 back is the Ollama weekly limit. The limit is real, and when you are in the middle of a productive cleanup pass it is the constraint that stops you, not the tool. That is the tradeoff for the always-available availability I mentioned earlier: the tool is always there until the weekly quota runs out, and then you wait.&lt;/p&gt;
&lt;h2&gt;Claude: Design and Polish, Then Regression&lt;/h2&gt;
&lt;p&gt;Claude is the tool I reach for when the work is the experience. If GLM 5.2 builds the skeleton, Claude does the finish work: layout, spacing, copy, empty states, and the small interactions that make a product feel intentional.&lt;/p&gt;
&lt;p&gt;The design sense is real. Claude will rework a screen so that it reads better, suggest copy that actually sounds like a human wrote it, and catch the UX inconsistencies that GLM 5.2 leaves behind. When I need a feature to feel finished rather than just functional, Claude is the right tool.&lt;/p&gt;
&lt;p&gt;The problem is what happens over time. After about two weeks of steady use, I saw a huge regression. The quality of the suggestions dropped noticeably. The design instincts that made Claude valuable early on seemed to flatten out. The output became more generic, less tuned to the project, and more like a template than a collaborator.&lt;/p&gt;
&lt;p&gt;I do not know exactly what causes that regression. It could be context management, it could be model drift, or it could be the way the tool adapts to repeated prompts. What I can say is that the drop was large enough that I stopped trusting Claude as the primary tool for a long-running project and started using it for targeted polish passes instead.&lt;/p&gt;
&lt;p&gt;A disclaimer on the Claude regression: the observations above were written a few weeks back using Claude Sonnet 4.6 and 4.8. I have been using Sonnet 5.0 for a week and Fable 5 for the last 24 hours, and the results are fantastic. The regression problem I described may turn out to be specific to the 4.x line, but it is too early to say for certain. I will update this post if the pattern holds or breaks over a longer stretch.&lt;/p&gt;
&lt;h2&gt;GLM 5.2: Catching Regressions, With Caveats&lt;/h2&gt;
&lt;p&gt;GLM 5.2 is the newest of the three in my workflow, and it is the one that surprised me. I am running GLM 5.2 through Ollama Cloud, which means the tool is always available with no quota window to wait out. Where GLM 5.2 really shines is identifying tiny lingering issues and regressions that slip past the other tools. It will flag a subtle edge case or a half-finished refactor that Codex and Claude both missed. The specific things it catches well are the structural ones: missing try/catch blocks, logic errors where a branch returns the wrong value or skips a needed step, and swallowed exceptions where an error is caught and silently dropped instead of being handled or surfaced. Those are exactly the bugs that survive a code review because the diff looks reasonable in isolation. Notably, Claude Sonnet 4.6 and 4.8 both missed these same missing and swallowed exceptions, so this is a real gap in Claude, not just a general blind spot. GLM 5.2 is also very good at finding missing test coverage, sometimes obsessively. It will flag an untested branch, an uncovered error path, or a public function with no corresponding test, and it will keep surfacing those gaps until they are addressed. That is genuinely useful, though it can tip into overkill when the coverage it demands is more than the change actually warrants.&lt;/p&gt;
&lt;p&gt;The catch is the false positive rate. Over the last 24 hours of work, I ran every GLM 5.2 finding through Claude to validate it, and roughly 40% of the issues GLM 5.2 flagged turned out not to be real problems. GLM 5.2 is great at surfacing candidates, but you cannot take its output at face value. The validation step is not optional; it is the difference between fixing a real regression and chasing a ghost.&lt;/p&gt;
&lt;p&gt;One thing that made the validation step more reliable was Claude Sonnet 5.0. Across the same set of flagged issues, Sonnet 5.0 was noticeably more certain about which fixes were actually needed than Opus 4.8 and Sonnet 4.6. Where Opus 4.8 and Sonnet 4.6 would hedge or suggest multiple possible interpretations, Sonnet 5.0 would commit to a clear diagnosis and a clear fix. That confidence is what made the 40% false positive rate manageable: I could trust Sonnet 5.0&#39;s judgment on which GLM 5.2 findings were real and which were noise.&lt;/p&gt;
&lt;p&gt;The other cost of GLM 5.2&#39;s thoroughness is token churn. GLM 5.2 second-guesses itself a lot. It will flag an issue, walk it back, re-flag it from a different angle, and then circle around to the original framing before settling. That looping behavior burns tokens fast. I burnt 45% of my weekly limit in two sittings, mostly on GLM 5.2 re-examining the same concerns instead of committing to a fix. The always-available availability I mentioned earlier cuts both ways: nothing stops the tool from spending that availability on re-deriving conclusions it already reached, and the weekly quota is what runs out first.&lt;/p&gt;
&lt;p&gt;There is also a reliability wrinkle I have not fully diagnosed. Some work times out once in a while, and I am not sure whether that is an Ollama thing or a GLM thing. The failure mode is that a run just stops responding and I have to restart it, which is annoying but not frequent enough to be a deal breaker. The other quirk is specific to GLM in VSCode: it keeps asking if we should pause frequently. Mid-task, it will surface a prompt checking whether I want to stop or continue, which breaks the flow when I am trying to let it run. I do not know whether that is a model behavior or a client behavior, but it is worth knowing before you commit to a long unattended session.&lt;/p&gt;
&lt;p&gt;There is one important caveat. GLM 5.2 ignored the &lt;code&gt;AGENTS.md&lt;/code&gt; file in this repository and implemented fixes directly on &lt;code&gt;main&lt;/code&gt; instead of branching out. The &lt;code&gt;AGENTS.md&lt;/code&gt; file documents the workflow rules for this repo, including the expectation that changes are surgical and tied to an issue. GLM 5.2 produced workable code, but it skipped the process guardrails entirely. That is the same pattern as the wife test problem from a different angle: the output was right, but the workflow around it was not. Code quality is not enough if the tool cannot also respect the conventions of the repo it is working in. One thing to note though: GLM 5.2 is more honest about its mistakes than Claude. Claude would not admit it failed to follow the &lt;code&gt;AGENTS.md&lt;/code&gt; file, or it would blame the failure on lack of memory from a different session. GLM 5.2 would own up to it.&lt;/p&gt;
&lt;p&gt;That is the pattern I have settled on. GLM 5.2 for implementation and for surfacing issues and regressions, Claude for the polish pass and validation. None of them is the whole answer. Together they cover the range of work a real project requires.&lt;/p&gt;
&lt;h2&gt;How I Actually Use Them Together&lt;/h2&gt;
&lt;p&gt;The practical workflow looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with GLM 5.2 for the implementation. GLM 5.2 produces reviewable, testable code that holds up under scrutiny.&lt;/li&gt;
&lt;li&gt;Run GLM 5.2 again to catch lingering issues and regressions it missed on the first pass, but validate every finding through Claude before acting on it.&lt;/li&gt;
&lt;li&gt;Run a polish pass with Claude on anything a user will see. Let Claude fix the copy, the spacing, and the empty states.&lt;/li&gt;
&lt;li&gt;Validate the polished result against the original implementation. If Claude changed the logic, that is a signal to review, not accept.&lt;/li&gt;
&lt;li&gt;For the riskiest changes, keep GLM 5.2 on a tight leash: review the diff carefully and lean on Claude Sonnet 5.0 or Fable 5 to validate the logic before accepting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point is not to pick a winner. The point is to use each tool for what it is actually good at and to stop expecting any single model to be uniformly best across code, design, and long-running reliability.&lt;/p&gt;
&lt;h2&gt;Going Forward: GLM and Claude Side by Side, Split by Criticality&lt;/h2&gt;
&lt;p&gt;The rate limits are the real constraint, and the workaround I have settled on is not a rotation but a split. The perfect model for me in the last few days is to run GLM 5.2 with an Ollama monthly plan in one window, working on a medium-criticality project, and Claude with Sonnet 5.0 in another window, working on a critical project. Both tools stay busy at the same time, and because the work is spread across two providers, I can get through a significant amount of work without triggering either one&#39;s limits.&lt;/p&gt;
&lt;p&gt;GLM 5.2 handles the medium-criticality work where its thoroughness at catching regressions and missing test coverage is an asset and its false positive rate is tolerable. Claude with Sonnet 5.0 handles the critical work where I need the most certain diagnoses and the cleanest fixes. The two windows do not compete for the same quota, so neither one becomes the bottleneck for the other.&lt;/p&gt;
&lt;p&gt;Claude is also on a monthly plan right now, but I am strongly considering moving it to an annual plan. The split setup has made Claude the tool I lean on for the work that matters most, and locking in the annual plan would remove the month-to-month friction around renewal and pricing. GLM 5.2 stays on the Ollama monthly plan for now, since it is the newer tool in my workflow and I want to keep the option to adjust as it matures.&lt;/p&gt;
&lt;h2&gt;Final Thought: Two $20 Plans Is Enough&lt;/h2&gt;
&lt;p&gt;As of today, here is where I have landed. I started on Codex, switched completely to Claude, and iterated from Sonnet 4.6 to 4.8 to finally 5. I was disappointed with 4.8; it did not feel like a clear step forward over 4.6, and in some ways it felt like a step sideways. That disappointment is what pushed me to look harder at the other tools. I finally settled on Sonnet 5 for daily routine work and Fable 5 for codebase analysis and validation. GLM 5.2 complements Fable 5 nicely on a less user-facing project I am simultaneously working on: GLM 5.2 surfaces the issues and does the implementation, and Fable 5 validates the codebase-level concerns that are hard to see from a single diff.&lt;/p&gt;
&lt;p&gt;My current thinking is that I will settle on two $20-a-month plans, one for Claude and one for Ollama. That is enough tokens to cover the day-to-day for a part-time hobbyist. I am not running any single tool all day, and the split across two providers means I am not paying for capacity I do not use. Two $20 plans, two tools, two windows, and the work gets done.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex/quickstart&quot;&gt;OpenAI Codex quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/claude&quot;&gt;Anthropic Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://z.ai/glm-5.2&quot;&gt;Z.ai GLM 5.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><link>http://blog.ervits.com/2026/07/codex-vs-claude-vs-glm-52-practical.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeN1shMwn7504z7u1yoHqz4v3DHDVAIcJfTJpjBwpGY6g7xy8e5_Cgk6oExJ1OLUy9ydzb3GPtKbIZ8bnbtEI9ZxOJOsQdp5QwyUmqWgSAy0n-vhT06DnOD8f5nAaMYTfqpX5yjxeFBLEtHnrbjratVlmsnECDFG6jY8qRZSFaJfZVgQkhoGMCszbslLyl/s72-c/2026-06-30-codex-vs-claude-vs-glm-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-4111944066132503634</guid><pubDate>Fri, 12 Jun 2026 16:51:01 +0000</pubDate><atom:updated>2026-06-12T14:08:04.521-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">claude</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">developer-tools</category><title>My Thoughts on AI: Week of June 12</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYfqXGhkQNAx0QeCy8QSPuO_igp6sjb1NXr0KxET177MPTtY-Cno4UR1NNP_GOrzA2bCATtj_3pNotEVuA3D3PLp9Xrp0wRt5O2bRmhyphenhyphenu_d1_Nb0wZ_G7pdoZRuCSesPwjiai2W4nio4aEMc7p1Y_Vr4145om2lgCnKkASHXtdtWWgsQKHM8joB3_2Hi6v/s1536/2026-06-12-codex-vs-claude-code-hero.png&quot; alt=&quot;Developer workspace comparing stable code execution with polished design review panels&quot;&gt;&lt;/p&gt;
&lt;p&gt;I am not trying to crown one AI tool as the winner forever. These tools change too fast for that. What matters to me is more practical: when I sit down to do real work, which one helps me keep going, make fewer messy mistakes, and understand what happened when the session is over?&lt;/p&gt;
&lt;p&gt;Codex and Claude Code both do serious coding work. They can read files, make changes, run commands, and help move a project forward. But they feel different. Codex feels more like the tool I use when I need to finish implementation. Claude feels stronger when the work needs taste, presentation, and design sense.&lt;/p&gt;
&lt;p&gt;This is also just where I am right now. My opinion is based on GPT-5.5 in Codex and Opus 4.8 in Claude Code. Fable 5 also came out this week, on June 9, 2026, so even while I am writing this, the ground is already moving.&lt;/p&gt;
&lt;h2&gt;Codex Feels Better When the Job Has a Clear Finish Line&lt;/h2&gt;
&lt;p&gt;Codex works best for me when the task is concrete. Read the repo. Follow the project rules. Make the change. Run the check. Tell me what passed and what failed.&lt;/p&gt;
&lt;p&gt;For that kind of work, Codex feels steady. But that same directness can feel too plain when the task needs design judgment. It can solve the ticket, but not always understand the vibe.&lt;/p&gt;
&lt;h2&gt;Claude Code Feels Better With Presentation and Design&lt;/h2&gt;
&lt;p&gt;I am seeing this right now while working on my wife&#39;s portfolio website. I am not really a designer type of person. She is a fashion designer, not a techie, so she looks at the result differently than I do.&lt;/p&gt;
&lt;p&gt;When she looked at Codex&#39;s design work, she felt it was lazy and hodgepodge. It did not follow her comments closely enough, and it made up some design choices that did not match what she asked for. Claude&#39;s results felt more polished. It followed her design cues much closer. It did not feel stubborn or lazy in the same way.&lt;/p&gt;
&lt;h2&gt;Plans, Limits, and Model Releases May Change My Stack&lt;/h2&gt;
&lt;p&gt;The pricing and limits also matter. I am thinking about downgrading Codex Pro and adding Claude Pro, because I may not need one tool to do everything. I might switch back and forth depending on what OpenAI and Anthropic release next.&lt;/p&gt;
&lt;p&gt;Fable 5 is part of that thought process. It came out the same week I was writing this, and I burned through my allotted amount in about 30 minutes. With Opus on the $20 Claude Pro plan, I have been able to finish some milestones in one sitting without hitting rate limits.&lt;/p&gt;
&lt;p&gt;Codex limits have felt less aggressive to me, even though it is not the same tier as Claude Pro. I only ran out of Codex tokens once, and that was on a day when I had three implementations running at the same time throughout the day.&lt;/p&gt;
&lt;h2&gt;Where I Land Right Now&lt;/h2&gt;
&lt;p&gt;I do not think the answer is &amp;quot;Codex forever&amp;quot; or &amp;quot;Claude forever.&amp;quot; For implementation-heavy work, Codex is still my default. For design-sensitive work, Claude is making a strong case.&lt;/p&gt;
&lt;p&gt;My subscription mix may change as the models change. I may flip-flop. That is fine. The real lesson for me is to use the right tool for the right kind of work, instead of forcing one assistant to be good at everything.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2026/05/my-thoughts-on-ai-week-of-may-25.html&quot;&gt;My Thoughts on AI: Week of May 25&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex&quot;&gt;OpenAI: Codex documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex/quickstart&quot;&gt;OpenAI: Codex quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex/pricing&quot;&gt;OpenAI: Codex pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/overview&quot;&gt;Anthropic: Claude Code overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/costs&quot;&gt;Anthropic: Manage Claude Code costs effectively&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/context-window&quot;&gt;Anthropic: Explore the Claude Code context window&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/claude/fable&quot;&gt;Anthropic: Claude Fable 5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.com/pricing&quot;&gt;Anthropic: Claude pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/06/my-thoughts-on-ai-week-of-june-12.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYfqXGhkQNAx0QeCy8QSPuO_igp6sjb1NXr0KxET177MPTtY-Cno4UR1NNP_GOrzA2bCATtj_3pNotEVuA3D3PLp9Xrp0wRt5O2bRmhyphenhyphenu_d1_Nb0wZ_G7pdoZRuCSesPwjiai2W4nio4aEMc7p1Y_Vr4145om2lgCnKkASHXtdtWWgsQKHM8joB3_2Hi6v/s72-c/2026-06-12-codex-vs-claude-code-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-6366365206959223515</guid><pubDate>Sat, 30 May 2026 19:55:06 +0000</pubDate><atom:updated>2026-06-12T14:13:54.042-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">android</category><category domain="http://www.blogger.com/atom/ns#">android-studio</category><category domain="http://www.blogger.com/atom/ns#">antigravity</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">crashlytics</category><category domain="http://www.blogger.com/atom/ns#">fitness</category><category domain="http://www.blogger.com/atom/ns#">gemini</category><title>My Experience Writing Zone 2 Trainer for Android</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIlYGiD3Z8G66Xvrwfh4QHX5x4UK_-bEEqgrTBBuxnnmi-pjp_qkAi7eZULL3rqL7L4QYolaZtWAr3wCaEC6cyEAFeGCNJq3RuSSPeZzHFhPT2JiN_gFFodyXryWsbOxvHovxsEt3lcM8tQY849yKwz3zwgjy9sSgovue6la1UTHG-eZq_Z3K9dsUsfkfB/s1672/2026-05-17-zone-2-trainer-android-hero.png&quot; alt=&quot;Android fitness app development workspace with heart rate zones and AI assistant panels&quot;&gt;&lt;/p&gt;
&lt;p&gt;Zone 2 Trainer started as a simple idea: I wanted a lightweight way to stay honest about easy aerobic work without committing to another subscription or buying another dedicated device. The app became both a fitness tool and a test case for how far modern AI coding assistants can carry a non-mobile developer.&lt;/p&gt;
&lt;p&gt;For the last couple of months, I have been developing an Android application to enhance my endurance and recovery. Most applications on the market require an active subscription and a special device. With the current developments in AI-assisted software development, I was able to step out of my comfort zone and build an application from scratch using Android Studio, Antigravity, and Codex.&lt;/p&gt;
&lt;p&gt;That matters to me because I am not a developer by trade. I am closer to a tinkerer who is comfortable with scripts, databases, and infrastructure than someone who spends every day building polished mobile apps. Zone 2 Trainer became a useful test of how far AI coding tools can take someone like me when the goal is a real app, not a weekend demo.&lt;/p&gt;
&lt;h2&gt;Starting With Android Studio and Gemini&lt;/h2&gt;
&lt;p&gt;Android development has come a long way since I first dabbled with it. I began with Android Studio because it was the environment I already knew. The built-in chat interface gave me enough help to write a first version with a functional, if rough, interface in a couple of days.&lt;/p&gt;
&lt;p&gt;That first phase was all about getting the app to exist. Gemini was helpful for moving quickly through unfamiliar Android patterns, but the output still needed a lot of shaping. The app worked, but it had the feel of a prototype.&lt;/p&gt;
&lt;h2&gt;Adding Polish With Antigravity&lt;/h2&gt;
&lt;p&gt;The second phase was a mix of Android Studio and Antigravity. This is where the app started to feel more professional. The built-in skills from Antigravity were especially useful for visual polish and user experience work.&lt;/p&gt;
&lt;p&gt;At this point, the difference was not just that the app looked better. The workflow changed. I could describe the direction I wanted, review the result, and keep tightening the interface without getting stuck on every small Android implementation detail.&lt;/p&gt;
&lt;h2&gt;Stabilizing With Codex&lt;/h2&gt;
&lt;p&gt;The third phase was stability and acceleration. This is where I added Codex to the mix and stabilized the app end to end. I improved the test coverage, chased down rough edges, and built a stronger foundation for future enhancements.&lt;/p&gt;
&lt;p&gt;Gemini was useful, but the limits were painful. The Gemini Pro plan had strict limits, and the most time-consuming part of the work became waiting between rate-limit windows before I could finish a feature. That was extremely limiting.&lt;/p&gt;
&lt;p&gt;I first used Codex through my ChatGPT Plus subscription. It helped with memory leaks and race conditions, but it still required a lot of waiting between sessions. After upgrading to the Pro tier, I was able to complete an entire feature set for a given session in one go. That changed the way I worked.&lt;/p&gt;
&lt;p&gt;The biggest difference was trust. Codex understood the nuance of my prompts better than Gemini did during this project, and the code quality improved. My Crashlytics reports stopped firing alerts about race conditions and crashes after I adopted Codex as the main coding assistant.&lt;/p&gt;
&lt;h2&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;The lifecycle of the app ended up falling into three phases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android Studio with Gemini for the first functional version.&lt;/li&gt;
&lt;li&gt;Android Studio with Antigravity for polish and product feel.&lt;/li&gt;
&lt;li&gt;Codex for stability, test coverage, and end-to-end implementation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This work predates my testing of Gemini 3.5, and most of Zone 2 Trainer was built using Gemini 3.1. I still plan to run 3.5 through its paces. But for this project, Codex became the tool that helped me finish the work instead of only starting it.&lt;/p&gt;
&lt;p&gt;The larger lesson is that AI coding tools are not magic, but they are changing who can build useful software. I still had to make decisions, test the app, and care about the details. The difference is that I was able to stay in the work long enough to turn an idea into something real.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/studio/gemini/get-started&quot;&gt;Gemini in Android Studio: Get started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://antigravity.google/&quot;&gt;Google Antigravity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex/quickstart&quot;&gt;OpenAI Codex quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://firebase.google.com/docs/crashlytics&quot;&gt;Firebase Crashlytics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-5/&quot;&gt;Google: Gemini 3.5 Flash announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/05/my-experience-writing-zone-2-trainer.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIlYGiD3Z8G66Xvrwfh4QHX5x4UK_-bEEqgrTBBuxnnmi-pjp_qkAi7eZULL3rqL7L4QYolaZtWAr3wCaEC6cyEAFeGCNJq3RuSSPeZzHFhPT2JiN_gFFodyXryWsbOxvHovxsEt3lcM8tQY849yKwz3zwgjy9sSgovue6la1UTHG-eZq_Z3K9dsUsfkfB/s72-c/2026-05-17-zone-2-trainer-android-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-138705579408394956</guid><pubDate>Sat, 30 May 2026 15:04:57 +0000</pubDate><atom:updated>2026-06-12T14:10:19.080-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">AI</category><category domain="http://www.blogger.com/atom/ns#">codex</category><category domain="http://www.blogger.com/atom/ns#">gemini</category><category domain="http://www.blogger.com/atom/ns#">openai</category><category domain="http://www.blogger.com/atom/ns#">openrouter</category><title>My Thoughts on AI: Week of May 25</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXJHJJwuHJReqf4pJZeOlRa2vMxtKN8kOYK4MVul2vvgpyze2t0M4BnFkYICo13fhzb-c1bc8E9H101HcrlMEYlPGEVQFQDBF2jJg0uxy7tl1wMHTkjOgMjg4al5D-rOUjlR9Xlcegd61obYaREEaQDTDSr01jW25Ru8Ma0bajdR3zhpWklxCNgCQeBBfm/s1672/2026-05-30-ai-week-of-may-25-hero.png&quot; alt=&quot;Abstract AI developer workspace with briefing cards and model routing lines&quot;&gt;&lt;/p&gt;
&lt;p&gt;The AI product climate this week was weirdly clarifying. I am still not ready to move my development workflow back to Gemini after the rate-limit frustration, but I also have to admit that Google shipped a few things that made me pay attention again.&lt;/p&gt;
&lt;p&gt;At the same time, OpenAI reminded me that even the best developer tooling can feel unnecessarily confusing when the billing model sprawls across too many surfaces. Then OpenRouter raised a serious round of funding and made the case for a future where developers do not have to bet everything on one model vendor.&lt;/p&gt;
&lt;p&gt;That is the pattern I keep coming back to: the tools are getting better, but the surrounding product experience still matters just as much as the model.&lt;/p&gt;
&lt;h2&gt;Gemini Is Back in My General-Purpose Rotation&lt;/h2&gt;
&lt;p&gt;I have complained plenty about Gemini rate limits. The stricter enforcement hit the wrong nerve for me because it made the product feel unpredictable at exactly the moment I wanted to rely on it more. Once a tool makes me ration my thinking, I start looking for a different tool.&lt;/p&gt;
&lt;p&gt;That is a big reason I moved my development work to Codex. For coding, I want a tool that can stay with the repo, preserve the thread of the work, and keep going through implementation and validation. Gemini no longer owns that lane for me.&lt;/p&gt;
&lt;p&gt;But credit where it is due: the new Flash model feels much better than I expected. Google says Gemini 3.5 Flash is now the default model in the Gemini app and AI Mode in Search, and it is clearly aiming for the everyday assistant lane rather than only the coding assistant lane.&lt;/p&gt;
&lt;p&gt;Daily Brief is the more interesting shift for me. The feature points Gemini toward the role I actually want it to play now: less pair programmer, more morning context engine. I do not need every AI tool to be my IDE co-pilot. Some of them are more useful when they help me understand the day, triage the noise, and surface what is worth paying attention to.&lt;/p&gt;
&lt;p&gt;So Gemini is not out. It is just moving to a different job in my stack.&lt;/p&gt;
&lt;h2&gt;OpenAI Billing Still Feels Too Fragmented&lt;/h2&gt;
&lt;p&gt;OpenAI has become the center of gravity for my development workflow, but the billing experience still feels harder than it needs to be.&lt;/p&gt;
&lt;p&gt;Part of the confusion is structural. OpenAI documents ChatGPT and the API platform as separate billing systems, and the API is billed separately from ChatGPT subscriptions. That may make perfect sense from an internal product architecture point of view. From a customer point of view, it can feel like three different doors into the same building, each with its own card on file.&lt;/p&gt;
&lt;p&gt;That is where my frustration comes from. I have a monthly subscription for the tools I use directly. Then there is API billing, which is a separate developer platform. Then there are pay-as-you-go and authorization flows that can look like new charges if you are not reading the fine print closely enough.&lt;/p&gt;
&lt;p&gt;OpenAI&#39;s help docs do explain pieces of this: API billing is separate from ChatGPT plans, API usage can be managed through the platform billing page, and a temporary card authorization can appear when adding an API payment method. The problem is that customers should not have to assemble the mental model from several help articles after a charge shows up.&lt;/p&gt;
&lt;p&gt;My recommendation is boring, which is usually a good sign: fold the product billing experience into one clear account view. Show ChatGPT subscriptions, API usage, pay-as-you-go setup, authorizations, limits, and receipts in one place. Let customers drill into details, but do not make them guess which product surface generated which charge.&lt;/p&gt;
&lt;p&gt;This is not just a billing complaint. It is a trust issue. If I cannot quickly understand what I am paying for, the product starts to feel more expensive than the line item says.&lt;/p&gt;
&lt;h2&gt;Codex Remote Connections Have the Promise and the Friction&lt;/h2&gt;
&lt;p&gt;Codex is still where I want to do development work. It fits the way I think about software: read the repo, make a focused change, run the checks, explain the proof.&lt;/p&gt;
&lt;p&gt;The remote connection story is promising, but it still feels like a developing feature. When a session loses connection or hides too much of the build context, the user has to reconstruct where the work actually is. That is the exact kind of friction an agentic coding tool should remove.&lt;/p&gt;
&lt;p&gt;The core product direction is right. The missing piece is continuity. I want the remote environment to make the development cycle more legible, not less: current branch, current diff, running commands, failed checks, last successful proof, and what remains. When that context is visible, the user can supervise the work instead of babysitting the connection.&lt;/p&gt;
&lt;p&gt;That is the difference between an impressive demo and a tool I trust in a real project.&lt;/p&gt;
&lt;h2&gt;OpenRouter Looks More Important Than Ever&lt;/h2&gt;
&lt;p&gt;OpenRouter&#39;s latest funding round caught my attention because it validates a pattern I already like: one API surface, many model choices, and the ability to route work based on cost, quality, latency, or availability.&lt;/p&gt;
&lt;p&gt;The company announced a $113 million Series B led by CapitalG, with a long list of infrastructure and enterprise investors. OpenRouter also said its weekly volume grew from 5 trillion to 25 trillion tokens over the last six months. Those numbers matter less to me as bragging rights and more as a signal that multi-model infrastructure is becoming a serious layer in the stack.&lt;/p&gt;
&lt;p&gt;I used OpenRouter while testing Zone 2 Trainer, and the appeal was immediate. Instead of wiring every provider separately, you get a standard API shape and a lot more flexibility. That does not remove the need to understand model differences, but it lowers the switching cost.&lt;/p&gt;
&lt;p&gt;That flexibility is going to matter more as model providers keep changing prices, limits, product names, and access tiers. If the frontier keeps moving this fast, the winning developer experience may be less about picking the one perfect model and more about keeping optionality.&lt;/p&gt;
&lt;h2&gt;Where I Landed This Week&lt;/h2&gt;
&lt;p&gt;This week did not make me less committed to Codex for development. If anything, it clarified why I moved there.&lt;/p&gt;
&lt;p&gt;But it did make me less dismissive of Gemini. Flash and Daily Brief make Gemini feel useful again as a daily assistant, even if I do not want it driving my repos. It also made me more convinced that OpenAI needs to simplify its billing story before the confusion becomes part of the brand. And OpenRouter continues to look like one of the more practical bets in AI infrastructure.&lt;/p&gt;
&lt;p&gt;So my stack is settling into something more nuanced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codex for development.&lt;/li&gt;
&lt;li&gt;Gemini for daily context and general-purpose assistance.&lt;/li&gt;
&lt;li&gt;OpenAI API when I need direct product integration.&lt;/li&gt;
&lt;li&gt;OpenRouter when flexibility and model choice matter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That feels healthier than trying to make one AI product do every job. The more these tools mature, the more I want each one to be clear about what it is best at.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-5/&quot;&gt;Google: Gemini 3.5 and Gemini app updates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.google/innovation-and-ai/products/gemini-app/next-evolution-gemini-app/&quot;&gt;Google: The next evolution of the Gemini app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/gemini/answer/17004136?sjid=6916750752200694103-NA&quot;&gt;Google: Gemini app limits and availability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.openai.com/en/articles/9039756-managing-billing-settings-on-chatgpt-web-and-platform&quot;&gt;OpenAI: Managing billing settings on ChatGPT Web and Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/api/pricing/&quot;&gt;OpenAI: API pricing FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.openai.com/en/articles/7438062-after-updating-my-api-payment-method-i-noticed-a-5-charge-is-this-a-temporary-hold&quot;&gt;OpenAI: API payment method authorization hold&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openrouter.ai/announcements/series-b&quot;&gt;OpenRouter: Series B announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/05/my-thoughts-on-ai-week-of-may-25.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXJHJJwuHJReqf4pJZeOlRa2vMxtKN8kOYK4MVul2vvgpyze2t0M4BnFkYICo13fhzb-c1bc8E9H101HcrlMEYlPGEVQFQDBF2jJg0uxy7tl1wMHTkjOgMjg4al5D-rOUjlR9Xlcegd61obYaREEaQDTDSr01jW25Ru8Ma0bajdR3zhpWklxCNgCQeBBfm/s72-c/2026-05-30-ai-week-of-may-25-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-8915524235534565718</guid><pubDate>Tue, 27 Jan 2026 21:20:00 +0000</pubDate><atom:updated>2026-06-12T14:16:39.282-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cache</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">oltp</category><category domain="http://www.blogger.com/atom/ns#">Postgres</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">proxy</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>Accelerating PostgreSQL with ReadySet Caching</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHv7kZ7eWTeb6ZDdwZAvmtWzGz7qzNtardtY8lqBizKoQf_23OrxjFu4Azpt1G7Xo9n1A88ZmFtR4PZ5uNilGhe6cWaOAGTKqbE9iADjjsGIcKzf1BbFDz9O6JMbesBxNoj-I7KVxGxl1VmmGpz-St5EyF4Q2CQlERi7WqzABvMOM2TL2yfzrowqYP1ZI6/s1659/2026-01-27-accelerating-postgresql-readyset-cache-turbo-hero.png&quot; alt=&quot;PostgreSQL-inspired database blocks streaming through an in-memory cache layer at turbo speed&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; The opinions expressed in this article are my own and do not necessarily reflect the views of my employer.&lt;/p&gt;
&lt;p&gt;This tutorial demonstrates how to use &lt;a href=&quot;https://github.com/readysettech/readyset&quot;&gt;ReadySet&lt;/a&gt; as a caching layer for PostgreSQL databases. ReadySet is a wire-compatible caching solution that sits between your application and database, providing sub-millisecond query performance without requiring code changes.&lt;/p&gt;
&lt;h2&gt;Why Cache PostgreSQL?&lt;/h2&gt;
&lt;p&gt;PostgreSQL is the world&#39;s most advanced open-source relational database, widely used for transactional workloads. Whether you&#39;re running PostgreSQL on-premises, in the cloud, or using a managed service, every query incurs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Network round-trip latency&lt;/strong&gt; – Depending on region and network conditions (e.g., 14ms from a client in New Jersey to a cluster in AWS us-east-1)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query processing overhead&lt;/strong&gt; – Parsing, planning, and execution on the database server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result serialization&lt;/strong&gt; – Packaging and transmitting results back to your application&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For read-heavy workloads with repetitive queries (dashboards, APIs, reports), this latency adds up quickly—especially when the same query is executed hundreds or thousands of times per minute.&lt;/p&gt;
&lt;h2&gt;What is ReadySet?&lt;/h2&gt;
&lt;p&gt;ReadySet is a SQL caching engine that sits between your application and database. It&#39;s &lt;strong&gt;wire-compatible&lt;/strong&gt; with PostgreSQL and MySQL, meaning your application connects to ReadySet exactly like it would connect to your database—no code changes required.&lt;/p&gt;
&lt;h3&gt;ReadySet Cloud vs. Self-Hosted&lt;/h3&gt;
&lt;p&gt;ReadySet is available in several deployment options:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&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;a href=&quot;https://readyset.io/cloud&quot;&gt;&lt;strong&gt;ReadySet Cloud&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Fully managed service with automatic scaling, monitoring, and maintenance. Ideal for production workloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/readysettech/readyset&quot;&gt;&lt;strong&gt;Self-Hosted&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Deploy ReadySet yourself using Docker or binaries. Full control over infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Note for this tutorial:&lt;/strong&gt; We use a &lt;strong&gt;self-hosted ReadySet instance&lt;/strong&gt; to demonstrate the setup process. For production workloads, consider using ReadySet Cloud for automatic scaling and maintenance.&lt;/p&gt;
&lt;h3&gt;How ReadySet Works&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;┌─────────────┐         ┌─────────────┐         ┌──────────────┐
│ Application │────────▶│  ReadySet   │────────▶│  PostgreSQL  │
│             │  :5433  │   (Cache)   │  :5432  │  (Upstream)  │
└─────────────┘         └─────────────┘         └──────────────┘
                              │
                              ▼
                     ┌─────────────────┐
                     │ In-Memory Cache │
                     │ (Dataflow Graph)│
                     └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Transparent Proxy&lt;/strong&gt; – ReadySet intercepts SQL queries from your application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selective Caching&lt;/strong&gt; – You explicitly choose which queries to cache using &lt;code&gt;CREATE CACHE&lt;/code&gt; commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incremental Maintenance&lt;/strong&gt; – For deep cache mode, ReadySet subscribes to PostgreSQL&#39;s replication stream and automatically updates cached results when underlying data changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache Hits&lt;/strong&gt; – Cached queries are served directly from ReadySet&#39;s in-memory dataflow graph, bypassing the upstream database entirely&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Key Differentiators&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Traditional Cache (Redis/Memcached)&lt;/th&gt;
&lt;th&gt;ReadySet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key-value API&lt;/td&gt;
&lt;td&gt;Standard SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache invalidation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual (application logic)&lt;/td&gt;
&lt;td&gt;Automatic (via replication)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code changes required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (caching logic)&lt;/td&gt;
&lt;td&gt;No (same connection string)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Result freshness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends on TTL/manual invalidation&lt;/td&gt;
&lt;td&gt;Real-time (deep cache) or TTL-based (shallow cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Why Pair PostgreSQL with ReadySet?&lt;/h2&gt;
&lt;p&gt;The combination addresses a specific architectural challenge: &lt;strong&gt;you want the convenience of a managed PostgreSQL service, but you can&#39;t tolerate the inherent latency of a cloud-hosted database for high-frequency reads.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Latency Impact&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Typical Latency&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct to PostgreSQL&lt;/td&gt;
&lt;td&gt;14-20ms&lt;/td&gt;
&lt;td&gt;Acceptable for occasional queries, batch jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Via ReadySet (uncached)&lt;/td&gt;
&lt;td&gt;15-21ms&lt;/td&gt;
&lt;td&gt;Passthrough with minimal overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Via ReadySet (cached)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1-5ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dashboards, APIs, real-time apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;When This Pairing Makes Sense&lt;/h3&gt;
&lt;h4&gt;Good Fit&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Read-heavy workloads (80%+ reads)&lt;/li&gt;
&lt;li&gt;Repetitive queries (same query patterns executed frequently)&lt;/li&gt;
&lt;li&gt;Latency-sensitive applications (dashboards, APIs, search)&lt;/li&gt;
&lt;li&gt;Teams wanting to reduce database load and improve response times&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Not Ideal For&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Write-heavy workloads (ReadySet caches reads, not writes)&lt;/li&gt;
&lt;li&gt;Highly dynamic queries with unique parameters each time&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before starting, ensure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker installed and running&lt;/li&gt;
&lt;li&gt;A PostgreSQL database with &lt;code&gt;customers&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt;, and &lt;code&gt;products&lt;/code&gt; tables&lt;/li&gt;
&lt;li&gt;Logical replication enabled for deep cache&lt;/li&gt;
&lt;li&gt;Database credentials with appropriate permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Enabling Logical Replication on PostgreSQL&lt;/h3&gt;
&lt;p&gt;For &lt;strong&gt;deep cache&lt;/strong&gt; mode, ReadySet requires logical replication to be enabled on your PostgreSQL database. This allows ReadySet to subscribe to the Write-Ahead Log (WAL) and automatically update cached query results when data changes.&lt;/p&gt;
&lt;h4&gt;Step 1: Check Current Replication Setting&lt;/h4&gt;
&lt;p&gt;Connect to your PostgreSQL database and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; wal_level;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the result is &lt;code&gt;replica&lt;/code&gt; or &lt;code&gt;minimal&lt;/code&gt;, you need to change it to &lt;code&gt;logical&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Step 2: Enable Logical Replication&lt;/h4&gt;
&lt;p&gt;Modify the &lt;code&gt;wal_level&lt;/code&gt; parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; wal_level &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;logical&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This change requires a database restart to take effect. In managed environments, this may be handled automatically or require a support request depending on your configuration.&lt;/p&gt;
&lt;h4&gt;Step 3: Verify the Change&lt;/h4&gt;
&lt;p&gt;After the restart, verify logical replication is enabled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; wal_level;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt; wal_level
-----------
 logical
(1 row)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Required Database Permissions&lt;/h4&gt;
&lt;p&gt;The database user connecting to ReadySet needs the following permissions:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&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;SELECT&lt;/code&gt; on cached tables&lt;/td&gt;
&lt;td&gt;Read data for snapshotting and query execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REPLICATION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subscribe to the logical replication stream (deep cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CREATE&lt;/code&gt; on the database&lt;/td&gt;
&lt;td&gt;Create the replication slot (or use &lt;code&gt;--disable-create-publication&lt;/code&gt; if manually created)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you cannot grant &lt;code&gt;REPLICATION&lt;/code&gt; privileges (common in managed environments), you can still use &lt;strong&gt;shallow cache&lt;/strong&gt; mode, which doesn&#39;t require logical replication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shallow Cache Alternative:&lt;/strong&gt; If enabling logical replication isn&#39;t possible, skip to the &lt;a href=&quot;#shallow-cache-setup&quot;&gt;Shallow Cache Setup&lt;/a&gt; section, which uses TTL-based caching instead of replication.&lt;/p&gt;
&lt;h2&gt;Sample Schema&lt;/h2&gt;
&lt;p&gt;This tutorial uses the following sample tables. Create these in your PostgreSQL database before starting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- Customers table&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; public.customers (
    customer_id SERIAL &lt;span class=&quot;hljs-keyword&quot;&gt;PRIMARY KEY&lt;/span&gt;,
    first_name &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;50&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;NOT NULL&lt;/span&gt;,
    last_name &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;50&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;NOT NULL&lt;/span&gt;,
    email &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt;),
    created_at &lt;span class=&quot;hljs-type&quot;&gt;TIMESTAMP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;CURRENT_TIMESTAMP&lt;/span&gt;
);

&lt;span class=&quot;hljs-comment&quot;&gt;-- Products table&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; public.products (
    product_id SERIAL &lt;span class=&quot;hljs-keyword&quot;&gt;PRIMARY KEY&lt;/span&gt;,
    product_name &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;NOT NULL&lt;/span&gt;,
    category &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;50&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;NOT NULL&lt;/span&gt;,
    price &lt;span class=&quot;hljs-type&quot;&gt;DECIMAL&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;NOT NULL&lt;/span&gt;
);

&lt;span class=&quot;hljs-comment&quot;&gt;-- Orders table&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; public.orders (
    order_id SERIAL &lt;span class=&quot;hljs-keyword&quot;&gt;PRIMARY KEY&lt;/span&gt;,
    customer_id &lt;span class=&quot;hljs-type&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;REFERENCES&lt;/span&gt; public.customers(customer_id),
    product_id &lt;span class=&quot;hljs-type&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;REFERENCES&lt;/span&gt; public.products(product_id),
    quantity &lt;span class=&quot;hljs-type&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NOT NULL&lt;/span&gt;,
    order_date &lt;span class=&quot;hljs-type&quot;&gt;DATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;CURRENT_DATE&lt;/span&gt;
);

&lt;span class=&quot;hljs-comment&quot;&gt;-- Sample data: Customers&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; public.customers (first_name, last_name, email) &lt;span class=&quot;hljs-keyword&quot;&gt;VALUES&lt;/span&gt;
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Alice&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Smith&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;alice@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Bob&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Johnson&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;bob@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Charlie&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Brown&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;charlie@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Diana&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Prince&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;diana@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Evan&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Wright&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;evan@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Fiona&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Gallagher&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;fiona@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;George&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Martin&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;george@example.com&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Hannah&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Montana&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;hannah@example.com&amp;#x27;&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;-- Sample data: Products&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; public.products (product_name, category, price) &lt;span class=&quot;hljs-keyword&quot;&gt;VALUES&lt;/span&gt;
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Wireless Mouse&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Electronics&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;25.99&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Gaming Monitor&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Electronics&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;300.00&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Novel: The Great Gatsby&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Books&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;12.50&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Mechanical Keyboard&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Electronics&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;120.50&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Running Shoes&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Footwear&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;89.99&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Coffee Maker&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Kitchen&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;45.00&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Yoga Mat&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Fitness&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;20.00&lt;/span&gt;),
    (&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Leather Jacket&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Apparel&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;150.00&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;-- Sample data: Orders&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; public.orders (customer_id, product_id, quantity, order_date) &lt;span class=&quot;hljs-keyword&quot;&gt;VALUES&lt;/span&gt;
    (&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-01&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-02&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-03&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-04&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-05&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-06&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-07&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;7&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-08&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;7&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-09&amp;#x27;&lt;/span&gt;),
    (&lt;span class=&quot;hljs-number&quot;&gt;8&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;8&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2023-10-10&amp;#x27;&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Baseline Performance (Direct to PostgreSQL)&lt;/h2&gt;
&lt;p&gt;Before implementing ReadySet, let&#39;s establish baseline query performance by connecting directly to PostgreSQL. This helps us measure the impact of caching.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enable timing in psql:&lt;/strong&gt; To see query execution times, enable timing in your psql session:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;\timing &lt;span class=&quot;hljs-keyword&quot;&gt;on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This displays the elapsed time after each query, which we&#39;ll use throughout this tutorial to measure performance.&lt;/p&gt;
&lt;h3&gt;Query 1: Revenue by Product Category&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    p.category,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; revenue
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; p.category
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; revenue &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;  category   | items_sold | revenue
-------------+------------+---------
 Electronics |          4 |  566.99
 Apparel     |          1 |  150.00
 Footwear    |          1 |   89.99
 Fitness     |          3 |   60.00
 Kitchen     |          1 |   45.00
 Books       |          3 |   37.50
(6 rows)

Time: 19.402 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query 2: Top Customers by Spending&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    c.first_name,
    c.last_name,
    &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(o.order_id) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; number_of_orders,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; total_spent
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; customers c
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; orders o &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; c.customer_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; o.customer_id
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; c.first_name, c.last_name
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; total_spent &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;
LIMIT &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt; first_name | last_name | number_of_orders | total_spent
------------+-----------+------------------+-------------
 Bob        | Johnson   |                2 |      420.50
 Hannah     | Montana   |                1 |      150.00
 Charlie    | Brown     |                1 |      120.50
(3 rows)

Time: 14.595 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query 3: Order Details with Customer Names&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    o.order_id,
    o.order_date,
    c.first_name &lt;span class=&quot;hljs-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27; &amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;||&lt;/span&gt; c.last_name &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; customer_name,
    p.product_name,
    p.price,
    o.quantity,
    (p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; total_cost
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; customers c &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.customer_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; c.customer_id
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt; order_id | order_date |  customer_name  |      product_name       | price  | quantity | total_cost
----------+------------+-----------------+-------------------------+--------+----------+------------
       11 | 2023-10-01 | Alice Smith     | Wireless Mouse          |  25.99 |        1 |      25.99
       12 | 2023-10-02 | Bob Johnson     | Gaming Monitor          | 300.00 |        1 |     300.00
       ...
(10 rows)

Time: 16.441 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Baseline Summary:&lt;/strong&gt; Direct queries may take longer depending on network latency and the distance between your client application and the PostgreSQL server. Co-locating the client and database in the same region or availability zone isn&#39;t always feasible, making caching an attractive alternative.&lt;/p&gt;
&lt;h2&gt;Understanding ReadySet Cache Modes&lt;/h2&gt;
&lt;p&gt;ReadySet supports two caching strategies:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deep Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uses PostgreSQL logical replication to keep caches in sync in real-time&lt;/td&gt;
&lt;td&gt;When you need always-fresh data with automatic invalidation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shallow Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Periodically refreshes cached data based on TTL and refresh intervals&lt;/td&gt;
&lt;td&gt;When eventual consistency is acceptable and replication isn&#39;t available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Deep Cache Setup&lt;/h2&gt;
&lt;p&gt;Deep cache leverages PostgreSQL&#39;s logical replication stream to automatically update cached query results whenever the underlying data changes.&lt;/p&gt;
&lt;h3&gt;Step 1: Create a Docker Volume for Cache Persistence&lt;/h3&gt;
&lt;p&gt;Create a named Docker volume to persist ReadySet&#39;s cache data across container restarts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker volume create readyset_cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Create a Publication (Manual Step)&lt;/h3&gt;
&lt;p&gt;If your PostgreSQL environment doesn&#39;t provide superuser access, you must manually create a publication for the tables you want to replicate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- Create publication for specific tables&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; PUBLICATION readyset &lt;span class=&quot;hljs-keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; public.customers, public.products, public.orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This step is required because the &lt;code&gt;--disable-create-publication&lt;/code&gt; flag is used, which prevents ReadySet from attempting to create the publication itself (which would fail without superuser privileges).&lt;/p&gt;
&lt;h3&gt;Step 3: Run ReadySet with Deep Cache Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker run -d -p 5433:5433 -p 6034:6034         \
--name readyset --&lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt;                            \
-e UPSTREAM_DB_URL=postgres://&amp;lt;username&amp;gt;:&amp;lt;password&amp;gt;@&amp;lt;host&amp;gt;:5432/postgres \
-e REPLICATION_TABLES=public.customers,public.orders,public.products \
-e LISTEN_ADDRESS=0.0.0.0:5433                  \
-e STORAGE_DIR=/data/cache \
-v readyset_cache:/data/cache \
readysettech/readyset:latest \
  --verify-skip \
  --disable-upstream-ssl-verification \
  --disable-create-publication \
  --disable-telemetry
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker Command Parameters Explained&lt;/h3&gt;
&lt;h4&gt;Port Mappings (&lt;code&gt;-p&lt;/code&gt;)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&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;-p 5433:5433&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps the ReadySet PostgreSQL-compatible port. Your application connects here instead of directly to your database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p 6034:6034&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps the ReadySet metrics and health check HTTP endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Container Options&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&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;-d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs the container in detached (background) mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--name readyset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assigns a friendly name to the container for easier management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--rm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatically removes the container when it stops (useful for development)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Environment Variables (&lt;code&gt;-e&lt;/code&gt;)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&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;UPSTREAM_DB_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The full PostgreSQL connection string to your database. Format: &lt;code&gt;postgres://user:password@host:port/database&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REPLICATION_TABLES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma-separated list of tables to replicate (schema.table format). ReadySet will subscribe to changes on these tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LISTEN_ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The address and port ReadySet listens on for incoming connections. &lt;code&gt;0.0.0.0:5433&lt;/code&gt; allows connections from any network interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STORAGE_DIR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Directory path inside the container where ReadySet stores persistent cache data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Volume Mount (&lt;code&gt;-v&lt;/code&gt;)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&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;-v readyset_cache:/data/cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a Docker named volume &lt;code&gt;readyset_cache&lt;/code&gt; and mounts it to &lt;code&gt;/data/cache&lt;/code&gt; inside the container. This persists cache data across container restarts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;ReadySet Flags (Command Arguments)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&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;--verify-skip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skips verification of cached query results against the upstream database. Improves performance but reduces safety checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--disable-upstream-ssl-verification&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disables SSL certificate verification when connecting to the upstream database. Required when the upstream uses self-signed certificates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--disable-create-publication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents ReadySet from attempting to create a PostgreSQL publication. Use this when you don&#39;t have superuser privileges and have manually created the publication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--disable-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disables sending anonymous usage statistics to ReadySet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Step 4: Monitor Snapshot Progress&lt;/h3&gt;
&lt;p&gt;After starting ReadySet, monitor the Docker logs to verify that the snapshot completes successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker logs readyset --follow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for these INFO messages indicating successful table snapshotting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;2026-01-27T16:15:09.210564Z  INFO replicators::postgres_connector::snapshot: Snapshotting tables tables=3 max_parallel_snapshot_tables=7
2026-01-27T16:15:09.210589Z  INFO Snapshotting table{table=&amp;quot;public&amp;quot;.&amp;quot;customers&amp;quot;}: replicators::postgres_connector::snapshot: Snapshotting table
2026-01-27T16:15:09.222170Z  INFO Snapshotting table{table=&amp;quot;public&amp;quot;.&amp;quot;orders&amp;quot;}: replicators::postgres_connector::snapshot: Snapshotting table
2026-01-27T16:15:09.229690Z  INFO Snapshotting table{table=&amp;quot;public&amp;quot;.&amp;quot;products&amp;quot;}: replicators::postgres_connector::snapshot: Snapshotting table
...
2026-01-27T16:15:09.536299Z  INFO replicators::noria_adapter: Snapshot finished
2026-01-27T16:15:09.574068Z  INFO replicators::noria_adapter: Streaming replication started position=(0/D014690, 0/D014690)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Wait for the &amp;quot;Snapshot finished&amp;quot; and &amp;quot;Streaming replication started&amp;quot; messages before creating caches. This ensures ReadySet has a complete copy of your data.&lt;/p&gt;
&lt;h2&gt;Shallow Cache Setup&lt;/h2&gt;
&lt;p&gt;Shallow cache is ideal when logical replication isn&#39;t available or when eventual consistency is acceptable. It uses TTL-based caching with periodic refreshes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Switching from Deep Cache?&lt;/strong&gt; If you previously ran ReadySet in deep cache mode, stop the existing container first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker stop readyset
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 1: Create a Docker Volume for Cache Persistence&lt;/h3&gt;
&lt;p&gt;If you haven&#39;t already created the volume (from the deep cache setup), create it now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker volume create readyset_cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Run ReadySet with Shallow Cache Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker run -d -p 5433:5433 -p 6034:6034         \
--name readyset --&lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt;                            \
-e UPSTREAM_DB_URL=postgres://&amp;lt;username&amp;gt;:&amp;lt;password&amp;gt;@&amp;lt;host&amp;gt;:5432/postgres \
-e LISTEN_ADDRESS=0.0.0.0:5433                  \
-e STORAGE_DIR=/data/cache \
-v readyset_cache:/data/cache \
readysettech/readyset:latest \
  --verify-skip \
  --disable-upstream-ssl-verification \
  --replication-tables-ignore &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;*.*&amp;#x27;&lt;/span&gt; \
  --disable-telemetry
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Additional Shallow Cache Parameters&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&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;--replication-tables-ignore &#39;*.*&#39;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ignores all tables for replication. This effectively disables deep cache mode and enables shallow caching only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;REPLICATION_TABLES&lt;/code&gt; environment variable is not set in shallow cache mode since we&#39;re ignoring all tables for replication.&lt;/p&gt;
&lt;h2&gt;Creating Cached Queries&lt;/h2&gt;
&lt;h3&gt;Step 1: Connect to ReadySet&lt;/h3&gt;
&lt;p&gt;Once ReadySet is running, connect to it using any PostgreSQL client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;postgresql://&amp;lt;username&amp;gt;:&amp;lt;password&amp;gt;@127.0.0.1:5433/postgres&amp;#x27;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Use the &lt;strong&gt;same username and password&lt;/strong&gt; as your upstream PostgreSQL database. ReadySet authenticates against the upstream database using these credentials.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The connection string uses &lt;code&gt;127.0.0.1:5433&lt;/code&gt; (ReadySet) instead of your PostgreSQL host. ReadySet proxies your queries to the upstream database while caching results.&lt;/p&gt;
&lt;h3&gt;Step 2: Verify ReadySet Status&lt;/h3&gt;
&lt;p&gt;Before creating caches, verify that ReadySet is connected and ready:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; READYSET STATUS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;            name            |          value
----------------------------+--------------------------
 Database Connection        | Connected
 Connection Count           | 1
 Status                     | Online
 Maximum Replication Offset | (0/B3021CF8, 0/B3021D28)
 Minimum Replication Offset | (0/B3021CF8, 0/B3021D28)
 Last started Controller    | 2026-01-27 14:37:48 UTC
 Last completed snapshot    | 2026-01-27 14:37:51 UTC
 Last started replication   | 2026-01-27 14:37:51 UTC
 Enabled Features           | None
(9 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify there are no caches yet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; CACHES;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt; query id | cache name | query text | properties | count
----------+------------+------------+------------+-------
(0 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Create Your Cached Queries&lt;/h3&gt;
&lt;h3&gt;Deep Cache Queries (CREATE DEEP CACHE ALWAYS)&lt;/h3&gt;
&lt;p&gt;Deep cache queries use &lt;code&gt;CREATE DEEP CACHE ALWAYS&lt;/code&gt; and are automatically kept in sync via replication:&lt;/p&gt;
&lt;h4&gt;Query 1: Revenue by Product Category&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; DEEP CACHE ALWAYS q1 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    p.category,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; revenue
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; p.category
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; revenue &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Query 2: Top Customers by Spending&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; DEEP CACHE ALWAYS q2 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    c.first_name,
    c.last_name,
    &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(o.order_id) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; number_of_orders,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; total_spent
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; customers c
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; orders o &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; c.customer_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; o.customer_id
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; c.first_name, c.last_name
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; total_spent &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;
LIMIT &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Query 3: Order Details with Customer Names&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; DEEP CACHE ALWAYS q3 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    o.order_id,
    o.order_date,
    CONCAT(c.first_name, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27; &amp;#x27;&lt;/span&gt;, c.last_name) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; customer_name,
    p.product_name,
    p.price,
    o.quantity,
    (p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; total_cost
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; customers c &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.customer_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; c.customer_id
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Shallow Cache Queries (CREATE SHALLOW CACHE)&lt;/h3&gt;
&lt;p&gt;Shallow cache queries include TTL and refresh policies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; SHALLOW CACHE POLICY TTL &lt;span class=&quot;hljs-number&quot;&gt;3600&lt;/span&gt; SECONDS REFRESH &lt;span class=&quot;hljs-keyword&quot;&gt;EVERY&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;600&lt;/span&gt; SECONDS
shallow_cache_q1 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    p.category,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; revenue
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; p.category
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; revenue &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Shallow Cache Policy Parameters&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&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;TTL 3600 SECONDS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Time-to-live: cached results expire after 3600 seconds (1 hour)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REFRESH EVERY 600 SECONDS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ReadySet proactively refreshes the cache every 600 seconds (10 minutes)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Additional Shallow Cache Examples&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- Top customers with shallow caching&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; SHALLOW CACHE POLICY TTL &lt;span class=&quot;hljs-number&quot;&gt;3600&lt;/span&gt; SECONDS REFRESH &lt;span class=&quot;hljs-keyword&quot;&gt;EVERY&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;600&lt;/span&gt; SECONDS
shallow_cache_q2 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    c.first_name,
    c.last_name,
    &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(o.order_id) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; number_of_orders,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; total_spent
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; customers c
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; orders o &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; c.customer_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; o.customer_id
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; c.first_name, c.last_name
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; total_spent &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;
LIMIT &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Order details with shallow caching&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; SHALLOW CACHE POLICY TTL &lt;span class=&quot;hljs-number&quot;&gt;3600&lt;/span&gt; SECONDS REFRESH &lt;span class=&quot;hljs-keyword&quot;&gt;EVERY&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;600&lt;/span&gt; SECONDS
shallow_cache_q3 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    o.order_id,
    o.order_date,
    c.first_name &lt;span class=&quot;hljs-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27; &amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;||&lt;/span&gt; c.last_name &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; customer_name,
    p.product_name,
    p.price,
    o.quantity,
    (p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; total_cost
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; customers c &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.customer_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; c.customer_id
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;||&lt;/code&gt; string concatenation operator is supported in shallow cache mode but not in deep cache mode (see Troubleshooting).&lt;/p&gt;
&lt;h3&gt;Step 4: Verify Queries Are Being Cached&lt;/h3&gt;
&lt;p&gt;After creating caches, confirm that your queries are hitting the cache instead of the upstream database.&lt;/p&gt;
&lt;h4&gt;Check Cache Status&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; CACHES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;count&lt;/code&gt; column shows how many times each cache has been hit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;      query id      | cache name |  query text (truncated)  | properties | count
--------------------+------------+--------------------------+------------+-------
 q_663bfa3e73146832 | q1         | SELECT &amp;quot;p&amp;quot;.&amp;quot;category&amp;quot;... | always     |     0
 q_272f720b813f03ee | q2         | SELECT &amp;quot;c&amp;quot;.&amp;quot;first_name&amp;quot;..| always     |     0
 q_a1b2c3d4e5f67890 | q3         | SELECT &amp;quot;o&amp;quot;.&amp;quot;order_id&amp;quot;... | always     |     0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Immediately after creating caches, the &lt;code&gt;count&lt;/code&gt; will be &lt;code&gt;0&lt;/code&gt;. Run your cached queries a few times, then check &lt;code&gt;SHOW CACHES&lt;/code&gt; again to see the hit count increase.&lt;/p&gt;
&lt;h4&gt;View All Proxied Queries&lt;/h4&gt;
&lt;p&gt;To see which queries are being cached vs. proxied to upstream:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; PROXIED QUERIES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This displays all queries ReadySet has seen, their cache status, and whether they&#39;re supported for caching.&lt;/p&gt;
&lt;h4&gt;Confirm Cache Hit with Timing&lt;/h4&gt;
&lt;p&gt;The easiest way to verify caching is working: run the same query twice and compare times.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- First run (may be uncached): ~15-20ms&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; p.category, &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold ...

&lt;span class=&quot;hljs-comment&quot;&gt;-- Second run (cached): ~1-5ms&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; p.category, &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the second query is significantly faster, it&#39;s being served from the cache.&lt;/p&gt;
&lt;h2&gt;Verifying Automatic Cache Invalidation (Deep Cache)&lt;/h2&gt;
&lt;p&gt;Deep cache automatically updates when underlying data changes. Let&#39;s demonstrate this with an INSERT.&lt;/p&gt;
&lt;h3&gt;Step 1: Query the Current Cache&lt;/h3&gt;
&lt;p&gt;First, run the revenue by category query and note the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    p.category,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; revenue
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; p.category
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; revenue &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;  category   | items_sold | revenue
-------------+------------+---------
 Electronics |          4 |  566.99
 Apparel     |          1 |     150
 Footwear    |          1 |   89.99
 Fitness     |          3 |   60.00
 Kitchen     |          1 |      45
 Books       |          3 |   37.50
(6 rows)

Time: 4.344 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Insert New Data&lt;/h3&gt;
&lt;p&gt;Add a new order for an Electronics product:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; orders (customer_id, product_id, quantity, order_date)
&lt;span class=&quot;hljs-keyword&quot;&gt;VALUES&lt;/span&gt; (&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2024-01-15&amp;#x27;&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Verify Cache Updated Automatically&lt;/h3&gt;
&lt;p&gt;Re-run the same query—the cache should reflect the new data &lt;strong&gt;without any manual invalidation&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    p.category,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; items_sold,
    &lt;span class=&quot;hljs-built_in&quot;&gt;SUM&lt;/span&gt;(p.price &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; o.quantity) &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; revenue
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; orders o
&lt;span class=&quot;hljs-keyword&quot;&gt;JOIN&lt;/span&gt; products p &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; o.product_id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; p.product_id
&lt;span class=&quot;hljs-keyword&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; p.category
&lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; revenue &lt;span class=&quot;hljs-keyword&quot;&gt;DESC&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;  category   | items_sold | revenue
-------------+------------+---------
 Electronics |          6 |  807.99
 Apparel     |          1 |     150
 Footwear    |          1 |   89.99
 Fitness     |          3 |   60.00
 Kitchen     |          1 |      45
 Books       |          3 |   37.50
(6 rows)

Time: 4.648 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; ReadySet subscribes to PostgreSQL&#39;s replication stream. When you INSERT/UPDATE/DELETE data, the change flows through ReadySet&#39;s dataflow graph, automatically updating all affected cached query results in real-time.&lt;/p&gt;
&lt;h2&gt;Performance Impact &amp;amp; Latency Analysis&lt;/h2&gt;
&lt;p&gt;As discussed in &lt;a href=&quot;#why-cache-postgresql&quot;&gt;Why Cache PostgreSQL?&lt;/a&gt;, databases incur network latency per query due to network round-trips and processing overhead. ReadySet eliminates most of these costs by serving pre-computed results from local memory.&lt;/p&gt;
&lt;h3&gt;A Note on Data Size&lt;/h3&gt;
&lt;p&gt;The performance results in this tutorial use a small sample dataset (10 orders, 8 customers, 8 products). With small datasets, the improvements may seem modest—but this actually understates ReadySet&#39;s value in production:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Small Dataset&lt;/th&gt;
&lt;th&gt;Large Dataset (millions of rows)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Upstream query time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast (data fits in memory)&lt;/td&gt;
&lt;td&gt;Slower (disk I/O, larger joins, more aggregation work)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cached query time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1-5ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1-5ms&lt;/strong&gt; (constant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Relative improvement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3-12x&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10-100x or more&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; As your data grows, upstream query latency increases due to larger table scans, more complex joins, and heavier aggregation workloads. However, &lt;strong&gt;cached query latency remains constant&lt;/strong&gt;—ReadySet serves pre-computed results regardless of the underlying data size. This means the performance gap between cached and uncached queries widens significantly as your dataset scales.&lt;/p&gt;
&lt;h3&gt;Deep Cache Performance Results&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Direct to PostgreSQL&lt;/th&gt;
&lt;th&gt;Via ReadySet (uncached)&lt;/th&gt;
&lt;th&gt;Via ReadySet (cached)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1: Revenue by Category&lt;/td&gt;
&lt;td&gt;19.4 ms&lt;/td&gt;
&lt;td&gt;18.5 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.7 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.3x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2: Top Customers&lt;/td&gt;
&lt;td&gt;14.6 ms&lt;/td&gt;
&lt;td&gt;~15 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.4x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3: Order Details&lt;/td&gt;
&lt;td&gt;16.4 ms&lt;/td&gt;
&lt;td&gt;~17 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.1 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.2x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Key Observations&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Uncached queries&lt;/strong&gt; pass through ReadySet to the upstream database with minimal overhead (~1ms)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cached queries&lt;/strong&gt; are served directly from ReadySet&#39;s in-memory dataflow graph&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex aggregations&lt;/strong&gt; (Q2 with COUNT, SUM, GROUP BY, ORDER BY, LIMIT) see the largest improvement&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple joins&lt;/strong&gt; (Q3) still benefit significantly but have lower relative improvement&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Shallow Cache Performance Results&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Direct to PostgreSQL&lt;/th&gt;
&lt;th&gt;Via ReadySet (shallow cached)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1: Revenue by Category&lt;/td&gt;
&lt;td&gt;19.4 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.9 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.0x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2: Top Customers&lt;/td&gt;
&lt;td&gt;14.6 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.6x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3: Order Details&lt;/td&gt;
&lt;td&gt;16.4 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.9x faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;When to Use Each Cache Mode&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; To verify your caches are working, see &lt;a href=&quot;#step-4-verify-queries-are-being-cached&quot;&gt;Step 4: Verify Queries Are Being Cached&lt;/a&gt; in the Creating Cached Queries section.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consideration&lt;/th&gt;
&lt;th&gt;Deep Cache&lt;/th&gt;
&lt;th&gt;Shallow Cache&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data freshness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time (milliseconds)&lt;/td&gt;
&lt;td&gt;Eventual (TTL-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires publication + replication&lt;/td&gt;
&lt;td&gt;Simpler setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OLTP, user-facing apps&lt;/td&gt;
&lt;td&gt;Analytics, dashboards, reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Latency reduction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3-12x improvement&lt;/td&gt;
&lt;td&gt;3-5x improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Upstream load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal (only replication stream)&lt;/td&gt;
&lt;td&gt;Periodic refresh queries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Latency Implications for Production&lt;/h3&gt;
&lt;h4&gt;User Experience&lt;/h4&gt;
&lt;p&gt;Reducing query latency from 15-20ms to 1-5ms dramatically improves perceived application responsiveness, especially for dashboard loading times, autocomplete/search-as-you-type features, and real-time analytics displays.&lt;/p&gt;
&lt;h4&gt;Throughput&lt;/h4&gt;
&lt;p&gt;Lower latency means each connection can handle more queries per second, improving overall system capacity.&lt;/p&gt;
&lt;h4&gt;Cost Optimization&lt;/h4&gt;
&lt;p&gt;Fewer queries hitting the upstream database can reduce compute costs.&lt;/p&gt;
&lt;h4&gt;Consistency Trade-offs&lt;/h4&gt;
&lt;p&gt;Deep cache stays consistent with upstream through replication. Shallow cache may serve stale data within the TTL window, which is acceptable for many read-heavy workloads.&lt;/p&gt;
&lt;h3&gt;Comparing Query Execution Plans&lt;/h3&gt;
&lt;p&gt;To understand query execution behavior, let&#39;s compare &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; output when executed directly on PostgreSQL vs. through ReadySet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Understanding the Timing Discrepancy:&lt;/strong&gt; The EXPLAIN ANALYZE output below shows &lt;strong&gt;database-side execution time&lt;/strong&gt; (0.1-0.15ms), while the performance tables earlier show &lt;strong&gt;total query time&lt;/strong&gt; (14-20ms). The difference is &lt;strong&gt;network round-trip latency&lt;/strong&gt;—the time for your query to travel to the database server and the results to return. This is why caching locally with ReadySet provides such dramatic improvements: it eliminates the network hop entirely for cached queries.&lt;/p&gt;
&lt;h4&gt;Query 1: Revenue by Category (2-table join)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL Direct:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;                                              QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=77.67..78.17 rows=200 width=158) (actual time=0.059..0.060 rows=6.00 loops=1)
   Sort Key: (sum((p.price * (o.quantity)::numeric))) DESC
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=2
   -&amp;gt;  HashAggregate  (cost=67.53..70.03 rows=200 width=158) (actual time=0.049..0.053 rows=6.00 loops=1)
         Group Key: p.category
         -&amp;gt;  Hash Join  (cost=14.72..46.28 rows=1700 width=138) (actual time=0.029..0.034 rows=10.00 loops=1)
               Hash Cond: (o.product_id = p.product_id)
               -&amp;gt;  Seq Scan on orders o
               -&amp;gt;  Hash -&amp;gt; Seq Scan on products p
 Planning Time: 0.137 ms
 Execution Time: 0.103 ms
(20 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Via ReadySet (Proxied to Upstream):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;                                              QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=77.67..78.17 rows=200 width=158) (actual time=0.057..0.059 rows=6.00 loops=1)
   Sort Key: (sum((p.price * (o.quantity)::numeric))) DESC
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=2
   -&amp;gt;  HashAggregate  (cost=67.53..70.03 rows=200 width=158) (actual time=0.048..0.052 rows=6.00 loops=1)
         Group Key: p.category
         -&amp;gt;  Hash Join  (cost=14.72..46.28 rows=1700 width=138) (actual time=0.028..0.032 rows=10.00 loops=1)
               Hash Cond: (o.product_id = p.product_id)
               -&amp;gt;  Seq Scan on orders o
               -&amp;gt;  Hash -&amp;gt; Seq Scan on products p
 Planning Time: 0.134 ms
 Execution Time: 0.102 ms
(20 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Query 2: Top Customers (3-table join with aggregation)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL Direct:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;                                              QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=92.56..92.57 rows=3 width=276) (actual time=0.090..0.092 rows=3.00 loops=1)
   Buffers: shared hit=3
   -&amp;gt;  Sort  (cost=92.56..92.88 rows=130 width=276) (actual time=0.089..0.091 rows=3.00 loops=1)
         Sort Key: (sum((p.price * (o.quantity)::numeric))) DESC
         Sort Method: top-N heapsort  Memory: 25kB
         -&amp;gt;  HashAggregate  (cost=89.25..90.88 rows=130 width=276) (actual time=0.077..0.082 rows=8.00 loops=1)
               Group Key: c.first_name, c.last_name
               -&amp;gt;  Hash Join  (cost=27.65..63.75 rows=1700 width=260) (actual time=0.054..0.061 rows=10.00 loops=1)
                     Hash Cond: (o.product_id = p.product_id)
                     -&amp;gt;  Hash Join  (cost=12.93..44.48 rows=1700 width=248) (actual time=0.028..0.032 rows=10.00 loops=1)
                           Hash Cond: (o.customer_id = c.customer_id)
                           -&amp;gt;  Seq Scan on orders o
                           -&amp;gt;  Hash -&amp;gt; Seq Scan on customers c
                     -&amp;gt;  Hash -&amp;gt; Seq Scan on products p
 Planning Time: 0.189 ms
 Execution Time: 0.149 ms
(30 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Via ReadySet (Proxied to Upstream):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;                                              QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=92.56..92.57 rows=3 width=276) (actual time=0.079..0.082 rows=3.00 loops=1)
   Buffers: shared hit=3
   -&amp;gt;  Sort  (cost=92.56..92.88 rows=130 width=276) (actual time=0.078..0.080 rows=3.00 loops=1)
         Sort Key: (sum((p.price * (o.quantity)::numeric))) DESC
         Sort Method: top-N heapsort  Memory: 25kB
         -&amp;gt;  HashAggregate  (cost=89.25..90.88 rows=130 width=276) (actual time=0.067..0.072 rows=8.00 loops=1)
               Group Key: c.first_name, c.last_name
               -&amp;gt;  Hash Join  (cost=27.65..63.75 rows=1700 width=260) (actual time=0.043..0.050 rows=10.00 loops=1)
                     Hash Cond: (o.product_id = p.product_id)
                     -&amp;gt;  Hash Join  (cost=12.93..44.48 rows=1700 width=248) (actual time=0.021..0.026 rows=10.00 loops=1)
                           Hash Cond: (o.customer_id = c.customer_id)
                           -&amp;gt;  Seq Scan on orders o
                           -&amp;gt;  Hash -&amp;gt; Seq Scan on customers c
                     -&amp;gt;  Hash -&amp;gt; Seq Scan on products p
 Planning Time: 0.187 ms
 Execution Time: 0.140 ms
(30 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Execution Time Comparison Summary&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;PostgreSQL Direct&lt;/th&gt;
&lt;th&gt;Via ReadySet&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Planning Time&lt;/td&gt;
&lt;td&gt;0.14 ms&lt;/td&gt;
&lt;td&gt;0.13 ms&lt;/td&gt;
&lt;td&gt;-2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Execution Time&lt;/td&gt;
&lt;td&gt;0.10 ms&lt;/td&gt;
&lt;td&gt;0.10 ms&lt;/td&gt;
&lt;td&gt;-1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Planning Time&lt;/td&gt;
&lt;td&gt;0.19 ms&lt;/td&gt;
&lt;td&gt;0.19 ms&lt;/td&gt;
&lt;td&gt;-1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Execution Time&lt;/td&gt;
&lt;td&gt;0.15 ms&lt;/td&gt;
&lt;td&gt;0.14 ms&lt;/td&gt;
&lt;td&gt;-6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;Key Insights&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Same Query Plan Structure&lt;/strong&gt;: Both executions use identical query plans (Limit → Sort → HashAggregate → Hash Joins → Seq Scans). ReadySet doesn&#39;t modify how the upstream database executes queries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Marginal Improvement on Proxied Queries&lt;/strong&gt;: Even for uncached queries that pass through to the upstream database, there&#39;s a slight performance improvement (~6% faster execution).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Real Win is Caching&lt;/strong&gt;: The EXPLAIN output above shows &lt;em&gt;uncached&lt;/em&gt; query behavior. For &lt;strong&gt;cached queries&lt;/strong&gt;, ReadySet bypasses this entire execution pipeline and serves pre-computed results directly from memory—reducing latency from &lt;strong&gt;14-20ms to 1-5ms&lt;/strong&gt; (a 3-12x improvement).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When a query is cached, ReadySet performs a simple key-value lookup in its in-memory dataflow graph rather than executing a traditional query plan against the database.&lt;/p&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;String Concatenation Error in Deep Cache&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Using &lt;code&gt;||&lt;/code&gt; for string concatenation in deep cache mode fails:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- This FAILS in deep cache mode&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; DEEP CACHE ALWAYS q3 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    c.first_name &lt;span class=&quot;hljs-operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27; &amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;||&lt;/span&gt; c.last_name &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; customer_name
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;ERROR: unknown: Error during RPC (extend_recipe): SQL SELECT query &amp;#x27;q3&amp;#x27; couldn&amp;#x27;t be added:
MIR node &amp;#x27;20&amp;#x27; couldn&amp;#x27;t be lowered to dataflow: The provided query is invalid:
cannot invoke &amp;#x27;||&amp;#x27; on left-side operand type VarChar(50); expected JSONB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Use the &lt;code&gt;CONCAT()&lt;/code&gt; function instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- This WORKS in deep cache mode&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; DEEP CACHE ALWAYS q3 &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    CONCAT(c.first_name, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27; &amp;#x27;&lt;/span&gt;, c.last_name) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; customer_name
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Common Issues&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cannot connect to ReadySet&lt;/td&gt;
&lt;td&gt;Ensure port 5433 is not blocked and the container is running (&lt;code&gt;docker ps&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publication doesn&#39;t exist&lt;/td&gt;
&lt;td&gt;Manually create the publication with &lt;code&gt;CREATE PUBLICATION readyset FOR TABLE ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL verification errors&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;--disable-upstream-ssl-verification&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache not updating&lt;/td&gt;
&lt;td&gt;For deep cache, verify the publication includes all required tables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Useful Commands&lt;/h2&gt;
&lt;h3&gt;View Cached Queries&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; CACHES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;View Query Status&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; PROXIED QUERIES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Drop a Cache&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;DROP&lt;/span&gt; CACHE q1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Check ReadySet Status&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; READYSET STATUS;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Monitor Docker Logs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker logs readyset --follow
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Stop ReadySet&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker stop readyset
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Remove Cache Volume (reset all caches)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker volume &lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt; readyset_cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cleanup&lt;/h2&gt;
&lt;p&gt;To completely remove ReadySet and start fresh:&lt;/p&gt;
&lt;h3&gt;Step 1: Stop the ReadySet Container&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker stop readyset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you used the &lt;code&gt;--rm&lt;/code&gt; flag when starting the container, it will be automatically removed when stopped.&lt;/p&gt;
&lt;h3&gt;Step 2: Remove the Docker Volume&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker volume &lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt; readyset_cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Drop the Publication (Deep Cache Only)&lt;/h3&gt;
&lt;p&gt;If you created a publication for deep cache mode, remove it from your PostgreSQL database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;DROP&lt;/span&gt; PUBLICATION IF &lt;span class=&quot;hljs-keyword&quot;&gt;EXISTS&lt;/span&gt; readyset;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Verify Cleanup&lt;/h3&gt;
&lt;p&gt;Confirm everything is removed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;# Check no readyset container exists&lt;/span&gt;
docker ps -a | grep readyset

&lt;span class=&quot;hljs-comment&quot;&gt;# Check volume is removed&lt;/span&gt;
docker volume &lt;span class=&quot;hljs-built_in&quot;&gt;ls&lt;/span&gt; | grep readyset_cache
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Acknowledgments&lt;/h2&gt;
&lt;p&gt;Thanks to the &lt;strong&gt;ReadySet team&lt;/strong&gt; for their support and guidance in getting this tutorial together—their responsiveness and deep product knowledge made the process smooth.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/readysettech/readyset&quot;&gt;ReadySet GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://readyset.io/cloud&quot;&gt;ReadySet Cloud&lt;/a&gt; – Fully managed caching service&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://readyset.io/docs/get-started&quot;&gt;ReadySet Documentation - Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/&quot;&gt;PostgreSQL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2026/01/accelerating-snowflake-postgresql-with.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHv7kZ7eWTeb6ZDdwZAvmtWzGz7qzNtardtY8lqBizKoQf_23OrxjFu4Azpt1G7Xo9n1A88ZmFtR4PZ5uNilGhe6cWaOAGTKqbE9iADjjsGIcKzf1BbFDz9O6JMbesBxNoj-I7KVxGxl1VmmGpz-St5EyF4Q2CQlERi7WqzABvMOM2TL2yfzrowqYP1ZI6/s72-c/2026-01-27-accelerating-postgresql-readyset-cache-turbo-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-1948321695828624585</guid><pubDate>Mon, 09 Sep 2024 20:27:00 +0000</pubDate><atom:updated>2026-06-22T16:22:41.896-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">changefeed</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">databse</category><category domain="http://www.blogger.com/atom/ns#">exactly once</category><category domain="http://www.blogger.com/atom/ns#">jms</category><category domain="http://www.blogger.com/atom/ns#">kafka</category><category domain="http://www.blogger.com/atom/ns#">message bus</category><category domain="http://www.blogger.com/atom/ns#">snowflake</category><title>Exploring Real-Time Data Ingestion into Snowflake Using CockroachDB, Redpanda, and Kafka Connect</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-9J-_mIVhiS9Tx0ljDE1tqgPhqGckOfnJNMSU9hzE2HgghTdP4IieP17prjGsGBb7-B7OciISbyPUV6CWGknwjMbCdiTs-Gv-eRncnqzEt5oHaxWN3XCwuo698l9hsl7fJMeSNrdLwp9cWTrx80h6qKP8kIK4Jw-tCs1buMlnuVbFrJS1zdKttPf50fdC/s1693/2024-08-15-snowflake-ingestion-cockroachdb-redpanda-connect-hero.png&quot; alt=&quot;Abstract real-time ingestion pipeline from database change events through a message bus and Kafka Connect into a cloud warehouse&quot;&gt;&lt;/p&gt;
&lt;p&gt;This article serves as a follow-up to previous discussions on data ingestion into Snowflake, specifically focusing on the complexities and efforts involved in integrating Change Data Capture (CDC) data with Snowflake in real time.&lt;/p&gt;
&lt;h2&gt;Previous Articles on Snowflake&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2024/08/tour-of-snowflake-ingestion-using.html&quot;&gt;Tour of Snowflake ingestion using CockroachDB and Redpanda Connect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2024/03/integrating-trino-with-snowflake.html&quot;&gt;Integrating Snowflake with Trino&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Previous Articles on CockroachDB CDC&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2023/08/emitting-protocol-buffers-using.html&quot;&gt;Emitting Protocol Buffers with CockroachDB CDC Queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2023/04/using-cockroachdb-cdc-with-apache-pulsar.html&quot;&gt;Using CockroachDB CDC with Apache Pulsar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/09/using-cockroachdb-cdc-with-azure-event.html&quot;&gt;Using CockroachDB CDC with Azure Event Hubs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/03/saas-galore-integrating-cockroachdb.html&quot;&gt;SaaS Galore: Integrating CockroachDB with Confluent Kafka, FiveTran and Snowflake&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/04/using-cockroachdb-cdc-with-confluent.html&quot;&gt;Using CockroachDB CDC with Confluent Cloud Kafka and Schema Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2020/01/cockroachdb-cdc-using-minio-as-cloud.html&quot;&gt;CockroachDB CDC using Minio as cloud storage sink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2020/01/cockroachdb-cdc-using-hadoop-ozone-s3.html&quot;&gt;CockroachDB CDC using Hadoop Ozone S3 Gateway as cloud storage sink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;This article builds upon the previous discussion in &lt;a href=&quot;https://blog.ervits.com/2024/08/tour-of-snowflake-ingestion-using.html&quot;&gt;Tour of Snowflake ingestion using CockroachDB and Redpanda Connect&lt;/a&gt;, where we investigated the process of streaming changefeeds from CockroachDB to Snowflake using Redpanda Connect and Snowpipe in batch mode. Here, we will shift our focus to Kafka Connect and demonstrate how both batch and streaming modes can be utilized for data ingestion into Snowflake.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deploy a CockroachDB cluster with enterprise changefeeds&lt;/li&gt;
&lt;li&gt;Deploy Snowflake&lt;/li&gt;
&lt;li&gt;Deploy Kafka Connect&lt;/li&gt;
&lt;li&gt;Verify&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Detailed Instructions&lt;/h2&gt;
&lt;h2&gt;Deploy a CockroachDB Cluster with Enterprise Changefeeds&lt;/h2&gt;
&lt;p&gt;Start by either launching a CockroachDB instance or utilizing a managed service.&lt;/p&gt;
&lt;p&gt;To enable CDC, execute the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING cluster.organization &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;&amp;lt;organization name&amp;gt;&amp;#x27;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING enterprise.license &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;&amp;lt;secret&amp;gt;&amp;#x27;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING kv.rangefeed.enabled &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify that changefeeds are enabled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; CLUSTER SETTING kv.rangefeed.enabled;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the value is &lt;code&gt;false&lt;/code&gt;, update it to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Create a source table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; cockroachdb (
     id &lt;span class=&quot;hljs-type&quot;&gt;INT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;PRIMARY KEY&lt;/span&gt;,
     &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; STRING &lt;span class=&quot;hljs-keyword&quot;&gt;DEFAULT&lt;/span&gt; md5(random()::text),
     created_at TIMESTAMPTZ &lt;span class=&quot;hljs-keyword&quot;&gt;DEFAULT&lt;/span&gt; now(),
     updated_at TIMESTAMPTZ &lt;span class=&quot;hljs-keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;NULL&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Insert random data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; cockroachdb &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
   (generate_series(&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;10000&lt;/span&gt;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update a row:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;UPDATE&lt;/span&gt; cockroachdb &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;UPDATED&amp;#x27;&lt;/span&gt;, updated_at &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; now() &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a changefeed job pointing to a local instance of Redpanda:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; CHANGEFEED &lt;span class=&quot;hljs-keyword&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; cockroachdb &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;kafka://redpanda:29092&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inspect the data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; cockroachdb LIMIT &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;  id |              value               |          created_at           |          updated_at
-----+----------------------------------+-------------------------------+--------------------------------
   1 | UPDATED                          | 2024-09-09 13:17:57.837984+00 | 2024-09-09 13:17:57.917108+00
   2 | 27a41183599c44251506e2971ba78426 | 2024-09-09 13:17:57.837984+00 | NULL
   3 | 3bf8bc26a750a15691ec4d7ddbb7f5e5 | 2024-09-09 13:17:57.837984+00 | NULL
   4 | b8c5786e8651ddfb3a68eabeadb52f2e | 2024-09-09 13:17:57.837984+00 | NULL
   5 | 3a24df165773639ce89d0d877e7103b7 | 2024-09-09 13:17:57.837984+00 | NULL
(5 rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to set up the Snowflake Kafka connector.&lt;/p&gt;
&lt;h2&gt;Deploy Snowflake&lt;/h2&gt;
&lt;p&gt;Create a database and schema for outputting changefeed data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;USE ROLE SYSADMIN;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE DATABASE KAFKADB;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE SCHEMA kafka_schema;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Follow the Snowflake &lt;a href=&quot;https://docs.snowflake.com/en/user-guide/kafka-connector-install&quot;&gt;documentation&lt;/a&gt; to configure the Kafka connector.&lt;/p&gt;
&lt;p&gt;Create the necessary tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; kafkatb_batch(
    RECORD_METADATA VARIANT,
    RECORD_CONTENT VARIANT
);

&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; kafkatb_streaming(
    RECORD_METADATA VARIANT,
    RECORD_CONTENT VARIANT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set up roles and permissions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- Use a role that can create and manage roles and privileges.&lt;/span&gt;
USE ROLE securityadmin;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Create a Snowflake role with the privileges to work with the connector.&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE ROLE kafka_connector_role_1;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Grant privileges on the database.&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; USAGE &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; DATABASE kafkadb &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Grant privileges on the schema.&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; USAGE &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; SCHEMA KAFKADB.kafka_schema &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; SCHEMA KAFKADB.kafka_schema &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; STAGE &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; SCHEMA KAFKADB.kafka_schema &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; PIPE &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; SCHEMA KAFKADB.kafka_schema &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Only required if the Kafka connector will load data into an existing table.&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; OWNERSHIP &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; KAFKADB.KAFKA_SCHEMA.kafkatb_batch &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; OWNERSHIP &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; KAFKADB.KAFKA_SCHEMA.kafkatb_streaming &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Grant the custom role to an existing user.&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; ROLE kafka_connector_role_1 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; username;

&lt;span class=&quot;hljs-comment&quot;&gt;-- Set the custom role as the default role for the user.&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;-- If you encounter an &amp;#x27;Insufficient privileges&amp;#x27; error, verify the role that has the OWNERSHIP privilege on the user.&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; username &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; DEFAULT_ROLE &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; kafka_connector_role_1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensure you follow the documentation for setting up key pair authentication for the Snowflake Kafka connector.&lt;/p&gt;
&lt;h2&gt;Deploy Kafka Connect&lt;/h2&gt;
&lt;p&gt;Run Redpanda using &lt;a href=&quot;https://github.com/dbist/snowflake-docker/blob/main/compose-redpandadata.yaml&quot;&gt;Docker Compose&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker compose -f compose-redpandadata.yaml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once up, navigate to the &lt;a href=&quot;http://localhost:8080/overview&quot;&gt;Redpanda Console&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Click into the &lt;code&gt;cockroachdb&lt;/code&gt; topic and install the Snowflake Kafka connector:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;confluent-hub install --no-prompt snowflakeinc/snowflake-kafka-connector:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the following configuration for Kafka Connect in distributed mode, saved as &lt;code&gt;connect-distributed.properties&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-properties&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;bootstrap.servers&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;172.18.0.3:29092&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;group.id&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;connect-cluster&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;key.converter&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;org.apache.kafka.connect.json.JsonConverter&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;value.converter&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;org.apache.kafka.connect.json.JsonConverter&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;key.converter.schemas.enable&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;value.converter.schemas.enable&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;offset.storage.topic&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;connect-offsets&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;offset.storage.replication.factor&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;config.storage.topic&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;connect-configs&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;config.storage.replication.factor&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;status.storage.topic&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;connect-status&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;status.storage.replication.factor&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;offset.flush.interval.ms&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;10000&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;plugin.path&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;/usr/share/confluent-hub-components,plugin.path=/usr/local/share/kafka/plugins,/usr/share/filestream-connectors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deploy Kafka Connect in distributed mode:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;./kafka-connect/bin/connect-distributed.sh connect-distributed.properties
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the Snowflake connector with the following configuration, saved as &lt;code&gt;snowflake-sink-batch.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;snowflake-sink-batch&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;config&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;connector.class&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;com.snowflake.kafka.connector.SnowflakeSinkConnector&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;tasks.max&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;8&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;topics&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.topic2table.map&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb:kafkatb_batch&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.count.records&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;10000&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.flush.time&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;60&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.size.bytes&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;5000000&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.url.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;account-name:443&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.user.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;username&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.private.key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;private-key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.private.key.passphrase&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.database.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafkadb&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.schema.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafka_schema&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.role.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafka_connector_role_1&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;key.converter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;org.apache.kafka.connect.storage.StringConverter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value.converter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;com.snowflake.kafka.connector.records.SnowflakeJsonConverter&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publish the connector configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;curl -d @&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;snowflake-sink-batch.json&amp;quot;&lt;/span&gt; -H &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Content-Type: application/json&amp;quot;&lt;/span&gt; -X POST http://kafka-connect:8083/connectors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the connector in the Kafka &lt;a href=&quot;http://localhost:8083/connectors&quot;&gt;Connect UI&lt;/a&gt; and in the &lt;a href=&quot;http://localhost:8080/connect-clusters/kafka-connect&quot;&gt;Kafka Connect&lt;/a&gt; section of the Redpanda Console.&lt;/p&gt;
&lt;p&gt;The comprehensive steps needed to set this up are thoroughly outlined in the &lt;a href=&quot;https://quickstarts.snowflake.com/guide/a_comprehensive_guide_to_ingesting_data_into_snowflake/#10&quot;&gt;tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Data will now flow into Snowflake in batch mode, with updates occurring every 60 seconds as determined by the &lt;code&gt;buffer.flush.time&lt;/code&gt; parameter.&lt;/p&gt;
&lt;p&gt;You can now query the data in Snowflake:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; kafkatb_batch LIMIT &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything is configured correctly, the data from CockroachDB should be available in Snowflake in real time or in batches, depending on your configuration.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;record_metadata&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;CreateTime&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1725887877966&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;[3]&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;offset&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;30007&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;partition&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;topic&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb&amp;quot;&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;record_content&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;after&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;2024-09-09T13:17:57.837984Z&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;updated_at&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;2024-09-09T13:17:57.917108Z&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;UPDATED&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to configure the connector in streaming mode. First, stop the current connector with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;curl -X DELETE http://localhost:8083/connectors/snowflake-sink-batch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The updated connector configuration will appear as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;snowflake-sink-streaming&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;config&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;connector.class&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;com.snowflake.kafka.connector.SnowflakeSinkConnector&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;tasks.max&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;8&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;topics&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.topic2table.map&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb:kafkatb_streaming&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.count.records&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;10000&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.flush.time&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;10&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.size.bytes&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;5000000&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.url.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;lt;snowflake-account&amp;gt;:443&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.user.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;username&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.private.key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;private-key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.private.key.passphrase&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.database.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafkadb&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.schema.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafka_schema&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.role.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafka_connector_role_1&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;key.converter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;org.apache.kafka.connect.storage.StringConverter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value.converter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;org.apache.kafka.connect.json.JsonConverter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value.converter.schemas.enable&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;false&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.ingestion.method&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;SNOWPIPE_STREAMING&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;errors.log.enable&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;schemas.enable&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;false&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Take note of the &lt;code&gt;snowflake.ingestion.method&lt;/code&gt; parameter. This feature removes the need to wait 60 seconds to push data to Snowflake, allowing us to reduce the &lt;code&gt;buffer.flush.time&lt;/code&gt; to 10 seconds.&lt;/p&gt;
&lt;p&gt;To deploy the connector, use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;curl -d @&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;snowflake-sink-streaming.json&amp;quot;&lt;/span&gt; -H &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Content-Type: application/json&amp;quot;&lt;/span&gt; -X POST http://kafka-connect:8083/connectors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shortly after deployment, the data will be available in the Snowflake table.&lt;/p&gt;
&lt;p&gt;The previous examples demonstrated how data was ingested into predefined Snowflake tables. The following method will automatically infer the schema from the Kafka messages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;snowflake-sink-streaming-schematized&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;config&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;connector.class&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;com.snowflake.kafka.connector.SnowflakeSinkConnector&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;tasks.max&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;8&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;topics&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.topic2table.map&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;cockroachdb:kafkatb_streaming_schematized&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.count.records&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;10000&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.flush.time&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;10&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;buffer.size.bytes&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;5000000&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.url.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;lt;snowflake-account&amp;gt;:443&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.user.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;username&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.private.key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;private-key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.private.key.passphrase&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.database.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafkadb&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.schema.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafka_schema&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.role.name&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;kafka_connector_role_1&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;key.converter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;org.apache.kafka.connect.storage.StringConverter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value.converter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;org.apache.kafka.connect.json.JsonConverter&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value.converter.schemas.enable&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;false&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.ingestion.method&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;SNOWPIPE_STREAMING&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;errors.log.enable&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;schemas.enable&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;false&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;snowflake.enable.schematization&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;TRUE&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save this as &lt;code&gt;snowflake-sink-streaming-schematized.json&lt;/code&gt; and deploy it using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;curl -d @&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;snowflake-sink-streaming-schematized.json&amp;quot;&lt;/span&gt; -H &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Content-Type: application/json&amp;quot;&lt;/span&gt; -X POST http://kafka-connect:8083/connectors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon deployment, a new table will be created in Snowflake with the following schema:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; KAFKADB.KAFKA_SCHEMA.KAFKATB_STREAMING_SCHEMATIZED (
    RECORD_METADATA VARIANT COMMENT &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;created by automatic table creation from Snowflake Kafka Connector&amp;#x27;&lt;/span&gt;,
    AFTER VARIANT COMMENT &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;column created by schema evolution from Snowflake Kafka Connector&amp;#x27;&lt;/span&gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To inspect the table, use the following query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; after &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; record &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; kafkatb_streaming_schematized LIMIT &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sample result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;2024-09-09T16:39:34.993226Z&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;18712&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;updated_at&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;null&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;0d6bd8a4a790aab95c97a084d17bd820&amp;quot;&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Verify&lt;/h2&gt;
&lt;p&gt;We can flatten the data for easier manipulation using the following query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;USE ROLE securityadmin;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; SCHEMA KAFKADB.kafka_schema &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; ROLE kafka_connector_role_1;

USE ROLE kafka_connector_role_1;
USE DATABASE KAFKADB;
USE SCHEMA KAFKA_SCHEMA;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; v_kafkatb_batch_flattened &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; PARSE_JSON(record_content:after):id &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; ID,
        PARSE_JSON(record_content:after):&lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VALUE&lt;/span&gt;,
        PARSE_JSON(record_content:after):created_at &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; CREATED_AT,
        PARSE_JSON(record_content:after):updated_at &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; UPDATED_AT
    &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; kafkatb_batch;

&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; v_kafkatb_batch_flattened LIMIT &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;ID      VALUE       CREATED_AT                    UPDATED_AT
1       &amp;quot;UPDATED&amp;quot;   &amp;quot;2024-09-09T13:17:57.837984Z&amp;quot; &amp;quot;2024-09-09T13:17:57.917108Z&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, for the schematized table, the view creation statement would be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; v_kafkatb_streaming_schematized_flattened &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; PARSE_JSON(after):id &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; ID,
        PARSE_JSON(after):&lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VALUE&lt;/span&gt;,
        PARSE_JSON(after):created_at &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; CREATED_AT,
        PARSE_JSON(after):updated_at &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; UPDATED_AT
    &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; kafkatb_streaming_schematized;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To verify the data flow, make an update in CockroachDB and check for the changes in Snowflake:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;UPDATE&lt;/span&gt; cockroachdb
  &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;UPDATED&amp;#x27;&lt;/span&gt;, updated_at &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; now()
&lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt;
  id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;20000&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Snowflake, execute the following query to confirm the update:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; v_kafkatb_streaming_schematized_flattened &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VALUE&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;UPDATED&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sample result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;ID      VALUE       CREATED_AT                    UPDATED_AT
20000   &amp;quot;UPDATED&amp;quot;   &amp;quot;2024-09-09T18:15:13.460078Z&amp;quot; &amp;quot;2024-09-09T18:16:56.550778Z&amp;quot;
19999   &amp;quot;UPDATED&amp;quot;   &amp;quot;2024-09-09T18:15:13.460078Z&amp;quot; &amp;quot;2024-09-09T18:15:27.365272Z&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this process, we explored Kafka Connect as a solution to stream changefeeds into Snowflake. This approach provides greater control over how messages are delivered to Snowflake, leveraging the Snowflake Kafka Connector with Snowpipe Streaming for real-time, reliable data ingestion.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-integrate-existing-systems-with-kafka-connect&quot;&gt;How to Integrate Existing Systems with Kafka Connect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.redpanda.com/blog/stream-data-kafka-connect-snowflake&quot;&gt;Streaming data to Snowflake with Kafka Connect and Redpanda&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redpanda-data/redpanda-examples/blob/main/docker-compose/redpanda-one-node.yml&quot;&gt;Red Panda Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2024/09/exploring-real-time-data-ingestion-into.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-9J-_mIVhiS9Tx0ljDE1tqgPhqGckOfnJNMSU9hzE2HgghTdP4IieP17prjGsGBb7-B7OciISbyPUV6CWGknwjMbCdiTs-Gv-eRncnqzEt5oHaxWN3XCwuo698l9hsl7fJMeSNrdLwp9cWTrx80h6qKP8kIK4Jw-tCs1buMlnuVbFrJS1zdKttPf50fdC/s72-c/2024-08-15-snowflake-ingestion-cockroachdb-redpanda-connect-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-9111246582152228912</guid><pubDate>Thu, 15 Aug 2024 14:18:00 +0000</pubDate><atom:updated>2026-06-12T14:22:10.364-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cdc</category><category domain="http://www.blogger.com/atom/ns#">change data capture</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">kafka</category><category domain="http://www.blogger.com/atom/ns#">redpanda</category><category domain="http://www.blogger.com/atom/ns#">snowflake</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>Tour of Snowflake ingestion using CockroachDB and Redpanda Connect</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSr2Wc32oeHqkQlKAXMm_LWF7o9UsotiBgJZ3YeFqH567W4lkZbHnpe5WvuOYFvO5aH38MNbX6KwOWsjLzdO82LyOxzxN1P6SQJNUFlerlR4DwYxoNluSvggh9y0nXKNAZOzw4ampuUeiNcgOvTNMmnZuNNLuT8vnJmLmAzX8HXx0V31Jj5q0f-NAuudIx/s1774/2024-09-09-snowflake-cockroachdb-redpanda-kafka-connect-hero.png&quot; alt=&quot;Abstract changefeed pipeline from distributed database nodes through Redpanda Connect into a cloud warehouse&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is an exploratory article delving into various ways to ingest real-time data into Snowflake. The premise of the article is to examine the level of difficulty and effort required to ingest change data capture (CDC) data into Snowflake.&lt;/p&gt;
&lt;h2&gt;Previous Articles on Snowflake&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2024/03/integrating-trino-with-snowflake.html&quot;&gt;Integrating Snowflake with Trino&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Previous Articles on CockroachDB CDC&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2023/04/using-cockroachdb-cdc-with-apache-pulsar.html&quot;&gt;Using CockroachDB CDC with Apache Pulsar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/09/using-cockroachdb-cdc-with-azure-event.html&quot;&gt;Using CockroachDB CDC with Azure Event Hubs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/03/saas-galore-integrating-cockroachdb.html&quot;&gt;SaaS Galore: Integrating CockroachDB with Confluent Kafka, FiveTran and Snowflake&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/04/using-cockroachdb-cdc-with-confluent.html&quot;&gt;Using CockroachDB CDC with Confluent Cloud Kafka and Schema Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2020/01/cockroachdb-cdc-using-minio-as-cloud.html&quot;&gt;CockroachDB CDC using Minio as cloud storage sink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2020/01/cockroachdb-cdc-using-hadoop-ozone-s3.html&quot;&gt;CockroachDB CDC using Hadoop Ozone S3 Gateway as cloud storage sink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;I work with financial services clients, and it&#39;s common to encounter a need for streaming changes in the operational datastore into a data warehouse or a data lake. A former colleague recently reached out for advice on the fastest and most efficient way to load trade data into Snowflake. I&#39;ve come up with at least three methods, which I will explore in a follow-up series of articles. However, I&#39;ve decided to first explore &lt;a href=&quot;https://www.redpanda.com/connect&quot;&gt;Redpanda Connect&lt;/a&gt;, a solution that has recently caught my attention. This is by no means a conclusive guide on how changefeed data must be loaded into Snowflake; we&#39;re merely exploring the possibilities and discussing the pros and cons in later articles.&lt;/p&gt;
&lt;p&gt;CockroachDB changefeeds are an enterprise feature and require a license. In this tutorial, I&#39;m using a free-to-start version of &lt;a href=&quot;https://www.cockroachlabs.com/lp/serverless/&quot;&gt;CockroachDB Serverless&lt;/a&gt;, which has enterprise changefeeds enabled.&lt;/p&gt;
&lt;h2&gt;High Level Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deploy a CockroachDB cluster with enterprise changefeeds&lt;/li&gt;
&lt;li&gt;Deploy Redpanda Connect&lt;/li&gt;
&lt;li&gt;Deploy Snowflake&lt;/li&gt;
&lt;li&gt;Verify&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step by Step Instructions&lt;/h2&gt;
&lt;h2&gt;Deploy a CockroachDB Cluster with Enterprise Changefeeds&lt;/h2&gt;
&lt;p&gt;Start an instance of CockroachDB or use the managed service.&lt;/p&gt;
&lt;p&gt;To enable CDC we need to execute the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING cluster.organization &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;&amp;lt;organization name&amp;gt;&amp;#x27;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING enterprise.license &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;&amp;lt;secret&amp;gt;&amp;#x27;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING kv.rangefeed.enabled &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I am using CockroachDB Serverless and the above steps are not necessary. You may confirm whether the changefeeds are indeed enabled using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; CLUSTER SETTING kv.rangefeed.enabled;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the value is &lt;code&gt;false&lt;/code&gt;, change it to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Generate sample data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; office_dogs (
     id &lt;span class=&quot;hljs-type&quot;&gt;INT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;PRIMARY KEY&lt;/span&gt;,
     name STRING);

&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;VALUES&lt;/span&gt;
   (&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Petee&amp;#x27;&lt;/span&gt;),
   (&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Carl&amp;#x27;&lt;/span&gt;);

&lt;span class=&quot;hljs-keyword&quot;&gt;UPDATE&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Petee H&amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&#39;ve populated the table and then updated a record, let&#39;s add more data to make it interesting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; generate_series(&lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;10000&lt;/span&gt;), md5(random()::string);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs LIMIT &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;id,name
1,Petee H
2,Carl
3,6e19280ae649efffa7a58584c7f46032
4,5e4e897f008bb752c8edfa64a3aed356
5,abc0d898318d27f23a43060f89d62e34
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(&lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploy Redpanda Connect&lt;/h2&gt;
&lt;p&gt;I&#39;m running Redpanda Connect in a Docker Compose file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker compose -f compose-redpanda.yaml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The contents of the file are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;services:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;redpanda:&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;container_name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;redpanda-connect&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;hostname:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;redpanda-connect&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;image:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;docker.redpanda.com/redpandadata/connect&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;volumes:&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./redpanda/connect.yaml:/connect.yaml&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/Users/aervits/.ssh/rsa_key.pem:/rsa_key.pem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I will be using the &lt;code&gt;connect.yaml&lt;/code&gt; file as the foundation to connect all the components in this article. For more detailed information, you can refer to the &lt;a href=&quot;https://docs.redpanda.com/redpanda-connect/guides/getting_started/&quot;&gt;documentation&lt;/a&gt; provided by Redpanda.&lt;/p&gt;
&lt;p&gt;The most basic configuration looks like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;input:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;stdin:&lt;/span&gt; {}

&lt;span class=&quot;hljs-attr&quot;&gt;pipeline:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;processors:&lt;/span&gt; []

&lt;span class=&quot;hljs-attr&quot;&gt;output:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;stdout:&lt;/span&gt; {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since I&#39;m using &lt;a href=&quot;https://docs.redpanda.com/redpanda-connect/components/inputs/cockroachdb_changefeed/#tabs-1-advanced&quot;&gt;CockroachDB&lt;/a&gt; input, mine looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;input:&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;# CockroachDB Input&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;label:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;cockroachdb_changefeed:&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;dsn:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;postgresql://&amp;lt;user&amp;gt;:&amp;lt;password&amp;gt;@&amp;lt;cockroachdb-cluster&amp;gt;:&amp;lt;port&amp;gt;/&amp;lt;database&amp;gt;?sslmode=verify-full&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;tls:&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;skip_cert_verify:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;hljs-comment&quot;&gt;#enable_renegotiation: false&lt;/span&gt;
      &lt;span class=&quot;hljs-comment&quot;&gt;#root_cas: &amp;quot;&amp;quot;&lt;/span&gt;
      &lt;span class=&quot;hljs-comment&quot;&gt;#root_cas_file: &amp;quot;&amp;quot;&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;client_certs:&lt;/span&gt; []
    &lt;span class=&quot;hljs-attr&quot;&gt;tables:&lt;/span&gt; [&lt;span class=&quot;hljs-string&quot;&gt;table_for_cdc&lt;/span&gt;] &lt;span class=&quot;hljs-comment&quot;&gt;# No default (required)&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;cursor_cache:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;# No default (optional)&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;auto_replay_nacks:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;hljs-attr&quot;&gt;pipeline:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;processors:&lt;/span&gt; []

&lt;span class=&quot;hljs-attr&quot;&gt;output:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;stdout:&lt;/span&gt; {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Leave the pipeline and output as default.&lt;/p&gt;
&lt;p&gt;For reference, I&#39;m including the &lt;a href=&quot;https://github.com/dbist/snowflake-docker&quot;&gt;repo&lt;/a&gt; with my source code where you can reference the values.&lt;/p&gt;
&lt;p&gt;If you have been following along, you may have noticed that I haven&#39;t started a changefeed job in CockroachDB. The &lt;code&gt;cockroachdb_changefeed&lt;/code&gt; input directly subscribes to the table, which can be observed by examining the logs using the command &lt;code&gt;docker logs redpanda-connect --follow&lt;/code&gt;. If you look at the &lt;code&gt;connect.yaml&lt;/code&gt; file, the output is sent to &lt;code&gt;stdout&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;{&amp;quot;primary_key&amp;quot;:&amp;quot;[9998]&amp;quot;,&amp;quot;row&amp;quot;:&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 9998, \&amp;quot;name\&amp;quot;: \&amp;quot;0794a9d1c99e8e47ee4515be6e0d736f\&amp;quot;}}&amp;quot;,&amp;quot;table&amp;quot;:&amp;quot;office_dogs&amp;quot;}
{&amp;quot;primary_key&amp;quot;:&amp;quot;[9999]&amp;quot;,&amp;quot;row&amp;quot;:&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 9999, \&amp;quot;name\&amp;quot;: \&amp;quot;c85a6b38154f7e3085d467d567141d45\&amp;quot;}}&amp;quot;,&amp;quot;table&amp;quot;:&amp;quot;office_dogs&amp;quot;}
{&amp;quot;primary_key&amp;quot;:&amp;quot;[10000]&amp;quot;,&amp;quot;row&amp;quot;:&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 10000, \&amp;quot;name\&amp;quot;: \&amp;quot;aae9e0849fff8f47e0371a4c06fb255b\&amp;quot;}}&amp;quot;,&amp;quot;table&amp;quot;:&amp;quot;office_dogs&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to configure Snowflake. We are not going to look at the available processors today.&lt;/p&gt;
&lt;h2&gt;Deploy Snowflake&lt;/h2&gt;
&lt;p&gt;I&#39;m using a Snowflake &lt;a href=&quot;https://signup.snowflake.com/&quot;&gt;trial account&lt;/a&gt;. You get a generous credit which should be sufficient to complete this tutorial.&lt;/p&gt;
&lt;p&gt;We need to create a database and a table where we will output the changefeed data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE DATABASE FROM_COCKROACH;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; OFFICE_DOGS (RECORD variant);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to create a user with &lt;a href=&quot;https://docs.snowflake.com/en/user-guide/key-pair-auth&quot;&gt;key-pair&lt;/a&gt; authentication as we&#39;re going to be using the Snowpipe service.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;openssl genrsa 2048 | openssl pkcs8 -topk8 -v2 des3 -inform PEM -out rsa_key.p8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We must use an encrypted key as Redpanda doesn&#39;t support unencrypted version.&lt;/p&gt;
&lt;p&gt;Generate a public key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;openssl rsa -&lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; rsa_key.p8 -pubout -out rsa_key.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly, generate a PEM file from the private key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;openssl pkcs8 -&lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; rsa_key.p8 -out rsa_key.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Snowflake, alter the user to use the key pair generated in the previous step.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; username &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; rsa_public_key&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;MIIB...&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now populate the &lt;code&gt;connect.yaml&lt;/code&gt; file with the required information for the &lt;code&gt;snowflake_put&lt;/code&gt; &lt;a href=&quot;https://docs.redpanda.com/redpanda-connect/components/outputs/snowflake_put/#snowpipe&quot;&gt;output&lt;/a&gt;. This output type is for commercial use and requires a license, but since we&#39;re using it for demo purposes, we are able to proceed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;output:&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;# Snowflake Output&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;label:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;snowflake_put:&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;account:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;snowflake-account&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;user:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;user&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;private_key_file:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;rsa_key.pem&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;role:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ACCOUNTADMIN&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;database:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;database&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;warehouse:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;warehouse&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;schema:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;schema&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;stage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;@%implicit_table_stage_name&amp;quot;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;path:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;path&amp;quot;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;upload_parallel_threads:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;compression:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;NONE&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;batching:&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;count:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;period:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;3s&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;processors:&lt;/span&gt;
        &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;archive:&lt;/span&gt;
            &lt;span class=&quot;hljs-attr&quot;&gt;format:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;json_array&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;max_in_flight:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we restart the Compose environment and tail the logs, we can see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;level=info msg=&amp;quot;Running main config from specified file&amp;quot; @service=benthos benthos_version=v4.32.1 path=/connect.yaml
level=info msg=&amp;quot;Listening for HTTP requests at: http://0.0.0.0:4195&amp;quot; @service=benthos
level=info msg=&amp;quot;Launching a Redpanda Connect instance, use CTRL+C to close&amp;quot; @service=benthos
level=info msg=&amp;quot;Output type snowflake_put is now active&amp;quot; @service=benthos label=&amp;quot;&amp;quot; path=root.output
level=info msg=&amp;quot;Input type cockroachdb_changefeed is now active&amp;quot; @service=benthos label=&amp;quot;&amp;quot; path=root.input
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&#39;s look at the implicit table stage and observe if anything changed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;LIST @&lt;span class=&quot;hljs-operator&quot;&gt;%&lt;/span&gt;office_dogs;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;| dogs/f2f3cf47-d6bc-46f4-88f2-c82519b67481.json | 1312 | 30f709e4962bae9d10b48565d22e9f32 | Wed, 14 Aug 2024 18:58:43 GMT |
| dogs/f6adbf39-3955-4848-93c3-06f873a88078.json | 1312 | 28be7a619ef1e139599077e977ea130b | Wed, 14 Aug 2024 18:58:13 GMT |
| dogs/f8705606-eb07-400a-9ffe-da6834fa1a30.json | 1296 | 5afbdce0e8929fc38a2eb5e0f12b96d6 | Wed, 14 Aug 2024 18:57:29 GMT |
| dogs/f9e5c01a-7dda-4e76-840d-13b8a1e4946a.json | 1296 | 5480c01f1578f67afe2761c7619e9123 | Wed, 14 Aug 2024 18:57:32 GMT |
| dogs/fad4efe7-3f3f-48bc-bdb4-9f0310abcf4d.json | 1312 | 5942c6e2dbaef5ee257d4a9b8e68827d | Wed, 14 Aug 2024 18:58:04 GMT |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The files are ready to be copied into a table. Let&#39;s create a pipe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE PIPE FROM_COCKROACH.PUBLIC.cockroach_pipe AUTO_INGEST &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;FALSE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; FROM_COCKROACH.PUBLIC.OFFICE_DOGS &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; @&lt;span class=&quot;hljs-operator&quot;&gt;%&lt;/span&gt;office_dogs) FILE_FORMAT &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; (TYPE &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; JSON COMPRESSION &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; AUTO STRIP_OUTER_ARRAY &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;TRUE&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last remaining step is to refresh the pipe.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; PIPE cockroach_pipe REFRESH;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;| dogs/ff0871b1-6f49-43a4-a929-958d07f74046.json | SENT   |
| dogs/ff131d8d-3781-4cf6-8700-edd50dbb87de.json | SENT   |
| dogs/ff216da1-4f9d-4b37-9776-bcd559dd4a6f.json | SENT   |
| dogs/ff221430-4c3a-46be-bbc2-d335cc6cc9e3.json | SENT   |
| dogs/ffbd7d45-5084-4e36-8907-61874ac652b4.json | SENT   |
| dogs/fffb5fa6-23cc-4450-934a-29ccf01c67b9.json | SENT   |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&#39;s query the table in Snowflake:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; OFFICE_DOGS LIMIT &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;primary_key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;[5241]&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;row&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 5241, \&amp;quot;name\&amp;quot;: \&amp;quot;5e0360a0d10d849afbbfa319a50bccf2\&amp;quot;}}&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;table&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;office_dogs&amp;quot;&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;primary_key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;[5242]&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;row&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 5242, \&amp;quot;name\&amp;quot;: \&amp;quot;62be250249afe74bfbc5dd356e7b0ad9\&amp;quot;}}&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;table&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;office_dogs&amp;quot;&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;primary_key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;[5243]&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;row&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 5243, \&amp;quot;name\&amp;quot;: \&amp;quot;7f286800a8a03e74938d09fdba52f869\&amp;quot;}}&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;table&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;office_dogs&amp;quot;&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;primary_key&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;[5244]&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;row&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;{\&amp;quot;after\&amp;quot;: {\&amp;quot;id\&amp;quot;: 5244, \&amp;quot;name\&amp;quot;: \&amp;quot;16a330b8f09bcd314f9760ffe26d0ae2\&amp;quot;}}&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&amp;quot;table&amp;quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;office_dogs&amp;quot;&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We expect 10,000 rows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(&lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; OFFICE_DOGS;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;+----------+
| COUNT(*) |
|----------|
|    10000 |
+----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The data is in JSON format. Let&#39;s create a view and flatten the data out.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; v_office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; PARSE_JSON(record:&lt;span class=&quot;hljs-type&quot;&gt;row&lt;/span&gt;):after:id::&lt;span class=&quot;hljs-type&quot;&gt;INTEGER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; id,
           PARSE_JSON(record:&lt;span class=&quot;hljs-type&quot;&gt;row&lt;/span&gt;):after:name::STRING &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; name &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; OFFICE_DOGS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Query the view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; v_office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;hljs-operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;+----+----------------------------------+
| ID | NAME                             |
|----+----------------------------------|
|  1 | Petee H                          |
|  2 | Carl                             |
|  3 | 6e19280ae649efffa7a58584c7f46032 |
|  4 | 5e4e897f008bb752c8edfa64a3aed356 |
|  5 | abc0d898318d27f23a43060f89d62e34 |
+----+----------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Verify&lt;/h2&gt;
&lt;p&gt;Let&#39;s make things a bit more interesting and delete data in CockroachDB.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Carl&amp;#x27;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Snowflake, let&#39;s refresh the pipe as of a few minutes ago:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; PIPE cockroach_pipe REFRESH MODIFIED_AFTER&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;2024-08-14T12:10:00-07:00&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice there are a couple of files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;+------------------------------------------------+--------+
| File                                           | Status |
|------------------------------------------------+--------|
| dogs/2a4ee400-6b37-4513-97cb-097764a340bc.json | SENT   |
| dogs/8f5b5b69-8a00-4dbf-979a-60c3814d96b4.json | SENT   |
+------------------------------------------------+--------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I must caution that if you run the &lt;code&gt;REFRESH&lt;/code&gt; manually, you may cause duplicates in your Snowflake data. We will look at better approaches in a future article.&lt;/p&gt;
&lt;p&gt;Let&#39;s look at the row count:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;+----------+
| COUNT(*) |
|----------|
|    10002 |
+----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The removal process didn&#39;t properly update in Snowflake as anticipated; it recognized the deleted records but failed to mirror the state in CockroachDB. We need to incorporate additional logic to achieve this. This will be a task for another time.&lt;/p&gt;
&lt;p&gt;Lastly, I would like to note that using Redpanda Connect as a Compose file is optional. You have the option to run the Docker container by executing the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker run --&lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt; -it -v ./redpanda/connect.yaml:/connect.yaml -v ./snowflake/rsa_key.pem:/rsa_key.pem docker.redpanda.com/redpandadata/connect run
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Today, we explored Redpanda Connect as a means to deliver streaming changefeeds into Snowflake. We&#39;ve only just begun to delve into this topic, and future articles will build upon the foundations laid today.&lt;/p&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2024/08/tour-of-snowflake-ingestion-using.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSr2Wc32oeHqkQlKAXMm_LWF7o9UsotiBgJZ3YeFqH567W4lkZbHnpe5WvuOYFvO5aH38MNbX6KwOWsjLzdO82LyOxzxN1P6SQJNUFlerlR4DwYxoNluSvggh9y0nXKNAZOzw4ampuUeiNcgOvTNMmnZuNNLuT8vnJmLmAzX8HXx0V31Jj5q0f-NAuudIx/s72-c/2024-09-09-snowflake-cockroachdb-redpanda-kafka-connect-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-5455907783244858525</guid><pubDate>Fri, 15 Mar 2024 19:16:00 +0000</pubDate><atom:updated>2026-06-12T14:23:15.404-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">Azure</category><category domain="http://www.blogger.com/atom/ns#">cloud</category><category domain="http://www.blogger.com/atom/ns#">data federation</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">query</category><category domain="http://www.blogger.com/atom/ns#">snowflake</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><category domain="http://www.blogger.com/atom/ns#">starburst</category><category domain="http://www.blogger.com/atom/ns#">trino</category><title>Integrating Snowflake with Trino</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFOF5nZedFslEJT-w93tc1BmLPsdLaIk8sL6OZXpmxOro6kEi-7r_WzihDAp9ntF4fOPHFxxzh49liQytsMamSfvLAuiMQGzNyfMsAI385TUo1qOSMmDUThhV5wnaK_FNlBlwgm2fMqOVf86IvM21QjZb9rBPrhP8lDMOBRIudGDFux9xcO_ZZLL_PzQ1J/s1693/2024-03-15-integrating-snowflake-with-trino-hero.png&quot; alt=&quot;Abstract cloud data warehouse connected to federated query engine nodes&quot;&gt;&lt;/p&gt;
&lt;p&gt;In today&#39;s discourse, we delve into the intricacies of accessing &lt;a href=&quot;https://www.snowflake.com/en/&quot;&gt;Snowflake&lt;/a&gt; via the &lt;a href=&quot;https://trino.io/&quot;&gt;Trino&lt;/a&gt; project. This article illuminates the seamless integration of Trino with Snowflake, offering a comprehensive analysis of its benefits and implications.&lt;/p&gt;
&lt;h2&gt;Previous Articles&lt;/h2&gt;
&lt;p&gt;Previous articles on Snowflake and Trino:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/12/saas-galore-integrating-cockroachdb.html&quot;&gt;SaaS Galore: Integrating CockroachDB with Confluent Kafka, FiveTran and Snowflake&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/10/data-federation-with-cockroachdb-and.html&quot;&gt;Data federation with CockroachDB and Presto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;A common query among potential adopters of Snowflake pertains to its compatibility with on-premise data and cloud platforms like Azure. In this article, we address this question head-on, exploring the feasibility of accessing Snowflake alongside on-premise data through the Trino project. Let&#39;s unravel the possibilities together.&lt;/p&gt;
&lt;h2&gt;High Level Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deploy Trino in Docker&lt;/li&gt;
&lt;li&gt;Get a trial Snowflake account&lt;/li&gt;
&lt;li&gt;Connect the dots&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step by Step Instructions&lt;/h2&gt;
&lt;p&gt;Navigating the landscape of data integration can be daunting, especially when considering the compatibility of Snowflake with on-premise environments. In this tutorial, we aim to simplify the process by utilizing a Docker environment to simulate on-premise conditions. Our approach prioritizes simplicity, leveraging standard Snowflake configurations and a basic Trino Docker setup. It&#39;s essential to consult your documentation for specific scenarios, but let&#39;s begin with the fundamentals.&lt;/p&gt;
&lt;h3&gt;Deploy Trino in Docker&lt;/h3&gt;
&lt;p&gt;I have a compose file called &lt;code&gt;compose-trino.yaml&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;services:&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;trino:&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;container_name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;trino&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;hostname:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;trino&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;build:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;trino/.&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;ports:&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;8080:8080&amp;quot;&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;environment:&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;_JAVA_OPTIONS=-Dfile.encoding=UTF-8&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;volumes:&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./trino/catalog:/etc/trino/catalog&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./trino/etc:/etc/trino&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the current directory, I have a folder called &lt;code&gt;trino&lt;/code&gt;. Within the folder, I have the following files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-dockerfile&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; trinodb/trino:&lt;span class=&quot;hljs-number&quot;&gt;442&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;LABEL&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; version=&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;1.0&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;LABEL&lt;/span&gt;&lt;span class=&quot;language-bash&quot;&gt; description=&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;trino container&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;ENV&lt;/span&gt; REFRESHED_AT &lt;span class=&quot;hljs-number&quot;&gt;2024&lt;/span&gt;_03_15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also have two more folders called &lt;code&gt;etc&lt;/code&gt; and &lt;code&gt;catalog&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Within the &lt;code&gt;catalog&lt;/code&gt; directory, I&#39;ve setup a &lt;code&gt;snowflake.properties&lt;/code&gt; file with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-properties&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;connector.name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;snowflake&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;connection-url&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;jdbc:snowflake://&amp;lt;account&amp;gt;.snowflakecomputing.com&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;connection-user&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;root&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;connection-password&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;secret&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.account&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;account&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.database&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;database&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.role&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;role&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.warehouse&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;warehouse&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you encounter any hurdles along the way, don&#39;t hesitate to refer to the comprehensive &lt;a href=&quot;https://trino.io/docs/current/connector/snowflake.html&quot;&gt;Trino documentation&lt;/a&gt;. Let&#39;s dive in!&lt;/p&gt;
&lt;p&gt;Once you set up the Snowflake environment, you can adjust these properties with your values.&lt;/p&gt;
&lt;p&gt;Within the &lt;code&gt;etc&lt;/code&gt; directory, I have a &lt;code&gt;jvm.config&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;--add-opens=java.base/java.nio=ALL-UNNAMED
-Djdk.module.illegalAccess=permit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These particular JDK flags are Snowflake specific.&lt;/p&gt;
&lt;p&gt;I also have &lt;code&gt;config.properties&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-properties&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;coordinator&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;node-scheduler.include-coordinator&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;http-server.http.port&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;8080&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;discovery.uri&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;http://example.net:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally, &lt;code&gt;node.properties&lt;/code&gt; with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-properties&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;node.environment&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;production&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;node.id&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;ffffffff-ffff-ffff-ffff-ffffffffffff&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;node.data-dir&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;/tmp/trino/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With everything in place, you&#39;re now ready to initiate the Compose environment. Execute the following command to start the environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker compose -f compose-trino.yaml up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon successful configuration, you should observe a running container named &lt;code&gt;trino&lt;/code&gt;. You can confirm this by executing the command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;f426506aa443   snowflake-docker-trino   &amp;quot;/usr/lib/trino/bin/...&amp;quot;   53 minutes ago   Up 47 minutes (healthy)   0.0.0.0:8080-&amp;gt;8080/tcp   trino
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you encounter any issues, you can further troubleshoot by examining the Trino logs using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker logs trino
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can access the Trino container with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it trino trino
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once logged in, you can verify the correct configuration of the Snowflake catalog by executing the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;show&lt;/span&gt; catalogs;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;  Catalog
-----------
 snowflake
 system
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Get a Trial Snowflake Account&lt;/h3&gt;
&lt;p&gt;For the next phase of this tutorial, kindly proceed to sign up for a &lt;a href=&quot;https://signup.snowflake.com/&quot;&gt;Snowflake Trial Account&lt;/a&gt; through the provided link. Opt for the standard edition since we won&#39;t be utilizing enterprise features. During the signup process, I&#39;ve selected the Azure eastus2 region for my Snowflake deployment.&lt;/p&gt;
&lt;p&gt;Upon completing the signup, you&#39;ll receive a verification email. Once verified, you&#39;ll gain access to your Snowflake environment. Retrieve the necessary details from the email sent by Snowflake, particularly the credentials, and populate the &lt;code&gt;snowflake.properties&lt;/code&gt; file located in the &lt;code&gt;trino/catalog&lt;/code&gt; directory.&lt;/p&gt;
&lt;h3&gt;Connect the Dots&lt;/h3&gt;
&lt;p&gt;Snowflake provides a variety of demo tutorials, including the Tasty Bytes series. For this tutorial, we&#39;ll focus on the &amp;quot;Load sample data with SQL from S3 bucket&amp;quot; worksheet. Alternatively, feel free to select a dataset of your preference.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; set the Role&lt;/span&gt;
USE ROLE accountadmin;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; set the Warehouse&lt;/span&gt;
USE WAREHOUSE compute_wh;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; create the Tasty Bytes Database&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE DATABASE tasty_bytes_sample_data;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; create the Raw POS (Point-of-Sale) Schema&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE SCHEMA tasty_bytes_sample_data.raw_pos;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; create the Raw Menu Table&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu
(
    menu_id NUMBER(&lt;span class=&quot;hljs-number&quot;&gt;19&lt;/span&gt;,&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;),
    menu_type_id NUMBER(&lt;span class=&quot;hljs-number&quot;&gt;38&lt;/span&gt;,&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;),
    menu_type &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;16777216&lt;/span&gt;),
    truck_brand_name &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;16777216&lt;/span&gt;),
    menu_item_id NUMBER(&lt;span class=&quot;hljs-number&quot;&gt;38&lt;/span&gt;,&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;),
    menu_item_name &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;16777216&lt;/span&gt;),
    item_category &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;16777216&lt;/span&gt;),
    item_subcategory &lt;span class=&quot;hljs-type&quot;&gt;VARCHAR&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;16777216&lt;/span&gt;),
    cost_of_goods_usd NUMBER(&lt;span class=&quot;hljs-number&quot;&gt;38&lt;/span&gt;,&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;),
    sale_price_usd NUMBER(&lt;span class=&quot;hljs-number&quot;&gt;38&lt;/span&gt;,&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;),
    menu_item_health_metrics_obj VARIANT
);

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; confirm the empty Menu table exists&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; create the Stage referencing the Blob location and CSV File Format&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;OR&lt;/span&gt; REPLACE STAGE tasty_bytes_sample_data.public.blob_stage
url &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;s3://sfquickstarts/tastybytes/&amp;#x27;&lt;/span&gt;
file_format &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; (type &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; csv);

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; query the Stage to find the Menu CSV file&lt;/span&gt;
LIST &lt;span class=&quot;hljs-variable&quot;&gt;@tasty_bytes_sample_data&lt;/span&gt;.public.blob_stage&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;raw_pos&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;menu&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; copy the Menu file into the Menu table&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;COPY&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;hljs-variable&quot;&gt;@tasty_bytes_sample_data&lt;/span&gt;.public.blob_stage&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;raw_pos&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;menu&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; how many rows are in the table?&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(&lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; row_count &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; what do the top 10 rows look like?&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; TOP &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; what menu items does the Freezing Point brand sell?&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
   menu_item_name
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu
&lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; truck_brand_name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Freezing Point&amp;#x27;&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; what is the profit on Mango Sticky Rice?&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
   menu_item_name,
   (sale_price_usd &lt;span class=&quot;hljs-operator&quot;&gt;-&lt;/span&gt; cost_of_goods_usd) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; profit_usd
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu
&lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; truck_brand_name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Freezing Point&amp;#x27;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; menu_item_name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Mango Sticky Rice&amp;#x27;&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;---&amp;gt; to finish, let&amp;#x27;s extract the Mango Sticky Rice ingredients from the semi-structured column&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
    m.menu_item_name,
    obj.value:&amp;quot;ingredients&amp;quot;::&lt;span class=&quot;hljs-keyword&quot;&gt;ARRAY&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; ingredients
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; tasty_bytes_sample_data.raw_pos.menu m,
    &lt;span class=&quot;hljs-keyword&quot;&gt;LATERAL&lt;/span&gt; FLATTEN (input &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;&amp;gt;&lt;/span&gt; m.menu_item_health_metrics_obj:menu_item_health_metrics) obj
&lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; truck_brand_name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Freezing Point&amp;#x27;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;AND&lt;/span&gt; menu_item_name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Mango Sticky Rice&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have a dataset in Snowflake, let&#39;s now pivot back to Trino and access the Snowflake data from there.&lt;/p&gt;
&lt;p&gt;If your Compose environment is currently active but lacks essential configurations such as &lt;code&gt;snowflake.database&lt;/code&gt;, &lt;code&gt;snowflake.warehouse&lt;/code&gt;, or other pertinent Snowflake properties, it&#39;s crucial to halt the environment. Prior to proceeding, ensure these properties are appropriately configured. Once adjusted, you can restart the Compose environment and continue with the integration process seamlessly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sh&quot;&gt;docker compose -f compose-trino.yaml down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Back in the &lt;code&gt;snowflake.properties&lt;/code&gt; file, change the properties to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-properties&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;connection-user&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;snowflakeuser&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;connection-password&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;snowflakepassword&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.database&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;tasty_bytes_sample_data&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.role&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;accountadmin&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot;&gt;snowflake.warehouse&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;compute_wh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the environment and access the Trino shell.&lt;/p&gt;
&lt;p&gt;Within the Trino shell, type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;use snowflake.raw_pos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the Snowflake catalog is already configured to connect to our Trino environment, we can omit the database name from the fully qualified table name. Pick any of the above queries from the Snowflake worksheet and try running them in the Trino container.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;COUNT&lt;/span&gt;(&lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; row_count
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; raw_pos.menu;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt; row_count
-----------
       100
(1 row)

Query 20240315_185131_00013_45g27, FINISHED, 1 node
Splits: 1 total, 1 done (100.00%)
0.84 [1 rows, 0B] [1 rows/s, 0B/s]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;
   menu_item_name
&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; raw_pos.menu
&lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; truck_brand_name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Freezing Point&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot;&gt;   menu_item_name
--------------------
 Lemonade
 Sugar Cone
 Waffle Cone
 Two Scoop Bowl
 Bottled Water
 Bottled Soda
 Ice Tea
 Ice Cream Sandwich
 Mango Sticky Rice
 Popsicle
(10 rows)

Query 20240315_185212_00015_45g27, FINISHED, 1 node
Splits: 1 total, 1 done (100.00%)
1.23 [10 rows, 0B] [8 rows/s, 0B/s]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Indeed, accessing Snowflake datasets using Trino from our local environment demonstrates the flexibility and interoperability of these tools. This integration allows us to seamlessly work with data across different platforms, enhancing our analytical capabilities and workflow efficiency.&lt;/p&gt;
&lt;p&gt;Additionally, you can access the Trino UI via &lt;a href=&quot;http://localhost:8080&quot;&gt;http://localhost:8080&lt;/a&gt;. With the default configuration, no password is required, and the username is set to &lt;code&gt;admin&lt;/code&gt;. By navigating to the &amp;quot;finished queries&amp;quot; section, you can review the queries you&#39;ve executed, providing valuable insights into your workflow and facilitating debugging if needed. This feature enhances visibility and transparency into your data operations within the Trino environment.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Trino and its commercial version, &lt;a href=&quot;https://www.starburst.io/&quot;&gt;Starburst&lt;/a&gt;, are potent tools for federating data across different sources. This article showcases how easily Snowflake can be accessed using local tools with Trino. The synergy between Snowflake and Trino offers a robust solution for data management and analytics, empowering organizations to leverage cloud data warehousing and distributed query processing for enhanced insights.&lt;/p&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2024/03/integrating-trino-with-snowflake.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFOF5nZedFslEJT-w93tc1BmLPsdLaIk8sL6OZXpmxOro6kEi-7r_WzihDAp9ntF4fOPHFxxzh49liQytsMamSfvLAuiMQGzNyfMsAI385TUo1qOSMmDUThhV5wnaK_FNlBlwgm2fMqOVf86IvM21QjZb9rBPrhP8lDMOBRIudGDFux9xcO_ZZLL_PzQ1J/s72-c/2024-03-15-integrating-snowflake-with-trino-hero.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-5627352526594780983</guid><pubDate>Thu, 30 Nov 2023 20:53:00 +0000</pubDate><atom:updated>2026-06-12T15:23:24.121-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">benchmark</category><category domain="http://www.blogger.com/atom/ns#">cockroach labs</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">performance</category><category domain="http://www.blogger.com/atom/ns#">pgbench</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">psql</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>Optimizing pgbench for CockroachDB Part 3</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuBWvIj9eZG9zIIt570S1BQTUmi7MCLGC83DsGc3gyxnrIqAKxt54FTk-Ll3Iksvl-BtLSPv4SqiG2yzabI4b2MIfuUS0gZplj9ZQa9nHjAGgMwQIc2Adgqyle23bHRf3Jb9YE1xax4Vf_krUZEfqpdNAaYZyBjGbuPLRWKRblz44eonepuQZQyahUyyX4/s1200/optimizing-pgbench-for-cockroachdb-part-3-hero.jpg&quot; alt=&quot;Optimizing pgbench for CockroachDB Part 3&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This is a long anticipated third article on optimizing &lt;a href=&quot;https://www.postgresql.org/docs/current/pgbench.html&quot;&gt;pgbench&lt;/a&gt; for CockroachDB. I&#39;ve been wanting to write a follow up ever since I learned about new improvements in the next release of CockroachDB.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Previous articles&lt;/h2&gt;
&lt;p&gt;Previous articles on PgBench and CockoachDB:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2021/10/using-pgbench-with-cockroachdb.html&quot;&gt;Using PgBench with CockroachDB Serverless&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/12/optimizing-pgbench-for-cockroachdb-part.html&quot;&gt;Optimizing PgBench for CockroachDB Part 1&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/12/optimizing-pgbench-for-cockroachdb-part_22.html&quot;&gt;Optimizing PgBench for CockroachDB Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;For the longest time CockroachDB only supported &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/transactions#serializable-isolation&quot;&gt;serializable isolation&lt;/a&gt;. In our next release after 23.1, CockroachDB will additionally support &lt;a href=&quot;https://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Read_committed&quot;&gt;read committed isolation&lt;/a&gt;. The idea here is that users coming from other RDBMS tend to face many challenges with application migrations due to the excessive contention stemming from serializable isolation. To ease the migration woes, read committed isolation will provide an easy on-ramp for those applications. Since read committed isolation is less strict, it has additional benefits of being more performant at the cost of data anomalies. Today, we&#39;re going to take everything we&#39;ve learned so far in my series and apply to read committed isolation.&lt;/p&gt;
&lt;h2&gt;High Level Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deploy a CockroachDB cluster&lt;/li&gt;
&lt;li&gt;Capture the baseline using serializable isolation&lt;/li&gt;
&lt;li&gt;Compare to read committed isolation&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step by step instructions&lt;/h3&gt;
&lt;h4&gt;Deploy a CockroachDB cluster&lt;/h4&gt;
&lt;p&gt;I&#39;ve designed this tutorial to be reproduceable, in fact I have a Docker &lt;a href=&quot;https://github.com/dbist/cockroach-docker/tree/main/postgresql&quot;&gt;environment&lt;/a&gt; you may use to try or use our &lt;a href=&quot;https://www.cockroachlabs.com/lp/serverless/&quot;&gt;serverless&lt;/a&gt; tier, the same way as I did in the original article.&lt;/p&gt;
&lt;h4&gt;Capture the baseline using serializable isolation&lt;/h4&gt;
&lt;p&gt;Let&#39;s create a user we are going to use to execute PgBench as this will allow us to assign session properties controlling the isolation level.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; foo &lt;span class=&quot;hljs-keyword&quot;&gt;WITH&lt;/span&gt; CREATEDB;
&lt;span class=&quot;hljs-keyword&quot;&gt;GRANT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;ALL&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;ON&lt;/span&gt; DATABASE example &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; foo;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initialize the benchmark&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql pgbench \
    --initialize \
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --no-vacuum \
    --scale=10 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;dropping old tables...
creating tables...
NOTICE:  storage parameter &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;fillfactor&amp;quot;&lt;/span&gt; is ignored
NOTICE:  storage parameter &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;fillfactor&amp;quot;&lt;/span&gt; is ignored
NOTICE:  storage parameter &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;fillfactor&amp;quot;&lt;/span&gt; is ignored
generating data (client-side)...
1000000 of 1000000 tuples (100%) &lt;span class=&quot;hljs-keyword&quot;&gt;done&lt;/span&gt; (elapsed 1.87 s, remaining 0.00 s)
creating primary keys...
&lt;span class=&quot;hljs-keyword&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; 32.49 s (drop tables 0.01 s, create tables 0.09 s, client-side generate 5.11 s, primary keys 27.28 s).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some readers will notice that I am not initializing the benchmark with foreign keys, i.e. &lt;code&gt;--foreign-keys&lt;/code&gt;. Though nothing prevents me from using the foreign keys constraints, since read committed is still work in progress, it does not currently demonstrate the performance benefits compared to serializable. In fact, my previous attempts testing read committed had shown serializable was more performant than the old &lt;a href=&quot;https://github.com/cockroachdb/cockroach/issues/109628&quot;&gt;implementation&lt;/a&gt; of read committed in CockroachDB. As the engineering works out the kinks, I imagine read committed will only get better with time.&lt;/p&gt;
&lt;p&gt;The next step is to run PgBench using the built-in tpc-b workload without optimizations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql \
 pgbench \
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --no-vacuum \
    --client=8 \
    --&lt;span class=&quot;hljs-built_in&quot;&gt;jobs&lt;/span&gt;=8 \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --scale=10 \
    --failures-detailed \
    --verbose-errors \
    --max-tries=3 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt; \
    -T 60 \
    -P 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;pgbench (16.1 (Debian 16.1-1.pgdg120+1), server 13.0.0)
pgbench: warning: scale option ignored, using count from pgbench_branches table (10)
pgbench: client 5 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/5/0 at timestamp 1701374364.725565161,1 too old; must write at or above 1701374364.783286327,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=fe1456c4 key=/Table/109/5/569793/0 iso=Serializable pri=0.00522733 epo=0 ts=1701374364.783286327,1 min=1701374364.518938744,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374364.725565161,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374365.018938744,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 5 repeats the transaction after the error (try 1/3)
pgbench: client 4 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/3/0 at timestamp 1701374364.881843744,1 too old; must write at or above 1701374364.902487702,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=1eb035c5 key=/Table/109/5/110840/0 iso=Serializable pri=0.00276957 epo=0 ts=1701374364.902487702,1 min=1701374364.803162244,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374364.881843744,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374365.303162244,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 4 repeats the transaction after the error (try 1/3)
pgbench: client 6 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/1/0 at timestamp 1701374365.916704036,1 too old; must write at or above 1701374365.933405161,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=cf8b382e key=/Table/109/5/279511/0 iso=Serializable pri=0.00573224 epo=0 ts=1701374365.933405161,1 min=1701374365.839919870,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374365.916704036,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374366.339919870,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 6 repeats the transaction after the error (try 1/3)
pgbench: client 3 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/9/0 at timestamp 1701374367.028168703,1 too old; must write at or above 1701374367.082025953,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=b37541b7 key=/Table/109/5/43919/0 iso=Serializable pri=0.01583132 epo=0 ts=1701374367.082025953,1 min=1701374366.918740453,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374367.028168703,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374367.418740453,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 3 repeats the transaction after the error (try 1/3)
progress: 5.0 s, 238.8 tps, lat 33.228 ms stddev 23.762, 0 failed, 4 retried, 4 retries
pgbench: client 1 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/7/0 at timestamp 1701374371.162224122,1 too old; must write at or above 1701374371.200365997,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=9b00388b key=/Table/109/5/173940/0 iso=Serializable pri=0.01393375 epo=0 ts=1701374371.200365997,1 min=1701374370.906479830,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374371.162224122,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374371.406479830,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 1 repeats the transaction after the error (try 1/3)
pgbench: client 2 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/7/0 at timestamp 1701374371.200365997,1 too old; must write at or above 1701374371.221628122,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=cecdb1ab key=/Table/109/5/548267/0 iso=Serializable pri=0.00346401 epo=0 ts=1701374371.221628122,1 min=1701374370.973058997,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374371.200365997,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374371.473058997,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 2 repeats the transaction after the error (try 1/3)
pgbench: client 1 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/7/0 at timestamp 1701374371.308297414,2 too old; must write at or above 1701374371.385186164,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=98bf761d key=/Table/109/5/173940/0 iso=Serializable pri=0.00580461 epo=0 ts=1701374371.385186164,1 min=1701374371.234436747,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374371.308297414,2 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374371.734436747,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 1 repeats the transaction after the error (try 2/3)
pgbench: client 4 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/3/0 at timestamp 1701374373.132044165,1 too old; must write at or above 1701374373.157914498,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=8def4300 key=/Table/109/5/477477/0 iso=Serializable pri=0.00196323 epo=0 ts=1701374373.157914498,1 min=1701374373.001536956,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374373.132044165,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374373.501536956,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 4 repeats the transaction after the error (try 1/3)
progress: 10.0 s, 225.6 tps, lat 35.522 ms stddev 30.196, 0 failed, 3 retried, 4 retries
progress: 15.0 s, 238.0 tps, lat 33.552 ms stddev 20.915, 0 failed, 0 retried, 0 retries
pgbench: client 3 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/6/0 at timestamp 1701374380.138378168,2 too old; must write at or above 1701374380.184955710,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=c6ab1fec key=/Table/109/5/796957/0 iso=Serializable pri=0.00219624 epo=0 ts=1701374380.184955710,1 min=1701374379.949286210,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374380.138378168,2 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374380.449286210,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 3 repeats the transaction after the error (try 1/3)
pgbench: client 4 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/9/0 at timestamp 1701374381.532448085,1 too old; must write at or above 1701374381.532448085,2: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=ab78ff5d key=/Table/109/5/129631/0 iso=Serializable pri=0.02671145 epo=0 ts=1701374381.532448085,2 min=1701374381.403506460,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374381.532448085,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374381.903506460,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 4 repeats the transaction after the error (try 1/3)
pgbench: client 7 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/10/0 at timestamp 1701374381.970990086,1 too old; must write at or above 1701374382.002286294,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=a3720795 key=/Table/109/5/796844/0 iso=Serializable pri=0.01176172 epo=0 ts=1701374382.002286294,1 min=1701374381.842546002,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374381.970990086,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374382.342546002,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 7 repeats the transaction after the error (try 1/3)
progress: 20.0 s, 215.8 tps, lat 36.918 ms stddev 26.072, 0 failed, 3 retried, 3 retries
progress: 25.0 s, 216.2 tps, lat 37.140 ms stddev 25.942, 0 failed, 0 retried, 0 retries
pgbench: client 1 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/9/0 at timestamp 1701374390.624237798,1 too old; must write at or above 1701374390.675212798,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=1e77e781 key=/Table/109/5/487347/0 iso=Serializable pri=0.01765513 epo=0 ts=1701374390.675212798,1 min=1701374390.436830756,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374390.624237798,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374390.936830756,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 1 repeats the transaction after the error (try 1/3)
pgbench: client 3 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/10/0 at timestamp 1701374391.884443715,1 too old; must write at or above 1701374391.909561465,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=1c27478f key=/Table/109/5/424569/0 iso=Serializable pri=0.03662449 epo=0 ts=1701374391.909561465,1 min=1701374391.702494549,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374391.884443715,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374392.202494549,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 3 repeats the transaction after the error (try 1/3)
pgbench: client 7 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/3/0 at timestamp 1701374393.572487549,1 too old; must write at or above 1701374393.572487549,2: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=fdacfb83 key=/Table/109/5/730699/0 iso=Serializable pri=0.03673262 epo=0 ts=1701374393.572487549,2 min=1701374393.376278716,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374393.572487549,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374393.876278716,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 7 repeats the transaction after the error (try 1/3)
progress: 30.0 s, 209.0 tps, lat 38.151 ms stddev 31.989, 0 failed, 3 retried, 3 retries
progress: 35.0 s, 195.0 tps, lat 40.949 ms stddev 28.077, 0 failed, 0 retried, 0 retries
pgbench: client 1 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/4/0 at timestamp 1701374399.887792969,1 too old; must write at or above 1701374399.911607302,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=855e0eca key=/Table/109/5/165844/0 iso=Serializable pri=0.01764135 epo=0 ts=1701374399.911607302,1 min=1701374399.542153677,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374399.887792969,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374400.042153677,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 1 repeats the transaction after the error (try 1/3)
progress: 40.0 s, 145.6 tps, lat 54.621 ms stddev 38.881, 0 failed, 1 retried, 1 retries
pgbench: client 2 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/2/0 at timestamp 1701374408.252897556,1 too old; must write at or above 1701374408.252897556,2: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=3c1d5707 key=/Table/109/5/271506/0 iso=Serializable pri=0.00554444 epo=0 ts=1701374408.252897556,2 min=1701374408.060554931,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374408.252897556,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374408.560554931,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 2 repeats the transaction after the error (try 1/3)
progress: 45.0 s, 204.8 tps, lat 39.241 ms stddev 36.493, 0 failed, 1 retried, 1 retries
pgbench: client 3 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/2/0 at timestamp 1701374413.178464892,1 too old; must write at or above 1701374413.221088309,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=f1f5cf8b key=/Table/109/5/382061/0 iso=Serializable pri=0.00426220 epo=0 ts=1701374413.221088309,1 min=1701374413.072555142,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374413.178464892,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374413.572555142,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 3 repeats the transaction after the error (try 1/3)
progress: 50.0 s, 227.0 tps, lat 35.512 ms stddev 26.517, 0 failed, 1 retried, 1 retries
pgbench: client 4 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/9/0 at timestamp 1701374417.439743852,1 too old; must write at or above 1701374417.512724644,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=ceae42c3 key=/Table/109/5/647391/0 iso=Serializable pri=0.00061239 epo=0 ts=1701374417.512724644,1 min=1701374417.170008102,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374417.439743852,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374417.670008102,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 4 repeats the transaction after the error (try 1/3)
pgbench: client 5 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/9/0 at timestamp 1701374417.557735311,1 too old; must write at or above 1701374417.583758061,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=921ad19c key=/Table/109/5/292793/0 iso=Serializable pri=0.01234524 epo=0 ts=1701374417.583758061,1 min=1701374417.337672519,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374417.557735311,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374417.837672519,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 5 repeats the transaction after the error (try 1/3)
progress: 55.0 s, 227.2 tps, lat 35.162 ms stddev 26.932, 0 failed, 2 retried, 2 retries
pgbench: client 3 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/9/0 at timestamp 1701374420.093249854,1 too old; must write at or above 1701374420.125789020,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=254d2603 key=/Table/109/5/924387/0 iso=Serializable pri=0.01254729 epo=0 ts=1701374420.125789020,1 min=1701374419.940787687,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374420.093249854,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374420.440787687,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 3 repeats the transaction after the error (try 1/3)
pgbench: client 2 got an error &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;command&lt;/span&gt; 8 (SQL) of script 0; ERROR:  restart transaction: TransactionRetryWithProtoRefreshError: WriteTooOldError: write &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; key /Table/110/5/2/0 at timestamp 1701374421.256457729,1 too old; must write at or above 1701374421.285648938,1: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;sql txn&amp;quot;&lt;/span&gt; meta={&lt;span class=&quot;hljs-built_in&quot;&gt;id&lt;/span&gt;=8a6584e6 key=/Table/109/5/931266/0 iso=Serializable pri=0.00333353 epo=0 ts=1701374421.285648938,1 min=1701374421.081633729,0 &lt;span class=&quot;hljs-built_in&quot;&gt;seq&lt;/span&gt;=2} lock=&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;stat&lt;/span&gt;=PENDING rts=1701374421.256457729,1 wto=&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; gul=1701374421.581633729,0
HINT:  See: https://www.cockroachlabs.com/docs/v23.2/transaction-retry-error-reference.html
pgbench: client 2 repeats the transaction after the error (try 1/3)
progress: 60.0 s, 232.0 tps, lat 34.473 ms stddev 23.692, 0 failed, 2 retried, 2 retries
transaction &lt;span class=&quot;hljs-built_in&quot;&gt;type&lt;/span&gt;: &amp;lt;&lt;span class=&quot;hljs-built_in&quot;&gt;builtin&lt;/span&gt;: TPC-B (&lt;span class=&quot;hljs-built_in&quot;&gt;sort&lt;/span&gt; of)&amp;gt;
scaling &lt;span class=&quot;hljs-built_in&quot;&gt;factor&lt;/span&gt;: 10
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 3
duration: 60 s
number of transactions actually processed: 12883
number of failed transactions: 0 (0.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of transactions retried: 20 (0.155%)
total number of retries: 21
latency average = 37.261 ms
latency stddev = 28.630 ms
initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; = 22.223 ms
tps = 214.593890 (without initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing surpising here as we&#39;ve seen these retries in my previous articles. PgBench introduced retry logic in the version 15 and we can see the workload retry until transactions succeed.&lt;/p&gt;
&lt;p&gt;Let&#39;s run our optimization from the previous article. You can find the workload in my &lt;a href=&quot;https://github.com/dbist/cockroach-docker/blob/main/postgresql/tpcb-cockroach.sql&quot;&gt;docker repo&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql pgbench \ 
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --no-vacuum \
    --file=tpcb-cockroach.sql@1 \
    --client=8 \
    --&lt;span class=&quot;hljs-built_in&quot;&gt;jobs&lt;/span&gt;=8 \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --scale=10 \
    --failures-detailed \
    --verbose-errors \
    --max-tries=3 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt; \
    -T 60 \
    -P 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;pgbench (16.1 (Debian 16.1-1.pgdg120+1), server 13.0.0)
progress: 5.0 s, 305.4 tps, lat 25.998 ms stddev 22.118, 0 failed, 0 retried, 0 retries
progress: 10.0 s, 297.2 tps, lat 26.940 ms stddev 28.511, 0 failed, 0 retried, 0 retries
progress: 15.0 s, 279.8 tps, lat 28.628 ms stddev 29.997, 0 failed, 0 retried, 0 retries
progress: 20.0 s, 279.0 tps, lat 28.608 ms stddev 26.844, 0 failed, 0 retried, 0 retries
progress: 25.0 s, 265.8 tps, lat 29.923 ms stddev 36.855, 0 failed, 0 retried, 0 retries
progress: 30.0 s, 160.0 tps, lat 50.258 ms stddev 79.162, 0 failed, 0 retried, 0 retries
progress: 35.0 s, 187.2 tps, lat 42.830 ms stddev 42.750, 0 failed, 0 retried, 0 retries
progress: 40.0 s, 293.4 tps, lat 27.239 ms stddev 26.405, 0 failed, 0 retried, 0 retries
progress: 45.0 s, 275.8 tps, lat 29.087 ms stddev 33.249, 0 failed, 0 retried, 0 retries
progress: 50.0 s, 284.8 tps, lat 28.038 ms stddev 30.830, 0 failed, 0 retried, 0 retries
progress: 55.0 s, 279.8 tps, lat 28.593 ms stddev 29.569, 0 failed, 0 retried, 0 retries
progress: 60.0 s, 279.2 tps, lat 28.617 ms stddev 30.770, 0 failed, 0 retried, 0 retries
transaction &lt;span class=&quot;hljs-built_in&quot;&gt;type&lt;/span&gt;: tpcb-cockroach.sql
scaling &lt;span class=&quot;hljs-built_in&quot;&gt;factor&lt;/span&gt;: 10
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 3
duration: 60 s
number of transactions actually processed: 15945
number of failed transactions: 0 (0.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of transactions retried: 0 (0.000%)
total number of retries: 0
latency average = 30.114 ms
latency stddev = 35.211 ms
initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; = 7.962 ms
tps = 265.499744 (without initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice a much cleaner run but we knew this already from the second article.&lt;/p&gt;
&lt;h4&gt;Compare to read committed isolation&lt;/h4&gt;
&lt;p&gt;We&#39;re now going to switch to read committed for the user &lt;code&gt;foo&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING sql.txn.read_committed_isolation.enabled &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;; 
&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; foo &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; default_transaction_isolation &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;read committed&amp;#x27;&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can connect to the cluster using the user account for &lt;code&gt;foo&lt;/code&gt; and confirm which isolation is being used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;foo&lt;span class=&quot;hljs-variable&quot;&gt;@lb&lt;/span&gt;:&lt;span class=&quot;hljs-number&quot;&gt;26000&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;defaultdb&lt;span class=&quot;hljs-operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SHOW&lt;/span&gt; default_transaction_isolation;                                                                            
  default_transaction_isolation
&lt;span class=&quot;hljs-comment&quot;&gt;---------------------------------&lt;/span&gt;
  read committed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can rerun the benchmark using the built-in workload and read committed&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql \            
 pgbench \
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --no-vacuum \
    --client=8 \
    --&lt;span class=&quot;hljs-built_in&quot;&gt;jobs&lt;/span&gt;=8 \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --scale=10 \
    --failures-detailed \
    --verbose-errors \
    --max-tries=3 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt; \
    -T 60 \
    -P 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;pgbench (16.1 (Debian 16.1-1.pgdg120+1), server 13.0.0)
pgbench: warning: scale option ignored, using count from pgbench_branches table (10)
progress: 5.0 s, 227.0 tps, lat 35.042 ms stddev 18.674, 0 failed, 0 retried, 0 retries
progress: 10.0 s, 213.8 tps, lat 37.408 ms stddev 20.119, 0 failed, 0 retried, 0 retries
progress: 15.0 s, 217.8 tps, lat 36.751 ms stddev 19.447, 0 failed, 0 retried, 0 retries
progress: 20.0 s, 215.8 tps, lat 36.922 ms stddev 19.299, 0 failed, 0 retried, 0 retries
progress: 25.0 s, 217.4 tps, lat 36.918 ms stddev 19.705, 0 failed, 0 retried, 0 retries
progress: 30.0 s, 216.2 tps, lat 36.902 ms stddev 20.906, 0 failed, 0 retried, 0 retries
progress: 35.0 s, 205.8 tps, lat 38.992 ms stddev 24.295, 0 failed, 0 retried, 0 retries
progress: 40.0 s, 211.6 tps, lat 37.757 ms stddev 21.103, 0 failed, 0 retried, 0 retries
progress: 45.0 s, 220.2 tps, lat 36.309 ms stddev 21.630, 0 failed, 0 retried, 0 retries
progress: 50.0 s, 217.6 tps, lat 36.876 ms stddev 20.284, 0 failed, 0 retried, 0 retries
progress: 55.0 s, 212.8 tps, lat 37.545 ms stddev 20.558, 0 failed, 0 retried, 0 retries
progress: 60.0 s, 218.8 tps, lat 36.568 ms stddev 19.725, 0 failed, 0 retried, 0 retries
transaction &lt;span class=&quot;hljs-built_in&quot;&gt;type&lt;/span&gt;: &amp;lt;&lt;span class=&quot;hljs-built_in&quot;&gt;builtin&lt;/span&gt;: TPC-B (&lt;span class=&quot;hljs-built_in&quot;&gt;sort&lt;/span&gt; of)&amp;gt;
scaling &lt;span class=&quot;hljs-built_in&quot;&gt;factor&lt;/span&gt;: 10
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 3
duration: 60 s
number of transactions actually processed: 12982
number of failed transactions: 0 (0.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of transactions retried: 0 (0.000%)
total number of retries: 0
latency average = 36.985 ms
latency stddev = 20.521 ms
initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; = 8.775 ms
tps = 216.191364 (without initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how well read committed isolation handles contention in this run. In our original execution, there were 21 retries and even though PgBench retried those transactions, some transactions had to be reprocessed, thereby wasting compute. Here however, those retries didn&#39;t even surface. We also processed 100 more transactions 12982 vs 12883 in the original run. This is not significant in a small scale test like mine but every bit counts when these tests are on larger scale.&lt;/p&gt;
&lt;p&gt;The next step we&#39;re going to be running the optimized workload with read committed&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql pgbench \    
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \                     
    --no-vacuum \
    --file=tpcb-cockroach.sql@1 \
    --client=8 \
    --&lt;span class=&quot;hljs-built_in&quot;&gt;jobs&lt;/span&gt;=8 \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --scale=10 \
    --failures-detailed \
    --verbose-errors \
    --max-tries=3 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt; \
    -T 60 \
    -P 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;pgbench (16.1 (Debian 16.1-1.pgdg120+1), server 13.0.0)
progress: 5.0 s, 283.0 tps, lat 28.122 ms stddev 25.956, 0 failed, 0 retried, 0 retries
progress: 10.0 s, 271.0 tps, lat 29.569 ms stddev 28.935, 0 failed, 0 retried, 0 retries
progress: 15.0 s, 265.8 tps, lat 30.030 ms stddev 29.334, 0 failed, 0 retried, 0 retries
progress: 20.0 s, 261.4 tps, lat 30.605 ms stddev 27.032, 0 failed, 0 retried, 0 retries
progress: 25.0 s, 280.0 tps, lat 28.631 ms stddev 27.362, 0 failed, 0 retried, 0 retries
progress: 30.0 s, 286.4 tps, lat 27.781 ms stddev 27.500, 0 failed, 0 retried, 0 retries
progress: 35.0 s, 281.0 tps, lat 28.577 ms stddev 25.379, 0 failed, 0 retried, 0 retries
progress: 40.0 s, 281.4 tps, lat 28.299 ms stddev 23.310, 0 failed, 0 retried, 0 retries
progress: 45.0 s, 270.6 tps, lat 29.701 ms stddev 30.521, 0 failed, 0 retried, 0 retries
progress: 50.0 s, 271.6 tps, lat 29.482 ms stddev 27.495, 0 failed, 0 retried, 0 retries
progress: 55.0 s, 268.6 tps, lat 29.640 ms stddev 26.566, 0 failed, 0 retried, 0 retries
progress: 60.0 s, 274.4 tps, lat 29.274 ms stddev 29.799, 0 failed, 0 retried, 0 retries
transaction &lt;span class=&quot;hljs-built_in&quot;&gt;type&lt;/span&gt;: tpcb-cockroach.sql
scaling &lt;span class=&quot;hljs-built_in&quot;&gt;factor&lt;/span&gt;: 10
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 3
duration: 60 s
number of transactions actually processed: 16484
number of failed transactions: 0 (0.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of transactions retried: 0 (0.000%)
total number of retries: 0
latency average = 29.125 ms
latency stddev = 27.490 ms
initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; = 5.965 ms
tps = 274.515057 (without initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Voila, we are processing 16484 transactions with read committed vs. 15945 with serializable. Again, there are trade-offs with data anomalies but if performance is a concern and the legacy application is built to work with read committed isolation, you will have better experience with read committed.&lt;/p&gt;
&lt;p&gt;For posterity, let me show you that foreign keys are supported with read committed and PgBench.&lt;/p&gt;
&lt;p&gt;We do have to switch back to serializable isolation as we have to initialize the workload again and currently truncate statement is not supported with read committed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;USER&lt;/span&gt; foo &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; default_transaction_isolation &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;serializable&amp;#x27;&lt;/span&gt;; 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql pgbench \    
    --initialize \
    --foreign-keys \          
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --no-vacuum \
    --scale=10 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;dropping old tables...
creating tables...
NOTICE:  storage parameter &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;fillfactor&amp;quot;&lt;/span&gt; is ignored
NOTICE:  storage parameter &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;fillfactor&amp;quot;&lt;/span&gt; is ignored
NOTICE:  storage parameter &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;fillfactor&amp;quot;&lt;/span&gt; is ignored
generating data (client-side)...
1000000 of 1000000 tuples (100%) &lt;span class=&quot;hljs-keyword&quot;&gt;done&lt;/span&gt; (elapsed 2.82 s, remaining 0.00 s)
creating primary keys...
creating foreign keys...
&lt;span class=&quot;hljs-keyword&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; 38.08 s (drop tables 0.21 s, create tables 0.06 s, client-side generate 5.81 s, primary keys 30.57 s, foreign keys 1.44 s).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;let&#39;s switch to read committed first&lt;/p&gt;
&lt;p&gt;Built-in workload using explicit transactions&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql \         
  pgbench \                                 
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --no-vacuum \
    --client=8 \
    --&lt;span class=&quot;hljs-built_in&quot;&gt;jobs&lt;/span&gt;=8 \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --scale=10 \
    --failures-detailed \
    --verbose-errors \
    --max-tries=3 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt; \
    -T 60 \
    -P 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;pgbench (16.1 (Debian 16.1-1.pgdg120+1), server 13.0.0)
pgbench: warning: scale option ignored, using count from pgbench_branches table (10)
progress: 5.0 s, 200.0 tps, lat 39.704 ms stddev 21.335, 0 failed, 0 retried, 0 retries
progress: 10.0 s, 193.6 tps, lat 41.307 ms stddev 21.592, 0 failed, 0 retried, 0 retries
progress: 15.0 s, 199.4 tps, lat 40.088 ms stddev 19.551, 0 failed, 0 retried, 0 retries
progress: 20.0 s, 197.0 tps, lat 40.730 ms stddev 21.442, 0 failed, 0 retried, 0 retries
progress: 25.0 s, 204.8 tps, lat 38.967 ms stddev 19.367, 0 failed, 0 retried, 0 retries
progress: 30.0 s, 198.4 tps, lat 40.368 ms stddev 22.252, 0 failed, 0 retried, 0 retries
progress: 35.0 s, 196.8 tps, lat 40.791 ms stddev 22.013, 0 failed, 0 retried, 0 retries
progress: 40.0 s, 192.4 tps, lat 41.471 ms stddev 21.291, 0 failed, 0 retried, 0 retries
progress: 45.0 s, 198.8 tps, lat 40.249 ms stddev 21.359, 0 failed, 0 retried, 0 retries
progress: 50.0 s, 193.0 tps, lat 41.309 ms stddev 22.361, 0 failed, 0 retried, 0 retries
progress: 55.0 s, 193.8 tps, lat 41.319 ms stddev 21.147, 0 failed, 0 retried, 0 retries
progress: 60.0 s, 190.6 tps, lat 42.024 ms stddev 22.474, 0 failed, 0 retried, 0 retries
transaction &lt;span class=&quot;hljs-built_in&quot;&gt;type&lt;/span&gt;: &amp;lt;&lt;span class=&quot;hljs-built_in&quot;&gt;builtin&lt;/span&gt;: TPC-B (&lt;span class=&quot;hljs-built_in&quot;&gt;sort&lt;/span&gt; of)&amp;gt;
scaling &lt;span class=&quot;hljs-built_in&quot;&gt;factor&lt;/span&gt;: 10
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 3
duration: 60 s
number of transactions actually processed: 11800
number of failed transactions: 0 (0.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of transactions retried: 0 (0.000%)
total number of retries: 0
latency average = 40.687 ms
latency stddev = 21.375 ms
initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; = 11.911 ms
tps = 196.451948 (without initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventhough read committed handles the contention, it still underperforms compared to the execution without foreign keys. Hopefully with further enhancement work in read committed implementation this improves.&lt;/p&gt;
&lt;p&gt;Optimized workload&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker &lt;span class=&quot;hljs-built_in&quot;&gt;exec&lt;/span&gt; -it postgresql pgbench \
    --host=&lt;span class=&quot;hljs-variable&quot;&gt;${PGHOST}&lt;/span&gt; \
    --no-vacuum \
    --file=tpcb-cockroach.sql@1 \
    --client=8 \
    --&lt;span class=&quot;hljs-built_in&quot;&gt;jobs&lt;/span&gt;=8 \
    --username=foo \
    --port=&lt;span class=&quot;hljs-variable&quot;&gt;${PGPORT}&lt;/span&gt; \
    --scale=10 \
    --failures-detailed \
    --verbose-errors \
    --max-tries=3 \
    &lt;span class=&quot;hljs-variable&quot;&gt;${PGDATABASE}&lt;/span&gt; \
    -T 60 \
    -P 5
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;pgbench (16.1 (Debian 16.1-1.pgdg120+1), server 13.0.0)
progress: 5.0 s, 233.2 tps, lat 34.079 ms stddev 26.993, 0 failed, 0 retried, 0 retries
progress: 10.0 s, 200.7 tps, lat 39.841 ms stddev 36.275, 0 failed, 0 retried, 0 retries
progress: 15.0 s, 220.3 tps, lat 36.399 ms stddev 29.333, 0 failed, 0 retried, 0 retries
progress: 20.0 s, 226.2 tps, lat 35.320 ms stddev 29.787, 0 failed, 0 retried, 0 retries
progress: 25.0 s, 220.0 tps, lat 36.370 ms stddev 28.896, 0 failed, 0 retried, 0 retries
progress: 30.0 s, 222.4 tps, lat 35.921 ms stddev 30.289, 0 failed, 0 retried, 0 retries
progress: 35.0 s, 214.0 tps, lat 37.306 ms stddev 34.033, 0 failed, 0 retried, 0 retries
progress: 40.0 s, 216.2 tps, lat 37.187 ms stddev 31.789, 0 failed, 0 retried, 0 retries
progress: 45.0 s, 227.6 tps, lat 35.093 ms stddev 31.301, 0 failed, 0 retried, 0 retries
progress: 50.0 s, 230.2 tps, lat 34.797 ms stddev 29.103, 0 failed, 0 retried, 0 retries
progress: 55.0 s, 226.2 tps, lat 35.291 ms stddev 30.328, 0 failed, 0 retried, 0 retries
progress: 60.0 s, 217.6 tps, lat 36.763 ms stddev 29.471, 0 failed, 0 retried, 0 retries
transaction &lt;span class=&quot;hljs-built_in&quot;&gt;type&lt;/span&gt;: tpcb-cockroach.sql
scaling &lt;span class=&quot;hljs-built_in&quot;&gt;factor&lt;/span&gt;: 10
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 3
duration: 60 s
number of transactions actually processed: 13279
number of failed transactions: 0 (0.000%)
number of serialization failures: 0 (0.000%)
number of deadlock failures: 0 (0.000%)
number of transactions retried: 0 (0.000%)
total number of retries: 0
latency average = 36.154 ms
latency stddev = 30.686 ms
initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; = 7.224 ms
tps = 221.166896 (without initial connection &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;As you see, read committed has performance advantages to serializable isolation, and as bugs get worked out, it will only get better. Considering the trade-offs with read committed isolation, if your application can tolerate the anomalies, it will provide a much better on-ramp experience for applications.&lt;/p&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2023/11/optimizing-pgbench-for-cockroachdb-part.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuBWvIj9eZG9zIIt570S1BQTUmi7MCLGC83DsGc3gyxnrIqAKxt54FTk-Ll3Iksvl-BtLSPv4SqiG2yzabI4b2MIfuUS0gZplj9ZQa9nHjAGgMwQIc2Adgqyle23bHRf3Jb9YE1xax4Vf_krUZEfqpdNAaYZyBjGbuPLRWKRblz44eonepuQZQyahUyyX4/s72-c/optimizing-pgbench-for-cockroachdb-part-3-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-3396476113901752994</guid><pubDate>Tue, 24 Oct 2023 19:36:00 +0000</pubDate><atom:updated>2026-06-12T15:21:08.767-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cockroach labs</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">flyway</category><category domain="http://www.blogger.com/atom/ns#">migration</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">schema migration</category><category domain="http://www.blogger.com/atom/ns#">Security</category><category domain="http://www.blogger.com/atom/ns#">ssl</category><category domain="http://www.blogger.com/atom/ns#">tls</category><title>Secure schema migrations with Flyway and CockroachDB Dedicated</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimtrNJawU5-JjOrRUx8K9c7rAip_nwLnbz5h90KWiAyuyVZiSweAZpU-aqclGWLguRWoo2zf28gyRXC5A58l6k4pq929BMpn34E_Qy1MQUq4DlaT4Q62W537qmNsJ7F2yN7_W_lHjSAk9kpFHeWWB9rLyitMFxRia6s0icmHH1b4RMTorCGeoX9N41FSDx/s1200/secure-schema-migrations-with-flyway-and-cockroachdb-dedicated-hero.jpg&quot; alt=&quot;Secure schema migrations with Flyway and CockroachDB Dedicated&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://www.cockroachlabs.com/docs/cockroachcloud/plan-your-cluster&quot;&gt;CockroachDB Dedicated&lt;/a&gt; comes with TLS enabled by default. At the minimum, a certificate authority is needed to authenticate. A customer was having issues using &lt;a href=&quot;https://flywaydb.org/&quot;&gt;Flyway&lt;/a&gt; and TLS.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;My previous articles on schema migrations and CockroachDB&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.ervits.com/2022/01/migrating-feature-toggles-with-unleash.html&quot;&gt;Migrating feature toggles with Unleash and CockroachDB&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;A prospect was having issues with TLS and Flyway schema migrations and since both topics are near and dear to my heart I felt obligated to respond.&lt;/p&gt;
&lt;h2&gt;High Level Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Deploy a CockroachDB Dedicated cluster&lt;/li&gt;
&lt;li&gt;Configure Flyway&lt;/li&gt;
&lt;li&gt;Verify&lt;/li&gt;
&lt;li&gt;Demonstrate the problem&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step by step instructions&lt;/h2&gt;
&lt;h4&gt;Deploy a CockroachDB Dedicated cluster&lt;/h4&gt;
&lt;p&gt;Spinning up a CockroachDB Dedicated cluster is fairly straight forward. Follow this &lt;a href=&quot;https://www.cockroachlabs.com/docs/cockroachcloud/create-your-cluster&quot;&gt;guide&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Configure Flyway&lt;/h4&gt;
&lt;p&gt;Setting up a Flyway migration depends on your deployment model. I don&#39;t use any sophisticated CI/CD pipeline and rely on Docker for my needs. I&#39;m also using the community edition and limited to certain capabilities.&lt;/p&gt;
&lt;p&gt;You need a flyway directory with two child directories, &lt;code&gt;conf&lt;/code&gt; and &lt;code&gt;sql&lt;/code&gt;. The conf directory containes the authentication details in a file called &lt;code&gt;flyway.conf&lt;/code&gt;. You can find the connection details in the cloud console or by typing the &lt;code&gt;\c&lt;/code&gt; metacommand in the SQL client to capture the connection string.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;Connection string: postgresql:&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;artem:&lt;span class=&quot;hljs-operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;hljs-variable&quot;&gt;@artem&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;-&lt;/span&gt;serverless&lt;span class=&quot;hljs-number&quot;&gt;-5954.&lt;/span&gt;g8z.cockroachlabs.cloud:&lt;span class=&quot;hljs-number&quot;&gt;26257&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;defaultdb?application_name&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;ccloud&lt;span class=&quot;hljs-operator&quot;&gt;&amp;amp;&lt;/span&gt;connect_timeout&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;&amp;amp;&lt;/span&gt;sslmode&lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt;verify&lt;span class=&quot;hljs-operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;full&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may have noticed that I am using a serverless cluster instead but it shouldn&#39;t be relevant. The steps in this article are applicable for both scenarios.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;flyway.url=jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb
flyway.user=artem
flyway.password=password
flyway.connectRetries=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Populate the sql directory with one or many migration files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;-rw-r--r--  1 artem  staff   39 Oct 20 14:41 V1_0__Create_database.sql
-rw-r--r--  1 artem  staff   96 Oct 20 14:56 V1_3__Seed_table.sql
-rw-r--r--  1 artem  staff   72 Oct 20 15:00 V1_1__Create_type.sql
-rw-r--r--  1 artem  staff  128 Oct 20 15:04 V1_2__Create_table.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Verify&lt;/h4&gt;
&lt;p&gt;You can test that everything works using the &lt;code&gt;info&lt;/code&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker run --&lt;span class=&quot;hljs-built_in&quot;&gt;rm&lt;/span&gt; \
    -v &lt;span class=&quot;hljs-variable&quot;&gt;$PWD&lt;/span&gt;/flyway/sql:/flyway/sql \
    -v &lt;span class=&quot;hljs-variable&quot;&gt;$PWD&lt;/span&gt;/flyway/conf:/flyway/conf \
    flyway/flyway \
    info
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;Flyway Community Edition 9.22.3 by Redgate
See release notes here: https://rd.gt/416ObMi

Database: jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb (PostgreSQL 13.0)
WARNING: Flyway upgrade recommended: CockroachDB 23.1 is newer than this version of Flyway and support has not been tested. The latest supported version of CockroachDB is 22.1.
Schema &lt;span class=&quot;hljs-built_in&quot;&gt;history&lt;/span&gt; table &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;public&amp;quot;&lt;/span&gt;.&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;flyway_schema_history&amp;quot;&lt;/span&gt; does not exist yet
Schema version: &amp;lt;&amp;lt; &lt;span class=&quot;hljs-string&quot;&gt;Empty Schema &amp;gt;&amp;gt;

+-----------+---------+-----------------+------+--------------+---------+----------+
| Category  | Version | Description     | Type | Installed On | State   | Undoable |
+-----------+---------+-----------------+------+--------------+---------+----------+
| Versioned | 1.0     | Create database | SQL  |              | Pending | No       |
| Versioned | 1.1     | Create type     | SQL  |              | Pending | No       |
| Versioned | 1.2     | Create table    | SQL  |              | Pending | No       |
| Versioned | 1.3     | Seed table      | SQL  |              | Pending | No       |
+-----------+---------+-----------------+------+--------------+---------+----------+
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This seems to work but if for any reason you may have TLS issues, you may need to include &lt;code&gt;&amp;amp;sslmode=&lt;/code&gt; and this is primarily the reason for this article.&lt;/p&gt;
&lt;p&gt;Here&#39;s a message you&#39;ll see if you try to circumvent the sslmode with &lt;code&gt;sslmode=disable&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;WARNING: Connection error: FATAL: server requires encryption
Retrying &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; 4 sec...
ERROR: Unable to obtain connection from database (jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=&lt;span class=&quot;hljs-built_in&quot;&gt;disable&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; user &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;artem&amp;#x27;&lt;/span&gt;: FATAL: server requires encryption
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The easiest way to address the problem is to use &lt;code&gt;sslmode=require&lt;/code&gt;, i.e.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;flyway.url=jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=require
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and it&#39;s in line with what we originally saw, but the problem with &lt;code&gt;require&lt;/code&gt; is that it makes no effort to validate the authenticity of the cluster CA certififacte, leading to man in the middle attack risks. Hence, we always recommend using the &lt;code&gt;sslmode=verify-full&lt;/code&gt;, which I will demonstrate shortly.&lt;/p&gt;
&lt;p&gt;Before we proceed, I want to highlight how docker compose makes this even easier as it eliminiates the need to type out the entire docker command, it will become obvious as we discuss the verify-full method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;version: &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;3.9&amp;#x27;&lt;/span&gt;

services:

  flyway:
    container_name: flyway
    hostname: flyway
    image: flyway/flyway
    entrypoint: [&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;flyway&amp;quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;info&amp;quot;&lt;/span&gt;]
    volumes:
      - &lt;span class=&quot;hljs-variable&quot;&gt;$PWD&lt;/span&gt;/flyway/sql:/flyway/sql
      - &lt;span class=&quot;hljs-variable&quot;&gt;$PWD&lt;/span&gt;/flyway/conf:/flyway/conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the following in a &lt;code&gt;docker-compose.yml&lt;/code&gt; file and then you can invoke it using the following command&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;docker compose -f docker-compose.yml up
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Demonstrate the problem&lt;/h4&gt;
&lt;p&gt;In some cases, a more stringent requirement for security is needed where we have to use &lt;code&gt;sslmode=verify-full&lt;/code&gt;. Unappologetically, I agree and advocate to use the more security option as much as possible.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;flyway.url=jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;flyway  | Retrying &lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; 4 sec...
flyway  | ERROR: Unable to obtain connection from database (jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full) &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; user &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;artem&amp;#x27;&lt;/span&gt;: Could not open SSL root certificate file /root/.postgresql/root.crt.
flyway  | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
flyway  | SQL State  : 08006
flyway  | Error Code : 0
flyway  | Message    : Could not open SSL root certificate file /root/.postgresql/root.crt.
flyway  | 
flyway  | Caused by: org.postgresql.util.PSQLException: Could not open SSL root certificate file /root/.postgresql/root.crt.
flyway  | Caused by: java.io.FileNotFoundException: /root/.postgresql/root.crt (No such file or directory)
flyway exited with code 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clearly, we&#39;ve not included the CA certificate but the system is smart enough to look for it in the default location. The problem is it&#39;s looking in the default location of the container, not the host system.&lt;/p&gt;
&lt;p&gt;What needs to be done is attaching the cert volume to the container. All we need to do is include a single additional line in our compose file and tell flyway.conf to look for the cert in the url.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;version:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;3.9&amp;#x27;&lt;/span&gt;

&lt;span class=&quot;hljs-attr&quot;&gt;services:&lt;/span&gt;

  &lt;span class=&quot;hljs-attr&quot;&gt;flyway:&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;container_name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;flyway&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;hostname:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;flyway&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;image:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;flyway/flyway&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;entrypoint:&lt;/span&gt; [&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;flyway&amp;quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;info&amp;quot;&lt;/span&gt;]
    &lt;span class=&quot;hljs-attr&quot;&gt;volumes:&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./flyway/sql:/flyway/sql&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./flyway/conf:/flyway/conf&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;$PWD/root.crt:/certs/root.crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;flyway.url=jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full&amp;amp;sslrootcert=/certs/root.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the &lt;code&gt;info&lt;/code&gt; command succeeds, let&#39;s make the final change to the compose file to run a migration upon a successful connection and use the &lt;code&gt;info&lt;/code&gt; command as container healthcheck.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-yml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;version:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;3.9&amp;#x27;&lt;/span&gt;

&lt;span class=&quot;hljs-attr&quot;&gt;services:&lt;/span&gt;

  &lt;span class=&quot;hljs-attr&quot;&gt;flyway:&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;container_name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;flyway&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;hostname:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;flyway&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;image:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;flyway/flyway&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;entrypoint:&lt;/span&gt; [&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;flyway&amp;quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;migrate&amp;quot;&lt;/span&gt;]
    &lt;span class=&quot;hljs-attr&quot;&gt;volumes:&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./flyway/sql:/flyway/sql&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;./flyway/conf:/flyway/conf&lt;/span&gt;
      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;$PWD/root.crt:/certs/root.crt&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;healthcheck:&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;test:&lt;/span&gt; [&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;CMD-SHELL&amp;quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;flyway&amp;quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;info&amp;quot;&lt;/span&gt;]
      &lt;span class=&quot;hljs-attr&quot;&gt;interval:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;10s&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;timeout:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;10s&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;retries:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;start_period:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;10s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now run the migration&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;flyway  | Flyway Community Edition 9.22.3 by Redgate
flyway  | See release notes here: https://rd.gt/416ObMi
flyway  | 
flyway  | Database: jdbc:postgresql://artem-serverless-5954.g8z.cockroachlabs.cloud:26257/defaultdb (PostgreSQL 13.0)
flyway  | WARNING: Flyway upgrade recommended: CockroachDB 23.1 is newer than this version of Flyway and support has not been tested. The latest supported version of CockroachDB is 22.1.
flyway  | Successfully validated 4 migrations (execution &lt;span class=&quot;hljs-keyword&quot;&gt;time&lt;/span&gt; 00:00.103s)
flyway  | Current version of schema &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;public&amp;quot;&lt;/span&gt;: &amp;lt;&amp;lt; &lt;span class=&quot;hljs-string&quot;&gt;Empty Schema &amp;gt;&amp;gt;
flyway  | Migrating schema &amp;quot;public&amp;quot; to version &amp;quot;1.0 - Create database&amp;quot; [non-transactional]
flyway  | Migrating schema &amp;quot;public&amp;quot; to version &amp;quot;1.1 - Create type&amp;quot; [non-transactional]
flyway  | Migrating schema &amp;quot;public&amp;quot; to version &amp;quot;1.2 - Create table&amp;quot; [non-transactional]
flyway  | Migrating schema &amp;quot;public&amp;quot; to version &amp;quot;1.3 - Seed table&amp;quot; [non-transactional]
flyway  | Successfully applied 4 migrations to schema &amp;quot;public&amp;quot;, now at version v1.3 (execution time 00:00.750s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;This wraps up our look at TLS certificate issues and containerized schema migrations. If you feel this article incorrectly assumes certain things, please leave feedback in the comments.&lt;/p&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2023/10/secure-schema-migrations-with-flyway.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEimtrNJawU5-JjOrRUx8K9c7rAip_nwLnbz5h90KWiAyuyVZiSweAZpU-aqclGWLguRWoo2zf28gyRXC5A58l6k4pq929BMpn34E_Qy1MQUq4DlaT4Q62W537qmNsJ7F2yN7_W_lHjSAk9kpFHeWWB9rLyitMFxRia6s0icmHH1b4RMTorCGeoX9N41FSDx/s72-c/secure-schema-migrations-with-flyway-and-cockroachdb-dedicated-hero.jpg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-7958774397568336816</guid><pubDate>Wed, 30 Aug 2023 15:22:00 +0000</pubDate><atom:updated>2026-06-13T13:07:12.983-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cdc</category><category domain="http://www.blogger.com/atom/ns#">change data capture</category><category domain="http://www.blogger.com/atom/ns#">cockroach labs</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">debezium</category><category domain="http://www.blogger.com/atom/ns#">event driven</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">protocol buffers</category><category domain="http://www.blogger.com/atom/ns#">SQL</category><title>Emitting Protocol Buffers with CockroachDB CDC Queries</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgn41LZjKrqnpRiIqw8NiM0KIhMJA7TpFp6nVy5Il8vaokhJhjeTvUsjQWra0kq1MMbJ6xRsJYmoL9YZOcfuPQaIfaSqkLs9awVjJfvPi3gzJ7tj8AK8MzXID5rPg_uJ22ikoETTc3wJQG6p6drV9Ia4iM9xMgJP4xJsQJrqYBBZrSF1thZEiFd-u78HBqD/s1024/protocol-buffers.jpeg&quot; alt=&quot;Emitting Protocol Buffers with CockroachDB CDC Queries&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;As of this writing, CockroachDB does not have official support for protobuf payload as part of change data capture. Today, I am going to demonstrate a feature that may or may not land in CockroachDB proper. The product team is looking into viability of the feature before we can officially support it.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Previous articles on CockroachDB CDC&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2023/04/using-cockroachdb-cdc-with-apache-pulsar.html&quot;&gt;Using CockroachDB CDC with Apache Pulsar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/09/using-cockroachdb-cdc-with-azure-event.html&quot;&gt;Using CockroachDB CDC with Azure Event Hubs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/04/using-cockroachdb-cdc-with-confluent.html&quot;&gt;Using CockroachDB CDC with Confluent Cloud Kafka and Schema Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/03/saas-galore-integrating-cockroachdb.html&quot;&gt;SaaS Galore: Integrating CockroachDB with Confluent Kafka, FiveTran and Snowflake&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2020/01/cockroachdb-cdc-using-minio-as-cloud.html&quot;&gt;CockroachDB CDC using Minio as cloud storage sink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2020/01/cockroachdb-cdc-using-hadoop-ozone-s3.html&quot;&gt;CockroachDB CDC using Hadoop Ozone S3 Gateway as cloud storage sink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Motivation&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://protobuf.dev/&quot;&gt;Protocol Buffers&lt;/a&gt; are language-neutral, platform-neutral extensible mechanisms for serializing structured data. It&#39;s a common choice for platforms needing to pass messages between systems. &lt;a href=&quot;https://github.com/cockroachdb/cockroach&quot;&gt;CockroachDB&lt;/a&gt; is a distributed SQL database built on a transactional and strongly-consistent key-value store. It scales horizontally; survives disk, machine, rack, and even datacenter failures with minimal latency disruption and no manual intervention; supports strongly-consistent ACID transactions; and provides a familiar SQL API for structuring, manipulating, and querying data. There is no official support for Protocol Buffers in CockroachDB Changefeeds, even though we use Protocol Buffers  extensively in code. A recent customer conversation led to this experiment where I&#39;m going to use several recent features to demonstrate ability to serialize CockroachDB rows to proto and emit via CDC Queries. This is the first time we&#39;re looking at &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/cdc-queries&quot;&gt;CDC Queries&lt;/a&gt;. This is a new flexible way to express CockroachDB streams.&lt;/p&gt;
&lt;p&gt;This tutorial assumes you have an enterprise license. Given the features in this tutorial are unavailable as a product, you have to follow the steps exactly as described to pull the right source code to make it work. These features are not available in any of the available offerings from Cockroach Labs.&lt;/p&gt;
&lt;h2&gt;High Level Steps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Build CockroachDB with the Protocol Buffers function&lt;/li&gt;
&lt;li&gt;Deploy a CockroachDB cluster with enterprise changefeeds&lt;/li&gt;
&lt;li&gt;Deploy a Kafka Consumer&lt;/li&gt;
&lt;li&gt;Verify&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step by step instructions&lt;/h2&gt;
&lt;h3&gt;Build CockroachDB with the Protocol Buffers function&lt;/h3&gt;
&lt;p&gt;Before I show you how to get this working, I&#39;d like to express my gratitude to Yevgeniy Miretskiy, who works on the CDC team for the capability and his mentorship to get this working. The source code for the feature is available in the following &lt;a href=&quot;https://github.com/cockroachdb/cockroach/pull/89955/commits&quot;&gt;commit&lt;/a&gt;. For brevity, I will skip the steps to setup a build environment.&lt;/p&gt;
&lt;p&gt;Check out the pull request&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;gh &lt;span class=&quot;hljs-built_in&quot;&gt;pr&lt;/span&gt; checkout 89955
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run the preliminary steps to build Cockroach from source.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;./dev doctor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally build the code&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;bazel build pkg/cmd/cockroach-short
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Navigate to the directory with the built package&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;cd&lt;/span&gt; _bazel/bin/pkg/cmd/cockroach
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploy a CockroachDB cluster with enterprise changefeeds&lt;/h2&gt;
&lt;p&gt;Start an instance of CockroachDB using the built package&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;./cockroach start-single-node --insecure --background
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To enable CDC we need to execute the following commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING cluster.organization &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;&amp;lt;organization name&amp;gt;&amp;#x27;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING enterprise.license &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;&amp;lt;secret&amp;gt;&amp;#x27;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; CLUSTER SETTING kv.rangefeed.enabled &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generate sample data&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE TABLE&lt;/span&gt; office_dogs (
     id &lt;span class=&quot;hljs-type&quot;&gt;INT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;PRIMARY KEY&lt;/span&gt;,
     name STRING);

&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT INTO&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;VALUES&lt;/span&gt;
   (&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Petee&amp;#x27;&lt;/span&gt;),
   (&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Carl&amp;#x27;&lt;/span&gt;);

&lt;span class=&quot;hljs-keyword&quot;&gt;UPDATE&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Petee H&amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;  id &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt;  name
&lt;span class=&quot;hljs-comment&quot;&gt;-----+----------&lt;/span&gt;
   &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; Petee H
   &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; Carl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function we are going to use to convert rows to Protocol Buffers is &lt;code&gt;crdb_internal.row_to_proto()&lt;/code&gt;. With the given pull request, this function is readily available for querying.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; crdb_internal.row_to_proto(office_dogs) &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;                          crdb_internal.row_to_proto
&lt;span class=&quot;hljs-comment&quot;&gt;------------------------------------------------------------------------------&lt;/span&gt;
  \x0a110a046e616d6512091a07506574656520480a0f0a026964120911000000000000f03f
  \x0a0e0a046e616d6512061a044361726c0a0f0a0269641209110000000000000040
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It takes the row and serializes as proto. We can decode the row back to human readable form using the following query, skip the &lt;code&gt;\x&lt;/code&gt; and copy the rest of the output into the following function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; crdb_internal.pb_to_json(&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;google.protobuf.Struct&amp;#x27;&lt;/span&gt;,
decode(&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;0a110a046e616d6512091a07506574656520480a0f0a026964120911000000000000f03f&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;hex&amp;#x27;&lt;/span&gt;)) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; proto;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;             proto
&lt;span class=&quot;hljs-comment&quot;&gt;--------------------------------&lt;/span&gt;
  {&amp;quot;id&amp;quot;: &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &amp;quot;name&amp;quot;: &amp;quot;Petee H&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; crdb_internal.pb_to_json(&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;google.protobuf.Struct&amp;#x27;&lt;/span&gt;,
decode(&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;0a0e0a046e616d6512061a044361726c0a0f0a0269641209110000000000000040&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;hex&amp;#x27;&lt;/span&gt;)) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; proto;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;            proto
&lt;span class=&quot;hljs-comment&quot;&gt;-----------------------------&lt;/span&gt;
  {&amp;quot;id&amp;quot;: &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &amp;quot;name&amp;quot;: &amp;quot;Carl&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can use this function in the CDC query, but first, let&#39;s set up a &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/changefeed-examples#create-a-changefeed-connected-to-a-webhook-sink&quot;&gt;webhook sink&lt;/a&gt; for a quick demonstration of changefeed queries.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;git &lt;span class=&quot;hljs-built_in&quot;&gt;clone&lt;/span&gt; https://github.com/cockroachlabs/cdc-webhook-sink-test-server.git
&lt;span class=&quot;hljs-built_in&quot;&gt;cd&lt;/span&gt; cdc-webhook-sink-test-server
&lt;span class=&quot;hljs-built_in&quot;&gt;cd&lt;/span&gt; go-https-server
&lt;span class=&quot;hljs-built_in&quot;&gt;chmod&lt;/span&gt; +x server.sh
./server.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;./server.sh
......+.........+.....+....+...+.....+...+.+.....+++++++++++++++++++++++++++++++++++++++++++++*.+...........+.+......+.....+...+....+...+......+...........+...+......+++++++++++++++++++++++++++++++++++++++++++++*......+.....+.............+...+..+.+.....................+......+..+.+++++
-----
2023/08/30 09:39:05 starting server on port 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With all of the basics in place, we can create a changefeed&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; CHANGEFEED &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;webhook-https://localhost:3000?insecure_tls_skip_verify=true&amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;WITH&lt;/span&gt; updated &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; crdb_internal.row_to_proto(office_dogs) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; proto &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; office_dogs;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;        job_id
&lt;span class=&quot;hljs-comment&quot;&gt;----------------------&lt;/span&gt;
  &lt;span class=&quot;hljs-number&quot;&gt;895654238351589377&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Verify&lt;/h2&gt;
&lt;p&gt;Looking at the terminal where the webhook sink is running&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;2023/08/30 11:11:25 {&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;payload&amp;quot;&lt;/span&gt;:[{&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;__crdb__&amp;quot;&lt;/span&gt;: {&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;updated&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;1693408285757033000.0000000000&amp;quot;&lt;/span&gt;}, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;proto&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;\\x0a0e0a046e616d6512061a044361726c0a0f0a0269641209110000000000000040&amp;quot;&lt;/span&gt;}],&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;length&amp;quot;&lt;/span&gt;:1}
2023/08/30 11:11:25 {&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;payload&amp;quot;&lt;/span&gt;:[{&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;__crdb__&amp;quot;&lt;/span&gt;: {&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;updated&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;1693408285757033000.0000000000&amp;quot;&lt;/span&gt;}, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;proto&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;\\x0a110a046e616d6512091a07506574656520480a0f0a026964120911000000000000f03f&amp;quot;&lt;/span&gt;}],&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;length&amp;quot;&lt;/span&gt;:1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&#39;s update a record in the office_dogs TABLE&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;UPDATE&lt;/span&gt; office_dogs &lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; name &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;Tarzan&amp;#x27;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;WHERE&lt;/span&gt; id &lt;span class=&quot;hljs-operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;; 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;2023/08/30 11:12:58 {&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;payload&amp;quot;&lt;/span&gt;:[{&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;__crdb__&amp;quot;&lt;/span&gt;: {&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;updated&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;1693408377084928000.0000000000&amp;quot;&lt;/span&gt;}, &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;proto&amp;quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;\\x0a100a046e616d6512081a065461727a616e0a0f0a026964120911000000000000f03f&amp;quot;&lt;/span&gt;}],&lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;length&amp;quot;&lt;/span&gt;:1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we use the decode function to inspect the payload&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; crdb_internal.pb_to_json(&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;google.protobuf.Struct&amp;#x27;&lt;/span&gt;, decode(&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;0a100a046e616d6512081a065461727a616e0a0f0a026964120911000000000000f03f&amp;#x27;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;hex&amp;#x27;&lt;/span&gt;)) &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; proto;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;             proto
&lt;span class=&quot;hljs-comment&quot;&gt;-------------------------------&lt;/span&gt;
  {&amp;quot;id&amp;quot;: &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &amp;quot;name&amp;quot;: &amp;quot;Tarzan&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have to mention that the emitted messages are of &lt;a href=&quot;https://protobuf.dev/reference/protobuf/google.protobuf/#struct&quot;&gt;dynamically typed format&lt;/a&gt; and not strongly typed. If your use case requires strongly typed, it&#39;s a conversation we have to have another time.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And this is how you can leverage CockroachDB CDC Queries with built-in functions. This function is  not available but it can be, given higher demand. Hopefully you&#39;ve found this article useful. Please reach out to our teach if you need this capability and we will consider it in the future.&lt;/p&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2023/08/emitting-protocol-buffers-using.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgn41LZjKrqnpRiIqw8NiM0KIhMJA7TpFp6nVy5Il8vaokhJhjeTvUsjQWra0kq1MMbJ6xRsJYmoL9YZOcfuPQaIfaSqkLs9awVjJfvPi3gzJ7tj8AK8MzXID5rPg_uJ22ikoETTc3wJQG6p6drV9Ia4iM9xMgJP4xJsQJrqYBBZrSF1thZEiFd-u78HBqD/s72-c/protocol-buffers.jpeg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-7996827713605896805</guid><pubDate>Thu, 15 Jun 2023 17:23:00 +0000</pubDate><atom:updated>2026-06-13T13:12:41.291-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">encryption</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">Security</category><category domain="http://www.blogger.com/atom/ns#">ssl</category><category domain="http://www.blogger.com/atom/ns#">til</category><category domain="http://www.blogger.com/atom/ns#">tls</category><title>CockroachDB TIL: Volume 13</title><description>&lt;div class=&quot;ervits-post&quot;&gt;
&lt;style&gt;
.ervits-post{font:18px/1.75 -apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Arial,sans-serif;color:#25313f;word-wrap:break-word}
.ervits-post h1,.ervits-post h2,.ervits-post h3{color:#111827;line-height:1.25}
.ervits-post h1{font-size:2.2em;margin-bottom:.4em}
.ervits-post h2{font-size:1.5em;margin-top:2em;border-bottom:1px solid #e5e7eb;padding-bottom:.3em}
.ervits-post h3{font-size:1.2em;margin-top:1.5em}
.ervits-post p{margin:1em 0}
.ervits-post a{color:#0f5e9c;text-underline-offset:3px}
.ervits-post a:hover{color:#0a4578}
.ervits-post img{max-width:100%;height:auto;display:block;margin:0 auto;border-radius:6px}
.ervits-post figure{margin:2em 0;text-align:center}
.ervits-post figcaption{font-size:.85em;color:#6b7280;margin-top:.5em}
.ervits-post blockquote{border-left:4px solid #0f5e9c;background:#f1f6fb;margin:1.5em 0;padding:1em 1.5em;border-radius:0 6px 6px 0}
.ervits-post blockquote p{margin:0}
.ervits-post code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;background:#f1f3f5;padding:2px 5px;border-radius:3px}
.ervits-post pre{overflow-x:auto;background:#111827;color:#f9fafb;padding:1.2em 1.5em;border-radius:8px;margin:1.5em 0;line-height:1.6}
.ervits-post pre code{background:none;padding:0;border-radius:0;font-size:.9em;color:inherit}
.ervits-post ul,.ervits-post ol{padding-left:1.5em;margin:1em 0}
.ervits-post li{margin:.25em 0}
.ervits-post hr{border:none;border-top:1px solid #e5e7eb;margin:2em 0}
.ervits-post table{border-collapse:collapse;width:100%;margin:1.5em 0}
.ervits-post th,.ervits-post td{border:1px solid #d1d5db;padding:.5em .75em;text-align:left}
.ervits-post th{background:#f3f4f6;font-weight:600}
.ervits-post .hljs-keyword,.ervits-post .hljs-selector-tag{color:#c678dd}
.ervits-post .hljs-built_in,.ervits-post .hljs-name{color:#e06c75}
.ervits-post .hljs-string{color:#98c379}
.ervits-post .hljs-attr,.ervits-post .hljs-attribute{color:#98c379}
.ervits-post .hljs-number,.ervits-post .hljs-literal{color:#d19a66}
.ervits-post .hljs-comment{color:#5c6370;font-style:italic}
.ervits-post .hljs-title,.ervits-post .hljs-section{color:#61afef}
.ervits-post .hljs-type{color:#e5c07b}
.ervits-post .hljs-meta{color:#56b6c2}
.ervits-post .hljs-deletion{color:#e06c75}
.ervits-post .hljs-addition{color:#98c379}
&lt;/style&gt;
&lt;p&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOdOM-ooCKYHswmiQLUPpnCSZH9TECyPkqJGkqv8TW2XIFk5sQlBGczmm_r0rvd88sCGN5SgjErFRgaqNxr4-o8Uzk1hZUYDnj3lPZ5Ma5agke1JDsoNJA23e7Qx4EuqlyyIbpI7HEqsiN5X4HCpRVNTllaeI99eMVV9xvf3sGJAxQ9C2lOwOlj2ZsrQRD/s1024/TIL.jpeg&quot; alt=&quot;CockroachDB TIL: Volume 13&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This is my series of articles covering short &amp;quot;Today I learned&amp;quot; topics as I work with CockroachDB. Today, we&#39;re diagnosing cert issues, looking at client verification of server certificates differences in the &lt;code&gt;cockroach&lt;/code&gt; and the &lt;code&gt;psql&lt;/code&gt; clients, and checking the node and the CA certificate expiration programmatically.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Previous articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2021/08/cockroachdb-til-volume-1.html&quot;&gt;Volume 1&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2021/08/cockroachdb-til-volume-2.html&quot;&gt;Volume 2&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2021/10/cockroachdb-til-volume-3.html&quot;&gt;Volume 3&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2021/10/cockroachdb-til-volume-4.html&quot;&gt;Volume 4&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/01/cockroachdb-til-volume-5.html&quot;&gt;Volume 5&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/03/cockroachdb-til-volume-6.html&quot;&gt;Volume 6&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/04/cockroachdb-til-volume-7.html&quot;&gt;Volume 7&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/08/cockroachdb-til-volume-8.html&quot;&gt;Volume 8&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/08/cockroachdb-til-volume-9.html&quot;&gt;Volume 9&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/10/cockroachdb-til-volume-10.html&quot;&gt;Volume 10&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2022/12/cockroachdb-til-volume-11.html&quot;&gt;Volume 11&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.ervits.com/2023/04/cockroachdb-til-volume-12.html&quot;&gt;Volume 12&lt;/a&gt;&lt;/br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Topics&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Topic 1: Diagnose certificate based authentication&lt;/li&gt;
&lt;li&gt;Topic 2: Differences between &lt;code&gt;cockroach&lt;/code&gt; and &lt;code&gt;psql&lt;/code&gt; clients for client verification of server certificates&lt;/li&gt;
&lt;li&gt;Topic 3: Capture the node and the CA certificate expiration programmatically&lt;/li&gt;
&lt;li&gt;Topic 4: Check the client certificate expiration&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Topic 1: Diagnose certificate based authentication&lt;/h2&gt;
&lt;p&gt;I was using &lt;a href=&quot;https://www.cockroachlabs.com/lp/serverless/&quot;&gt;CockroachDB Serverless&lt;/a&gt; with a product called &lt;a href=&quot;https://docs.polyscale.ai/&quot;&gt;PolyScale.ai&lt;/a&gt; for an &lt;a href=&quot;https://blog.ervits.com/2023/05/reducing-network-latency-and-improving.html&quot;&gt;article&lt;/a&gt; I was writing and I&#39;ve run into a strange issue with certificate-based authN. When you provision the instance of their service, it provides a client connection string.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/461296/237752616-e5c6c1ff-b491-4fa8-bc90-ff9e00dd27ac.png&quot; alt=&quot;connection string&quot;&gt;&lt;/p&gt;
&lt;p&gt;I used the connection string to connect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@psedge.global:5432/defaultdb?sslmode=require&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I attempted to connect, I&#39;d received an error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql: error: connection to server at &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;psedge.global&amp;quot;&lt;/span&gt; (104.248.109.242), port 5432 failed: SSL error: certificate verify failed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The error message confused me as it was pointing to a port. What made everything more confusing was that the service reported it had connected just fine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/461296/237752618-0ecdeac6-4346-47e4-bf46-272245890cb0.png&quot; alt=&quot;connection test&quot;&gt;&lt;/p&gt;
&lt;p&gt;I thought the issue was with the CA cert Cockroach relies on and I was stuck until I reached out to the engineering team. My team suggested to run the following command to verify the certificate coming from PolyScale:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;/opt/homebrew/Cellar/openssl@3/3.1.0/bin/openssl s_client -showcerts -connect psedge.global:5432 -starttls postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need openssl version 3.1 or later, the one I had by default on my OSX laptop did not work. The response of the command will show the certificate details.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify &lt;span class=&quot;hljs-built_in&quot;&gt;return&lt;/span&gt;:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
verify &lt;span class=&quot;hljs-built_in&quot;&gt;return&lt;/span&gt;:1
depth=0 C = US, ST = California, L = Redwood City, O = &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Polyscale, Inc&amp;quot;&lt;/span&gt;, CN = *.polyscale.global
verify &lt;span class=&quot;hljs-built_in&quot;&gt;return&lt;/span&gt;:1
---
Certificate chain
 0 s:C = US, ST = California, L = Redwood City, O = &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Polyscale, Inc&amp;quot;&lt;/span&gt;, CN = *.polyscale.global
   i:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Oct  4 00:00:00 2022 GMT; NotAfter: Jul 28 23:59:59 2023 GMT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CockroachDB Serverless uses Let&#39;s Encrypt for the Certificate Authority. Although not necessary, you can download the CA cert and store it in your filesystem.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/461296/238383937-fb67c9a5-c378-4f71-ac1e-fd29fb63fd2c.png&quot; alt=&quot;cert&quot;&gt;&lt;/p&gt;
&lt;p&gt;That&#39;s the certificate I had in my filesystem. Similarly, you can use the above command to view the details of the certificate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;/opt/homebrew/Cellar/openssl@3/3.1.0/bin/openssl s_client -showcerts -connect artem-serverless-3312.g8z.cockroachlabs.cloud:26257 -starttls postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;Server certificate
subject=CN = *.cockroachlabs.cloud
issuer=C = US, O = Let&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;s Encrypt, CN = R3
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The workaround here is to remove the root.crt as it was causing conflicts with the PolyScale cert. As soon as you remove the cert, you can authenticate successfully.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@psedge.global:5432/defaultdb?sslmode=require&amp;quot;&lt;/span&gt;
psql (15.3 (Homebrew), server 13.0.0)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;help&amp;quot;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;help&lt;/span&gt;.

defaultdb=&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, you can also use the &lt;code&gt;cockroach&lt;/code&gt; client&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;cockroach sql --url &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@psedge.global:5432/defaultdb?application_name=19592894-26b0-42f7-aea8-2b4e1cb8ec24&amp;amp;sslmode=require&amp;quot;&lt;/span&gt; 
&lt;span class=&quot;hljs-comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# Welcome to the CockroachDB SQL shell.&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# All statements must be terminated by a semicolon.&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# To exit, type: \q.&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# Client version: CockroachDB CCL v22.2.9 (aarch64-apple-darwin21.2, built 2023/05/08 13:26:12, go1.19.6)&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# Server version: CockroachDB CCL v22.2.7 (x86_64-pc-linux-gnu, built 2023/03/28 19:47:29, go1.19.6)&lt;/span&gt;

warning: server version older than client! proceed with caution; some features may not be available.

&lt;span class=&quot;hljs-comment&quot;&gt;# Cluster ID: 20735006-5a39-49c3-29b8-3d4c8befcbf8&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;# Enter \? for a brief introduction.&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;#&lt;/span&gt;
username@psedge.global:5432/defaultdb&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now what if we&#39;d like to specify the cert we wanted to use to connect, we can capture the cert coming from PolyScale and store it in our filesystem.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;/opt/homebrew/Cellar/openssl@3/3.1.0/bin/openssl s_client -showcerts -connect psedge.global:5432 -starttls postgres | openssl x509 -text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the output into a new file with the BEGIN and END included.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save it a new file and then use it in the connection string with &lt;code&gt;sslrootcert=file&lt;/code&gt; argument.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@psedge.global:5432/defaultdb?sslmode=require&amp;amp;sslrootcert=polyscale.crt&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql (15.3 (Homebrew), server 13.0.0)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;help&amp;quot;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;help&lt;/span&gt;.

defaultdb=&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can even combine the certs into a single super cert by copying the contents of both certs including the BEGIN and END, i.e.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have both certs stored in this cert, we can connect without any issues.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;cockroach sql --url &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@psedge.global:5432/defaultdb?sslmode=verify-full&amp;amp;sslrootcert=/Users/artem/.postgresql/super.crt&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and likewise, use the same cert to connect to CockroachDB:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;cockroach sql --url &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@artem-serverless-3312.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full&amp;amp;sslrootcert=/Users/artem/.postgresql/super.crt&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The lesson here is either have all certs in your default directory and explicitly specify them with &lt;code&gt;sslrootcert&lt;/code&gt; or remove &lt;code&gt;sslrootcert&lt;/code&gt; from the connection string and make sure your filesystem does not have any residual certs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Topic 2: Differences between &lt;code&gt;cockroach&lt;/code&gt; and &lt;code&gt;psql&lt;/code&gt; clients for client verification of server certificates&lt;/h2&gt;
&lt;p&gt;While I was writing the first topic, I noticed a slight difference in behavior between the &lt;code&gt;cockroach&lt;/code&gt; and &lt;code&gt;psql&lt;/code&gt; clients when it comes to client verification of server certificates. Unlike the &lt;code&gt;cockroach&lt;/code&gt; client, &lt;code&gt;psql&lt;/code&gt; client expects a CA certificate in the default location. From the Postgresql &lt;a href=&quot;https://www.postgresql.org/docs/current/libpq-ssl.html#LIBQ-SSL-CERTIFICATES&quot;&gt;docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To allow server certificate verification, one or more root certificates must be placed in the file ~/.postgresql/root.crt in the user&#39;s home directory. (On Microsoft Windows the file is named %APPDATA%\postgresql\root.crt.) Intermediate certificates should also be added to the file if they are needed to link the certificate chain sent by the server to the root certificates stored on the client.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Certificate Revocation List (CRL) entries are also checked if the file ~/.postgresql/root.crl exists (%APPDATA%\postgresql\root.crl on Microsoft Windows).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The location of the root certificate file and the CRL can be changed by setting the connection parameters sslrootcert and sslcrl or the environment variables PGSSLROOTCERT and PGSSLCRL. sslcrldir or the environment variable PGSSLCRLDIR can also be used to specify a directory containing CRL files.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cockroach&lt;/code&gt; client will not rely on the default &lt;code&gt;~/.postgresql/root.crt&lt;/code&gt; location to make the authN. Case in point, use the following connection string while not having any certs in the default directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;cockroach sql --url &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@artem-serverless-3312.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=require&amp;quot;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will work, now let&#39;s switch the sslmode to &lt;code&gt;verify-full&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;cockroach sql --url &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@artem-serverless-3312.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full&amp;quot;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will also work. Now let&#39;s switch to the &lt;code&gt;psql&lt;/code&gt; client&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@artem-serverless-3312.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=require&amp;quot;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, and &lt;code&gt;sslmode-verify&lt;/code&gt;?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;postgres://username:password@artem-serverless-3312.g8z.cockroachlabs.cloud:26257/defaultdb?sslmode=verify-full&amp;quot;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;psql: error: connection to server at &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;artem-serverless-3312.g8z.cockroachlabs.cloud&amp;quot;&lt;/span&gt; (34.138.180.55), port 26257 failed: root certificate file &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;/Users/artem/.postgresql/root.crt&amp;quot;&lt;/span&gt; does not exist
Either provide the file or change sslmode to &lt;span class=&quot;hljs-built_in&quot;&gt;disable&lt;/span&gt; server certificate verification.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need access to the the root certificate as it cannot be implicitly read from the internet.&lt;/p&gt;
&lt;p&gt;Download the cert and &lt;code&gt;sslmode-verify&lt;/code&gt; will now work.&lt;/p&gt;
&lt;p&gt;THe lesson here if you want to verify the server certificate using the &lt;code&gt;psql&lt;/code&gt; client, make sure the certificate is stored in the filesystem. The &lt;code&gt;cockroach&lt;/code&gt; client is more forgiving in that regard.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Topic 3: Check the node and the CA certificate expiration programmatically&lt;/h2&gt;
&lt;p&gt;A customer asks: how do you check the client certificate expiration programmatically? You can of course scrape the node metrics endpoint and capture the info, you can also navigate to the Advanced Debug page and look at the certificate page, but is there a way to check using SQL? The answer is yes.&lt;/p&gt;
&lt;p&gt;We expose the certificate metrics using the following names, &lt;code&gt;security.certificate.expiration.ca&lt;/code&gt; and &lt;code&gt;security.certificate.expiration.node&lt;/code&gt; for the Certificate Authority and node certificates respectively. CockroachDB persists this data in a table &lt;code&gt;crdb_internal.kv_node_status&lt;/code&gt;. We can issue the following query to retrieve this information.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;-- CA cert&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; to_timestamp((metrics&lt;span class=&quot;hljs-operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;security.certificate.expiration.ca&amp;#x27;&lt;/span&gt;)::&lt;span class=&quot;hljs-type&quot;&gt;FLOAT&lt;/span&gt;)::TIMESTAMPTZ &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; crdb_internal.kv_node_status;  

&lt;span class=&quot;hljs-comment&quot;&gt;-- node cert&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; to_timestamp((metrics&lt;span class=&quot;hljs-operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;security.certificate.expiration.node&amp;#x27;&lt;/span&gt;)::&lt;span class=&quot;hljs-type&quot;&gt;FLOAT&lt;/span&gt;)::TIMESTAMPTZ &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; crdb_internal.kv_node_status;  
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;  &lt;span class=&quot;hljs-number&quot;&gt;2028&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;-05&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;-20&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;15&lt;/span&gt;:&lt;span class=&quot;hljs-number&quot;&gt;26&lt;/span&gt;:&lt;span class=&quot;hljs-number&quot;&gt;26&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt;
  ...
  &lt;span class=&quot;hljs-number&quot;&gt;2028&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;-05&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;-20&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;15&lt;/span&gt;:&lt;span class=&quot;hljs-number&quot;&gt;26&lt;/span&gt;:&lt;span class=&quot;hljs-number&quot;&gt;25&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will output for every node in the cluster. Now you can build your own monitoring and alerting solution to alarm you when your certificates are about to expire, that&#39;s if you don&#39;t offload the certificate management to an external system.&lt;/p&gt;
&lt;p&gt;https://www.cockroachlabs.com/docs/stable/metrics.html&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Topic 4: Check the client certificate expiration&lt;/h2&gt;
&lt;p&gt;In the previous topic we discussed how to check node and certifacte authority expiration. We left out the client certifactes because keeping this information in the database is not feasible. There may be thousands of client certs. We are going to cover how to check the client certifacte expiration below, keep in mind this is an area of active development and the proper user experience is not yet realized.&lt;/p&gt;
&lt;p&gt;I used the following &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/create-security-certificates-openssl.html#see-also&quot;&gt;tutorial&lt;/a&gt; to create a cluster, CA, node and client certificates.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ca.cnf&lt;/code&gt; file has a field that controls the cert expiry. Any node and client certs using this &lt;code&gt;ca.cnf&lt;/code&gt; configuration file will honor the expiration date specified in this file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;[ CA_default ]
default_days = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Skipping the rest of the tutorial to the step where we create a second client user, we can use the same commands from the tutorial to generate a client certificate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;openssl genrsa -out certs/client.expired.key 2048
&lt;span class=&quot;hljs-built_in&quot;&gt;chmod&lt;/span&gt; 400 certs/client.expired.key 
openssl req -new -config client.cnf -key certs/client.expired.key -out client.expired.csr -batch
openssl ca -config ca.cnf -keyfile my-safe-directory/ca.key -cert certs/ca.crt -policy signing_policy -extensions signing_client_req -out certs/client.expired.crt -outdir certs/ -&lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; client.expired.csr -batch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output will contain the expiration date.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;Using configuration from ca.cnf
Check that the request matches the signature
Signature ok
The Subject&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;s Distinguished Name is as follows
organizationName      :ASN.1 12:&amp;#x27;&lt;/span&gt;Cockroach&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;
commonName            :ASN.1 12:&amp;#x27;&lt;/span&gt;expired&lt;span class=&quot;hljs-string&quot;&gt;&amp;#x27;
Certificate is to be certified until May 27 17:23:04 2023 GMT (1 days)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have a client cert, we can use the &lt;code&gt;cockroach cert&lt;/code&gt; command to list out all of the client certificates in the certificate directory and their expiration.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;cockroach cert --certs-dir certs list
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-sql&quot;&gt;Certificate directory: expired_certs
  Usage  &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt;  Certificate File  &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt;      Key File      &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt;  Expires   &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt;     Notes     &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; Error
&lt;span class=&quot;hljs-comment&quot;&gt;---------+--------------------+--------------------+------------+---------------+--------&lt;/span&gt;
  Client &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; client.expired.crt &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; client.expired.key &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;2023&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;05&lt;/span&gt;&lt;span class=&quot;hljs-operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;27&lt;/span&gt; &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;user&lt;/span&gt;: expired &lt;span class=&quot;hljs-operator&quot;&gt;|&lt;/span&gt;
(&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;hljs-type&quot;&gt;row&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does not show the time of day when client certificate is going to expire. We can use the &lt;code&gt;openssl x509 -text&lt;/code&gt; command to inspect the certificate for a more detailed expiry.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;openssl x509 -text -&lt;span class=&quot;hljs-keyword&quot;&gt;in&lt;/span&gt; expired_certs/client.expired.crt | grep &lt;span class=&quot;hljs-string&quot;&gt;&amp;quot;Not After&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;  Not After : May 27 17:23:04 2023 GMT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have an open &lt;a href=&quot;https://github.com/cockroachdb/cockroach/issues/100699&quot;&gt;issue&lt;/a&gt; to surface a metric to expose clients with expiring certs. We can discuss how to automate the check when the metric is available.&lt;/p&gt;
&lt;p&gt;Finally, one more method to check for cert expiration is with the &lt;code&gt;openssl verify&lt;/code&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;openssl verify -CAfile certs/ca.crt expired/client.expired.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot;&gt;O = Cockroach, CN = expired
error 10 at 0 depth lookup:certificate has expired
expired/client.expired.crt: verification failed: 10 (certificate has expired)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;/div&gt;</description><link>http://blog.ervits.com/2023/06/cockroachdb-til-volume-13.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOdOM-ooCKYHswmiQLUPpnCSZH9TECyPkqJGkqv8TW2XIFk5sQlBGczmm_r0rvd88sCGN5SgjErFRgaqNxr4-o8Uzk1hZUYDnj3lPZ5Ma5agke1JDsoNJA23e7Qx4EuqlyyIbpI7HEqsiN5X4HCpRVNTllaeI99eMVV9xvf3sGJAxQ9C2lOwOlj2ZsrQRD/s72-c/TIL.jpeg" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-8829716355049900851</guid><pubDate>Wed, 17 May 2023 18:51:00 +0000</pubDate><atom:updated>2023-05-18T12:54:32.736-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cdc</category><category domain="http://www.blogger.com/atom/ns#">change data capture</category><category domain="http://www.blogger.com/atom/ns#">cockroach labs</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">duckdb</category><category domain="http://www.blogger.com/atom/ns#">extension</category><category domain="http://www.blogger.com/atom/ns#">htap</category><category domain="http://www.blogger.com/atom/ns#">olap</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><title>Using DuckDB with CockroachDB</title><description>&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLtkedeDYwx4KIhMJw91DYcmRm26-XPWc7VFxFwLdKic20hFXo9iAmMlYZtFMBL13vNITseFYmdIBAEy_7prvIvEGLIqMfvG1-4BQTouxINON6dJnr0UU0dngi06TE15cNBoeghM6j3HsGE3ys9gtXcVMRgL-DDmgKJVwnjI7q_oYoXhS5KhQ_G5I27A/s582/logo_duck.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; clear: left; float: left;&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; width=&quot;200&quot; data-original-height=&quot;360&quot; data-original-width=&quot;582&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLtkedeDYwx4KIhMJw91DYcmRm26-XPWc7VFxFwLdKic20hFXo9iAmMlYZtFMBL13vNITseFYmdIBAEy_7prvIvEGLIqMfvG1-4BQTouxINON6dJnr0UU0dngi06TE15cNBoeghM6j3HsGE3ys9gtXcVMRgL-DDmgKJVwnjI7q_oYoXhS5KhQ_G5I27A/s200/logo_duck.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;

&lt;script src=&quot;https://gist.github.com/dbist/4f923967cf08b47c218341d57e8aedac.js&quot;&gt;&lt;/script&gt;</description><link>http://blog.ervits.com/2023/05/using-duckdb-with-cockroachdb.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLtkedeDYwx4KIhMJw91DYcmRm26-XPWc7VFxFwLdKic20hFXo9iAmMlYZtFMBL13vNITseFYmdIBAEy_7prvIvEGLIqMfvG1-4BQTouxINON6dJnr0UU0dngi06TE15cNBoeghM6j3HsGE3ys9gtXcVMRgL-DDmgKJVwnjI7q_oYoXhS5KhQ_G5I27A/s72-c/logo_duck.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-131852631012013354</guid><pubDate>Fri, 12 May 2023 16:36:00 +0000</pubDate><atom:updated>2023-05-12T12:36:08.090-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cache</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">cockroachlabs</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">kv</category><category domain="http://www.blogger.com/atom/ns#">nosql</category><category domain="http://www.blogger.com/atom/ns#">polyscale</category><category domain="http://www.blogger.com/atom/ns#">polyscale.ai</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">redis</category><category domain="http://www.blogger.com/atom/ns#">serverless</category><title>Reducing network latency and improving read performance with CockroachDB and PolyScale.ai</title><description>&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhA3cC-7GhURsGcyrqUE8oVlrxabft6mEYpXbcWbSFj9evOn7R3F61--Wy2F_nWfHxD7Sh0GtaKfp3XbRKMfzpugcoYn-ncIa30b19IDTTtMTxkDWxne_UK4BI8lTfY5CiYl7F-Et_ZNEXOYab84dNp_c0Ew1crZ4gEfIXtF1uvWaO8lC5gZyJeczBWLg/s316/logo.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; clear: left; float: left;&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; width=&quot;320&quot; data-original-height=&quot;119&quot; data-original-width=&quot;316&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhA3cC-7GhURsGcyrqUE8oVlrxabft6mEYpXbcWbSFj9evOn7R3F61--Wy2F_nWfHxD7Sh0GtaKfp3XbRKMfzpugcoYn-ncIa30b19IDTTtMTxkDWxne_UK4BI8lTfY5CiYl7F-Et_ZNEXOYab84dNp_c0Ew1crZ4gEfIXtF1uvWaO8lC5gZyJeczBWLg/s320/logo.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;


&lt;script src=&quot;https://gist.github.com/dbist/0798ee941979f7b8538966213d003537.js&quot;&gt;&lt;/script&gt;</description><link>http://blog.ervits.com/2023/05/reducing-network-latency-and-improving.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhA3cC-7GhURsGcyrqUE8oVlrxabft6mEYpXbcWbSFj9evOn7R3F61--Wy2F_nWfHxD7Sh0GtaKfp3XbRKMfzpugcoYn-ncIa30b19IDTTtMTxkDWxne_UK4BI8lTfY5CiYl7F-Et_ZNEXOYab84dNp_c0Ew1crZ4gEfIXtF1uvWaO8lC5gZyJeczBWLg/s72-c/logo.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-9115602254449687995</guid><pubDate>Thu, 27 Apr 2023 19:27:00 +0000</pubDate><atom:updated>2023-04-27T15:27:12.836-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cockroach labs</category><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">connection</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">ha</category><category domain="http://www.blogger.com/atom/ns#">haproxy</category><category domain="http://www.blogger.com/atom/ns#">high availability</category><category domain="http://www.blogger.com/atom/ns#">load balancer</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><category domain="http://www.blogger.com/atom/ns#">psql</category><title>Load balancer high availability with CockroachDB and HAProxy</title><description>&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy4mYc8zpeI5CRu8kemyCDwRFDXFTZqkvD5J_O-IJaPi6gbOLqijnicubkVnRXe9bYCXF0Rg1Bq2dQrXj_sRFE--GKHl99nT5SmZqLt7GcbgWtHMRsQLDnIebVPTfJyKyYFToSeGEy795TBWZZ5K0upe-dSiIXccE6J0Lr7ocRTLoUcwBBjRCxVuzBeA/s589/lb-ha-topology.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; clear: left; float: left;&quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; width=&quot;200&quot; data-original-height=&quot;218&quot; data-original-width=&quot;589&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy4mYc8zpeI5CRu8kemyCDwRFDXFTZqkvD5J_O-IJaPi6gbOLqijnicubkVnRXe9bYCXF0Rg1Bq2dQrXj_sRFE--GKHl99nT5SmZqLt7GcbgWtHMRsQLDnIebVPTfJyKyYFToSeGEy795TBWZZ5K0upe-dSiIXccE6J0Lr7ocRTLoUcwBBjRCxVuzBeA/s200/lb-ha-topology.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;

&lt;script src=&quot;https://gist.github.com/dbist/1237705e859a74c5e945158d063ea8cf.js&quot;&gt;&lt;/script&gt;</description><link>http://blog.ervits.com/2023/04/load-balancer-high-availability-with.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy4mYc8zpeI5CRu8kemyCDwRFDXFTZqkvD5J_O-IJaPi6gbOLqijnicubkVnRXe9bYCXF0Rg1Bq2dQrXj_sRFE--GKHl99nT5SmZqLt7GcbgWtHMRsQLDnIebVPTfJyKyYFToSeGEy795TBWZZ5K0upe-dSiIXccE6J0Lr7ocRTLoUcwBBjRCxVuzBeA/s72-c/lb-ha-topology.png" height="72" width="72"/><thr:total>0</thr:total></item><item><guid isPermaLink="false">tag:blogger.com,1999:blog-4228537051045607583.post-8951527621543382939</guid><pubDate>Fri, 21 Apr 2023 19:59:00 +0000</pubDate><atom:updated>2023-04-21T16:08:26.627-04:00</atom:updated><category domain="http://www.blogger.com/atom/ns#">cockroachdb</category><category domain="http://www.blogger.com/atom/ns#">cockroachlabs</category><category domain="http://www.blogger.com/atom/ns#">connection pool</category><category domain="http://www.blogger.com/atom/ns#">database</category><category domain="http://www.blogger.com/atom/ns#">failure</category><category domain="http://www.blogger.com/atom/ns#">fault tolerance</category><category domain="http://www.blogger.com/atom/ns#">high availability</category><category domain="http://www.blogger.com/atom/ns#">pgbouncer</category><category domain="http://www.blogger.com/atom/ns#">pgcat</category><category domain="http://www.blogger.com/atom/ns#">postgresql</category><title>Connection pool high availability with CockroachDB and PgCat</title><description>&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2AXwT6u0zzhDIF60Lt5EU-w5JZm9kc8YKEmBvwWgWDz2A1OEC9ueZo738aLgiapQFdJnGsZxJjiympGSEa8MOf5mD4nczVvygja-uMbMNgka16ka3BliCbHy-fyMRLUkusdJIjqItHgXiA1SdmdwtwYcwp7eKcNOmJIYUIRIipo7rPBegu4B6mzUTnw/s589/TOPOLOGY.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;&quot; border=&quot;0&quot; width=&quot;200&quot; data-original-height=&quot;302&quot; data-original-width=&quot;589&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2AXwT6u0zzhDIF60Lt5EU-w5JZm9kc8YKEmBvwWgWDz2A1OEC9ueZo738aLgiapQFdJnGsZxJjiympGSEa8MOf5mD4nczVvygja-uMbMNgka16ka3BliCbHy-fyMRLUkusdJIjqItHgXiA1SdmdwtwYcwp7eKcNOmJIYUIRIipo7rPBegu4B6mzUTnw/s200/TOPOLOGY.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;
&lt;script src=&quot;https://gist.github.com/dbist/071999199e39a159988789c229b8ecfc.js&quot;&gt;&lt;/script&gt;</description><link>http://blog.ervits.com/2023/04/connection-pooling-and-proxy-high.html</link><author>noreply@blogger.com (Artem Ervits)</author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2AXwT6u0zzhDIF60Lt5EU-w5JZm9kc8YKEmBvwWgWDz2A1OEC9ueZo738aLgiapQFdJnGsZxJjiympGSEa8MOf5mD4nczVvygja-uMbMNgka16ka3BliCbHy-fyMRLUkusdJIjqItHgXiA1SdmdwtwYcwp7eKcNOmJIYUIRIipo7rPBegu4B6mzUTnw/s72-c/TOPOLOGY.png" height="72" width="72"/><thr:total>0</thr:total></item></channel></rss>